From 6c39e5130fc451defc47db26114c95999e2417e7 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Fri, 11 Jul 2025 13:34:00 +0300 Subject: [PATCH] F: Introducing AI UI pt1 --- rsconcept/frontend/src/app/global-dialogs.tsx | 5 ++ .../frontend/src/app/navigation/menu-ai.tsx | 59 +++++++++++++++ .../{user-menu.tsx => menu-user.tsx} | 4 +- .../src/app/navigation/navigation-button.tsx | 2 +- .../src/app/navigation/navigation.tsx | 9 ++- rsconcept/frontend/src/app/urls.ts | 4 +- rsconcept/frontend/src/components/icons.tsx | 2 + .../frontend/src/features/ai/backend/types.ts | 8 ++ .../src/features/ai/dialogs/dlg-ai-prompt.tsx | 74 +++++++++++++++++++ .../src/features/ai/dialogs/index.tsx | 1 + .../src/features/ai/models/prompting-api.ts | 0 .../src/features/ai/models/prompting.ts | 29 ++++++++ rsconcept/frontend/src/stores/dialogs.ts | 8 +- rsconcept/frontend/src/utils/constants.ts | 3 +- 14 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 rsconcept/frontend/src/app/navigation/menu-ai.tsx rename rsconcept/frontend/src/app/navigation/{user-menu.tsx => menu-user.tsx} (91%) create mode 100644 rsconcept/frontend/src/features/ai/backend/types.ts create mode 100644 rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt.tsx create mode 100644 rsconcept/frontend/src/features/ai/dialogs/index.tsx create mode 100644 rsconcept/frontend/src/features/ai/models/prompting-api.ts create mode 100644 rsconcept/frontend/src/features/ai/models/prompting.ts diff --git a/rsconcept/frontend/src/app/global-dialogs.tsx b/rsconcept/frontend/src/app/global-dialogs.tsx index a6bbbff6..ffc79178 100644 --- a/rsconcept/frontend/src/app/global-dialogs.tsx +++ b/rsconcept/frontend/src/app/global-dialogs.tsx @@ -140,6 +140,9 @@ const DlgCreateSchema = React.lazy(() => const DlgImportSchema = React.lazy(() => 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 = () => { const active = useDialogsStore(state => state.active); @@ -208,5 +211,7 @@ export const GlobalDialogs = () => { return ; case DialogType.IMPORT_SCHEMA: return ; + case DialogType.AI_PROMPT: + return ; } }; diff --git a/rsconcept/frontend/src/app/navigation/menu-ai.tsx b/rsconcept/frontend/src/app/navigation/menu-ai.tsx new file mode 100644 index 00000000..3ddd5cf9 --- /dev/null +++ b/rsconcept/frontend/src/app/navigation/menu-ai.tsx @@ -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) { + menu.hide(); + router.push({ path: urls.prompt_templates, newTab: event.ctrlKey || event.metaKey }); + } + + function handleCreatePrompt(event: React.MouseEvent) { + event.preventDefault(); + event.stopPropagation(); + menu.hide(); + showAIPrompt({}); + } + + return ( +
+ } + onClick={menu.toggle} + /> + + + } + onClick={handleCreatePrompt} + /> + } + onClick={navigateTemplates} + disabled={!user?.is_staff} + /> + +
+ ); +} diff --git a/rsconcept/frontend/src/app/navigation/user-menu.tsx b/rsconcept/frontend/src/app/navigation/menu-user.tsx similarity index 91% rename from rsconcept/frontend/src/app/navigation/user-menu.tsx rename to rsconcept/frontend/src/app/navigation/menu-user.tsx index 49db1775..d5c07aeb 100644 --- a/rsconcept/frontend/src/app/navigation/user-menu.tsx +++ b/rsconcept/frontend/src/app/navigation/menu-user.tsx @@ -9,11 +9,11 @@ import { useConceptNavigation } from './navigation-context'; import { UserButton } from './user-button'; import { UserDropdown } from './user-dropdown'; -export function UserMenu() { +export function MenuUser() { const router = useConceptNavigation(); const menu = useDropdown(); return ( -
+
}> router.push({ path: urls.login, force: true })} diff --git a/rsconcept/frontend/src/app/navigation/navigation-button.tsx b/rsconcept/frontend/src/app/navigation/navigation-button.tsx index 572084ae..03eb6cad 100644 --- a/rsconcept/frontend/src/app/navigation/navigation-button.tsx +++ b/rsconcept/frontend/src/app/navigation/navigation-button.tsx @@ -24,7 +24,7 @@ export function NavigationButton({ icon, title, hideTitle, className, style, onC style={style} > {icon ? icon : null} - {text ? {text} : null} + {text ? {text} : null} ); } diff --git a/rsconcept/frontend/src/app/navigation/navigation.tsx b/rsconcept/frontend/src/app/navigation/navigation.tsx index 7acb6a1d..f49f186c 100644 --- a/rsconcept/frontend/src/app/navigation/navigation.tsx +++ b/rsconcept/frontend/src/app/navigation/navigation.tsx @@ -8,10 +8,11 @@ import { useDialogsStore } from '@/stores/dialogs'; import { urls } from '../urls'; import { Logo } from './logo'; +import { MenuAI } from './menu-ai'; +import { MenuUser } from './menu-user'; import { NavigationButton } from './navigation-button'; import { useConceptNavigation } from './navigation-context'; import { ToggleNavigation } from './toggle-navigation'; -import { UserMenu } from './user-menu'; export function Navigation() { const { push } = useConceptNavigation(); @@ -41,11 +42,13 @@ export function Navigation() {
-
+
} onClick={navigateCreateNew} /> } onClick={navigateLibrary} /> } onClick={navigateHelp} /> - + + +
diff --git a/rsconcept/frontend/src/app/urls.ts b/rsconcept/frontend/src/app/urls.ts index d5ce6e11..52452fc7 100644 --- a/rsconcept/frontend/src/app/urls.ts +++ b/rsconcept/frontend/src/app/urls.ts @@ -19,7 +19,8 @@ export const routes = { rsforms: 'rsforms', oss: 'oss', icons: 'icons', - database_schema: 'database-schema' + database_schema: 'database-schema', + prompt_templates: 'prompt-templates' } as const; /** Internal navigation URLs. */ @@ -37,6 +38,7 @@ export const urls = { library: `/${routes.library}`, library_filter: (strategy: string) => `/library?filter=${strategy}`, create_schema: `/${routes.create_schema}`, + prompt_templates: `/${routes.prompt_templates}`, manuals: `/${routes.manuals}`, help_topic: (topic: string) => `/manuals?topic=${topic}`, schema: (id: number | string, version?: number | string) => diff --git a/rsconcept/frontend/src/components/icons.tsx b/rsconcept/frontend/src/components/icons.tsx index 4e8bd635..b19cbcbf 100644 --- a/rsconcept/frontend/src/components/icons.tsx +++ b/rsconcept/frontend/src/components/icons.tsx @@ -63,6 +63,8 @@ export { PiFileCsv as IconCSV } from 'react-icons/pi'; // ==== User status ======= 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 { TbUserEdit as IconEditor } from 'react-icons/tb'; export { TbUserSearch as IconUserSearch } from 'react-icons/tb'; diff --git a/rsconcept/frontend/src/features/ai/backend/types.ts b/rsconcept/frontend/src/features/ai/backend/types.ts new file mode 100644 index 00000000..1bfda4b9 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/backend/types.ts @@ -0,0 +1,8 @@ +/** Represents AI prompt. */ +export interface IPromptTemplate { + id: number; + owner: number | null; + label: string; + description: string; + text: string; +} diff --git a/rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt.tsx b/rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt.tsx new file mode 100644 index 00000000..a46ea50d --- /dev/null +++ b/rsconcept/frontend/src/features/ai/dialogs/dlg-ai-prompt.tsx @@ -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(mockPrompts[0]); + + return ( + { + e.preventDefault(); + // Placeholder for generate logic + }} + className='w-120 px-6 cc-column' + > +
+ + +
+ {selectedPrompt && ( +
+
Label:
+
{selectedPrompt.label}
+
Description:
+
{selectedPrompt.description}
+
Template Text:
+
{selectedPrompt.text}
+
+ )} +
+ ); +} diff --git a/rsconcept/frontend/src/features/ai/dialogs/index.tsx b/rsconcept/frontend/src/features/ai/dialogs/index.tsx new file mode 100644 index 00000000..8974e3aa --- /dev/null +++ b/rsconcept/frontend/src/features/ai/dialogs/index.tsx @@ -0,0 +1 @@ +export { DlgAIPromptDialog } from './dlg-ai-prompt'; diff --git a/rsconcept/frontend/src/features/ai/models/prompting-api.ts b/rsconcept/frontend/src/features/ai/models/prompting-api.ts new file mode 100644 index 00000000..e69de29b diff --git a/rsconcept/frontend/src/features/ai/models/prompting.ts b/rsconcept/frontend/src/features/ai/models/prompting.ts new file mode 100644 index 00000000..5c4d07e7 --- /dev/null +++ b/rsconcept/frontend/src/features/ai/models/prompting.ts @@ -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]; diff --git a/rsconcept/frontend/src/stores/dialogs.ts b/rsconcept/frontend/src/stores/dialogs.ts index 9722705a..edee5811 100644 --- a/rsconcept/frontend/src/stores/dialogs.ts +++ b/rsconcept/frontend/src/stores/dialogs.ts @@ -1,5 +1,6 @@ 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 DlgCloneLibraryItemProps } from '@/features/library/dialogs/dlg-clone-library-item'; import { type DlgCreateVersionProps } from '@/features/library/dialogs/dlg-create-version'; @@ -67,7 +68,8 @@ export const DialogType = { GRAPH_PARAMETERS: 27, SHOW_TERM_GRAPH: 28, CREATE_SCHEMA: 29, - IMPORT_SCHEMA: 30 + IMPORT_SCHEMA: 30, + AI_PROMPT: 31 } as const; export type DialogType = (typeof DialogType)[keyof typeof DialogType]; @@ -110,6 +112,7 @@ interface DialogsStore { showEditCst: (props: DlgEditCstProps) => void; showCreateSchema: (props: DlgCreateSchemaProps) => void; showImportSchema: (props: DlgImportSchemaProps) => void; + showAIPrompt: (props: DlgAIPromptDialogProps) => void; } export const useDialogsStore = create()(set => ({ @@ -151,5 +154,6 @@ export const useDialogsStore = create()(set => ({ showUploadRSForm: props => set({ active: DialogType.UPLOAD_RSFORM, props: props }), showEditCst: props => set({ active: DialogType.EDIT_CONSTITUENTA, 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 }) })); diff --git a/rsconcept/frontend/src/utils/constants.ts b/rsconcept/frontend/src/utils/constants.ts index 16b543d0..ca0933f2 100644 --- a/rsconcept/frontend/src/utils/constants.ts +++ b/rsconcept/frontend/src/utils/constants.ts @@ -79,7 +79,8 @@ export const globalIDs = { library_item_editor: 'library_item_editor', constituenta_editor: 'constituenta_editor', graph_schemas: 'graph_schemas_tooltip', - user_dropdown: 'user_dropdown' + user_dropdown: 'user_dropdown', + ai_dropdown: 'ai_dropdown' } as const; /** Prefixes for generating unique keys for lists. */