mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-14 12:50:37 +03:00
F: Introducing AI UI pt1
This commit is contained in:
parent
f7fe5b1cdb
commit
6c39e5130f
|
@ -140,6 +140,9 @@ const DlgCreateSchema = React.lazy(() =>
|
||||||
const DlgImportSchema = React.lazy(() =>
|
const DlgImportSchema = React.lazy(() =>
|
||||||
import('@/features/oss/dialogs/dlg-import-schema').then(module => ({ default: module.DlgImportSchema }))
|
import('@/features/oss/dialogs/dlg-import-schema').then(module => ({ default: module.DlgImportSchema }))
|
||||||
);
|
);
|
||||||
|
const DlgAIPromptDialog = React.lazy(() =>
|
||||||
|
import('@/features/ai/dialogs/dlg-ai-prompt').then(module => ({ default: module.DlgAIPromptDialog }))
|
||||||
|
);
|
||||||
|
|
||||||
export const GlobalDialogs = () => {
|
export const GlobalDialogs = () => {
|
||||||
const active = useDialogsStore(state => state.active);
|
const active = useDialogsStore(state => state.active);
|
||||||
|
@ -208,5 +211,7 @@ export const GlobalDialogs = () => {
|
||||||
return <DlgCreateSchema />;
|
return <DlgCreateSchema />;
|
||||||
case DialogType.IMPORT_SCHEMA:
|
case DialogType.IMPORT_SCHEMA:
|
||||||
return <DlgImportSchema />;
|
return <DlgImportSchema />;
|
||||||
|
case DialogType.AI_PROMPT:
|
||||||
|
return <DlgAIPromptDialog />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
59
rsconcept/frontend/src/app/navigation/menu-ai.tsx
Normal file
59
rsconcept/frontend/src/app/navigation/menu-ai.tsx
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { useAuth } from '@/features/auth/backend/use-auth';
|
||||||
|
|
||||||
|
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
|
||||||
|
import { IconAssistant, IconChat, IconTemplates } from '@/components/icons';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { urls } from '../urls';
|
||||||
|
|
||||||
|
import { NavigationButton } from './navigation-button';
|
||||||
|
import { useConceptNavigation } from './navigation-context';
|
||||||
|
|
||||||
|
export function MenuAI() {
|
||||||
|
const router = useConceptNavigation();
|
||||||
|
const menu = useDropdown();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const showAIPrompt = useDialogsStore(state => state.showAIPrompt);
|
||||||
|
|
||||||
|
function navigateTemplates(event: React.MouseEvent<Element>) {
|
||||||
|
menu.hide();
|
||||||
|
router.push({ path: urls.prompt_templates, newTab: event.ctrlKey || event.metaKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreatePrompt(event: React.MouseEvent<Element>) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
menu.hide();
|
||||||
|
showAIPrompt({});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={menu.ref} onBlur={menu.handleBlur} className='flex items-center justify-start relative h-full'>
|
||||||
|
<NavigationButton
|
||||||
|
title='ИИ помощник' //
|
||||||
|
hideTitle={menu.isOpen}
|
||||||
|
aria-expanded={menu.isOpen}
|
||||||
|
aria-controls={globalIDs.ai_dropdown}
|
||||||
|
icon={<IconAssistant size='1.5rem' />}
|
||||||
|
onClick={menu.toggle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dropdown id={globalIDs.ai_dropdown} className='min-w-[12ch] max-w-48' stretchLeft isOpen={menu.isOpen}>
|
||||||
|
<DropdownButton
|
||||||
|
text='Запрос'
|
||||||
|
title='Создать запрос'
|
||||||
|
icon={<IconChat size='1rem' />}
|
||||||
|
onClick={handleCreatePrompt}
|
||||||
|
/>
|
||||||
|
<DropdownButton
|
||||||
|
text='Шаблоны'
|
||||||
|
title={user?.is_staff ? 'Шаблоны запросов' : 'Доступно только зарегистрированным пользователям'}
|
||||||
|
icon={<IconTemplates size='1rem' />}
|
||||||
|
onClick={navigateTemplates}
|
||||||
|
disabled={!user?.is_staff}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -9,11 +9,11 @@ import { useConceptNavigation } from './navigation-context';
|
||||||
import { UserButton } from './user-button';
|
import { UserButton } from './user-button';
|
||||||
import { UserDropdown } from './user-dropdown';
|
import { UserDropdown } from './user-dropdown';
|
||||||
|
|
||||||
export function UserMenu() {
|
export function MenuUser() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} onBlur={menu.handleBlur} className='flex items-center justify-start relative h-full pr-2'>
|
<div ref={menu.ref} onBlur={menu.handleBlur} className='flex items-center justify-start relative h-full'>
|
||||||
<Suspense fallback={<Loader circular scale={1.5} />}>
|
<Suspense fallback={<Loader circular scale={1.5} />}>
|
||||||
<UserButton
|
<UserButton
|
||||||
onLogin={() => router.push({ path: urls.login, force: true })}
|
onLogin={() => router.push({ path: urls.login, force: true })}
|
|
@ -24,7 +24,7 @@ export function NavigationButton({ icon, title, hideTitle, className, style, onC
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{icon ? icon : null}
|
{icon ? icon : null}
|
||||||
{text ? <span className='hidden sm:inline'>{text}</span> : null}
|
{text ? <span className='hidden md:inline'>{text}</span> : null}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,11 @@ import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { urls } from '../urls';
|
import { urls } from '../urls';
|
||||||
|
|
||||||
import { Logo } from './logo';
|
import { Logo } from './logo';
|
||||||
|
import { MenuAI } from './menu-ai';
|
||||||
|
import { MenuUser } from './menu-user';
|
||||||
import { NavigationButton } from './navigation-button';
|
import { NavigationButton } from './navigation-button';
|
||||||
import { useConceptNavigation } from './navigation-context';
|
import { useConceptNavigation } from './navigation-context';
|
||||||
import { ToggleNavigation } from './toggle-navigation';
|
import { ToggleNavigation } from './toggle-navigation';
|
||||||
import { UserMenu } from './user-menu';
|
|
||||||
|
|
||||||
export function Navigation() {
|
export function Navigation() {
|
||||||
const { push } = useConceptNavigation();
|
const { push } = useConceptNavigation();
|
||||||
|
@ -41,11 +42,13 @@ export function Navigation() {
|
||||||
<div className='flex items-center mr-auto cursor-pointer' onClick={!size.isSmall ? navigateHome : undefined}>
|
<div className='flex items-center mr-auto cursor-pointer' onClick={!size.isSmall ? navigateHome : undefined}>
|
||||||
<Logo />
|
<Logo />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center pr-2'>
|
||||||
<NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} />
|
<NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} />
|
||||||
<NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} />
|
<NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} />
|
||||||
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
|
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
|
||||||
<UserMenu />
|
|
||||||
|
<MenuAI />
|
||||||
|
<MenuUser />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -19,7 +19,8 @@ export const routes = {
|
||||||
rsforms: 'rsforms',
|
rsforms: 'rsforms',
|
||||||
oss: 'oss',
|
oss: 'oss',
|
||||||
icons: 'icons',
|
icons: 'icons',
|
||||||
database_schema: 'database-schema'
|
database_schema: 'database-schema',
|
||||||
|
prompt_templates: 'prompt-templates'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/** Internal navigation URLs. */
|
/** Internal navigation URLs. */
|
||||||
|
@ -37,6 +38,7 @@ export const urls = {
|
||||||
library: `/${routes.library}`,
|
library: `/${routes.library}`,
|
||||||
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}`,
|
||||||
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) =>
|
||||||
|
|
|
@ -63,6 +63,8 @@ export { PiFileCsv as IconCSV } from 'react-icons/pi';
|
||||||
|
|
||||||
// ==== User status =======
|
// ==== User status =======
|
||||||
export { LuCircleUserRound as IconUser } from 'react-icons/lu';
|
export { LuCircleUserRound as IconUser } from 'react-icons/lu';
|
||||||
|
export { FaUserAstronaut as IconAssistant } from 'react-icons/fa6';
|
||||||
|
export { IoChatbubblesOutline as IconChat } from 'react-icons/io5';
|
||||||
export { FaCircleUser as IconUser2 } from 'react-icons/fa6';
|
export { FaCircleUser as IconUser2 } from 'react-icons/fa6';
|
||||||
export { TbUserEdit as IconEditor } from 'react-icons/tb';
|
export { TbUserEdit as IconEditor } from 'react-icons/tb';
|
||||||
export { TbUserSearch as IconUserSearch } from 'react-icons/tb';
|
export { TbUserSearch as IconUserSearch } from 'react-icons/tb';
|
||||||
|
|
8
rsconcept/frontend/src/features/ai/backend/types.ts
Normal file
8
rsconcept/frontend/src/features/ai/backend/types.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/** Represents AI prompt. */
|
||||||
|
export interface IPromptTemplate {
|
||||||
|
id: number;
|
||||||
|
owner: number | null;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
text: string;
|
||||||
|
}
|
74
rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt.tsx
Normal file
74
rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt.tsx
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
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',
|
||||||
|
description: 'A simple greeting prompt.',
|
||||||
|
text: 'Hello, ${name}! How can I assist you today?'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
owner: null,
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
1
rsconcept/frontend/src/features/ai/dialogs/index.tsx
Normal file
1
rsconcept/frontend/src/features/ai/dialogs/index.tsx
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { DlgAIPromptDialog } from './dlg-ai-prompt';
|
29
rsconcept/frontend/src/features/ai/models/prompting.ts
Normal file
29
rsconcept/frontend/src/features/ai/models/prompting.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/** Represents prompt variable type. */
|
||||||
|
export const PromptVariableType = {
|
||||||
|
BLOCK: 'block',
|
||||||
|
BLOCK_TITLE: 'block.title',
|
||||||
|
BLOCK_DESCRIPTION: 'block.description',
|
||||||
|
BLOCK_CONTENTS: 'block.contents',
|
||||||
|
|
||||||
|
OSS: 'oss',
|
||||||
|
OSS_CONTENTS: 'oss.contents',
|
||||||
|
OSS_ALIAS: 'oss.alias',
|
||||||
|
OSS_TITLE: 'oss.title',
|
||||||
|
OSS_DESCRIPTION: 'oss.description',
|
||||||
|
|
||||||
|
SCHEMA: 'schema',
|
||||||
|
SCHEMA_ALIAS: 'schema.alias',
|
||||||
|
SCHEMA_TITLE: 'schema.title',
|
||||||
|
SCHEMA_DESCRIPTION: 'schema.description',
|
||||||
|
SCHEMA_THESAURUS: 'schema.thesaurus',
|
||||||
|
SCHEMA_GRAPH: 'schema.graph',
|
||||||
|
SCHEMA_TYPE_GRAPH: 'schema.type-graph',
|
||||||
|
|
||||||
|
CONSTITUENTA: 'constituent',
|
||||||
|
CONSTITUENTA_ALIAS: 'constituent.alias',
|
||||||
|
CONSTITUENTA_CONVENTION: 'constituent.convention',
|
||||||
|
CONSTITUENTA_DEFINITION: 'constituent.definition',
|
||||||
|
CONSTITUENTA_DEFINITION_FORMAL: 'constituent.definition-formal',
|
||||||
|
CONSTITUENTA_EXPRESSION_TREE: 'constituent.expression-tree'
|
||||||
|
} as const;
|
||||||
|
export type PromptVariableType = (typeof PromptVariableType)[keyof typeof PromptVariableType];
|
|
@ -1,5 +1,6 @@
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { type DlgAIPromptDialogProps } from '@/features/ai/dialogs/dlg-ai-prompt';
|
||||||
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';
|
||||||
|
@ -67,7 +68,8 @@ export const DialogType = {
|
||||||
GRAPH_PARAMETERS: 27,
|
GRAPH_PARAMETERS: 27,
|
||||||
SHOW_TERM_GRAPH: 28,
|
SHOW_TERM_GRAPH: 28,
|
||||||
CREATE_SCHEMA: 29,
|
CREATE_SCHEMA: 29,
|
||||||
IMPORT_SCHEMA: 30
|
IMPORT_SCHEMA: 30,
|
||||||
|
AI_PROMPT: 31
|
||||||
} as const;
|
} as const;
|
||||||
export type DialogType = (typeof DialogType)[keyof typeof DialogType];
|
export type DialogType = (typeof DialogType)[keyof typeof DialogType];
|
||||||
|
|
||||||
|
@ -110,6 +112,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDialogsStore = create<DialogsStore>()(set => ({
|
export const useDialogsStore = create<DialogsStore>()(set => ({
|
||||||
|
@ -151,5 +154,6 @@ export const useDialogsStore = create<DialogsStore>()(set => ({
|
||||||
showUploadRSForm: props => set({ active: DialogType.UPLOAD_RSFORM, props: props }),
|
showUploadRSForm: props => set({ active: DialogType.UPLOAD_RSFORM, props: props }),
|
||||||
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 })
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -79,7 +79,8 @@ export const globalIDs = {
|
||||||
library_item_editor: 'library_item_editor',
|
library_item_editor: 'library_item_editor',
|
||||||
constituenta_editor: 'constituenta_editor',
|
constituenta_editor: 'constituenta_editor',
|
||||||
graph_schemas: 'graph_schemas_tooltip',
|
graph_schemas: 'graph_schemas_tooltip',
|
||||||
user_dropdown: 'user_dropdown'
|
user_dropdown: 'user_dropdown',
|
||||||
|
ai_dropdown: 'ai_dropdown'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/** Prefixes for generating unique keys for lists. */
|
/** Prefixes for generating unique keys for lists. */
|
||||||
|
|
Loading…
Reference in New Issue
Block a user