mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-14 12:50:37 +03:00
F: Implementing template evaluation
This commit is contained in:
parent
ca150d3bd3
commit
d83ebfa8d5
|
@ -25,7 +25,7 @@ export function MenuAI() {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
menu.hide();
|
menu.hide();
|
||||||
showAIPrompt({});
|
showAIPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { ModalForm } from '@/components/modal';
|
|
||||||
|
|
||||||
import { type IPromptTemplate } from '../backend/types';
|
|
||||||
|
|
||||||
export interface DlgAIPromptDialogProps {
|
|
||||||
onPromptSelected?: (prompt: IPromptTemplate) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
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}'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export function DlgAIPromptDialog() {
|
|
||||||
const [selectedPrompt, setSelectedPrompt] = useState<IPromptTemplate | null>(mockPrompts[0]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalForm
|
|
||||||
header='AI Prompt Generator'
|
|
||||||
submitText='Generate Prompt'
|
|
||||||
canSubmit={!!selectedPrompt}
|
|
||||||
onSubmit={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
// Placeholder for generate logic
|
|
||||||
}}
|
|
||||||
className='w-120 px-6 cc-column'
|
|
||||||
>
|
|
||||||
<div className='mb-4'>
|
|
||||||
<label htmlFor='prompt-select' className='block mb-2 font-semibold'>
|
|
||||||
Select a prompt:
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id='prompt-select'
|
|
||||||
className='w-full border rounded p-2'
|
|
||||||
value={selectedPrompt?.id}
|
|
||||||
onChange={e => {
|
|
||||||
const prompt = mockPrompts.find(p => String(p.id) === e.target.value) || null;
|
|
||||||
setSelectedPrompt(prompt);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{mockPrompts.map(prompt => (
|
|
||||||
<option key={prompt.id} value={prompt.id}>
|
|
||||||
{prompt.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{selectedPrompt && (
|
|
||||||
<div className='mb-4'>
|
|
||||||
<div className='font-semibold'>Label:</div>
|
|
||||||
<div className='mb-2'>{selectedPrompt.label}</div>
|
|
||||||
<div className='font-semibold'>Description:</div>
|
|
||||||
<div className='mb-2'>{selectedPrompt.description}</div>
|
|
||||||
<div className='font-semibold'>Template Text:</div>
|
|
||||||
<pre className='bg-gray-100 p-2 rounded'>{selectedPrompt.text}</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ModalForm>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Suspense, useState } from 'react';
|
||||||
|
|
||||||
|
import { ComboBox } from '@/components/input/combo-box';
|
||||||
|
import { Loader } from '@/components/loader';
|
||||||
|
import { ModalView } from '@/components/modal';
|
||||||
|
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
|
||||||
|
|
||||||
|
import { useAvailableTemplatesSuspense } from '../../backend/use-available-templates';
|
||||||
|
|
||||||
|
import { TabPromptResult } from './tab-prompt-result';
|
||||||
|
import { TabPromptSelect } from './tab-prompt-select';
|
||||||
|
import { TabPromptVariables } from './tab-prompt-variables';
|
||||||
|
|
||||||
|
export const TabID = {
|
||||||
|
SELECT: 0,
|
||||||
|
RESULT: 1,
|
||||||
|
VARIABLES: 2
|
||||||
|
} as const;
|
||||||
|
type TabID = (typeof TabID)[keyof typeof TabID];
|
||||||
|
|
||||||
|
export function DlgAIPromptDialog() {
|
||||||
|
const [activeTab, setActiveTab] = useState<TabID>(TabID.SELECT);
|
||||||
|
const [selected, setSelected] = useState<number | null>(null);
|
||||||
|
const { items: prompts } = useAvailableTemplatesSuspense();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalView header='Генератор запросом LLM' className='w-100 sm:w-160 px-6'>
|
||||||
|
<Tabs selectedIndex={activeTab} onSelect={index => setActiveTab(index as TabID)}>
|
||||||
|
<TabList className='mb-3 mx-auto w-fit flex border divide-x rounded-none'>
|
||||||
|
<TabLabel label='Шаблон' />
|
||||||
|
<TabLabel label='Результат' disabled={!selected} />
|
||||||
|
<TabLabel label='Переменные' disabled={!selected} />
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<div className='h-120 flex flex-col gap-2'>
|
||||||
|
<ComboBox
|
||||||
|
id='prompt-select'
|
||||||
|
items={prompts}
|
||||||
|
value={prompts?.find(p => p.id === selected) ?? null}
|
||||||
|
onChange={item => setSelected(item?.id ?? 0)}
|
||||||
|
idFunc={item => String(item.id)}
|
||||||
|
labelValueFunc={item => item.label}
|
||||||
|
labelOptionFunc={item => item.label}
|
||||||
|
placeholder='Выберите шаблон'
|
||||||
|
className='w-full'
|
||||||
|
/>
|
||||||
|
<Suspense fallback={<Loader />}>
|
||||||
|
<TabPanel>{selected ? <TabPromptSelect promptID={selected} /> : null}</TabPanel>
|
||||||
|
<TabPanel>{selected ? <TabPromptResult promptID={selected} /> : null}</TabPanel>
|
||||||
|
<TabPanel>{selected ? <TabPromptVariables promptID={selected} /> : null}</TabPanel>
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</ModalView>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './dlg-ai-prompt';
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { MiniButton } from '@/components/control';
|
||||||
|
import { IconClone } from '@/components/icons';
|
||||||
|
import { TextArea } from '@/components/input';
|
||||||
|
import { infoMsg } from '@/utils/labels';
|
||||||
|
|
||||||
|
import { usePromptTemplateSuspense } from '../../backend/use-prompt-template';
|
||||||
|
import { PromptVariableType } from '../../models/prompting';
|
||||||
|
import { extractPromptVariables } from '../../models/prompting-api';
|
||||||
|
import { evaluatePromptVariable, useAIStore } from '../../stores/ai-context';
|
||||||
|
|
||||||
|
interface TabPromptResultProps {
|
||||||
|
promptID: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabPromptResult({ promptID }: TabPromptResultProps) {
|
||||||
|
const { promptTemplate } = usePromptTemplateSuspense(promptID);
|
||||||
|
const context = useAIStore();
|
||||||
|
const variables = useMemo(() => {
|
||||||
|
return promptTemplate ? extractPromptVariables(promptTemplate.text) : [];
|
||||||
|
}, [promptTemplate]);
|
||||||
|
|
||||||
|
const generatedMessage = (() => {
|
||||||
|
if (!promptTemplate) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
let result = promptTemplate.text;
|
||||||
|
for (const variable of variables) {
|
||||||
|
const type = Object.values(PromptVariableType).find(t => t === variable);
|
||||||
|
let value = '';
|
||||||
|
if (type) {
|
||||||
|
value = evaluatePromptVariable(type, context) ?? '';
|
||||||
|
}
|
||||||
|
result = result.replace(new RegExp(`\{\{${variable}\}\}`, 'g'), value || `${variable}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
})();
|
||||||
|
|
||||||
|
function handleCopyPrompt() {
|
||||||
|
void navigator.clipboard.writeText(generatedMessage);
|
||||||
|
toast.success(infoMsg.promptReady);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative'>
|
||||||
|
<MiniButton
|
||||||
|
title='Скопировать в буфер обмена'
|
||||||
|
className='absolute -top-23 left-0'
|
||||||
|
icon={<IconClone size='1.25rem' className='icon-green' />}
|
||||||
|
onClick={handleCopyPrompt}
|
||||||
|
disabled={!generatedMessage}
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
aria-label='Сгенерированное сообщение'
|
||||||
|
value={generatedMessage}
|
||||||
|
placeholder='Текст шаблона пуст'
|
||||||
|
disabled
|
||||||
|
fitContent
|
||||||
|
className='w-full max-h-100 min-h-12'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { TextArea } from '@/components/input';
|
||||||
|
|
||||||
|
import { usePromptTemplateSuspense } from '../../backend/use-prompt-template';
|
||||||
|
|
||||||
|
interface TabPromptSelectProps {
|
||||||
|
promptID: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabPromptSelect({ promptID }: TabPromptSelectProps) {
|
||||||
|
const { promptTemplate } = usePromptTemplateSuspense(promptID);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='cc-column'>
|
||||||
|
{promptTemplate && (
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
<TextArea
|
||||||
|
id='prompt-label'
|
||||||
|
label='Название' //
|
||||||
|
value={promptTemplate.label}
|
||||||
|
disabled
|
||||||
|
noResize
|
||||||
|
rows={1}
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
id='prompt-description'
|
||||||
|
label='Описание'
|
||||||
|
value={promptTemplate.description}
|
||||||
|
disabled
|
||||||
|
noResize
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
id='prompt-text' //
|
||||||
|
label='Текст шаблона'
|
||||||
|
value={promptTemplate.text}
|
||||||
|
disabled
|
||||||
|
noResize
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { usePromptTemplateSuspense } from '../../backend/use-prompt-template';
|
||||||
|
import { describePromptVariable } from '../../labels';
|
||||||
|
import { PromptVariableType } from '../../models/prompting';
|
||||||
|
import { extractPromptVariables } from '../../models/prompting-api';
|
||||||
|
import { useAvailableVariables } from '../../stores/use-available-variables';
|
||||||
|
|
||||||
|
interface TabPromptVariablesProps {
|
||||||
|
promptID: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabPromptVariables({ promptID }: TabPromptVariablesProps) {
|
||||||
|
const { promptTemplate } = usePromptTemplateSuspense(promptID);
|
||||||
|
const variables = extractPromptVariables(promptTemplate.text);
|
||||||
|
const availableTypes = useAvailableVariables();
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{variables.length === 0 && <li>Нет переменных</li>}
|
||||||
|
{variables.map(variable => {
|
||||||
|
const type = Object.values(PromptVariableType).find(t => t === variable);
|
||||||
|
const isAvailable = !!type && availableTypes.includes(type);
|
||||||
|
return (
|
||||||
|
<li key={variable} className={isAvailable ? 'text-green-700 font-bold' : 'text-gray-500'}>
|
||||||
|
{variable} — {type ? describePromptVariable(type) : 'Неизвестная переменная'}
|
||||||
|
{isAvailable ? ' (доступна)' : ' (нет в контексте)'}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { create } from 'zustand';
|
||||||
|
|
||||||
import { type IBlock, type IOperationSchema } from '@/features/oss/models/oss';
|
import { type IBlock, type IOperationSchema } from '@/features/oss/models/oss';
|
||||||
import { type IConstituenta, type IRSForm } from '@/features/rsform';
|
import { type IConstituenta, type IRSForm } from '@/features/rsform';
|
||||||
|
import { labelCstTypification } from '@/features/rsform/labels';
|
||||||
|
|
||||||
import { PromptVariableType } from '../models/prompting';
|
import { PromptVariableType } from '../models/prompting';
|
||||||
|
|
||||||
|
@ -55,10 +56,23 @@ export function evaluatePromptVariable(variableType: PromptVariableType, context
|
||||||
case PromptVariableType.OSS:
|
case PromptVariableType.OSS:
|
||||||
return context.currentOSS?.title ?? '';
|
return context.currentOSS?.title ?? '';
|
||||||
case PromptVariableType.SCHEMA:
|
case PromptVariableType.SCHEMA:
|
||||||
return context.currentSchema?.title ?? '';
|
return context.currentSchema ? generateSchemaPrompt(context.currentSchema) : '';
|
||||||
case PromptVariableType.BLOCK:
|
case PromptVariableType.BLOCK:
|
||||||
return context.currentBlock?.title ?? '';
|
return context.currentBlock?.title ?? '';
|
||||||
case PromptVariableType.CONSTITUENTA:
|
case PromptVariableType.CONSTITUENTA:
|
||||||
return context.currentConstituenta?.alias ?? '';
|
return context.currentConstituenta?.alias ?? '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====== Internals =========
|
||||||
|
function generateSchemaPrompt(schema: IRSForm): string {
|
||||||
|
let body = `Название концептуальной схемы: ${schema.title}\n`;
|
||||||
|
body += `[${schema.alias}] Описание: "${schema.description}"\n\n`;
|
||||||
|
body += 'Понятия:\n';
|
||||||
|
schema.items.forEach(item => {
|
||||||
|
body += `${item.alias} - "${labelCstTypification(item)}" - "${item.term_resolved}" - "${
|
||||||
|
item.definition_formal
|
||||||
|
}" - "${item.definition_resolved}" - "${item.convention}"\n`;
|
||||||
|
});
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
import { useAIStore } from '@/features/ai/stores/ai-context';
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
import { useLibrarySearchStore } from '@/features/library';
|
import { useLibrarySearchStore } from '@/features/library';
|
||||||
import { useDeleteItem } from '@/features/library/backend/use-delete-item';
|
import { useDeleteItem } from '@/features/library/backend/use-delete-item';
|
||||||
|
@ -30,6 +31,7 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
||||||
const searchLocation = useLibrarySearchStore(state => state.location);
|
const searchLocation = useLibrarySearchStore(state => state.location);
|
||||||
|
const setCurrentOSS = useAIStore(state => state.setCurrentOSS);
|
||||||
|
|
||||||
const { user } = useAuthSuspense();
|
const { user } = useAuthSuspense();
|
||||||
const { schema } = useOssSuspense({ itemID: itemID });
|
const { schema } = useOssSuspense({ itemID: itemID });
|
||||||
|
@ -50,6 +52,11 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
||||||
adminMode: adminMode
|
adminMode: adminMode
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentOSS(schema);
|
||||||
|
return () => setCurrentOSS(null);
|
||||||
|
}, [schema, setCurrentOSS]);
|
||||||
|
|
||||||
function navigateTab(tab: OssTabID) {
|
function navigateTab(tab: OssTabID) {
|
||||||
const url = urls.oss_props({
|
const url = urls.oss_props({
|
||||||
id: schema.id,
|
id: schema.id,
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
import { useAIStore } from '@/features/ai/stores/ai-context';
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
import { useLibrarySearchStore } from '@/features/library';
|
import { useLibrarySearchStore } from '@/features/library';
|
||||||
import { useDeleteItem } from '@/features/library/backend/use-delete-item';
|
import { useDeleteItem } from '@/features/library/backend/use-delete-item';
|
||||||
|
@ -68,6 +69,8 @@ export const RSEditState = ({
|
||||||
const showCreateCst = useDialogsStore(state => state.showCreateCst);
|
const showCreateCst = useDialogsStore(state => state.showCreateCst);
|
||||||
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
||||||
const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
|
const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
|
||||||
|
const setCurrentSchema = useAIStore(state => state.setCurrentSchema);
|
||||||
|
const setCurrentConstituenta = useAIStore(state => state.setCurrentConstituenta);
|
||||||
|
|
||||||
useAdjustRole({
|
useAdjustRole({
|
||||||
isOwner: isOwned,
|
isOwner: isOwned,
|
||||||
|
@ -76,6 +79,16 @@ export const RSEditState = ({
|
||||||
adminMode: adminMode
|
adminMode: adminMode
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentSchema(schema);
|
||||||
|
return () => setCurrentSchema(null);
|
||||||
|
}, [schema, setCurrentSchema]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentConstituenta(activeCst);
|
||||||
|
return () => setCurrentConstituenta(null);
|
||||||
|
}, [activeCst, setCurrentConstituenta]);
|
||||||
|
|
||||||
function handleSetFocus(newValue: IConstituenta | null) {
|
function handleSetFocus(newValue: IConstituenta | null) {
|
||||||
setFocusCst(newValue);
|
setFocusCst(newValue);
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
import { type DlgAIPromptDialogProps } from '@/features/ai/dialogs/dlg-ai-prompt';
|
|
||||||
import { type DlgCreatePromptTemplateProps } from '@/features/ai/dialogs/dlg-create-prompt-template';
|
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';
|
||||||
|
@ -115,7 +114,7 @@ interface DialogsStore {
|
||||||
showEditCst: (props: DlgEditCstProps) => void;
|
showEditCst: (props: DlgEditCstProps) => void;
|
||||||
showCreateSchema: (props: DlgCreateSchemaProps) => void;
|
showCreateSchema: (props: DlgCreateSchemaProps) => void;
|
||||||
showImportSchema: (props: DlgImportSchemaProps) => void;
|
showImportSchema: (props: DlgImportSchemaProps) => void;
|
||||||
showAIPrompt: (props: DlgAIPromptDialogProps) => void;
|
showAIPrompt: () => void;
|
||||||
showCreatePromptTemplate: (props: DlgCreatePromptTemplateProps) => void;
|
showCreatePromptTemplate: (props: DlgCreatePromptTemplateProps) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,6 +158,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: () => set({ active: DialogType.AI_PROMPT, props: null }),
|
||||||
showCreatePromptTemplate: props => set({ active: DialogType.CREATE_PROMPT_TEMPLATE, props: props })
|
showCreatePromptTemplate: props => set({ active: DialogType.CREATE_PROMPT_TEMPLATE, props: props })
|
||||||
}));
|
}));
|
||||||
|
|
Loading…
Reference in New Issue
Block a user