Просмотр исходного кода

Merge pull request #9594 from weseek/feat/160664-implement-access-scope-dropdown

feat: Implement EditScopePage
Yuki Takei 1 год назад
Родитель
Сommit
0947d79ef7

+ 22 - 0
apps/app/public/static/locales/en_US/translation.json

@@ -508,6 +508,28 @@
       "share": "Assistant Sharing",
       "pages": "Reference Pages",
       "instruction": "Assistant Instructions"
+    },
+    "access_scope": {
+      "owner": "All pages accessible by {{username}}",
+      "groups": "Specify groups",
+      "publicOnly": "Public pages only"
+    },
+    "share_scope": {
+      "owner": {
+        "label": "{{username}} only"
+      },
+      "publicOnly": {
+        "label": "Public",
+        "desc": "Shared with all users"
+      },
+      "groups": {
+        "label": "Specify groups",
+        "desc": "Shared only with members of selected groups"
+      },
+      "sameAsAccessScope": {
+        "label": "Same as page access scope",
+        "desc": "Shared with the same scope as page access"
+      }
     }
   },
   "link_edit": {

+ 22 - 0
apps/app/public/static/locales/fr_FR/translation.json

@@ -503,6 +503,28 @@
       "share": "Partage de l'assistant",
       "pages": "Pages de référence",
       "instruction": "Instructions de l'assistant"
+    },
+    "access_scope": {
+      "owner": "Toutes les pages accessibles par {{username}}",
+      "groups": "Spécifier les groupes",
+      "publicOnly": "Pages publiques uniquement"
+    },
+    "share_scope": {
+      "owner": {
+        "label": "Seulement {{username}}"
+      },
+      "publicOnly": {
+        "label": "Public",
+        "desc": "Partagé avec tous les utilisateurs"
+      },
+      "groups": {
+        "label": "Spécifier les groupes",
+        "desc": "Partagé uniquement avec les membres des groupes sélectionnés"
+      },
+      "sameAsAccessScope": {
+        "label": "Même portée que l'accès à la page",
+        "desc": "Partagé avec la même portée que l'accès à la page"
+      }
     }
   },
   "link_edit": {

+ 22 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -541,6 +541,28 @@
       "share": "アシスタントの共有",
       "pages": "参照ページ",
       "instruction": "アシスタントへの指示"
+    },
+    "access_scope": {
+      "owner": "{{username}} がアクセス可能な全てのページ",
+      "groups": "グループを指定",
+      "publicOnly": "公開ページのみ"
+    },
+    "share_scope": {
+      "owner": {
+        "label": "{{username}} のみ"
+      },
+      "publicOnly": {
+        "label": "全体公開",
+        "desc": "すべてのユーザーに共有されます"
+      },
+      "groups": {
+        "label": "グループを指定",
+        "desc": "選択したグループのメンバーにのみ共有されます"
+      },
+      "sameAsAccessScope": {
+        "label": "ページのアクセス権限と同じ範囲",
+        "desc": "ページのアクセス権限と同じ範囲で共有されます"
+      }
     }
   },
   "link_edit": {

+ 22 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -497,6 +497,28 @@
       "share": "助理共享",
       "pages": "参考页面",
       "instruction": "助理指示"
+    },
+    "access_scope": {
+      "owner": "{{username}} 可访问的所有页面",
+      "groups": "指定群组",
+      "publicOnly": "仅公开页面"
+    },
+    "share_scope": {
+      "owner": {
+        "label": "仅 {{ username }}"
+      },
+      "publicOnly": {
+        "label": "公开",
+        "desc": "与所有用户共享"
+      },
+      "groups": {
+        "label": "指定群组",
+        "desc": "仅与选定组的成员共享"
+      },
+      "sameAsAccessScope": {
+        "label": "与页面访问范围相同",
+        "desc": "与页面访问范围相同的范围共享"
+      }
     }
   },
   "link_edit": {

+ 66 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AccessScopeDropdown.tsx

@@ -0,0 +1,66 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import {
+  UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem, Label,
+} from 'reactstrap';
+
+import { useCurrentUser } from '~/stores-universal/context';
+
+import { AiAssistantAccessScope } from '../../../../interfaces/ai-assistant';
+
+type Props = {
+  isDisabled: boolean,
+  isDisabledGroups: boolean,
+  selectedAccessScope: AiAssistantAccessScope,
+  onSelect: (accessScope: AiAssistantAccessScope) => void,
+}
+
+export const AccessScopeDropdown: React.FC<Props> = (props: Props) => {
+  const {
+    isDisabled,
+    isDisabledGroups,
+    selectedAccessScope,
+    onSelect,
+  } = props;
+
+  const { t } = useTranslation();
+  const { data: currentUser } = useCurrentUser();
+
+  const getAccessScopeLabel = useCallback((accessScope: AiAssistantAccessScope) => {
+    const baseLabel = `modal_ai_assistant.access_scope.${accessScope}`;
+    return accessScope === AiAssistantAccessScope.OWNER
+      ? t(baseLabel, { username: currentUser?.username })
+      : t(baseLabel);
+  }, [currentUser?.username, t]);
+
+  const selectAccessScopeHandler = useCallback((accessScope: AiAssistantAccessScope) => {
+    onSelect(accessScope);
+  }, [onSelect]);
+
+  return (
+    <div className="mb-4">
+      <Label className="text-secondary mb-2">ページのアクセス権限</Label>
+      <UncontrolledDropdown>
+        <DropdownToggle
+          disabled={isDisabled}
+          caret
+          className="btn-outline-secondary bg-transparent"
+        >
+          {getAccessScopeLabel(selectedAccessScope)}
+        </DropdownToggle>
+        <DropdownMenu>
+          { [AiAssistantAccessScope.OWNER, AiAssistantAccessScope.GROUPS, AiAssistantAccessScope.PUBLIC_ONLY].map(accessScope => (
+            <DropdownItem
+              disabled={isDisabledGroups && accessScope === AiAssistantAccessScope.GROUPS}
+              onClick={() => selectAccessScopeHandler(accessScope)}
+              key={accessScope}
+            >
+              {getAccessScopeLabel(accessScope)}
+            </DropdownItem>
+          ))}
+        </DropdownMenu>
+      </UncontrolledDropdown>
+    </div>
+  );
+};

+ 136 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditShare.tsx

@@ -0,0 +1,136 @@
+import React, { useCallback, useState } from 'react';
+
+import {
+  ModalBody, Input, Label,
+} from 'reactstrap';
+
+import { AiAssistantShareScope, AiAssistantAccessScope } from '~/features/openai/interfaces/ai-assistant';
+import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
+import { useSWRxUserRelatedGroups } from '~/stores/user';
+
+import { AccessScopeDropdown } from './AccessScopeDropdown';
+import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
+import { SelectUserGroupModal } from './SelectUserGroupModal';
+import { ShareScopeSwitch } from './ShareScopeSwitch';
+
+const ScopeType = {
+  ACCESS: 'Access',
+  SHARE: 'Share',
+} as const;
+
+type ScopeType = typeof ScopeType[keyof typeof ScopeType];
+
+type Props = {
+  selectedShareScope: AiAssistantShareScope,
+  selectedAccessScope: AiAssistantAccessScope,
+  selectedUserGroupsForShareScope: PopulatedGrantedGroup[],
+  selectedUserGroupsForAccessScope: PopulatedGrantedGroup[],
+  onSelectShareScope: (scope: AiAssistantShareScope) => void,
+  onSelectAccessScope: (scope: AiAssistantAccessScope) => void,
+  onSelectShareScopeUserGroups: (userGroup: PopulatedGrantedGroup) => void,
+  onSelectAccessScopeUserGroups: (userGroup: PopulatedGrantedGroup) => void,
+}
+
+export const AiAssistantManagementEditShare = (props: Props): JSX.Element => {
+  const {
+    selectedShareScope,
+    selectedAccessScope,
+    selectedUserGroupsForShareScope,
+    selectedUserGroupsForAccessScope,
+    onSelectShareScope,
+    onSelectAccessScope,
+    onSelectShareScopeUserGroups,
+    onSelectAccessScopeUserGroups,
+  } = props;
+
+  const { data: userRelatedGroups } = useSWRxUserRelatedGroups();
+  const hasNoRelatedGroups = userRelatedGroups == null || userRelatedGroups.relatedGroups.length === 0;
+
+  const [isShared, setIsShared] = useState(false);
+  const [isSelectUserGroupModalOpen, setIsSelectUserGroupModalOpen] = useState(false);
+  const [selectedUserGroupType, setSelectedUserGroupType] = useState<ScopeType>(ScopeType.ACCESS);
+
+  const changeShareToggleHandler = useCallback(() => {
+    setIsShared((prev) => {
+      if (prev) { // if isShared === true
+        onSelectShareScope(AiAssistantShareScope.SAME_AS_ACCESS_SCOPE);
+        onSelectAccessScope(AiAssistantAccessScope.OWNER);
+      }
+      else {
+        onSelectShareScope(AiAssistantShareScope.PUBLIC_ONLY);
+      }
+      return !prev;
+    });
+  }, [onSelectAccessScope, onSelectShareScope]);
+
+  const selectGroupScopeHandler = useCallback((scopeType: ScopeType) => {
+    setSelectedUserGroupType(scopeType);
+    setIsSelectUserGroupModalOpen(true);
+  }, []);
+
+  const selectShareScopeHandler = useCallback((shareScope: AiAssistantShareScope) => {
+    onSelectShareScope(shareScope);
+    if (shareScope === AiAssistantShareScope.GROUPS && !hasNoRelatedGroups) {
+      selectGroupScopeHandler(ScopeType.SHARE);
+    }
+  }, [hasNoRelatedGroups, onSelectShareScope, selectGroupScopeHandler]);
+
+  const selectAccessScopeHandler = useCallback((accessScope: AiAssistantAccessScope) => {
+    onSelectAccessScope(accessScope);
+    if (accessScope === AiAssistantAccessScope.GROUPS && !hasNoRelatedGroups) {
+      selectGroupScopeHandler(ScopeType.ACCESS);
+    }
+  }, [hasNoRelatedGroups, onSelectAccessScope, selectGroupScopeHandler]);
+
+
+  return (
+    <>
+      <AiAssistantManagementHeader />
+
+      <ModalBody className="px-4">
+        <div className="form-check form-switch mb-4">
+          <Input
+            type="switch"
+            role="switch"
+            id="shareAssistantSwitch"
+            className="form-check-input"
+            checked={isShared}
+            onChange={changeShareToggleHandler}
+          />
+          <Label className="form-check-label" for="shareAssistantSwitch">
+            アシスタントを共有する
+          </Label>
+        </div>
+
+        <AccessScopeDropdown
+          isDisabled={!isShared}
+          isDisabledGroups={hasNoRelatedGroups}
+          selectedAccessScope={selectedAccessScope}
+          onSelect={selectAccessScopeHandler}
+        />
+
+        <ShareScopeSwitch
+          isDisabled={!isShared}
+          isDisabledGroups={hasNoRelatedGroups}
+          selectedShareScope={selectedShareScope}
+          onSelect={selectShareScopeHandler}
+        />
+
+        <SelectUserGroupModal
+          isOpen={isSelectUserGroupModalOpen}
+          userRelatedGroups={userRelatedGroups?.relatedGroups}
+          closeModal={() => setIsSelectUserGroupModalOpen(false)}
+          selectedUserGroups={selectedUserGroupType === ScopeType.ACCESS ? selectedUserGroupsForAccessScope : selectedUserGroupsForShareScope}
+          onSelect={(userGroup) => {
+            if (selectedUserGroupType === ScopeType.ACCESS) {
+              onSelectAccessScopeUserGroups(userGroup);
+            }
+            else {
+              onSelectShareScopeUserGroups(userGroup);
+            }
+          }}
+        />
+      </ModalBody>
+    </>
+  );
+};

+ 34 - 5
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHome.tsx

@@ -1,23 +1,47 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 
 import { useTranslation } from 'react-i18next';
 import {
   ModalHeader, ModalBody, ModalFooter, Input,
 } from 'reactstrap';
 
+import { AiAssistantShareScope } from '~/features/openai/interfaces/ai-assistant';
+import { useCurrentUser } from '~/stores-universal/context';
+
 import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
 
 type Props = {
+  name: string;
+  description: string;
   instruction: string;
+  shareScope: AiAssistantShareScope
+  onNameChange: (value: string) => void;
+  onDescriptionChange: (value: string) => void;
+  onCreateAiAssistant: () => Promise<void>
 }
 
 export const AiAssistantManagementHome = (props: Props): JSX.Element => {
-  const { instruction } = props;
+  const {
+    name,
+    description,
+    instruction,
+    shareScope,
+    onNameChange,
+    onDescriptionChange,
+    onCreateAiAssistant,
+  } = props;
 
   const { t } = useTranslation();
-
+  const { data: currentUser } = useCurrentUser();
   const { close: closeAiAssistantManagementModal, changePageMode } = useAiAssistantManagementModal();
 
+  const getShareScopeLabel = useCallback((shareScope: AiAssistantShareScope) => {
+    const baseLabel = `modal_ai_assistant.share_scope.${shareScope}.label`;
+    return shareScope === AiAssistantShareScope.OWNER
+      ? t(baseLabel, { username: currentUser?.username })
+      : t(baseLabel);
+  }, [currentUser?.username, t]);
+
   return (
     <>
       <ModalHeader tag="h4" toggle={closeAiAssistantManagementModal} className="pe-4">
@@ -33,6 +57,8 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
               placeholder="アシスタント名を入力"
               bsSize="lg"
               className="border-0 border-bottom border-2 px-0 rounded-0"
+              value={name}
+              onChange={e => onNameChange(e.target.value)}
             />
           </div>
 
@@ -45,6 +71,8 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
               type="textarea"
               placeholder="内容や用途のメモを表示させることができます"
               rows="4"
+              value={description}
+              onChange={e => onDescriptionChange(e.target.value)}
             />
             <small className="text-secondary d-block mt-2">
               メモの内容はアシスタントの処理に影響しません。
@@ -54,11 +82,12 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
           <div>
             <button
               type="button"
+              onClick={() => { changePageMode(AiAssistantManagementModalPageMode.SHARE) }}
               className="btn w-100 d-flex justify-content-between align-items-center py-3 mb-2 border-0"
             >
               <span className="fw-normal">{t('modal_ai_assistant.page_mode_title.share')}</span>
               <div className="d-flex align-items-center text-secondary">
-                <span>UserNameのみ</span>
+                <span>{getShareScopeLabel(shareScope)}</span>
                 <span className="material-symbols-outlined ms-2 align-middle">chevron_right</span>
               </div>
             </button>
@@ -93,7 +122,7 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
 
         <ModalFooter>
           <button type="button" className="btn btn-outline-secondary" onClick={() => {}}>キャンセル</button>
-          <button type="button" className="btn btn-primary" onClick={() => {}}>アシスタントを作成する</button>
+          <button type="button" className="btn btn-primary" onClick={onCreateAiAssistant}>アシスタントを作成する</button>
         </ModalFooter>
       </div>
     </>

+ 109 - 109
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal.tsx

@@ -1,10 +1,13 @@
 import React, { useCallback, useState } from 'react';
 
+import type { IGrantedGroup } from '@growi/core';
 import { useTranslation } from 'react-i18next';
 import { Modal, TabContent, TabPane } from 'reactstrap';
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
+import { AiAssistantAccessScope, AiAssistantShareScope } from '~/features/openai/interfaces/ai-assistant';
 import type { IPageForItem } from '~/interfaces/page';
+import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import loggerFactory from '~/utils/logger';
 
 import type { SelectedPage } from '../../../../interfaces/selected-page';
@@ -13,6 +16,7 @@ import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } fro
 
 import { AiAssistantManagementEditInstruction } from './AiAssistantManagementEditInstruction';
 import { AiAssistantManagementEditPages } from './AiAssistantManagementEditPages';
+import { AiAssistantManagementEditShare } from './AiAssistantManagementEditShare';
 import { AiAssistantManagementHome } from './AiAssistantManagementHome';
 
 import styles from './AiAssistantManagementModal.module.scss';
@@ -21,39 +25,117 @@ const moduleClass = styles['grw-ai-assistant-management'] ?? '';
 
 const logger = loggerFactory('growi:openai:client:components:AiAssistantManagementModal');
 
+// PopulatedGrantedGroup[] -> IGrantedGroup[]
+const convertToGrantedGroups = (selectedGroups: PopulatedGrantedGroup[]): IGrantedGroup[] => {
+  return selectedGroups.map(group => ({
+    type: group.type,
+    item: group.item._id,
+  }));
+};
+
 const AiAssistantManagementModalSubstance = (): JSX.Element => {
   // Hooks
   const { t } = useTranslation();
-  const { data: aiAssistantManagementModalData } = useAiAssistantManagementModal();
+  const { data: aiAssistantManagementModalData, close: closeAiAssistantManagementModal } = useAiAssistantManagementModal();
 
   const pageMode = aiAssistantManagementModalData?.pageMode ?? AiAssistantManagementModalPageMode.HOME;
 
   // States
+  const [name, setName] = useState<string>('');
+  const [description, setDescription] = useState<string>('');
+  const [selectedShareScope, setSelectedShareScope] = useState<AiAssistantShareScope>(AiAssistantShareScope.SAME_AS_ACCESS_SCOPE);
+  const [selectedAccessScope, setSelectedAccessScope] = useState<AiAssistantAccessScope>(AiAssistantAccessScope.OWNER);
+  const [selectedUserGroupsForAccessScope, setSelectedUserGroupsForAccessScope] = useState<PopulatedGrantedGroup[]>([]);
+  const [selectedUserGroupsForShareScope, setSelectedUserGroupsForShareScope] = useState<PopulatedGrantedGroup[]>([]);
   const [selectedPages, setSelectedPages] = useState<SelectedPage[]>([]);
   const [instruction, setInstruction] = useState<string>(t('modal_ai_assistant.default_instruction'));
 
-  // Functions
-  const clickCreateAiAssistantHandler = useCallback(async() => {
+
+  /*
+  *  For AiAssistantManagementHome methods
+  */
+  const changeNameHandler = useCallback((value: string) => {
+    setName(value);
+  }, []);
+
+  const changeDescriptionHandler = useCallback((value: string) => {
+    setDescription(value);
+  }, []);
+
+  const createAiAssistantHandler = useCallback(async() => {
     try {
       const pagePathPatterns = selectedPages
         .map(selectedPage => (selectedPage.isIncludeSubPage ? `${selectedPage.page.path}/*` : selectedPage.page.path))
         .filter((path): path is string => path !== undefined && path !== null);
 
+      const grantedGroupsForShareScope = convertToGrantedGroups(selectedUserGroupsForShareScope);
+      const grantedGroupsForAccessScope = convertToGrantedGroups(selectedUserGroupsForAccessScope);
+
       await createAiAssistant({
-        name: 'test',
-        description: 'test',
+        name,
+        description,
         additionalInstruction: instruction,
         pagePathPatterns,
-        shareScope: 'publicOnly',
-        accessScope: 'publicOnly',
+        shareScope: selectedShareScope,
+        accessScope: selectedAccessScope,
+        grantedGroupsForShareScope: selectedShareScope === AiAssistantShareScope.GROUPS ? grantedGroupsForShareScope : undefined,
+        grantedGroupsForAccessScope: selectedAccessScope === AiAssistantAccessScope.GROUPS ? grantedGroupsForAccessScope : undefined,
       });
+
       toastSuccess('アシスタントを作成しました');
+      closeAiAssistantManagementModal();
     }
     catch (err) {
       toastError('アシスタントの作成に失敗しました');
       logger.error(err);
     }
-  }, [instruction, selectedPages]);
+  }, [
+    closeAiAssistantManagementModal,
+    description,
+    instruction,
+    name,
+    selectedAccessScope,
+    selectedPages,
+    selectedShareScope,
+    selectedUserGroupsForAccessScope,
+    selectedUserGroupsForShareScope,
+  ]);
+
+
+  /*
+  *  For AiAssistantManagementEditShare methods
+  */
+  const selectShareScopeHandler = useCallback((shareScope: AiAssistantShareScope) => {
+    setSelectedShareScope(shareScope);
+  }, []);
+
+  const selectAccessScopeHandler = useCallback((accessScope: AiAssistantAccessScope) => {
+    setSelectedAccessScope(accessScope);
+  }, []);
+
+  const selectShareScopeUserGroups = useCallback((targetUserGroup: PopulatedGrantedGroup) => {
+    const selectedUserGroupIds = selectedUserGroupsForShareScope.map(userGroup => userGroup.item._id);
+    if (selectedUserGroupIds.includes(targetUserGroup.item._id)) {
+      // if selected, remove it
+      setSelectedUserGroupsForShareScope(selectedUserGroupsForShareScope.filter(userGroup => userGroup.item._id !== targetUserGroup.item._id));
+    }
+    else {
+      // if not selected, add it
+      setSelectedUserGroupsForShareScope([...selectedUserGroupsForShareScope, targetUserGroup]);
+    }
+  }, [selectedUserGroupsForShareScope]);
+
+  const selectAccessScopeUserGroups = useCallback((targetUserGroup: PopulatedGrantedGroup) => {
+    const selectedUserGroupIds = selectedUserGroupsForAccessScope.map(userGroup => userGroup.item._id);
+    if (selectedUserGroupIds.includes(targetUserGroup.item._id)) {
+      // if selected, remove it
+      setSelectedUserGroupsForAccessScope(selectedUserGroupsForAccessScope.filter(userGroup => userGroup.item._id !== targetUserGroup.item._id));
+    }
+    else {
+      // if not selected, add it
+      setSelectedUserGroupsForAccessScope([...selectedUserGroupsForAccessScope, targetUserGroup]);
+    }
+  }, [selectedUserGroupsForAccessScope]);
 
 
   /*
@@ -87,7 +169,26 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
       <TabContent activeTab={pageMode}>
         <TabPane tabId={AiAssistantManagementModalPageMode.HOME}>
           <AiAssistantManagementHome
+            name={name}
+            description={description}
+            shareScope={selectedShareScope}
             instruction={instruction}
+            onNameChange={changeNameHandler}
+            onDescriptionChange={changeDescriptionHandler}
+            onCreateAiAssistant={createAiAssistantHandler}
+          />
+        </TabPane>
+
+        <TabPane tabId={AiAssistantManagementModalPageMode.SHARE}>
+          <AiAssistantManagementEditShare
+            selectedShareScope={selectedShareScope}
+            selectedAccessScope={selectedAccessScope}
+            selectedUserGroupsForShareScope={selectedUserGroupsForShareScope}
+            selectedUserGroupsForAccessScope={selectedUserGroupsForAccessScope}
+            onSelectShareScope={selectShareScopeHandler}
+            onSelectAccessScope={selectAccessScopeHandler}
+            onSelectAccessScopeUserGroups={selectAccessScopeUserGroups}
+            onSelectShareScopeUserGroups={selectShareScopeUserGroups}
           />
         </TabPane>
 
@@ -108,107 +209,6 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
         </TabPane>
       </TabContent>
     </>
-    // <div className="px-4">
-    //   <ModalBody>
-    //     <Form>
-    //       <FormGroup className="mb-4">
-    //         <Label className="mb-2 ">アシスタント名</Label>
-    //         <Input
-    //           type="text"
-    //           placeholder="アシスタント名を入力"
-    //           className="border rounded"
-    //         />
-    //       </FormGroup>
-
-  //       <FormGroup className="mb-4">
-  //         <div className="d-flex align-items-center mb-2">
-  //           <Label className="mb-0">アシスタントの種類</Label>
-  //           <span className="ms-1 fs-5 material-symbols-outlined text-secondary">help</span>
-  //         </div>
-  //         <div className="d-flex gap-4">
-  //           <FormGroup check>
-  //             <Input type="checkbox" defaultChecked />
-  //             <Label check>ナレッジアシスタント</Label>
-  //           </FormGroup>
-  //           <FormGroup check>
-  //             <Input type="checkbox" />
-  //             <Label check>エディタアシスタント</Label>
-  //           </FormGroup>
-  //           <FormGroup check>
-  //             <Input type="checkbox" />
-  //             <Label check>ラーニングアシスタント</Label>
-  //           </FormGroup>
-  //         </div>
-  //       </FormGroup>
-
-  //       <FormGroup className="mb-4">
-  //         <div className="d-flex align-items-center mb-2">
-  //           <Label className="mb-0">共有範囲</Label>
-  //           <span className="ms-1 fs-5 material-symbols-outlined text-secondary">help</span>
-  //         </div>
-  //         <Input type="select" className="border rounded w-50">
-  //           <option>自分のみ</option>
-  //         </Input>
-  //       </FormGroup>
-
-  //       <FormGroup className="mb-4">
-  //         <div className="d-flex align-items-center mb-2">
-  //           <Label className="mb-0">参照するページ</Label>
-  //           <span className="ms-1 fs-5 material-symbols-outlined text-secondary">help</span>
-  //         </div>
-  //         <SelectedPageList selectedPages={selectedPages} onRemove={clickRmoveSelectedPageHandler} />
-  //         <button
-  //           type="button"
-  //           className="btn btn-outline-primary d-flex align-items-center gap-1"
-  //           onClick={clickOpenPageSelectModalHandler}
-  //         >
-  //           <span>+</span>
-  //           追加する
-  //         </button>
-  //       </FormGroup>
-
-  //       <FormGroup>
-  //         <div className="d-flex align-items-center mb-2">
-  //           <Label className="mb-0 me-2">アシスタントへの指示</Label>
-  //           <label className="form-label form-check-label">
-  //             <span className="badge text-bg-danger mt-2">
-  //               必須
-  //             </span>
-  //           </label>
-  //         </div>
-  //         <Input
-  //           type="textarea"
-  //           placeholder="アシスタントに実行して欲しい内容を具体的に記入してください"
-  //           className="border rounded"
-  //           rows={4}
-  //         />
-  //       </FormGroup>
-
-  //       <FormGroup>
-  //         <div className="d-flex align-items-center mb-2">
-  //           <Label className="mb-0 me-2">アシスタントのメモ</Label>
-  //           <label className="form-label form-check-label">
-  //             <span className="badge text-bg-secondary mt-2">
-  //               必須
-  //             </span>
-  //           </label>
-  //         </div>
-  //         <Input
-  //           type="textarea"
-  //           placeholder="内容や用途のメモを表示させることができます"
-  //           className="border rounded"
-  //           rows={4}
-  //         />
-  //         <p className="mt-1 text-muted">メモ内容はアシスタントには影響しません。</p>
-  //       </FormGroup>
-  //     </Form>
-  //   </ModalBody>
-
-  //   <ModalFooter className="border-0 pt-0 mb-3">
-  //     <button type="button" className="btn btn-outline-secondary" onClick={() => {}}>キャンセル</button>
-  //     <button type="button" className="btn btn-primary" onClick={clickCreateAiAssistantHandler}>作成</button>
-  //   </ModalFooter>
-  // </div>
   );
 };
 

+ 74 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/SelectUserGroupModal.tsx

@@ -0,0 +1,74 @@
+import React, { useCallback } from 'react';
+
+import { GroupType } from '@growi/core';
+import { useTranslation } from 'react-i18next';
+import {
+  Modal, ModalHeader, ModalBody,
+} from 'reactstrap';
+
+import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
+
+type Props = {
+  isOpen: boolean,
+  userRelatedGroups?: PopulatedGrantedGroup[],
+  selectedUserGroups: PopulatedGrantedGroup[],
+  closeModal: () => void,
+  onSelect: (userGroup: PopulatedGrantedGroup) => void,
+}
+
+const SelectUserGroupModalSubstance: React.FC<Props> = (props: Props) => {
+  const {
+    userRelatedGroups,
+    selectedUserGroups,
+    onSelect,
+    closeModal,
+  } = props;
+
+  const { t } = useTranslation();
+
+  const checked = useCallback((targetUserGroup: PopulatedGrantedGroup) => {
+    const selectedUserGroupIds = selectedUserGroups.map(userGroup => userGroup.item._id);
+    return selectedUserGroupIds.includes(targetUserGroup.item._id);
+  }, [selectedUserGroups]);
+
+  return (
+    <ModalBody className="d-flex flex-column">
+      {userRelatedGroups != null && userRelatedGroups.map(userGroup => (
+        <button
+          className="btn btn-outline-primary d-flex justify-content-start mb-3 mx-4 align-items-center p-3"
+          type="button"
+          key={userGroup.item._id}
+          onClick={() => onSelect(userGroup)}
+        >
+          <input type="checkbox" checked={checked(userGroup)} onChange={() => {}} />
+          <p className="ms-3 mb-0">{userGroup.item.name}</p>
+          {userGroup.type === GroupType.externalUserGroup && <span className="ms-2 badge badge-pill badge-info">{userGroup.item.provider}</span>}
+          {/* TODO: Replace <div className="small">(TBD) List group members</div> */}
+        </button>
+      ))}
+      <button
+        type="button"
+        className="btn btn-primary mt-2 mx-auto"
+        onClick={closeModal}
+      >
+        {t('Done')}
+      </button>
+
+    </ModalBody>
+  );
+};
+
+export const SelectUserGroupModal: React.FC<Props> = (props) => {
+  const { t } = useTranslation();
+
+  const { isOpen, closeModal } = props;
+
+  return (
+    <Modal isOpen={isOpen} toggle={closeModal}>
+      <ModalHeader toggle={closeModal}>
+        {t('user_group.select_group')}
+      </ModalHeader>
+      <SelectUserGroupModalSubstance {...props} />
+    </Modal>
+  );
+};

