MAJOR REFACTOR (#494)
* move index to home folder, create state and context files and barrell folder * Sanity Check Commit: reducer added to home.tsx manual QA all working * WIP: promptBar * fix missing json parse on folders and prompts * split context and add promptbar context * add context to nested prompt componets and componetize Folder componet * remove log * Create buttons folder and componetize sidebar action button * tidy up prompt handlers * componetized sidebar * added back chatbar componet to left side sidebar * monster commit: Componetized the common code between chatbar and promptbar into new componet Sidebar and added context to both bars * add useFetch service * added prettier import sort to keep imports ordered and easier to indentify * added react query and useFetch to work with RQ * added apiService, errorService and reactQuery * add callback and tidy up error service * refactor chat and child componets to useContext * fix extra calls and bad calls to mel endpoint * minor import cleanup --------- Co-authored-by: jc.durbin <jc.durbin@ardanis.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { Dispatch, createContext } from 'react';
|
||||
|
||||
import { ActionType } from '@/hooks/useCreateReducer';
|
||||
|
||||
import { Prompt } from '@/types/prompt';
|
||||
|
||||
import { PromptbarInitialState } from './Promptbar.state';
|
||||
|
||||
export interface PromptbarContextProps {
|
||||
state: PromptbarInitialState;
|
||||
dispatch: Dispatch<ActionType<PromptbarInitialState>>;
|
||||
handleCreatePrompt: () => void;
|
||||
handleDeletePrompt: (prompt: Prompt) => void;
|
||||
handleUpdatePrompt: (prompt: Prompt) => void;
|
||||
}
|
||||
|
||||
const PromptbarContext = createContext<PromptbarContextProps>(undefined!);
|
||||
|
||||
export default PromptbarContext;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Prompt } from '@/types/prompt';
|
||||
|
||||
export interface PromptbarInitialState {
|
||||
searchTerm: string;
|
||||
filteredPrompts: Prompt[];
|
||||
}
|
||||
|
||||
export const initialState: PromptbarInitialState = {
|
||||
searchTerm: '',
|
||||
filteredPrompts: [],
|
||||
};
|
||||
+110
-128
@@ -1,50 +1,85 @@
|
||||
import { Folder } from '@/types/folder';
|
||||
import { Prompt } from '@/types/prompt';
|
||||
import {
|
||||
IconFolderPlus,
|
||||
IconMistOff,
|
||||
IconPlus,
|
||||
} from '@tabler/icons-react';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PromptFolders } from '../Folders/Prompt/PromptFolders';
|
||||
import { Search } from '../Sidebar/Search';
|
||||
import { PromptbarSettings } from './PromptbarSettings';
|
||||
import { Prompts } from './Prompts';
|
||||
|
||||
interface Props {
|
||||
prompts: Prompt[];
|
||||
folders: Folder[];
|
||||
onCreateFolder: (name: string) => void;
|
||||
onDeleteFolder: (folderId: string) => void;
|
||||
onUpdateFolder: (folderId: string, name: string) => void;
|
||||
onCreatePrompt: () => void;
|
||||
onUpdatePrompt: (prompt: Prompt) => void;
|
||||
onDeletePrompt: (prompt: Prompt) => void;
|
||||
}
|
||||
import { useCreateReducer } from '@/hooks/useCreateReducer';
|
||||
|
||||
export const Promptbar: FC<Props> = ({
|
||||
folders,
|
||||
prompts,
|
||||
onCreateFolder,
|
||||
onDeleteFolder,
|
||||
onUpdateFolder,
|
||||
onCreatePrompt,
|
||||
onUpdatePrompt,
|
||||
onDeletePrompt,
|
||||
}) => {
|
||||
import { savePrompts } from '@/utils/app/prompts';
|
||||
|
||||
import { OpenAIModels } from '@/types/openai';
|
||||
import { Prompt } from '@/types/prompt';
|
||||
|
||||
import HomeContext from '@/pages/api/home/home.context';
|
||||
|
||||
import { PromptFolders } from './components/PromptFolders';
|
||||
import { PromptbarSettings } from './components/PromptbarSettings';
|
||||
import { Prompts } from './components/Prompts';
|
||||
|
||||
import Sidebar from '../Sidebar';
|
||||
import PromptbarContext from './PromptBar.context';
|
||||
import { PromptbarInitialState, initialState } from './Promptbar.state';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const Promptbar = () => {
|
||||
const { t } = useTranslation('promptbar');
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const [filteredPrompts, setFilteredPrompts] = useState<Prompt[]>(prompts);
|
||||
|
||||
const handleUpdatePrompt = (prompt: Prompt) => {
|
||||
onUpdatePrompt(prompt);
|
||||
setSearchTerm('');
|
||||
const promptBarContextValue = useCreateReducer<PromptbarInitialState>({
|
||||
initialState,
|
||||
});
|
||||
|
||||
const {
|
||||
state: { prompts, defaultModelId, showPromptbar },
|
||||
dispatch: homeDispatch,
|
||||
handleCreateFolder,
|
||||
} = useContext(HomeContext);
|
||||
|
||||
const {
|
||||
state: { searchTerm, filteredPrompts },
|
||||
dispatch: promptDispatch,
|
||||
} = promptBarContextValue;
|
||||
|
||||
const handleTogglePromptbar = () => {
|
||||
homeDispatch({ field: 'showPromptbar', value: !showPromptbar });
|
||||
localStorage.setItem('showPromptbar', JSON.stringify(!showPromptbar));
|
||||
};
|
||||
|
||||
const handleCreatePrompt = () => {
|
||||
if (defaultModelId) {
|
||||
const newPrompt: Prompt = {
|
||||
id: uuidv4(),
|
||||
name: `Prompt ${prompts.length + 1}`,
|
||||
description: '',
|
||||
content: '',
|
||||
model: OpenAIModels[defaultModelId],
|
||||
folderId: null,
|
||||
};
|
||||
|
||||
const updatedPrompts = [...prompts, newPrompt];
|
||||
|
||||
homeDispatch({ field: 'prompts', value: updatedPrompts });
|
||||
|
||||
savePrompts(updatedPrompts);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePrompt = (prompt: Prompt) => {
|
||||
onDeletePrompt(prompt);
|
||||
setSearchTerm('');
|
||||
const updatedPrompts = prompts.filter((p) => p.id !== prompt.id);
|
||||
|
||||
homeDispatch({ field: 'prompts', value: updatedPrompts });
|
||||
savePrompts(updatedPrompts);
|
||||
};
|
||||
|
||||
const handleUpdatePrompt = (prompt: Prompt) => {
|
||||
const updatedPrompts = prompts.map((p) => {
|
||||
if (p.id === prompt.id) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
return p;
|
||||
});
|
||||
homeDispatch({ field: 'prompts', value: updatedPrompts });
|
||||
|
||||
savePrompts(updatedPrompts);
|
||||
};
|
||||
|
||||
const handleDrop = (e: any) => {
|
||||
@@ -56,28 +91,17 @@ export const Promptbar: FC<Props> = ({
|
||||
folderId: e.target.dataset.folderId,
|
||||
};
|
||||
|
||||
onUpdatePrompt(updatedPrompt);
|
||||
handleUpdatePrompt(updatedPrompt);
|
||||
|
||||
e.target.style.background = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
const allowDrop = (e: any) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const highlightDrop = (e: any) => {
|
||||
e.target.style.background = '#343541';
|
||||
};
|
||||
|
||||
const removeHighlight = (e: any) => {
|
||||
e.target.style.background = 'none';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTerm) {
|
||||
setFilteredPrompts(
|
||||
prompts.filter((prompt) => {
|
||||
promptDispatch({
|
||||
field: 'filteredPrompts',
|
||||
value: prompts.filter((prompt) => {
|
||||
const searchable =
|
||||
prompt.name.toLowerCase() +
|
||||
' ' +
|
||||
@@ -86,85 +110,43 @@ export const Promptbar: FC<Props> = ({
|
||||
prompt.content.toLowerCase();
|
||||
return searchable.includes(searchTerm.toLowerCase());
|
||||
}),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setFilteredPrompts(prompts);
|
||||
promptDispatch({ field: 'filteredPrompts', value: prompts });
|
||||
}
|
||||
}, [searchTerm, prompts]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed top-0 right-0 z-50 flex h-full w-[260px] flex-none flex-col space-y-2 bg-[#202123] p-2 text-[14px] transition-all sm:relative sm:top-0`}
|
||||
<PromptbarContext.Provider
|
||||
value={{
|
||||
...promptBarContextValue,
|
||||
handleCreatePrompt,
|
||||
handleDeletePrompt,
|
||||
handleUpdatePrompt,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="text-sidebar flex w-[190px] flex-shrink-0 cursor-pointer select-none items-center gap-3 rounded-md border border-white/20 p-3 text-white transition-colors duration-200 hover:bg-gray-500/10"
|
||||
onClick={() => {
|
||||
onCreatePrompt();
|
||||
setSearchTerm('');
|
||||
}}
|
||||
>
|
||||
<IconPlus size={16} />
|
||||
{t('New prompt')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="flex items-center flex-shrink-0 gap-3 p-3 ml-2 text-sm text-white transition-colors duration-200 border rounded-md cursor-pointer border-white/20 hover:bg-gray-500/10"
|
||||
onClick={() => onCreateFolder(t('New folder'))}
|
||||
>
|
||||
<IconFolderPlus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{prompts.length > 1 && (
|
||||
<Search
|
||||
placeholder={t('Search prompts...') || ''}
|
||||
searchTerm={searchTerm}
|
||||
onSearch={setSearchTerm}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-grow overflow-auto">
|
||||
{folders.length > 0 && (
|
||||
<div className="flex pb-2 border-b border-white/20">
|
||||
<PromptFolders
|
||||
searchTerm={searchTerm}
|
||||
prompts={filteredPrompts}
|
||||
folders={folders}
|
||||
onUpdateFolder={onUpdateFolder}
|
||||
onDeleteFolder={onDeleteFolder}
|
||||
// prompt props
|
||||
onDeletePrompt={handleDeletePrompt}
|
||||
onUpdatePrompt={handleUpdatePrompt}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{prompts.length > 0 ? (
|
||||
<div
|
||||
className="pt-2"
|
||||
onDrop={(e) => handleDrop(e)}
|
||||
onDragOver={allowDrop}
|
||||
onDragEnter={highlightDrop}
|
||||
onDragLeave={removeHighlight}
|
||||
>
|
||||
<Prompts
|
||||
prompts={filteredPrompts.filter((prompt) => !prompt.folderId)}
|
||||
onUpdatePrompt={handleUpdatePrompt}
|
||||
onDeletePrompt={handleDeletePrompt}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-8 text-center text-white opacity-50 select-none">
|
||||
<IconMistOff className="mx-auto mb-3" />
|
||||
<span className="text-[14px] leading-normal">
|
||||
{t('No prompts.')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PromptbarSettings />
|
||||
</div>
|
||||
<Sidebar<Prompt>
|
||||
side={'right'}
|
||||
isOpen={showPromptbar}
|
||||
addItemButtonTitle={t('New prompt')}
|
||||
itemComponent={
|
||||
<Prompts
|
||||
prompts={filteredPrompts.filter((prompt) => !prompt.folderId)}
|
||||
/>
|
||||
}
|
||||
folderComponent={<PromptFolders />}
|
||||
items={filteredPrompts}
|
||||
searchTerm={searchTerm}
|
||||
handleSearchTerm={(searchTerm: string) =>
|
||||
promptDispatch({ field: 'searchTerm', value: searchTerm })
|
||||
}
|
||||
toggleOpen={handleTogglePromptbar}
|
||||
handleCreateItem={handleCreatePrompt}
|
||||
handleCreateFolder={() => handleCreateFolder(t('New folder'), 'prompt')}
|
||||
handleDrop={handleDrop}
|
||||
/>
|
||||
</PromptbarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Promptbar;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Prompt } from '@/types/prompt';
|
||||
import { FC } from 'react';
|
||||
import { PromptComponent } from './Prompt';
|
||||
|
||||
interface Props {
|
||||
prompts: Prompt[];
|
||||
onUpdatePrompt: (prompt: Prompt) => void;
|
||||
onDeletePrompt: (prompt: Prompt) => void;
|
||||
}
|
||||
|
||||
export const Prompts: FC<Props> = ({
|
||||
prompts,
|
||||
onUpdatePrompt,
|
||||
onDeletePrompt,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
{prompts
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((prompt, index) => (
|
||||
<PromptComponent
|
||||
key={index}
|
||||
prompt={prompt}
|
||||
onUpdatePrompt={onUpdatePrompt}
|
||||
onDeletePrompt={onDeletePrompt}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,29 +1,66 @@
|
||||
import { Prompt } from '@/types/prompt';
|
||||
import {
|
||||
IconBulbFilled,
|
||||
IconCheck,
|
||||
IconTrash,
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
import { DragEvent, FC, useEffect, useState } from 'react';
|
||||
import {
|
||||
DragEvent,
|
||||
MouseEventHandler,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { Prompt } from '@/types/prompt';
|
||||
|
||||
import SidebarActionButton from '@/components/Buttons/SidebarActionButton';
|
||||
|
||||
import PromptbarContext from '../PromptBar.context';
|
||||
import { PromptModal } from './PromptModal';
|
||||
|
||||
interface Props {
|
||||
prompt: Prompt;
|
||||
onUpdatePrompt: (prompt: Prompt) => void;
|
||||
onDeletePrompt: (prompt: Prompt) => void;
|
||||
}
|
||||
|
||||
export const PromptComponent: FC<Props> = ({
|
||||
prompt,
|
||||
onUpdatePrompt,
|
||||
onDeletePrompt,
|
||||
}) => {
|
||||
export const PromptComponent = ({ prompt }: Props) => {
|
||||
const {
|
||||
dispatch: promptDispatch,
|
||||
handleUpdatePrompt,
|
||||
handleDeletePrompt,
|
||||
} = useContext(PromptbarContext);
|
||||
|
||||
const [showModal, setShowModal] = useState<boolean>(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
|
||||
const handleUpdate = (prompt: Prompt) => {
|
||||
handleUpdatePrompt(prompt);
|
||||
promptDispatch({ field: 'searchTerm', value: '' });
|
||||
};
|
||||
|
||||
const handleDelete: MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (isDeleting) {
|
||||
handleDeletePrompt(prompt);
|
||||
promptDispatch({ field: 'searchTerm', value: '' });
|
||||
}
|
||||
|
||||
setIsDeleting(false);
|
||||
};
|
||||
|
||||
const handleCancelDelete: MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsDeleting(false);
|
||||
};
|
||||
|
||||
const handleOpenDeleteModal: MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsDeleting(true);
|
||||
};
|
||||
|
||||
const handleDragStart = (e: DragEvent<HTMLButtonElement>, prompt: Prompt) => {
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.setData('prompt', JSON.stringify(prompt));
|
||||
@@ -63,44 +100,21 @@ export const PromptComponent: FC<Props> = ({
|
||||
|
||||
{(isDeleting || isRenaming) && (
|
||||
<div className="absolute right-1 z-10 flex text-gray-300">
|
||||
<button
|
||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (isDeleting) {
|
||||
onDeletePrompt(prompt);
|
||||
}
|
||||
|
||||
setIsDeleting(false);
|
||||
}}
|
||||
>
|
||||
<SidebarActionButton handleClick={handleDelete}>
|
||||
<IconCheck size={18} />
|
||||
</button>
|
||||
</SidebarActionButton>
|
||||
|
||||
<button
|
||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDeleting(false);
|
||||
}}
|
||||
>
|
||||
<SidebarActionButton handleClick={handleCancelDelete}>
|
||||
<IconX size={18} />
|
||||
</button>
|
||||
</SidebarActionButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isDeleting && !isRenaming && (
|
||||
<div className="absolute right-1 z-10 flex text-gray-300">
|
||||
<button
|
||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDeleting(true);
|
||||
}}
|
||||
>
|
||||
<SidebarActionButton handleClick={handleOpenDeleteModal}>
|
||||
<IconTrash size={18} />
|
||||
</button>
|
||||
</SidebarActionButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -108,7 +122,7 @@ export const PromptComponent: FC<Props> = ({
|
||||
<PromptModal
|
||||
prompt={prompt}
|
||||
onClose={() => setShowModal(false)}
|
||||
onUpdatePrompt={onUpdatePrompt}
|
||||
onUpdatePrompt={handleUpdate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FolderInterface } from '@/types/folder';
|
||||
|
||||
import HomeContext from '@/pages/api/home/home.context';
|
||||
|
||||
import Folder from '@/components/Folder';
|
||||
import { PromptComponent } from '@/components/Promptbar/components/Prompt';
|
||||
|
||||
import PromptbarContext from '../PromptBar.context';
|
||||
|
||||
export const PromptFolders = () => {
|
||||
const {
|
||||
state: { folders },
|
||||
} = useContext(HomeContext);
|
||||
|
||||
const {
|
||||
state: { searchTerm, filteredPrompts },
|
||||
handleUpdatePrompt,
|
||||
} = useContext(PromptbarContext);
|
||||
|
||||
const handleDrop = (e: any, folder: FolderInterface) => {
|
||||
if (e.dataTransfer) {
|
||||
const prompt = JSON.parse(e.dataTransfer.getData('prompt'));
|
||||
|
||||
const updatedPrompt = {
|
||||
...prompt,
|
||||
folderId: folder.id,
|
||||
};
|
||||
|
||||
handleUpdatePrompt(updatedPrompt);
|
||||
}
|
||||
};
|
||||
|
||||
const PromptFolders = (currentFolder: FolderInterface) =>
|
||||
filteredPrompts
|
||||
.filter((p) => p.folderId)
|
||||
.map((prompt, index) => {
|
||||
if (prompt.folderId === currentFolder.id) {
|
||||
return (
|
||||
<div key={index} className="ml-5 gap-2 border-l pl-2">
|
||||
<PromptComponent prompt={prompt} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col pt-2">
|
||||
{folders
|
||||
.filter((folder) => folder.type === 'prompt')
|
||||
.map((folder, index) => (
|
||||
<Folder
|
||||
key={index}
|
||||
searchTerm={searchTerm}
|
||||
currentFolder={folder}
|
||||
handleDrop={handleDrop}
|
||||
folderComponent={PromptFolders(folder)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
import { Prompt } from '@/types/prompt';
|
||||
import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { Prompt } from '@/types/prompt';
|
||||
|
||||
interface Props {
|
||||
prompt: Prompt;
|
||||
onClose: () => void;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { FC } from "react";
|
||||
import { FC } from 'react';
|
||||
|
||||
interface Props {}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
import { Prompt } from '@/types/prompt';
|
||||
|
||||
import { PromptComponent } from './Prompt';
|
||||
|
||||
interface Props {
|
||||
prompts: Prompt[];
|
||||
}
|
||||
|
||||
export const Prompts: FC<Props> = ({ prompts }) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
{prompts
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((prompt, index) => (
|
||||
<PromptComponent key={index} prompt={prompt} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Promptbar';
|
||||
Reference in New Issue
Block a user