knowledge-assistant.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import type { Dispatch, SetStateAction, RefObject } from 'react';
  2. import {
  3. useCallback, useMemo, useState, useEffect,
  4. } from 'react';
  5. import { apiv3Post } from '~/client/util/apiv3-client';
  6. import { SseMessageSchema, type SseMessage } from '~/features/openai/interfaces/knowledge-assistant/sse-schemas';
  7. import { handleIfSuccessfullyParsed } from '~/features/openai/utils/handle-if-successfully-parsed';
  8. import type { MessageLog } from '../../interfaces/message';
  9. import type { IThreadRelationHasId } from '../../interfaces/thread-relation';
  10. import { ThreadType } from '../../interfaces/thread-relation';
  11. import { AiAssistantChatInitialView } from '../components/AiAssistant/AiAssistantSidebar/AiAssistantChatInitialView';
  12. import { MessageCard, type MessageCardRole } from '../components/AiAssistant/AiAssistantSidebar/MessageCard';
  13. import { useAiAssistantSidebar } from '../stores/ai-assistant';
  14. import { useSWRMUTxMessages } from '../stores/message';
  15. import { useSWRMUTxThreads } from '../stores/thread';
  16. interface CreateThread {
  17. (aiAssistantId: string, initialUserMessage: string): Promise<IThreadRelationHasId>;
  18. }
  19. interface PostMessage {
  20. (aiAssistantId: string, threadId: string, userMessage: string, summaryMode?: boolean): Promise<Response>;
  21. }
  22. interface ProcessMessage {
  23. (data: unknown, handler: {
  24. onMessage: (data: SseMessage) => void}
  25. ): void;
  26. }
  27. interface GenerateMessageCard {
  28. (role: MessageCardRole, children: string): JSX.Element;
  29. }
  30. type UseKnowledgeAssistant = () => {
  31. createThread: CreateThread
  32. postMessage: PostMessage
  33. processMessage: ProcessMessage
  34. // Views
  35. initialView: JSX.Element
  36. generateMessageCard: GenerateMessageCard,
  37. headerIcon: JSX.Element
  38. headerText: JSX.Element
  39. placeHolder: string
  40. }
  41. export const useKnowledgeAssistant: UseKnowledgeAssistant = () => {
  42. // Hooks
  43. const { data: aiAssistantSidebarData } = useAiAssistantSidebar();
  44. const { aiAssistantData } = aiAssistantSidebarData ?? {};
  45. const { threadData } = aiAssistantSidebarData ?? {};
  46. const { trigger: mutateThreadData } = useSWRMUTxThreads(aiAssistantData?._id);
  47. // States
  48. const [currentThreadTitle, setCurrentThreadId] = useState(threadData?.title);
  49. // Functions
  50. const createThread: CreateThread = useCallback(async(aiAssistantId, initialUserMessage) => {
  51. const response = await apiv3Post<IThreadRelationHasId>('/openai/thread', {
  52. type: ThreadType.KNOWLEDGE,
  53. aiAssistantId,
  54. initialUserMessage,
  55. });
  56. const thread = response.data;
  57. setCurrentThreadId(thread.title);
  58. // No need to await because data is not used
  59. mutateThreadData();
  60. return thread;
  61. }, [mutateThreadData]);
  62. const postMessage: PostMessage = useCallback(async(aiAssistantId, threadId, userMessage, summaryMode) => {
  63. const response = await fetch('/_api/v3/openai/message', {
  64. method: 'POST',
  65. headers: { 'Content-Type': 'application/json' },
  66. body: JSON.stringify({
  67. aiAssistantId,
  68. threadId,
  69. userMessage,
  70. summaryMode,
  71. }),
  72. });
  73. return response;
  74. }, []);
  75. const processMessage: ProcessMessage = useCallback((data, handler) => {
  76. handleIfSuccessfullyParsed(data, SseMessageSchema, (data: SseMessage) => {
  77. handler.onMessage(data);
  78. });
  79. }, []);
  80. // Views
  81. const headerIcon = useMemo(() => {
  82. return <span className="growi-custom-icons growi-ai-chat-icon me-3 fs-4">ai_assistant</span>;
  83. }, []);
  84. const headerText = useMemo(() => {
  85. return <>{currentThreadTitle ?? aiAssistantData?.name}</>;
  86. }, [aiAssistantData?.name, currentThreadTitle]);
  87. const placeHolder = useMemo(() => { return 'sidebar_ai_assistant.knowledge_assistant_placeholder' }, []);
  88. const initialView = useMemo(() => {
  89. if (aiAssistantSidebarData?.aiAssistantData == null) {
  90. return <></>;
  91. }
  92. return (
  93. <AiAssistantChatInitialView
  94. description={aiAssistantSidebarData.aiAssistantData.description}
  95. additionalInstruction={aiAssistantSidebarData.aiAssistantData.additionalInstruction}
  96. pagePathPatterns={aiAssistantSidebarData.aiAssistantData.pagePathPatterns}
  97. />
  98. );
  99. }, [aiAssistantSidebarData?.aiAssistantData]);
  100. const generateMessageCard: GenerateMessageCard = useCallback((role, children) => {
  101. return (
  102. <MessageCard
  103. role={role}
  104. >
  105. {children}
  106. </MessageCard>
  107. );
  108. }, []);
  109. return {
  110. createThread,
  111. postMessage,
  112. processMessage,
  113. // Views
  114. initialView,
  115. generateMessageCard,
  116. headerIcon,
  117. headerText,
  118. placeHolder,
  119. };
  120. };
  121. export const useFetchAndSetMessageDataEffect = (setMessageLogs: Dispatch<SetStateAction<MessageLog[]>>, threadId?: string): void => {
  122. const { data: aiAssistantSidebarData } = useAiAssistantSidebar();
  123. const { trigger: mutateMessageData } = useSWRMUTxMessages(aiAssistantSidebarData?.aiAssistantData?._id, threadId);
  124. useEffect(() => {
  125. const fetchAndSetMessageData = async() => {
  126. const messageData = await mutateMessageData();
  127. if (messageData != null) {
  128. const normalizedMessageData = messageData.data
  129. .reverse()
  130. .filter(message => message.metadata?.shouldHideMessage !== 'true');
  131. setMessageLogs(() => {
  132. return normalizedMessageData.map((message, index) => (
  133. {
  134. id: index.toString(),
  135. content: message.content[0].type === 'text' ? message.content[0].text.value : '',
  136. isUserMessage: message.role === 'user',
  137. }
  138. ));
  139. });
  140. }
  141. };
  142. if (threadId != null) {
  143. fetchAndSetMessageData();
  144. }
  145. }, [mutateMessageData, setMessageLogs, threadId]);
  146. };
  147. export const useAiAssistantSidebarCloseEffect = (sidebarRef: RefObject<HTMLDivElement>): void => {
  148. const { data, close } = useAiAssistantSidebar();
  149. useEffect(() => {
  150. const handleClickOutside = (event: MouseEvent) => {
  151. if (data?.isOpened && sidebarRef.current && !sidebarRef.current.contains(event.target as Node) && !data.isEditorAssistant) {
  152. close();
  153. }
  154. };
  155. document.addEventListener('mousedown', handleClickOutside);
  156. return () => {
  157. document.removeEventListener('mousedown', handleClickOutside);
  158. };
  159. }, [close, data?.isEditorAssistant, data?.isOpened, sidebarRef]);
  160. };