Explorar o código

Merge pull request #9518 from weseek/feat/159531-chat-modal

feat: Chat Modal
Yuki Takei hai 1 ano
pai
achega
436b76b331

+ 44 - 4
apps/app/src/features/openai/chat/components/AiChatModal/AiChatModal.tsx

@@ -1,12 +1,12 @@
 import type { KeyboardEvent } from 'react';
 import type { KeyboardEvent } from 'react';
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useState } from 'react';
 
 
 import { useForm, Controller } from 'react-hook-form';
 import { useForm, Controller } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import {
 import {
   Collapse,
   Collapse,
   Modal, ModalBody, ModalFooter, ModalHeader,
   Modal, ModalBody, ModalFooter, ModalHeader,
-  UncontrolledTooltip,
+  UncontrolledTooltip, Input,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
@@ -14,6 +14,7 @@ import { toastError } from '~/client/util/toastr';
 import { useGrowiCloudUri } from '~/stores-universal/context';
 import { useGrowiCloudUri } from '~/stores-universal/context';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { SelectedPageList } from '../../../client/components/Common/SelectedPageList';
 import { useRagSearchModal } from '../../../client/stores/rag-search';
 import { useRagSearchModal } from '../../../client/stores/rag-search';
 import { MessageErrorCode, StreamErrorCode } from '../../../interfaces/message-error';
 import { MessageErrorCode, StreamErrorCode } from '../../../interfaces/message-error';
 
 
@@ -193,7 +194,46 @@ const AiChatModalSubstance = (): JSX.Element => {
   return (
   return (
     <>
     <>
       <ModalBody className="pb-0 pt-3 pt-lg-4 px-3 px-lg-4">
       <ModalBody className="pb-0 pt-3 pt-lg-4 px-3 px-lg-4">
-        <div className="vstack gap-4 pb-4">
+        <div className="d-flex mb-4">
+          <Input type="select" className="border rounded">
+            <option>
+              GROWI AI の機能について
+            </option>
+          </Input>
+
+          <button type="button" className="btn btn-outline-secondary bg-transparent ms-2">
+            <span className="fs-5 material-symbols-outlined">edit</span>
+          </button>
+
+          <button type="button" className="btn btn-outline-secondary bg-transparent ms-2">
+            <span className="fs-5 material-symbols-outlined">add</span>
+          </button>
+        </div>
+
+        <div className="text-muted mb-4">
+          ここに設定したアシスタントの説明が入ります。ここに設定したアシスタントの説明が入ります。
+        </div>
+
+        <div className="mb-4">
+          <p className="mb-2">アシスタントへの指示</p>
+          <div className="p-3 alert alert-primary">
+            <p className="mb-0 text-break">
+              あなたは生成AIの専門家および、リサーチャーです。ナレッジベースのWikiツールである GROWIのAI機能に関する情報を提示したり、使われている技術に関する説明をしたりします。
+            </p>
+          </div>
+        </div>
+
+        <div className="d-flex align-items-center mb-2">
+          <p className="mb-0">参照するページ</p>
+          <span className="ms-1 fs-5 material-symbols-outlined text-secondary">help</span>
+        </div>
+        <SelectedPageList selectedPages={[
+          { page: { _id: '1', path: '/Project/GROWI/新機能/GROWI AI' }, isIncludeSubPage: true },
+          { page: { _id: '2', path: '/AI導入検討/調査' }, isIncludeSubPage: false },
+        ]}
+        />
+
+        <div className="vstack gap-4 pb-2">
           { messageLogs.map(message => (
           { messageLogs.map(message => (
             <MessageCard key={message.id} role={message.isUserMessage ? 'user' : 'assistant'}>{message.content}</MessageCard>
             <MessageCard key={message.id} role={message.isUserMessage ? 'user' : 'assistant'}>{message.content}</MessageCard>
           )) }
           )) }
@@ -315,7 +355,7 @@ export const AiChatModal = (): JSX.Element => {
     <Modal size="lg" isOpen={isOpened} toggle={closeRagSearchModal} className={moduleClass} scrollable>
     <Modal size="lg" isOpen={isOpened} toggle={closeRagSearchModal} className={moduleClass} scrollable>
 
 
       <ModalHeader tag="h4" toggle={closeRagSearchModal} className="pe-4">
       <ModalHeader tag="h4" toggle={closeRagSearchModal} className="pe-4">
-        <span className="growi-custom-icons growi-ai-chat-icon me-3 fs-4">knowledge_assistant</span>
+        <span className="growi-custom-icons growi-ai-chat-icon me-3 fs-4">ai_assistant</span>
         <span className="fw-bold">{t('modal_aichat.title')}</span>
         <span className="fw-bold">{t('modal_aichat.title')}</span>
         <span className="fs-5 text-body-secondary ms-3">{t('modal_aichat.title_beta_label')}</span>
         <span className="fs-5 text-body-secondary ms-3">{t('modal_aichat.title_beta_label')}</span>
       </ModalHeader>
       </ModalHeader>

+ 11 - 29
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManegementModal.tsx

@@ -1,4 +1,4 @@
-import React, { memo, useCallback, useState } from 'react';
+import React, { useCallback, useState } from 'react';
 
 
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import {
 import {
@@ -8,42 +8,19 @@ import {
 import type { IPageForItem } from '~/interfaces/page';
 import type { IPageForItem } from '~/interfaces/page';
 import { usePageSelectModal } from '~/stores/modal';
 import { usePageSelectModal } from '~/stores/modal';
 
 
+import type { SelectedPage } from '../../../interfaces/selected-page';
 import { useAiAssistantManegementModal } from '../../stores/ai-assistant';
 import { useAiAssistantManegementModal } from '../../stores/ai-assistant';
+import { SelectedPageList } from '../Common/SelectedPageList';
 
 
 import styles from './AiAssistantManegementModal.module.scss';
 import styles from './AiAssistantManegementModal.module.scss';
 
 
 const moduleClass = styles['grw-ai-assistant-manegement'] ?? '';
 const moduleClass = styles['grw-ai-assistant-manegement'] ?? '';
 
 
-
-type SelectedPage = {
-  page: IPageForItem,
-  isIncludeSubPage: boolean,
-}
-
-const SelectedPageList = memo(({ selectedPages }: { selectedPages: SelectedPage[] }): JSX.Element => {
-  const { t } = useTranslation();
-
-  if (selectedPages.length === 0) {
-    return <></>;
-  }
-
-  return (
-    <div className="mb-3">
-      {selectedPages.map(({ page, isIncludeSubPage }) => (
-        <p key={page._id} className="mb-1">
-          <code>{ page.path }</code>
-          {isIncludeSubPage && <span className="badge rounded-pill text-bg-secondary ms-2">{t('Include Subordinated Page')}</span>}
-        </p>
-      ))}
-    </div>
-  );
-});
-
 const AiAssistantManegementModalSubstance = (): JSX.Element => {
 const AiAssistantManegementModalSubstance = (): JSX.Element => {
   const { open: openPageSelectModal } = usePageSelectModal();
   const { open: openPageSelectModal } = usePageSelectModal();
   const [selectedPages, setSelectedPages] = useState<SelectedPage[]>([]);
   const [selectedPages, setSelectedPages] = useState<SelectedPage[]>([]);
 
 
-  const onClickOpenPageSelectModalButton = useCallback(() => {
+  const clickOpenPageSelectModalHandler = useCallback(() => {
     const onSelected = (page: IPageForItem, isIncludeSubPage: boolean) => {
     const onSelected = (page: IPageForItem, isIncludeSubPage: boolean) => {
       const selectedPageIds = selectedPages.map(selectedPage => selectedPage.page._id);
       const selectedPageIds = selectedPages.map(selectedPage => selectedPage.page._id);
       if (page._id != null && !selectedPageIds.includes(page._id)) {
       if (page._id != null && !selectedPageIds.includes(page._id)) {
@@ -55,6 +32,11 @@ const AiAssistantManegementModalSubstance = (): JSX.Element => {
   }, [openPageSelectModal, selectedPages]);
   }, [openPageSelectModal, selectedPages]);
 
 
 
 
+  const clickRmoveSelectedPageHandler = useCallback((pageId: string) => {
+    setSelectedPages(selectedPages.filter(selectedPage => selectedPage.page._id !== pageId));
+  }, [selectedPages]);
+
+
   return (
   return (
     <div className="px-4">
     <div className="px-4">
       <ModalBody>
       <ModalBody>
@@ -104,11 +86,11 @@ const AiAssistantManegementModalSubstance = (): JSX.Element => {
               <Label className="mb-0">参照するページ</Label>
               <Label className="mb-0">参照するページ</Label>
               <span className="ms-1 fs-5 material-symbols-outlined text-secondary">help</span>
               <span className="ms-1 fs-5 material-symbols-outlined text-secondary">help</span>
             </div>
             </div>
-            <SelectedPageList selectedPages={selectedPages} />
+            <SelectedPageList selectedPages={selectedPages} onRemove={clickRmoveSelectedPageHandler} />
             <button
             <button
               type="button"
               type="button"
               className="btn btn-outline-primary d-flex align-items-center gap-1"
               className="btn btn-outline-primary d-flex align-items-center gap-1"
-              onClick={onClickOpenPageSelectModalButton}
+              onClick={clickOpenPageSelectModalHandler}
             >
             >
               <span>+</span>
               <span>+</span>
               追加する
               追加する

+ 29 - 0
apps/app/src/features/openai/client/components/Common/SelectedPageList.tsx

@@ -0,0 +1,29 @@
+import { memo } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import type { SelectedPage } from '../../../interfaces/selected-page';
+
+export const SelectedPageList = memo(({ selectedPages, onRemove }: { selectedPages: SelectedPage[], onRemove?: (pageId?: string) => void }): JSX.Element => {
+  const { t } = useTranslation();
+
+  if (selectedPages.length === 0) {
+    return <></>;
+  }
+
+  return (
+    <div className="mb-3">
+      {selectedPages.map(({ page, isIncludeSubPage }) => (
+        <div key={page._id} className="mb-1 d-flex align-items-center">
+          <code>{ page.path }</code>
+          {isIncludeSubPage && <span className="badge rounded-pill text-bg-secondary ms-2">{t('Include Subordinated Page')}</span>}
+          {onRemove != null && page._id != null && page._id && (
+            <button className="btn border-0 " type="button" onClick={() => onRemove(page._id)}>
+              <span className="fs-5 material-symbols-outlined text-secondary">delete</span>
+            </button>
+          )}
+        </div>
+      ))}
+    </div>
+  );
+});

+ 6 - 0
apps/app/src/features/openai/interfaces/selected-page.ts

@@ -0,0 +1,6 @@
+import type { IPageForItem } from '~/interfaces/page';
+
+export type SelectedPage = {
+  page: IPageForItem,
+  isIncludeSubPage: boolean,
+}