+ 52 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/ShareScopeSwitch.tsx

@@ -0,0 +1,52 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+import {
+  Input, Label, FormGroup,
+} from 'reactstrap';
+
+import { AiAssistantShareScope } from '../../../../interfaces/ai-assistant';
+
+type Props = {
+  isDisabled: boolean,
+  isDisabledGroups: boolean,
+  selectedShareScope: AiAssistantShareScope,
+  onSelect: (shareScope: AiAssistantShareScope) => void,
+}
+
+export const ShareScopeSwitch: React.FC<Props> = (props: Props) => {
+  const {
+    isDisabled,
+    isDisabledGroups,
+    selectedShareScope,
+    onSelect,
+  } = props;
+
+  const { t } = useTranslation();
+
+  return (
+    <div className="mb-4">
+      <Label className="text-secondary mb-3">アシスタントの共有範囲</Label>
+      <div className="d-flex flex-column gap-3">
+
+        {[AiAssistantShareScope.PUBLIC_ONLY, AiAssistantShareScope.GROUPS, AiAssistantShareScope.SAME_AS_ACCESS_SCOPE].map(shareScope => (
+          <FormGroup check key={shareScope}>
+            <Input
+              type="radio"
+              name="shareScope"
+              id="shareGroup"
+              className="form-check-input"
+              disabled={isDisabled || (isDisabledGroups && shareScope === AiAssistantShareScope.GROUPS)}
+              onChange={() => onSelect(shareScope)}
+              checked={selectedShareScope === shareScope}
+            />
+            <Label check for="shareGroup" className="d-flex flex-column">
+              <span>{t(`modal_ai_assistant.share_scope.${shareScope}.label`)}</span>
+              <small className="text-secondary">{t(`modal_ai_assistant.share_scope.${shareScope}.desc`)}</small>
+            </Label>
+          </FormGroup>
+        ))}
+      </div>
+    </div>
+  );
+};

+ 2 - 1
apps/app/src/features/openai/interfaces/ai-assistant.ts

@@ -6,7 +6,8 @@ import type { VectorStore } from '../server/models/vector-store';
 *  Objects
 */
 export const AiAssistantShareScope = {
-  PUBLIC_ONLY: 'publicOnly',
+  SAME_AS_ACCESS_SCOPE: 'sameAsAccessScope',
+  PUBLIC_ONLY: 'publicOnly', // TODO: Rename to "PUBLIC"
   OWNER: 'owner',
   GROUPS: 'groups',
 } as const;