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 { LuFolderClosed as IconFolderClosed } from 'react-icons/lu';
export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu'; export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu';
export { TbHelpOctagon as IconHelp } from 'react-icons/tb'; 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 { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
export { RiPushpinFill as IconPin } from 'react-icons/ri'; export { RiPushpinFill as IconPin } from 'react-icons/ri';
export { RiUnpinLine as IconUnpin } 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> = { const describePromptVariableRecord: Record<PromptVariableType, string> = {
[PromptVariableType.BLOCK]: 'Текущий блок операционной схемы', [PromptVariableType.BLOCK]: 'Текущий блок операционной схемы',
[PromptVariableType.OSS]: 'Текущая операционная схема', [PromptVariableType.OSS]: 'Текущая операционная схема',
[PromptVariableType.SCHEMA]: 'Текущая концептуальный схема', [PromptVariableType.SCHEMA]: 'Текущая концептуальная схема',
[PromptVariableType.CONSTITUENTA]: 'Текущая конституента' [PromptVariableType.CONSTITUENTA]: 'Текущая конституента'
}; };
const mockPromptVariableRecord: Record<PromptVariableType, string> = {
[PromptVariableType.BLOCK]: 'Пример: Текущий блок операционной схемы',
[PromptVariableType.OSS]: 'Пример: Текущая операционная схема',
[PromptVariableType.SCHEMA]: 'Пример: Текущая концептуальная схема',
[PromptVariableType.CONSTITUENTA]: 'Пример: Текущая конституента'
};
/** Retrieves description for {@link PromptVariableType}. */ /** Retrieves description for {@link PromptVariableType}. */
export function describePromptVariable(itemType: PromptVariableType): string { export function describePromptVariable(itemType: PromptVariableType): string {
return describePromptVariableRecord[itemType] ?? `UNKNOWN VARIABLE TYPE: ${itemType}`; 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. /** 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. * 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; 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 no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client'; 'use client';
import { useRef } from 'react'; import { useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; 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 { useMutatingPrompts } from '@/features/ai/backend/use-mutating-prompts';
import { useUpdatePromptTemplate } from '@/features/ai/backend/use-update-prompt-template'; import { useUpdatePromptTemplate } from '@/features/ai/backend/use-update-prompt-template';
import { generateSample } from '@/features/ai/models/prompting-api';
import { useAuthSuspense } from '@/features/auth'; import { useAuthSuspense } from '@/features/auth';
import { MiniButton } from '@/components/control';
import { IconSample } from '@/components/icons';
import { Checkbox, TextArea, TextInput } from '@/components/input'; import { Checkbox, TextArea, TextInput } from '@/components/input';
import { cn } from '@/components/utils'; import { cn } from '@/components/utils';
import { useModificationStore } from '@/stores/modification'; import { useModificationStore } from '@/stores/modification';
import { globalIDs } from '@/utils/constants'; import { globalIDs, PARAMETER } from '@/utils/constants';
import { import {
type IPromptTemplate, type IPromptTemplate,
@ -33,6 +38,8 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
const isProcessing = useMutatingPrompts(); const isProcessing = useMutatingPrompts();
const setIsModified = useModificationStore(state => state.setIsModified); const setIsModified = useModificationStore(state => state.setIsModified);
const { updatePromptTemplate } = useUpdatePromptTemplate(); const { updatePromptTemplate } = useUpdatePromptTemplate();
const [sampleResult, setSampleResult] = useState<string | null>(null);
const [debouncedResult] = useDebounce(sampleResult, PARAMETER.moveDuration);
const { const {
control, control,
@ -50,6 +57,7 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
is_shared: promptTemplate.is_shared is_shared: promptTemplate.is_shared
} }
}); });
const text = useWatch({ control, name: 'text' });
const prevReset = useRef(toggleReset); const prevReset = useRef(toggleReset);
const prevTemplate = useRef(promptTemplate); const prevTemplate = useRef(promptTemplate);
@ -63,6 +71,7 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
text: promptTemplate.text, text: promptTemplate.text,
is_shared: promptTemplate.is_shared is_shared: promptTemplate.is_shared
}); });
setSampleResult(null);
} }
const prevDirty = useRef(isDirty); const prevDirty = useRef(isDirty);
@ -81,7 +90,7 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
return ( return (
<form <form
id={globalIDs.prompt_editor} 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)} onSubmit={event => void handleSubmit(onSubmit)(event)}
> >
<TextInput <TextInput
@ -101,11 +110,13 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
<TextArea <TextArea
id='prompt_text' id='prompt_text'
label='Содержание' // label='Содержание' //
fitContent
className='disabled:min-h-9 max-h-64'
{...register('text')} {...register('text')}
error={errors.text} error={errors.text}
disabled={isProcessing || !isMutable} disabled={isProcessing || !isMutable}
/> />
<div className='flex justify-between'>
<Controller <Controller
name='is_shared' name='is_shared'
control={control} control={control}
@ -121,6 +132,22 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
/> />
)} )}
/> />
<MiniButton
title='Сгенерировать пример запроса'
icon={<IconSample size='1.25rem' className='icon-primary' />}
onClick={() => setSampleResult(!!sampleResult ? null : generateSample(text))}
/>
</div>
<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> </form>
); );
} }

View File

@ -59,7 +59,7 @@ export function TabEditTemplate({ activeID }: TabEditTemplateProps) {
/> />
) : null} ) : null}
<FormPromptTemplate <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} isMutable={isMutable}
promptTemplate={promptTemplate} promptTemplate={promptTemplate}
toggleReset={toggleReset} 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() { 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; selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK ? selectedItems[0] : null;
const selectedSchema = selectedOperation?.result ?? 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 closePanel = usePreferencesStore(state => state.toggleShowOssSidePanel);
const sidePanelHeight = useMainHeight(); 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 { @utility cc-parsing-result {
transition-property: clip-path, padding, margin, border, height; transition-property: clip-path, padding, margin, border, height;
transition-duration: var(--duration-move); transition-duration: var(--duration-move);