Open kirin-ri opened 8 months ago
import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react';
interface Props {
onClick: any;
side: 'left' | 'right';
}
export const CloseSidebarButton = ({ onClick, side }: Props) => {
return (
<>
<button
className={`fixed top-5 ${
side === 'right' ? 'right-[270px]' : 'left-[270px]'
} z-50 h-7 w-7 hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:${
side === 'right' ? 'right-[270px]' : 'left-[270px]'
} sm:h-8 sm:w-8 sm:text-neutral-700`}
onClick={onClick}
>
{side === 'right' ? <IconArrowBarRight /> : <IconArrowBarLeft />}
</button>
<div
onClick={onClick}
className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden"
></div>
</>
);
};
export const OpenSidebarButton = ({ onClick, side }: Props) => {
return (
<button
className={`fixed top-2.5 ${
side === 'right' ? 'right-2' : 'left-2'
} z-50 h-7 w-7 text-white hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:${
side === 'right' ? 'right-2' : 'left-2'
} sm:h-8 sm:w-8 sm:text-neutral-700`}
onClick={onClick}
>
{side === 'right' ? <IconArrowBarLeft /> : <IconArrowBarRight />}
</button>
);
};
import { IconFolderPlus, IconMistOff, IconPlus, IconFile } from '@tabler/icons-react';
import { ReactNode, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react';
import {
CloseSidebarButton,
OpenSidebarButton,
} from './components/OpenCloseButton';
interface Reference {
id: string;
title: string;
description: string;
}
interface Props<T> {
isOpen: boolean;
side: 'left' | 'right';
items: T[];
toggleOpen: () => void;
}
const ReferenceSidebar = <T,>({
isOpen,
side,
items,
toggleOpen,
}: Props<T>) => {
const { t } = useTranslation('promptbar');
const [hoveredItemId, setHoveredItemId] = useState<string | null>(null);
const allowDrop = (e: any) => {
e.preventDefault();
};
const highlightDrop = (e: any) => {
e.target.style.background = '#343541';
};
const removeHighlight = (e: any) => {
e.target.style.background = 'none';
};
const itemsample = [
{id:'1',title:'実績発電原子炉に係る新規制基準の考え方について.pdf',description:'…見直しを行うために「発電用軽水型原子炉の新規制基準に関する検討チーム」(以下「検討チーム」という。)を組織し、発電用軽水型原子炉の新規制基準策定のための検討を開始した。検討チーム…'},
{id:'2',title:'実績発電原子炉に係る新規制基準の考え方について.pdf',description:'…見直しを行うために「発電用軽水型原子炉の新規制基準に関する検討チーム」(以下「検討チーム」という。)を組織し、発電用軽水型原子炉の新規制基準策定のための検討を開始した。検討チーム…'},
{id:'3',title:'実績発電原子炉に係る新規制基準の考え方について.pdf',description:'…見直しを行うために「発電用軽水型原子炉の新規制基準に関する検討チーム」(以下「検討チーム」という。)を組織し、発電用軽水型原子炉の新規制基準策定のための検討を開始した。検討チーム…'},
]
const headerStyle = {
display: 'flex',
justiftContent: 'space-between',
fontSize: '15px',
marginBottom: '15px',
width: '100%',
}
const titleIcon = {
display: 'flex',
marginRight: '8px',
}
const listItemStyle = (isHovered: boolean) => ({
marginBottom: '10px',
border: '1px solid white',
padding: '10px',
borderRadius: '5px',
backgroundColor: isHovered ? '#0056b3' : '#343541', // Change background color on hover
transition: 'background-color 0.3s',
});
const listTitleStyle = {
marginBottom: '10px',
}
return isOpen ? (
<div>
<div className={`fixed top-0 ${side}-0 z-40 flex h-full w-[400px] flex-none flex-col space-y-2 bg-[#202123] p-2 text-[14px] transition-all sm:relative sm:top-0`}>
<div style={headerStyle}>
<div style={titleIcon}>
<IconFile />
参照リスト
<CloseSidebarButton onClick={toggleOpen} side={side} />
</div>
</div>
<ul>
{itemsample.map((item) => (
<a href="test" target="_blank" rel="noopener noreferrer">
{/* <li
key={item.id}
className="p-2 bg-[#343541] rounded-md text-white"
style={listItemStyle}
> */}
<li
style={listItemStyle(item.id === hoveredItemId)}
onMouseEnter={() => setHoveredItemId(item.id)}
onMouseLeave={() => setHoveredItemId(null)}
>
<h3 style={listTitleStyle}>{item.title}</h3>
<p>{item.description}</p>
</li>
</a>
))}
</ul>
</div>
</div>
) : (
<OpenSidebarButton onClick={toggleOpen} side={side} />
);
};
export default ReferenceSidebar;
<div style={headerStyle}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<IconFile style={{ marginRight: '8px' }} />
参照リスト
</div>
<div onClick={toggleOpen} style={{ cursor: 'pointer' }}>
{side === 'right' ? <IconArrowBarLeft /> : <IconArrowBarRight />}
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '10px' }}>
{/* 关闭按钮,放在最上方,并向右对齐 */}
<div onClick={toggleOpen} style={{ cursor: 'pointer' }}>
{side === 'right' ? <IconArrowBarLeft /> : <IconArrowBarRight />}
</div>
</div>
<div style={{ marginBottom: '20px', display: 'flex', alignItems: 'center', paddingLeft: '10px' }}>
{/* 参照リスト标题 */}
<IconFile style={{ marginRight: '8px' }} />
参照リスト
</div>
{/* 使用flex布局使标题和关闭按钮水平对齐,并在容器最右侧显示关闭按钮 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', padding: '0 10px' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<IconFile style={{ marginRight: '8px' }} />
参照リスト
</div>
{/* 将关闭按钮直接放在这里,而不是额外的div中 */}
<div onClick={toggleOpen} style={{ cursor: 'pointer' }}>
{side === 'right' ? <IconArrowBarLeft /> : <IconArrowBarRight />}
</div>
</div>
import { IconFolderPlus, IconMistOff, IconPlus, IconFile } from '@tabler/icons-react';
import { ReactNode, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react';
import {
CloseSidebarButton,
OpenSidebarButton,
} from './components/OpenCloseButton';
interface Reference {
id: string;
title: string;
description: string;
}
interface Props<T> {
isOpen: boolean;
side: 'left' | 'right';
items: T[];
toggleOpen: () => void;
}
const ReferenceSidebar = <T,>({
isOpen,
side,
items,
toggleOpen,
}: Props<T>) => {
const { t } = useTranslation('promptbar');
const [hoveredItemId, setHoveredItemId] = useState<string | null>(null);
const allowDrop = (e: any) => {
e.preventDefault();
};
const highlightDrop = (e: any) => {
e.target.style.background = '#343541';
};
const removeHighlight = (e: any) => {
e.target.style.background = 'none';
};
const itemsample = [
{id:'1',title:'実績発電原子炉に係る新規制基準の考え方について.pdf',description:'…見直しを行うために「発電用軽水型原子炉の新規制基準に関する検討チーム」(以下「検討チーム」という。)を組織し、発電用軽水型原子炉の新規制基準策定のための検討を開始した。検討チーム…'},
{id:'2',title:'実績発電原子炉に係る新規制基準の考え方について.pdf',description:'…見直しを行うために「発電用軽水型原子炉の新規制基準に関する検討チーム」(以下「検討チーム」という。)を組織し、発電用軽水型原子炉の新規制基準策定のための検討を開始した。検討チーム…'},
{id:'3',title:'実績発電原子炉に係る新規制基準の考え方について.pdf',description:'…見直しを行うために「発電用軽水型原子炉の新規制基準に関する検討チーム」(以下「検討チーム」という。)を組織し、発電用軽水型原子炉の新規制基準策定のための検討を開始した。検討チーム…'},
{id:'4',title:'実績発電原子炉に係る新規制基準の考え方について.pdf',description:'…見直しを行うために「発電用軽水型原子炉の新規制基準に関する検討チーム」(以下「検討チーム」という。)を組織し、発電用軽水型原子炉の新規制基準策定のための検討を開始した。検討チーム…'},
{id:'5',title:'実績発電原子炉に係る新規制基準の考え方について.pdf',description:'…見直しを行うために「発電用軽水型原子炉の新規制基準に関する検討チーム」(以下「検討チーム」という。)を組織し、発電用軽水型原子炉の新規制基準策定のための検討を開始した。検討チーム…'},
]
const headerStyle = {
display: 'flex',
justiftContent: 'space-between',
alignItems:'center',
fontSize: '15px',
marginBottom: '15px',
width: '100%',
}
const titleIcon = {
display: 'flex',
marginRight: '8px',
}
const listItemStyle = (isHovered: boolean) => ({
marginBottom: '10px',
border: '1px solid white',
padding: '20px',
borderRadius: '5px',
backgroundColor: isHovered ? '#0056b3' : '#343541', // Change background color on hover
transition: 'background-color 0.3s',
maxWidth: '350px',
});
const listTitleStyle = {
marginBottom: '10px',
}
return isOpen ? (
<div>
<div className={`fixed top-0 ${side}-0 z-40 flex h-full w-[400px] flex-none flex-col space-y-2 bg-[#202123] p-2 text-[14px] transition-all sm:relative sm:top-0`}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', padding: '0 10px' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<IconFile style={{ marginRight: '8px' }} />
参照リスト
</div>
<div onClick={toggleOpen} style={{ cursor: 'pointer' }}>
{side === 'right' ? <IconArrowBarRight /> : <IconArrowBarLeft />}
</div>
</div>
<div style={{overflowY:'auto',flex:1,padding:'0 10px'}}>
<ul>
{itemsample.map((item) => (
<a href="test" target="_blank" rel="noopener noreferrer">
<li
style={listItemStyle(item.id === hoveredItemId)}
onMouseEnter={() => setHoveredItemId(item.id)}
onMouseLeave={() => setHoveredItemId(null)}
>
<h3 style={listTitleStyle}>{item.title}</h3>
<p>{item.description}</p>
</li>
</a>
))}
</ul>
</div>
</div>
</div>
) : (
<OpenSidebarButton onClick={toggleOpen} side={side} />
);
};
export default ReferenceSidebar;
<Sidebar<Conversation>
side={'left'}
isOpen={showChatbar}
addItemButtonTitle={t('New chat')}
itemComponent={<Conversations conversations={filteredConversations} />}
folderComponent={<ChatFolders searchTerm={searchTerm} />}
items={filteredConversations}
searchTerm={searchTerm}
handleSearchTerm={(searchTerm: string) =>
chatDispatch({ field: 'searchTerm', value: searchTerm })
}
toggleOpen={handleToggleChatbar}
handleCreateItem={handleNewConversation}
handleCreateFolder={() => handleCreateFolder(t('New folder'), 'chat')}
handleDrop={handleDrop}
footerComponent={<ChatbarSettings />}
/>
<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}
/>
import { IconFolderPlus, IconMistOff, IconPlus } from '@tabler/icons-react';
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import {
CloseSidebarButton,
OpenSidebarButton,
} from './components/OpenCloseButton';
import Search from '../Search';
import Promptbar from '../Promptbar';
interface Props<T> {
isOpen: boolean;
addItemButtonTitle: string;
side: 'left' | 'right';
items: T[];
itemComponent: ReactNode;
folderComponent: ReactNode;
footerComponent?: ReactNode;
searchTerm: string;
handleSearchTerm: (searchTerm: string) => void;
toggleOpen: () => void;
handleCreateItem: () => void;
handleCreateFolder: () => void;
handleDrop: (e: any) => void;
}
const Sidebar = <T,>({
isOpen,
addItemButtonTitle,
side,
items,
itemComponent,
folderComponent,
footerComponent,
searchTerm,
handleSearchTerm,
toggleOpen,
handleCreateItem,
handleCreateFolder,
handleDrop,
}: Props<T>) => {
const { t } = useTranslation('promptbar');
const allowDrop = (e: any) => {
e.preventDefault();
};
const highlightDrop = (e: any) => {
e.target.style.background = '#343541';
};
const removeHighlight = (e: any) => {
e.target.style.background = 'none';
};
return isOpen ? (
<div>
<div
className={`fixed top-0 ${side}-0 z-40 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`}
>
<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={() => {
handleCreateItem();
handleSearchTerm('');
}}
>
<IconPlus size={16} />
{addItemButtonTitle}
</button>
<button
className="ml-2 flex flex-shrink-0 cursor-pointer items-center gap-3 rounded-md border border-white/20 p-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
onClick={handleCreateFolder}
>
<IconFolderPlus size={16} />
</button>
</div>
<Search
placeholder={t('Search...') || ''}
searchTerm={searchTerm}
onSearch={handleSearchTerm}
/>
<div className="flex-grow overflow-auto">
{items?.length > 0 && (
<div className="flex border-b border-white/20 pb-2">
{folderComponent}
</div>
)}
{items?.length > 0 ? (
<div
className="pt-2"
onDrop={handleDrop}
onDragOver={allowDrop}
onDragEnter={highlightDrop}
onDragLeave={removeHighlight}
>
{itemComponent}
</div>
) : (
<div className="mt-8 select-none text-center text-white opacity-50">
<IconMistOff className="mx-auto mb-3" />
<span className="text-[14px] leading-normal">
{t('No data.')}
</span>
</div>
)}
</div>
{footerComponent}
</div>
<CloseSidebarButton onClick={toggleOpen} side={side} />
</div>
) : (
<OpenSidebarButton onClick={toggleOpen} side={side} />
);
};
export default Sidebar;
<Sidebar
isOpen={showSidebar}
toggleOpen={handleToggleSidebar}
side={'left'}
>
{showChatbar && (
<>
<div>
<h2>{t('Chat')}</h2>
<Conversations conversations={filteredConversations} />
<ChatFolders searchTerm={searchTerm} />
<button onClick={handleNewConversation}>{t('New chat')}</button>
</div>
<div>
<h2>{t('Prompt')}</h2>
<Prompts prompts={filteredPrompts.filter((prompt) => !prompt.folderId)} />
<PromptFolders />
<button onClick={handleCreatePrompt}>{t('New prompt')}</button>
</div>
</>
)}
</Sidebar>
<Sidebar
isOpen={showSidebar}
toggleOpen={handleToggleSidebar}
side={'left'}
addItemButtonTitle={showChatbar ? t('New chat') : t('New prompt')}
searchTerm={searchTerm}
handleSearchTerm={(searchTerm: string) =>
showChatbar
? chatDispatch({ field: 'searchTerm', value: searchTerm })
: promptDispatch({ field: 'searchTerm', value: searchTerm })
}
handleCreateItem={showChatbar ? handleNewConversation : handleCreatePrompt}
handleCreateFolder={() =>
showChatbar
? handleCreateFolder(t('New folder'), 'chat')
: handleCreateFolder(t('New folder'), 'prompt')
}
handleDrop={handleDrop}
>
<div className="flex flex-col h-full">
{showChatbar ? (
<>
<div>
<h2>{t('Chat')}</h2>
<Conversations conversations={filteredConversations} />
<ChatFolders searchTerm={searchTerm} />
</div>
<ChatbarSettings />
</>
) : (
<div>
<h2>{t('Prompt')}</h2>
<Prompts
prompts={filteredPrompts.filter((prompt) => !prompt.folderId)}
/>
<PromptFolders />
</div>
)}
</div>
</Sidebar>
import { useCallback, useContext, useEffect } from 'react';
import { useTranslation } from 'next-i18next';
import { useCreateReducer } from '@/hooks/useCreateReducer';
import { DEFAULT_SYSTEM_PROMPT, DEFAULT_TEMPERATURE } from '@/utils/app/const';
import { saveConversation, saveConversations } from '@/utils/app/conversation';
import { saveFolders } from '@/utils/app/folders';
import { exportData, importData } from '@/utils/app/importExport';
import { Conversation } from '@/types/chat';
import { LatestExportFormat, SupportedExportFormats } from '@/types/export';
import { OpenAIModels } from '@/types/openai';
import { PluginKey } from '@/types/plugin';
import HomeContext from '@/pages/api/home/home.context';
import { ChatFolders } from './components/ChatFolders';
import { ChatbarSettings } from './components/ChatbarSettings';
import { Conversations } from './components/Conversations';
import Sidebar from '../Sidebar';
import ChatbarContext from './Chatbar.context';
import { ChatbarInitialState, initialState } from './Chatbar.state';
import { v4 as uuidv4 } from 'uuid';
export const Chatbar = () => {
const { t } = useTranslation('sidebar');
const chatBarContextValue = useCreateReducer<ChatbarInitialState>({
initialState,
});
const {
state: { conversations, showChatbar, defaultModelId, folders, pluginKeys },
dispatch: homeDispatch,
handleCreateFolder,
handleNewConversation,
handleUpdateConversation,
} = useContext(HomeContext);
const {
state: { searchTerm, filteredConversations },
dispatch: chatDispatch,
} = chatBarContextValue;
const handleApiKeyChange = useCallback(
(apiKey: string) => {
homeDispatch({ field: 'apiKey', value: apiKey });
localStorage.setItem('apiKey', apiKey);
},
[homeDispatch],
);
const handlePluginKeyChange = (pluginKey: PluginKey) => {
if (pluginKeys.some((key) => key.pluginId === pluginKey.pluginId)) {
const updatedPluginKeys = pluginKeys.map((key) => {
if (key.pluginId === pluginKey.pluginId) {
return pluginKey;
}
return key;
});
homeDispatch({ field: 'pluginKeys', value: updatedPluginKeys });
localStorage.setItem('pluginKeys', JSON.stringify(updatedPluginKeys));
} else {
homeDispatch({ field: 'pluginKeys', value: [...pluginKeys, pluginKey] });
localStorage.setItem(
'pluginKeys',
JSON.stringify([...pluginKeys, pluginKey]),
);
}
};
const handleClearPluginKey = (pluginKey: PluginKey) => {
const updatedPluginKeys = pluginKeys.filter(
(key) => key.pluginId !== pluginKey.pluginId,
);
if (updatedPluginKeys.length === 0) {
homeDispatch({ field: 'pluginKeys', value: [] });
localStorage.removeItem('pluginKeys');
return;
}
homeDispatch({ field: 'pluginKeys', value: updatedPluginKeys });
localStorage.setItem('pluginKeys', JSON.stringify(updatedPluginKeys));
};
const handleExportData = () => {
exportData();
};
const handleImportConversations = (data: SupportedExportFormats) => {
const { history, folders, prompts }: LatestExportFormat = importData(data);
homeDispatch({ field: 'conversations', value: history });
homeDispatch({
field: 'selectedConversation',
value: history[history.length - 1],
});
homeDispatch({ field: 'folders', value: folders });
homeDispatch({ field: 'prompts', value: prompts });
window.location.reload();
};
const handleClearConversations = () => {
defaultModelId &&
homeDispatch({
field: 'selectedConversation',
value: {
id: uuidv4(),
name: t('New Conversation'),
messages: [],
model: OpenAIModels[defaultModelId],
prompt: DEFAULT_SYSTEM_PROMPT,
temperature: DEFAULT_TEMPERATURE,
folderId: null,
},
});
homeDispatch({ field: 'conversations', value: [] });
localStorage.removeItem('conversationHistory');
localStorage.removeItem('selectedConversation');
const updatedFolders = folders.filter((f) => f.type !== 'chat');
homeDispatch({ field: 'folders', value: updatedFolders });
saveFolders(updatedFolders);
};
const handleDeleteConversation = (conversation: Conversation) => {
const updatedConversations = conversations.filter(
(c) => c.id !== conversation.id,
);
homeDispatch({ field: 'conversations', value: updatedConversations });
chatDispatch({ field: 'searchTerm', value: '' });
saveConversations(updatedConversations);
if (updatedConversations.length > 0) {
homeDispatch({
field: 'selectedConversation',
value: updatedConversations[updatedConversations.length - 1],
});
saveConversation(updatedConversations[updatedConversations.length - 1]);
} else {
defaultModelId &&
homeDispatch({
field: 'selectedConversation',
value: {
id: uuidv4(),
name: t('New Conversation'),
messages: [],
model: OpenAIModels[defaultModelId],
prompt: DEFAULT_SYSTEM_PROMPT,
temperature: DEFAULT_TEMPERATURE,
folderId: null,
},
});
localStorage.removeItem('selectedConversation');
}
};
const handleToggleChatbar = () => {
homeDispatch({ field: 'showChatbar', value: !showChatbar });
localStorage.setItem('showChatbar', JSON.stringify(!showChatbar));
};
const handleDrop = (e: any) => {
if (e.dataTransfer) {
const conversation = JSON.parse(e.dataTransfer.getData('conversation'));
handleUpdateConversation(conversation, { key: 'folderId', value: 0 });
chatDispatch({ field: 'searchTerm', value: '' });
e.target.style.background = 'none';
}
};
useEffect(() => {
if (searchTerm) {
chatDispatch({
field: 'filteredConversations',
value: conversations.filter((conversation) => {
const searchable =
conversation.name.toLocaleLowerCase() +
' ' +
conversation.messages.map((message) => message.content).join(' ');
return searchable.toLowerCase().includes(searchTerm.toLowerCase());
}),
});
} else {
chatDispatch({
field: 'filteredConversations',
value: conversations,
});
}
}, [searchTerm, conversations]);
return (
<ChatbarContext.Provider
value={{
...chatBarContextValue,
handleDeleteConversation,
handleClearConversations,
handleImportConversations,
handleExportData,
handlePluginKeyChange,
handleClearPluginKey,
handleApiKeyChange,
}}
>
<Sidebar<Conversation>
side={'left'}
isOpen={showChatbar}
addItemButtonTitle={t('New chat')}
itemComponent={<Conversations conversations={filteredConversations} />}
folderComponent={<ChatFolders searchTerm={searchTerm} />}
items={filteredConversations}
searchTerm={searchTerm}
handleSearchTerm={(searchTerm: string) =>
chatDispatch({ field: 'searchTerm', value: searchTerm })
}
toggleOpen={handleToggleChatbar}
handleCreateItem={handleNewConversation}
handleCreateFolder={() => handleCreateFolder(t('New folder'), 'chat')}
handleDrop={handleDrop}
footerComponent={<ChatbarSettings />}
/>
</ChatbarContext.Provider>
);
};
``