F: Implementing PromptEdit pt3

This commit is contained in:
Ivan 2025-07-15 21:59:38 +03:00
parent 56652095af
commit 56f1bcacad
8 changed files with 112 additions and 23 deletions

View File

@ -48,6 +48,7 @@ export { LuFolderOpen as IconFolderOpened } from 'react-icons/lu';
export { LuFolderClosed as IconFolderClosed } from 'react-icons/lu';
export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu';
export { TbHelpOctagon as IconHelp } from 'react-icons/tb';
export { LuPresentation as IconSample } from 'react-icons/lu';
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
export { RiPushpinFill as IconPin } from 'react-icons/ri';
export { RiUnpinLine as IconUnpin } from 'react-icons/ri';

View File

@ -3,11 +3,26 @@ import { PromptVariableType } from './models/prompting';
const describePromptVariableRecord: Record<PromptVariableType, string> = {
[PromptVariableType.BLOCK]: 'Текущий блок операционной схемы',
[PromptVariableType.OSS]: 'Текущая операционная схема',
[PromptVariableType.SCHEMA]: 'Текущая концептуальный схема',
[PromptVariableType.SCHEMA]: 'Текущая концептуальная схема',
[PromptVariableType.CONSTITUENTA]: 'Текущая конституента'
};
const mockPromptVariableRecord: Record<PromptVariableType, string> = {
[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}`;
}
/** Retrieves mock text for {@link PromptVariableType}. */
export function mockPromptVariable(variable: string): string {
if (!Object.values(PromptVariableType).includes(variable as PromptVariableType)) {
return variable;
}
return mockPromptVariableRecord[variable as PromptVariableType] ?? `UNKNOWN VARIABLE: ${variable}`;
}

View File

@ -1,3 +1,5 @@
import { mockPromptVariable } from '../labels';
/** 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.
* */
@ -10,3 +12,18 @@ export function extractPromptVariables(target: string): string[] {
}
return result;
}
/** Generates a sample text from a target templates. */
export function generateSample(target: string): string {
const variables = extractPromptVariables(target);
if (variables.length === 0) {
return target;
}
let result = target;
for (const variable of variables) {
const mockText = mockPromptVariable(variable);
const escapedVar = variable.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
result = result.replace(new RegExp(`\\{\\{${escapedVar}\\}\\}`, 'g'), mockText);
}
return result;
}

View File

@ -1,18 +1,23 @@
'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 { useRef, useState } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx';
import { useDebounce } from 'use-debounce';
import { useMutatingPrompts } from '@/features/ai/backend/use-mutating-prompts';
import { useUpdatePromptTemplate } from '@/features/ai/backend/use-update-prompt-template';
import { generateSample } from '@/features/ai/models/prompting-api';
import { useAuthSuspense } from '@/features/auth';
import { MiniButton } from '@/components/control';
import { IconSample } from '@/components/icons';
import { Checkbox, TextArea, TextInput } from '@/components/input';
import { cn } from '@/components/utils';
import { useModificationStore } from '@/stores/modification';
import { globalIDs } from '@/utils/constants';
import { globalIDs, PARAMETER } from '@/utils/constants';
import {
type IPromptTemplate,
@ -33,6 +38,8 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
const isProcessing = useMutatingPrompts();
const setIsModified = useModificationStore(state => state.setIsModified);
const { updatePromptTemplate } = useUpdatePromptTemplate();
const [sampleResult, setSampleResult] = useState<string | null>(null);
const [debouncedResult] = useDebounce(sampleResult, PARAMETER.moveDuration);
const {
control,
@ -50,6 +57,7 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
is_shared: promptTemplate.is_shared
}
});
const text = useWatch({ control, name: 'text' });
const prevReset = useRef(toggleReset);
const prevTemplate = useRef(promptTemplate);
@ -63,6 +71,7 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
text: promptTemplate.text,
is_shared: promptTemplate.is_shared
});
setSampleResult(null);
}
const prevDirty = useRef(isDirty);
@ -81,7 +90,7 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
return (
<form
id={globalIDs.prompt_editor}
className={cn('flex flex-col gap-3 px-6', className)}
className={cn('flex flex-col gap-3 px-6 py-2', className)}
onSubmit={event => void handleSubmit(onSubmit)(event)}
>
<TextInput
@ -101,26 +110,44 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
<TextArea
id='prompt_text'
label='Содержание' //
fitContent
className='disabled:min-h-9 max-h-64'
{...register('text')}
error={errors.text}
disabled={isProcessing || !isMutable}
/>
<div className='flex justify-between'>
<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}
/>
)}
/>
<MiniButton
title='Сгенерировать пример запроса'
icon={<IconSample size='1.25rem' className='icon-primary' />}
onClick={() => setSampleResult(!!sampleResult ? null : generateSample(text))}
/>
</div>
<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}
/>
)}
/>
<div className={clsx('cc-prompt-result overflow-y-hidden', sampleResult !== null && 'open')}>
<TextArea
fitContent
className='mt-3 max-h-64 min-h-12'
label='Пример запроса'
value={sampleResult ?? debouncedResult ?? ''}
disabled
/>
</div>
</form>
);
}

View File

@ -59,7 +59,7 @@ export function TabEditTemplate({ activeID }: TabEditTemplateProps) {
/>
) : null}
<FormPromptTemplate
className='mt-12 xs:mt-4 w-100 md:w-180 min-w-70'
className='mt-8 xs:mt-0 w-100 md:w-180 min-w-70'
isMutable={isMutable}
promptTemplate={promptTemplate}
toggleReset={toggleReset}

View File

@ -1,3 +1,18 @@
import { describePromptVariable } from '../../labels';
import { PromptVariableType } from '../../models/prompting';
/** Displays all prompt variable types with their descriptions. */
export function TabViewVariables() {
return <div className='pt-8 border rounded'>View all variables</div>;
return (
<div className='pt-8'>
<ul className='space-y-1'>
{Object.values(PromptVariableType).map(variableType => (
<li key={variableType} className='flex flex-col'>
<span className='font-math text-primary'>{`{{${variableType}}}`}</span>
<span className='font-main text-muted-foreground'>{describePromptVariable(variableType)}</span>
</li>
))}
</ul>
</div>
);
}

View File

@ -30,7 +30,7 @@ export function SidePanel({ isMounted, className }: SidePanelProps) {
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK ? selectedItems[0] : null;
const selectedSchema = selectedOperation?.result ?? null;
const debouncedMounted = useDebounce(isMounted, PARAMETER.moveDuration);
const [debouncedMounted] = useDebounce(isMounted, PARAMETER.moveDuration);
const closePanel = usePreferencesStore(state => state.toggleShowOssSidePanel);
const sidePanelHeight = useMainHeight();

View File

@ -221,6 +221,20 @@
}
}
@utility cc-prompt-result {
transition-property: clip-path, height;
transition-duration: var(--duration-move);
transition-timing-function: var(--ease-in-out);
clip-path: inset(0% 0% 100% 0%);
height: 0;
&.open {
clip-path: inset(0% 0% 0% 0%);
height: 100%;
}
}
@utility cc-parsing-result {
transition-property: clip-path, padding, margin, border, height;
transition-duration: var(--duration-move);