knowledge-assistant.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import type { Dispatch, SetStateAction, RefObject } from 'react';
  2. import {
  3. useCallback, useMemo, useState, useEffect,
  4. } from 'react';
  5. import { useForm, type UseFormReturn } from 'react-hook-form';
  6. import { useTranslation } from 'react-i18next';
  7. import { UncontrolledTooltip } from 'reactstrap';
  8. import { apiv3Post } from '~/client/util/apiv3-client';
  9. import { SseMessageSchema, type SseMessage } from '~/features/openai/interfaces/knowledge-assistant/sse-schemas';
  10. import { handleIfSuccessfullyParsed } from '~/features/openai/utils/handle-if-successfully-parsed';
  11. import type { MessageLog } from '../../interfaces/message';
  12. import type { IThreadRelationHasId } from '../../interfaces/thread-relation';
  13. import { ThreadType } from '../../interfaces/thread-relation';
  14. import { AiAssistantChatInitialView } from '../components/AiAssistant/AiAssistantSidebar/AiAssistantChatInitialView';
  15. import { MessageCard, type MessageCardRole } from '../components/AiAssistant/AiAssistantSidebar/MessageCard';
  16. import { useAiAssistantSidebar } from '../stores/ai-assistant';
  17. import { useSWRMUTxMessages } from '../stores/message';
  18. import { useSWRMUTxThreads } from '../stores/thread';
  19. interface CreateThread {
  20. (aiAssistantId: string, initialUserMessage: string): Promise<IThreadRelationHasId>;
  21. }
  22. interface PostMessage {
  23. (aiAssistantId: string, threadId: string, formData: FormData): Promise<Response>;
  24. }
  25. interface ProcessMessage {
  26. (data: unknown, handler: {
  27. onMessage: (data: SseMessage) => void}
  28. ): void;
  29. }
  30. interface GenerateMessageCard {
  31. (role: MessageCardRole, children: string): JSX.Element;
  32. }
  33. interface GenerateSummaryModeSwitch {
  34. (isGenerating: boolean): JSX.Element
  35. }
  36. export interface FormData {
  37. input: string
  38. summaryMode?: boolean
  39. }
  40. type UseKnowledgeAssistant = () => {
  41. createThread: CreateThread
  42. postMessage: PostMessage
  43. processMessage: ProcessMessage
  44. form: UseFormReturn<FormData>
  45. resetForm: () => void
  46. // Views
  47. initialView: JSX.Element
  48. generateMessageCard: GenerateMessageCard
  49. generateSummaryModeSwitch: GenerateSummaryModeSwitch
  50. headerIcon: JSX.Element
  51. headerText: JSX.Element
  52. placeHolder: string
  53. }
  54. export const useKnowledgeAssistant: UseKnowledgeAssistant = () => {
  55. // Hooks
  56. const { data: aiAssistantSidebarData } = useAiAssistantSidebar();
  57. const { aiAssistantData } = aiAssistantSidebarData ?? {};
  58. const { threadData } = aiAssistantSidebarData ?? {};
  59. const { trigger: mutateThreadData } = useSWRMUTxThreads(aiAssistantData?._id);
  60. const { t } = useTranslation();
  61. const form = useForm<FormData>({
  62. defaultValues: {
  63. input: '',
  64. summaryMode: true,
  65. },
  66. });
  67. // States
  68. const [currentThreadTitle, setCurrentThreadId] = useState(threadData?.title);
  69. // Functions
  70. const resetForm = useCallback(() => {
  71. const summaryMode = form.getValues('summaryMode');
  72. form.reset({ input: '', summaryMode });
  73. }, [form]);
  74. const createThread: CreateThread = useCallback(async(aiAssistantId, initialUserMessage) => {
  75. const response = await apiv3Post<IThreadRelationHasId>('/openai/thread', {
  76. type: ThreadType.KNOWLEDGE,
  77. aiAssistantId,
  78. initialUserMessage,
  79. });
  80. const thread = response.data;
  81. setCurrentThreadId(thread.title);
  82. // No need to await because data is not used
  83. mutateThreadData();
  84. return thread;
  85. }, [mutateThreadData]);
  86. const postMessage: PostMessage = useCallback(async(aiAssistantId, threadId, formData) => {
  87. const response = await fetch('/_api/v3/openai/message', {
  88. method: 'POST',
  89. headers: { 'Content-Type': 'application/json' },
  90. body: JSON.stringify({
  91. aiAssistantId,
  92. threadId,
  93. userMessage: formData.input,
  94. summaryMode: form.getValues('summaryMode'),
  95. }),
  96. });
  97. return response;
  98. }, [form]);
  99. const processMessage: ProcessMessage = useCallback((data, handler) => {
  100. handleIfSuccessfullyParsed(data, SseMessageSchema, (data: SseMessage) => {
  101. handler.onMessage(data);
  102. });
  103. }, []);
  104. // Views
  105. const headerIcon = useMemo(() => {
  106. return <span className="growi-custom-icons growi-ai-chat-icon me-3 fs-4">ai_assistant</span>;
  107. }, []);
  108. const headerText = useMemo(() => {
  109. return <>{currentThreadTitle ?? aiAssistantData?.name}</>;
  110. }, [aiAssistantData?.name, currentThreadTitle]);
  111. const placeHolder = useMemo(() => { return 'sidebar_ai_assistant.knowledge_assistant_placeholder' }, []);
  112. const initialView = useMemo(() => {
  113. if (aiAssistantSidebarData?.aiAssistantData == null) {
  114. return <></>;
  115. }
  116. return (
  117. <AiAssistantChatInitialView
  118. description={aiAssistantSidebarData.aiAssistantData.description}
  119. pagePathPatterns={aiAssistantSidebarData.aiAssistantData.pagePathPatterns}
  120. />
  121. );
  122. }, [aiAssistantSidebarData?.aiAssistantData]);
  123. const generateMessageCard: GenerateMessageCard = useCallback((role, children) => {
  124. return (
  125. <MessageCard
  126. role={role}
  127. >
  128. {children}
  129. </MessageCard>
  130. );
  131. }, []);
  132. const generateSummaryModeSwitch: GenerateSummaryModeSwitch = useCallback((isGenerating) => {
  133. return (
  134. <div className="form-check form-switch">
  135. <input
  136. id="swSummaryMode"
  137. type="checkbox"
  138. role="switch"
  139. className="form-check-input"
  140. {...form.register('summaryMode')}
  141. disabled={form.formState.isSubmitting || isGenerating}
  142. />
  143. <label className="form-check-label" htmlFor="swSummaryMode">
  144. {t('sidebar_ai_assistant.summary_mode_label')}
  145. </label>
  146. {/* Help */}
  147. <a
  148. id="tooltipForHelpOfSummaryMode"
  149. role="button"
  150. className="ms-1"
  151. >
  152. <span className="material-symbols-outlined fs-6" style={{ lineHeight: 'unset' }}>help</span>
  153. </a>
  154. <UncontrolledTooltip
  155. target="tooltipForHelpOfSummaryMode"
  156. >
  157. {t('sidebar_ai_assistant.summary_mode_help')}
  158. </UncontrolledTooltip>
  159. </div>
  160. );
  161. }, [form, t]);
  162. return {
  163. createThread,
  164. postMessage,
  165. processMessage,
  166. form,
  167. resetForm,
  168. // Views
  169. initialView,
  170. generateMessageCard,
  171. generateSummaryModeSwitch,
  172. headerIcon,
  173. headerText,
  174. placeHolder,
  175. };
  176. };
  177. export const useFetchAndSetMessageDataEffect = (setMessageLogs: Dispatch<SetStateAction<MessageLog[]>>, threadId?: string): void => {
  178. const { data: aiAssistantSidebarData } = useAiAssistantSidebar();
  179. const { trigger: mutateMessageData } = useSWRMUTxMessages(aiAssistantSidebarData?.aiAssistantData?._id, threadId);
  180. useEffect(() => {
  181. const fetchAndSetMessageData = async() => {
  182. const messageData = await mutateMessageData();
  183. if (messageData != null) {
  184. const normalizedMessageData = messageData.data
  185. .reverse()
  186. .filter(message => message.metadata?.shouldHideMessage !== 'true');
  187. setMessageLogs(() => {
  188. return normalizedMessageData.map((message, index) => (
  189. {
  190. id: index.toString(),
  191. content: message.content[0].type === 'text' ? message.content[0].text.value : '',
  192. isUserMessage: message.role === 'user',
  193. }
  194. ));
  195. });
  196. }
  197. };
  198. if (threadId != null) {
  199. fetchAndSetMessageData();
  200. }
  201. }, [mutateMessageData, setMessageLogs, threadId]);
  202. };
  203. export const useAiAssistantSidebarCloseEffect = (sidebarRef: RefObject<HTMLDivElement>): void => {
  204. const { data, close } = useAiAssistantSidebar();
  205. useEffect(() => {
  206. const handleClickOutside = (event: MouseEvent) => {
  207. if (data?.isOpened && sidebarRef.current && !sidebarRef.current.contains(event.target as Node) && !data.isEditorAssistant) {
  208. close();
  209. }
  210. };
  211. document.addEventListener('mousedown', handleClickOutside);
  212. return () => {
  213. document.removeEventListener('mousedown', handleClickOutside);
  214. };
  215. }, [close, data?.isEditorAssistant, data?.isOpened, sidebarRef]);
  216. };