Browse Source

Merge branch 'feat/growi-ai-next' into feat/unified-merge-view

Shun Miyazawa 1 year ago
parent
commit
7cf9beb16d
37 changed files with 451 additions and 126 deletions
  1. 61 3
      apps/app/public/static/locales/en_US/translation.json
  2. 61 3
      apps/app/public/static/locales/fr_FR/translation.json
  3. 61 3
      apps/app/public/static/locales/ja_JP/translation.json
  4. 61 3
      apps/app/public/static/locales/zh_CN/translation.json
  5. 1 1
      apps/app/src/client/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  6. 1 1
      apps/app/src/client/components/Sidebar/SidebarNav/PrimaryItems.tsx
  7. 6 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantChatSidebar/AiAssistantChatSidebar.module.scss
  8. 7 9
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantChatSidebar/AiAssistantChatSidebar.tsx
  9. 1 1
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AccessScopeDropdown.tsx
  10. 4 3
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditInstruction.tsx
  11. 1 1
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditPages.tsx
  12. 3 1
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditShare.tsx
  13. 10 10
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementHome.tsx
  14. 2 2
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal.tsx
  15. 1 1
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/ShareScopeSwitch.tsx
  16. 13 9
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/ShareScopeWarningModal.tsx
  17. 4 1
      apps/app/src/features/openai/client/components/AiAssistant/OpenDefaultAiAssistantButton.tsx
  18. 6 3
      apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantSubstance.tsx
  19. 13 9
      apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantTree.tsx
  20. 2 2
      apps/app/src/features/openai/interfaces/thread-relation.ts
  21. 2 2
      apps/app/src/features/openai/server/models/thread-relation.ts
  22. 2 7
      apps/app/src/features/openai/server/routes/delete-ai-assistant.ts
  23. 2 2
      apps/app/src/features/openai/server/routes/get-messages.ts
  24. 1 2
      apps/app/src/features/openai/server/routes/get-threads.ts
  25. 3 6
      apps/app/src/features/openai/server/routes/middlewares/upsert-ai-assistant-validator.ts
  26. 1 2
      apps/app/src/features/openai/server/routes/thread.ts
  27. 10 0
      apps/app/src/features/openai/server/services/client-delegator/azure-openai-client-delegator.ts
  28. 1 0
      apps/app/src/features/openai/server/services/client-delegator/interfaces.ts
  29. 10 0
      apps/app/src/features/openai/server/services/client-delegator/openai-client-delegator.ts
  30. 1 1
      apps/app/src/features/openai/server/services/cron/vector-store-file-deletion-cron.ts
  31. 51 0
      apps/app/src/features/openai/server/services/delete-ai-assistant.ts
  32. 3 3
      apps/app/src/features/openai/server/services/normalize-data/normalize-thread-relation-expired-at/normalize-thread-relation-expired-at.integ.ts
  33. 26 27
      apps/app/src/features/openai/server/services/openai.ts
  34. 3 0
      apps/app/src/server/routes/apiv3/users.js
  35. 3 3
      apps/app/src/server/service/config-manager/config-definition.ts
  36. 11 3
      apps/app/src/server/service/normalize-data/delete-vector-stores-orphaned-from-ai-assistant.ts
  37. 2 2
      apps/app/src/server/service/normalize-data/index.ts

+ 61 - 3
apps/app/public/static/locales/en_US/translation.json

@@ -153,7 +153,7 @@
   "Bookmarks": "Bookmarks",
   "In-App Notification": "Notifications",
   "AI Assistant": "AI Assistant",
-  "Knowledge Assistant": "Knowledge Assistant",
+  "Knowledge Assistant": "Knowledge Assistant (Beta)",
   "original_path": "Original path",
   "new_path": "New path",
   "duplicated_path": "Duplicated path",
@@ -495,6 +495,8 @@
     "selected_editable_revision": "Selected Page Body (Editable)"
   },
   "sidebar_aichat": {
+    "instruction_label": "Assistant instructions",
+    "reference_pages_label": "Reference pages",
     "placeholder": "Ask me anything.",
     "summary_mode_label": "Summary mode",
     "summary_mode_help": "Concise answer within 2-3 sentences",
@@ -507,19 +509,45 @@
     "show_error_detail": "Show error details"
   },
   "modal_ai_assistant": {
+    "header": {
+      "update_assistant": "Update Assistant",
+      "add_new_assistant": "Add New Assistant"
+    },
+    "assistant_name_placeholder": "Enter assistant name",
+    "page_count": "{{count}} pages",
+    "memo": {
+      "title": "Assistant memo",
+      "optional": "Optional",
+      "placeholder": "You can display notes about content and usage",
+      "description": "The contents of the memo do not affect the assistant's processing."
+    },
+    "submit_button": {
+      "update_assistant": "Update Assistant",
+      "create_assistant": "Create Assistant"
+    },
+    "toaster": {
+      "create_success": "Assistant has been created",
+      "update_success": "Assistant has been updated",
+      "create_failed": "Failed to create assistant",
+      "update_failed": "Failed to update assistant"
+    },
     "edit_page_description": "Edit pages that the assistant can reference.<br> The assistant can reference up to {{limitLearnablePageCountPerAssistant}} pages including child pages.",
     "default_instruction": "You are the knowledge assistant for this Wiki. Please provide support according to the following guidelines:\n\n- Analyze document relevance and connect information\n- Suggest new perspectives\n- Provide accurate information based on understanding the intent of questions\nI will provide information in a structured format when necessary.",
+    "add_page_button": "Add page",
     "page_mode_title": {
       "share": "Assistant Sharing",
       "pages": "Reference Pages",
       "instruction": "Assistant Instructions"
     },
+    "share_assistant": "Share assistant",
+    "page_access_permission": "Page access permission",
     "access_scope": {
       "owner": "All pages accessible by {{username}}",
       "groups": "Specify groups",
       "publicOnly": "Public pages only"
     },
     "share_scope": {
+      "title": "Assistant sharing scope",
       "owner": {
         "label": "{{username}} only"
       },
@@ -535,6 +563,37 @@
         "label": "Same as page access scope",
         "desc": "Shared with the same scope as page access"
       }
+    },
+    "instructions": {
+      "description": "You can set instructions that determine how the assistant behaves.<br>The assistant will answer and analyze based on these instructions.",
+      "reset_to_default": "Reset to default"
+    }
+  },
+  "share_scope_warning_modal": {
+    "header_title": "Confirm Sharing Scope",
+    "warning_message": "This assistant includes pages with limited access.<br>With the current settings, information from these pages may be shared beyond their original access permissions through the assistant.",
+    "selected_pages_label": "Selected page paths",
+    "confirmation_message": "Please confirm that you understand the content of these pages may be shared within the assistant's public scope if you proceed.",
+    "button": {
+      "review": "Review settings",
+      "proceed": "Understand and proceed"
+    }
+  },
+  "default_ai_assistant": {
+    "not_set": "Default assistant is not set"
+  },
+  "ai_assistant_tree": {
+    "add_assistant": "Add Assistant",
+    "my_assistants": "My Assistants",
+    "team_assistants": "Team Assistants",
+    "thread_does_not_exist": "No threads exist",
+    "toaster": {
+      "ai_assistant_deleted_success": "Assistant deleted",
+      "ai_assistant_deleted_failed": "Failed to delete assistant",
+      "thread_deleted_success": "Thread deleted",
+      "thread_deleted_failed": "Failed to delete thread",
+      "ai_assistant_set_default_success": "Default assistant set successfully",
+      "ai_assistant_set_default_failed": "Failed to set default assistant"
     }
   },
   "link_edit": {
@@ -906,8 +965,7 @@
   },
   "sidebar_header": {
     "show_wip_page": "Show WIP",
-    "size_s": "Size: S",
-    "size_l": "Size: L"
+    "compact_view": "Compact View"
   },
   "create_page": {
     "untitled": "Untitled"

+ 61 - 3
apps/app/public/static/locales/fr_FR/translation.json

@@ -154,7 +154,7 @@
   "Bookmarks": "Favoris",
   "In-App Notification": "Notifications",
   "AI Assistant": "Assistant IA",
-  "Knowledge Assistant": "Assistant de Connaissance",
+  "Knowledge Assistant": "Assistant de Connaissances (Bêta)",
   "original_path": "Chemin originel",
   "new_path": "Nouveau chemin",
   "duplicated_path": "Chemin dupliqué",
@@ -490,6 +490,8 @@
     "selected_editable_revision": "Corps de page sélectionné (Modifiable)"
   },
   "sidebar_aichat": {
+    "instruction_label": "Instructions pour l'assistant",
+    "reference_pages_label": "Pages de référence",
     "placeholder": "Demandez-moi n'importe quoi.",
     "summary_mode_label": "Mode résumé",
     "summary_mode_help": "Réponse concise en 2-3 phrases",
@@ -502,19 +504,45 @@
     "show_error_detail": "Détails de l'exposition"
   },
   "modal_ai_assistant": {
+    "header": {
+      "update_assistant": "Mettre à jour l'assistant",
+      "add_new_assistant": "Ajouter un nouvel assistant"
+    },
+    "assistant_name_placeholder": "Entrer le nom de l'assistant",
+    "page_count": "{{count}} pages",
+    "memo": {
+      "title": "Note sur l'assistant",
+      "optional": "Optionnel",
+      "placeholder": "Vous pouvez afficher des notes sur le contenu et l'utilisation",
+      "description": "Le contenu de la note n'affecte pas le traitement de l'assistant."
+    },
+    "submit_button": {
+      "update_assistant": "Mettre à jour l'assistant",
+      "create_assistant": "Créer l'assistant"
+    },
+    "toaster": {
+      "create_success": "L'assistant a été créé",
+      "update_success": "L'assistant a été mis à jour",
+      "create_failed": "Échec de la création de l'assistant",
+      "update_failed": "Échec de la mise à jour de l'assistant"
+    },
     "edit_page_description": "Modifier les pages que l'assistant peut référencer.<br> L'assistant peut référencer jusqu'à {{limitLearnablePageCountPerAssistant}} pages, y compris les pages enfants.",
     "default_instruction": "Vous êtes l'assistant de connaissances pour ce Wiki. Veuillez fournir un support selon les directives suivantes :\n\n- Analyser la pertinence des documents et relier les informations\n- Proposer de nouvelles perspectives\n- Fournir des informations précises en comprenant l'intention des questions\nJe fournirai les informations sous forme structurée si nécessaire.",
+    "add_page_button": "Ajouter une page",
     "page_mode_title": {
       "share": "Partage de l'assistant",
       "pages": "Pages de référence",
       "instruction": "Instructions de l'assistant"
     },
+    "share_assistant": "Partager l'assistant",
+    "page_access_permission": "Autorisation d'accès à la page",
     "access_scope": {
       "owner": "Toutes les pages accessibles par {{username}}",
       "groups": "Spécifier les groupes",
       "publicOnly": "Pages publiques uniquement"
     },
     "share_scope": {
+      "title": "Portée de partage de l'assistant",
       "owner": {
         "label": "Seulement {{username}}"
       },
@@ -530,6 +558,37 @@
         "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"
       }
+    },
+    "instructions": {
+      "description": "Vous pouvez définir des instructions qui déterminent le comportement de l'assistant.<br>L'assistant répondra et analysera en fonction de ces instructions.",
+      "reset_to_default": "Réinitialiser par défaut"
+    }
+  },
+  "share_scope_warning_modal": {
+    "header_title": "Confirmation de la portée de partage",
+    "warning_message": "Cet assistant comprend des pages à accès limité.<br>Avec les paramètres actuels, les informations de ces pages peuvent être partagées au-delà de leurs autorisations d'accès d'origine via l'assistant.",
+    "selected_pages_label": "Chemins de pages sélectionnés",
+    "confirmation_message": "Veuillez confirmer que vous comprenez que le contenu de ces pages peut être partagé dans la portée publique de l'assistant si vous continuez.",
+    "button": {
+      "review": "Réviser les paramètres",
+      "proceed": "Comprendre et continuer"
+    }
+  },
+  "default_ai_assistant": {
+    "not_set": "L'assistant par défaut n'est pas configuré"
+  },
+ "ai_assistant_tree": {
+    "add_assistant": "Ajouter un assistant",
+    "my_assistants": "Mes assistants",
+    "team_assistants": "Assistants d'équipe",
+    "thread_does_not_exist": "Aucune discussion",
+    "toaster": {
+      "ai_assistant_deleted_success": "Assistant supprimé",
+      "ai_assistant_deleted_failed": "Échec de la suppression de l'assistant",
+      "thread_deleted_success": "Discussion supprimée",
+      "thread_deleted_failed": "Échec de la suppression de la discussion",
+      "ai_assistant_set_default_success": "Assistant par défaut défini avec succès",
+      "ai_assistant_set_default_failed": "Échec de la définition de l'assistant par défaut"
     }
   },
   "link_edit": {
@@ -901,8 +960,7 @@
   },
   "sidebar_header": {
     "show_wip_page": "Voir brouillon",
-    "size_s": "Taille: P",
-    "size_l": "Taille: G"
+    "compact_view": "Vue compacte"
   },
   "sync-latest-revision-body": {
     "menuitem": "Synchroniser avec la dernière révision",

+ 61 - 3
apps/app/public/static/locales/ja_JP/translation.json

@@ -154,7 +154,7 @@
   "Bookmarks": "ブックマーク",
   "In-App Notification": "通知",
   "AI Assistant": "AI アシスタント",
-  "Knowledge Assistant": "ナレッジアシスタント",
+  "Knowledge Assistant": "ナレッジアシスタント (ベータ版)",
   "original_path": "元のパス",
   "new_path": "新しいパス",
   "duplicated_path": "重複したパス",
@@ -528,6 +528,8 @@
     "selected_editable_revision": "保存するページ本文(編集可能)"
   },
   "sidebar_aichat": {
+    "instruction_label": "アシスタントへの指示",
+    "reference_pages_label": "参照するページ",
     "placeholder": "ききたいことを入力してください",
     "summary_mode_label": "要約モード",
     "summary_mode_help": "2~3文以内の簡潔な回答",
@@ -540,19 +542,45 @@
     "show_error_detail": "詳細を表示"
   },
   "modal_ai_assistant": {
+    "header": {
+      "update_assistant": "アシスタントの更新",
+      "add_new_assistant": "新規アシスタントの追加"
+    },
+    "assistant_name_placeholder": "アシスタント名を入力",
+    "page_count": "{{count}} ページ",
+    "memo": {
+      "title": "アシスタントのメモ",
+      "optional": "任意",
+      "placeholder": "内容や用途のメモを表示させることができます",
+      "description": "メモの内容はアシスタントの処理に影響しません。"
+    },
+    "submit_button": {
+      "update_assistant": "アシスタントを更新する",
+      "create_assistant": "アシスタントを作成する"
+    },
+    "toaster": {
+      "create_success": "アシスタントが作成されました",
+      "update_success": "アシスタントが更新されました",
+      "create_failed": "アシスタントの作成に失敗しました",
+      "update_failed": "アシスタントの更新に失敗しました"
+    },
     "default_instruction": "あなたはこのWikiの知識アシスタントです。以下の方針で支援を行ってください:\n\n- 文書の関連性分析と情報の関連付け\n- 新しい視点の提案\n- 質問の意図を理解した的確な情報提供 必要に応じて構造化された形式で情報を提供します。",
     "edit_page_description": " アシスタントが参照するページを編集します。<br> 参照できるページは配下ページも含めて {{limitLearnablePageCountPerAssistant}} ページまでです。",
+    "add_page_button": "ページを追加する",
     "page_mode_title": {
       "share": "アシスタントの共有",
       "pages": "参照ページ",
       "instruction": "アシスタントへの指示"
     },
+    "share_assistant": "アシスタントを共有する",
+    "page_access_permission": "ページのアクセス権限",
     "access_scope": {
       "owner": "{{username}} がアクセス可能な全てのページ",
       "groups": "グループを指定",
       "publicOnly": "公開ページのみ"
     },
     "share_scope": {
+      "title": "アシスタントの共有範囲",
       "owner": {
         "label": "{{username}} のみ"
       },
@@ -568,6 +596,37 @@
         "label": "ページのアクセス権限と同じ範囲",
         "desc": "ページのアクセス権限と同じ範囲で共有されます"
       }
+    },
+    "instructions": {
+      "description": "アシスタントの振る舞いを決める指示文を設定できます。<br>この指示に従ってにアシスタントの回答や分析を行います。",
+      "reset_to_default": "デフォルトに戻す"
+    }
+  },
+  "share_scope_warning_modal": {
+    "header_title": "共有範囲の確認",
+    "warning_message": "このアシスタントには限定公開されているページが含まれています。<br />現在の設定では、アシスタントを通じてこれらのページの情報が、本来のアクセス権限を超えて共有される可能性があります。",
+    "selected_pages_label": "選択されているページパス",
+    "confirmation_message": "続行する場合、これらのページの内容がアシスタントの公開範囲内で共有される可能性があることを確認してください。",
+    "button": {
+      "review": "設定を見直す",
+      "proceed": "理解して続行する"
+    }
+  },
+  "default_ai_assistant": {
+    "not_set": "デフォルトアシスタントが設定されていません"
+  },
+  "ai_assistant_tree": {
+    "add_assistant": "アシスタントを追加する",
+    "my_assistants": "マイアシスタント",
+    "team_assistants": "チームアシスタント",
+    "thread_does_not_exist": "スレッドが存在しません",
+    "toaster": {
+      "ai_assistant_deleted_success": "アシスタントを削除しました",
+      "ai_assistant_deleted_failed": "アシスタントの削除に失敗しました",
+      "thread_deleted_success": "スレッドを削除しました",
+      "thread_deleted_failed": "スレッドの削除に失敗しました",
+      "ai_assistant_set_default_success": "デフォルトアシスタントを設定しました",
+      "ai_assistant_set_default_failed": "デフォルトアシスタントの設定に失敗しました"
     }
   },
   "link_edit": {
@@ -939,8 +998,7 @@
   },
   "sidebar_header": {
     "show_wip_page": "WIP を表示",
-    "size_s": "サイズ: S",
-    "size_l": "サイズ: L"
+    "compact_view": "コンパクト表示"
   },
   "create_page": {
     "untitled": "無題のページ"

+ 61 - 3
apps/app/public/static/locales/zh_CN/translation.json

@@ -159,7 +159,7 @@
   "Bookmarks": "书签",
   "In-App Notification": "通知",
   "AI Assistant": "AI助手",
-  "Knowledge Assistant": "知识助手",
+  "Knowledge Assistant": "知识助手 (测试版)",
   "original_path": "Original path",
   "new_path": "New path",
   "duplicated_path": "Duplicated path",
@@ -485,6 +485,8 @@
     "selected_editable_revision": "选定的可编辑页面正文"
   },
   "sidebar_aichat": {
+    "instruction_label": "助手指令",
+    "reference_pages_label": "参考页面",
     "placeholder": "问我任何问题。",
     "summary_mode_label": "摘要模式",
     "summary_mode_help": "简洁回答在2-3句话内",
@@ -497,19 +499,45 @@
     "show_error_detail": "显示详情"
   },
   "modal_ai_assistant": {
+    "header": {
+      "update_assistant": "更新助手",
+      "add_new_assistant": "添加新助手"
+    },
+    "assistant_name_placeholder": "输入助手名称",
+    "page_count": "{{count}} 页",
+    "memo": {
+      "title": "助手备忘录",
+      "optional": "可选",
+      "placeholder": "您可以显示关于内容和用途的备注",
+      "description": "备忘录的内容不会影响助手的处理。"
+    },
+    "submit_button": {
+      "update_assistant": "更新助手",
+      "create_assistant": "创建助手"
+    },
+    "toaster": {
+      "create_success": "助手已创建",
+      "update_success": "助手已更新",
+      "create_failed": "创建助手失败",
+      "update_failed": "更新助手失败"
+    },
     "edit_page_description": "编辑助手可以参考的页面。<br> 助手可以参考最多 {{limitLearnablePageCountPerAssistant}} 个页面,包括子页面。",
     "default_instruction": "您是这个Wiki的知识助手。请按照以下方针提供支持:\n\n- 分析文档相关性并连接信息\n- 提出新的观点\n- 理解问题意图并提供准确信息\n必要时我会以结构化的形式提供信息。",
+    "add_page_button": "添加页面",
     "page_mode_title": {
       "share": "助理共享",
       "pages": "参考页面",
       "instruction": "助理指示"
     },
+    "share_assistant": "共享助手",
+    "page_access_permission": "页面访问权限",
     "access_scope": {
       "owner": "{{username}} 可访问的所有页面",
       "groups": "指定群组",
       "publicOnly": "仅公开页面"
     },
     "share_scope": {
+      "title": "助手共享范围",
       "owner": {
         "label": "仅 {{ username }}"
       },
@@ -525,6 +553,37 @@
         "label": "与页面访问范围相同",
         "desc": "与页面访问范围相同的范围共享"
       }
+    },
+    "instructions": {
+      "description": "您可以设置决定助手行为的指令。<br>助手将根据这些指令进行回答和分析。",
+      "reset_to_default": "恢复默认设置"
+    }
+  },
+  "share_scope_warning_modal": {
+    "header_title": "确认共享范围",
+    "warning_message": "此助手包含访问受限的页面。<br>使用当前设置,这些页面的信息可能通过助手超出其原始访问权限范围进行共享。",
+    "selected_pages_label": "已选择的页面路径",
+    "confirmation_message": "如果继续,请确认您了解这些页面的内容可能会在助手的公开范围内共享。",
+    "button": {
+      "review": "重新检查设置",
+      "proceed": "了解并继续"
+    }
+  },
+  "default_ai_assistant": {
+    "not_set": "未设置默认助手"
+  },
+  "ai_assistant_tree": {
+    "add_assistant": "添加助手",
+    "my_assistants": "我的助手",
+    "team_assistants": "团队助手",
+    "thread_does_not_exist": "暂无会话",
+    "toaster": {
+      "ai_assistant_deleted_success": "已删除助手",
+      "ai_assistant_deleted_failed": "删除助手失败",
+      "thread_deleted_success": "已删除会话",
+      "thread_deleted_failed": "删除会话失败",
+      "ai_assistant_set_default_success": "已成功设置默认助手",
+      "ai_assistant_set_default_failed": "设置默认助手失败"
     }
   },
   "link_edit": {
@@ -910,8 +969,7 @@
   },
   "sidebar_header": {
     "show_wip_page": "显示 WIP",
-    "size_s": "尺寸: S",
-    "size_l": "尺寸: L"
+    "compact_view": "紧凑视图"
   },
   "create_page": {
     "untitled": "Untitled"

+ 1 - 1
apps/app/src/client/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx

@@ -201,7 +201,7 @@ export const RecentChangesHeader = ({
                 onChange={() => {}}
               />
               <label className="form-check-label pe-none" aria-disabled="true">
-                {isSmall ? t('sidebar_header.size_s') : t('sidebar_header.size_l')}
+                {t('sidebar_header.compact_view')}
               </label>
             </div>
           </li>

+ 1 - 1
apps/app/src/client/components/Sidebar/SidebarNav/PrimaryItems.tsx

@@ -42,7 +42,7 @@ export const PrimaryItems = memo((props: Props) => {
           sidebarMode={sidebarMode}
           contents={SidebarContentsType.AI_ASSISTANT}
           label="AI Assistant"
-          iconName="ai_assistant"
+          iconName="growi_ai"
           isCustomIcon
           onHover={onItemHover}
         />

+ 6 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantChatSidebar/AiAssistantChatSidebar.module.scss

@@ -3,6 +3,12 @@
 @use '@growi/ui/scss/atoms/btn-muted';
 
 .grw-ai-assistant-chat-sidebar :global {
+  z-index: bs.$zindex-fixed + 2;
+  width: 100%;
+
+  @include bs.media-breakpoint-up(sm) {
+    width: 500px;
+  }
 
   .textarea-ask {
     max-height: 30vh;

+ 7 - 9
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantChatSidebar/AiAssistantChatSidebar.tsx

@@ -31,9 +31,6 @@ const logger = loggerFactory('growi:openai:client:components:AiAssistantChatSide
 
 const moduleClass = styles['grw-ai-assistant-chat-sidebar'] ?? '';
 
-const RIGHT_SIDEBAR_WIDTH = 500;
-
-
 const handleIfSuccessfullyParsed = <T, >(data: T, zSchema: z.ZodSchema<T>,
   callback: (data: T) => void,
 ): void => {
@@ -88,7 +85,9 @@ const AiAssistantChatSidebarSubstance: React.FC<AiAssistantChatSidebarSubstanceP
     const fetchAndSetMessageData = async() => {
       const messageData = await mutateMessageData();
       if (messageData != null) {
-        const normalizedMessageData = messageData.data.filter(message => message.metadata?.shouldHideMessage !== 'true');
+        const normalizedMessageData = messageData.data
+          .reverse()
+          .filter(message => message.metadata?.shouldHideMessage !== 'true');
 
         setMessageLogs(() => {
           return normalizedMessageData.map((message, index) => (
@@ -265,7 +264,7 @@ const AiAssistantChatSidebarSubstance: React.FC<AiAssistantChatSidebarSubstanceP
   return (
     <>
       <div className="d-flex flex-column vh-100">
-        <div className="d-flex align-items-center p-3 border-bottom">
+        <div className="d-flex align-items-center p-3 border-bottom position-sticky top-0 bg-body z-1">
           <span className="growi-custom-icons growi-ai-chat-icon me-3 fs-4">ai_assistant</span>
           <h5 className="mb-0 fw-bold flex-grow-1 text-truncate">{currentThreadTitle ?? aiAssistantData.name}</h5>
           <button
@@ -304,7 +303,7 @@ const AiAssistantChatSidebarSubstance: React.FC<AiAssistantChatSidebarSubstanceP
                 </p>
 
                 <div>
-                  <p className="text-body-secondary">アシスタントへの指示</p>
+                  <p className="text-body-secondary">{t('sidebar_aichat.instruction_label')}</p>
                   <div className="card bg-body-tertiary border-0">
                     <div className="card-body p-3">
                       <p className="fs-6 text-body-secondary mb-0">
@@ -316,7 +315,7 @@ const AiAssistantChatSidebarSubstance: React.FC<AiAssistantChatSidebarSubstanceP
 
                 <div>
                   <div className="d-flex align-items-center">
-                    <p className="text-body-secondary mb-0">参照するページ</p>
+                    <p className="text-body-secondary mb-0">{t('sidebar_aichat.reference_pages_label')}</p>
                   </div>
                   <div className="d-flex flex-column gap-1">
                     { aiAssistantData.pagePathPatterns.map(pagePathPattern => (
@@ -460,8 +459,7 @@ export const AiAssistantChatSidebar: FC = memo((): JSX.Element => {
   return (
     <div
       ref={sidebarRef}
-      className={`position-fixed top-0 end-0 h-100 border-start bg-body shadow-sm ${moduleClass}`}
-      style={{ zIndex: 1500, width: `${RIGHT_SIDEBAR_WIDTH}px` }}
+      className={`position-fixed top-0 end-0 h-100 border-start bg-body shadow-sm overflow-hidden ${moduleClass}`}
       data-testid="grw-right-sidebar"
     >
       <SimpleBar

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

@@ -40,7 +40,7 @@ export const AccessScopeDropdown: React.FC<Props> = (props: Props) => {
 
   return (
     <div className="mb-4">
-      <Label className="text-secondary mb-2">ページのアクセス権限</Label>
+      <Label className="text-secondary mb-2">{t('modal_ai_assistant.page_access_permission')}</Label>
       <UncontrolledDropdown>
         <DropdownToggle
           disabled={isDisabled}

+ 4 - 3
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditInstruction.tsx

@@ -1,3 +1,4 @@
+import { useTranslation } from 'react-i18next';
 import { ModalBody, Input } from 'reactstrap';
 
 import { AiAssistantManagementHeader } from './AiAssistantManagementHeader';
@@ -10,6 +11,7 @@ type Props = {
 
 export const AiAssistantManagementEditInstruction = (props: Props): JSX.Element => {
   const { instruction, onChange, onReset } = props;
+  const { t } = useTranslation();
 
   return (
     <>
@@ -17,8 +19,7 @@ export const AiAssistantManagementEditInstruction = (props: Props): JSX.Element
 
       <ModalBody className="px-4">
         <p className="text-secondary py-1">
-          アシスタントの振る舞いを決める指示文を設定できます。<br />
-          この指示に従ってにアシスタントの回答や分析を行います。
+          {t('modal_ai_assistant.instructions.description')}
         </p>
 
         <Input
@@ -31,7 +32,7 @@ export const AiAssistantManagementEditInstruction = (props: Props): JSX.Element
         />
 
         <button type="button" onClick={onReset} className="btn btn-outline-secondary btn-sm">
-          デフォルトに戻す
+          {t('modal_ai_assistant.instructions.reset_to_default')}
         </button>
       </ModalBody>
     </>

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

@@ -48,7 +48,7 @@ export const AiAssistantManagementEditPages = (props: Props): JSX.Element => {
           className="btn btn-outline-primary w-100 mb-3 d-flex align-items-center justify-content-center"
         >
           <span className="material-symbols-outlined me-2">add</span>
-          ページを追加する
+          {t('modal_ai_assistant.add_page_button')}
         </button>
 
         <SelectedPageList selectedPages={selectedPages} onRemove={onRemove} />

+ 3 - 1
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementEditShare.tsx

@@ -2,6 +2,7 @@ import React, {
   useCallback, useState, useEffect,
 } from 'react';
 
+import { useTranslation } from 'react-i18next';
 import {
   ModalBody, Input, Label,
 } from 'reactstrap';
@@ -45,6 +46,7 @@ export const AiAssistantManagementEditShare = (props: Props): JSX.Element => {
     onSelectAccessScopeUserGroups,
   } = props;
 
+  const { t } = useTranslation();
   const { data: userRelatedGroups } = useSWRxUserRelatedGroups();
   const hasNoRelatedGroups = userRelatedGroups == null || userRelatedGroups.relatedGroups.length === 0;
 
@@ -109,7 +111,7 @@ export const AiAssistantManagementEditShare = (props: Props): JSX.Element => {
             onChange={changeShareToggleHandler}
           />
           <Label className="form-check-label" for="shareAssistantSwitch">
-            アシスタントを共有する
+            {t('modal_ai_assistant.share_assistant')}
           </Label>
         </div>
 

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

@@ -115,8 +115,8 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
   return (
     <>
       <ModalHeader tag="h4" toggle={closeAiAssistantManagementModal} className="pe-4">
-        <span className="growi-custom-icons growi-ai-assistant-icon me-3 fs-4">ai_assistant</span>
-        <span className="fw-bold">{t(shouldEdit ? 'アシスタントの更新' : '新規アシスタントの追加')}</span> {/* TODO i18n */}
+        <span className="growi-custom-icons growi-ai-assistant-icon me-3 fs-4">growi_ai</span>
+        <span className="fw-bold">{t(shouldEdit ? 'modal_ai_assistant.header.update_assistant' : 'modal_ai_assistant.header.add_new_assistant')}</span>
       </ModalHeader>
 
       <div className="px-4">
@@ -124,7 +124,7 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
           <div className="mb-4 growi-ai-assistant-name">
             <Input
               type="text"
-              placeholder="アシスタント名を入力"
+              placeholder={t('modal_ai_assistant.assistant_name_placeholder')}
               bsSize="lg"
               className="border-0 border-bottom border-2 px-0 rounded-0"
               value={name}
@@ -134,18 +134,18 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
 
           <div className="mb-4">
             <div className="d-flex align-items-center mb-2">
-              <span className="text-secondary">アシスタントのメモ</span>
-              <span className="badge text-bg-secondary ms-2">任意</span>
+              <span className="text-secondary">{t('modal_ai_assistant.memo.title')}</span>
+              <span className="badge text-bg-secondary ms-2">{t('modal_ai_assistant.memo.optional')}</span>
             </div>
             <Input
               type="textarea"
-              placeholder="内容や用途のメモを表示させることができます"
+              placeholder={t('modal_ai_assistant.memo.placeholder')}
               rows="4"
               value={description}
               onChange={e => onDescriptionChange(e.target.value)}
             />
             <small className="text-secondary d-block mt-2">
-              メモの内容はアシスタントの処理に影響しません。
+              {t('modal_ai_assistant.memo.description')}
             </small>
           </div>
 
@@ -169,7 +169,7 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
             >
               <span className="fw-normal">{t('modal_ai_assistant.page_mode_title.pages')}</span>
               <div className="d-flex align-items-center text-secondary">
-                <span>{`${totalSelectedPageCount} ページ`}</span>
+                <span>{t('modal_ai_assistant.page_count', { count: totalSelectedPageCount })}</span>
                 <span className="material-symbols-outlined ms-2 align-middle">chevron_right</span>
               </div>
             </button>
@@ -196,7 +196,7 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
             className="btn btn-outline-secondary"
             onClick={closeAiAssistantManagementModal}
           >
-            キャンセル
+            {t('Cancel')}
           </button>
 
           <button
@@ -205,7 +205,7 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
             className="btn btn-primary"
             onClick={upsertAiAssistantHandler}
           >
-            {t(shouldEdit ? 'アシスタントを更新する' : 'アシスタントを作成する')}
+            {t(shouldEdit ? 'modal_ai_assistant.submit_button.update_assistant' : 'modal_ai_assistant.submit_button.create_assistant')}
           </button>
         </ModalFooter>
       </div>

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

@@ -150,12 +150,12 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
         await createAiAssistant(reqBody);
       }
 
-      toastSuccess(shouldEdit ? 'アシスタントが更新されました' : 'アシスタントが作成されました');
+      toastSuccess(shouldEdit ? t('modal_ai_assistant.toaster.update_success') : t('modal_ai_assistant.toaster.create_success'));
       mutateAiAssistants();
       closeAiAssistantManagementModal();
     }
     catch (err) {
-      toastError(shouldEdit ? 'アシスタントの更新に失敗しました' : 'アシスタントの作成に失敗しました');
+      toastError(shouldEdit ? t('modal_ai_assistant.toaster.update_failed') : t('modal_ai_assistant.toaster.create_failed'));
       logger.error(err);
     }
   // eslint-disable-next-line max-len

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

@@ -26,7 +26,7 @@ export const ShareScopeSwitch: React.FC<Props> = (props: Props) => {
 
   return (
     <div className="mb-4">
-      <Label className="text-secondary mb-3">アシスタントの共有範囲</Label>
+      <Label className="text-secondary mb-3">{t('modal_ai_assistant.share_scope.title')}</Label>
       <div className="d-flex flex-column gap-3">
 
         {[AiAssistantShareScope.PUBLIC_ONLY, AiAssistantShareScope.GROUPS, AiAssistantShareScope.SAME_AS_ACCESS_SCOPE].map(shareScope => (

+ 13 - 9
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/ShareScopeWarningModal.tsx

@@ -1,5 +1,6 @@
 import React, { useCallback } from 'react';
 
+import { useTranslation } from 'react-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
@@ -21,6 +22,8 @@ export const ShareScopeWarningModal = (props: Props): JSX.Element => {
     onSubmit,
   } = props;
 
+  const { t } = useTranslation();
+
   const upsertAiAssistantHandler = useCallback(() => {
     closeModal();
     onSubmit();
@@ -31,18 +34,19 @@ export const ShareScopeWarningModal = (props: Props): JSX.Element => {
       <ModalHeader toggle={closeModal}>
         <div className="d-flex align-items-center">
           <span className="material-symbols-outlined text-warning me-2 fs-4">warning</span>
-          <span className="text-warning fw-bold">共有範囲の確認</span>
+          <span className="text-warning fw-bold">{t('share_scope_warning_modal.header_title')}</span>
         </div>
       </ModalHeader>
 
       <ModalBody className="py-4 px-4">
-        <p className="mb-4">
-          このアシスタントには限定公開されているページが含まれています。<br />
-          現在の設定では、アシスタントを通じてこれらのページの情報が、本来のアクセス権限を超えて共有される可能性があります。
-        </p>
+        <p
+          className="mb-4"
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{ __html: t('share_scope_warning_modal.warning_message') }}
+        />
 
         <div className="mb-4">
-          <p className="mb-2 text-secondary">選択されているページパス</p>
+          <p className="mb-2 text-secondary">{t('share_scope_warning_modal.selected_pages_label')}</p>
           {selectedPages.map(selectedPage => (
             <code key={selectedPage.page.path}>
               {selectedPage.page.path}
@@ -51,7 +55,7 @@ export const ShareScopeWarningModal = (props: Props): JSX.Element => {
         </div>
 
         <p>
-          続行する場合、これらのページの内容がアシスタントの公開範囲内で共有される可能性があることを確認してください。
+          {t('share_scope_warning_modal.confirmation_message')}
         </p>
       </ModalBody>
 
@@ -61,7 +65,7 @@ export const ShareScopeWarningModal = (props: Props): JSX.Element => {
           className="btn btn-outline-secondary"
           onClick={closeModal}
         >
-          設定を見直す
+          {t('share_scope_warning_modal.button.review')}
         </button>
 
         <button
@@ -69,7 +73,7 @@ export const ShareScopeWarningModal = (props: Props): JSX.Element => {
           className="btn btn-warning"
           onClick={upsertAiAssistantHandler}
         >
-          理解して続行する
+          {t('share_scope_warning_modal.button.proceed')}
         </button>
       </ModalFooter>
     </Modal>

+ 4 - 1
apps/app/src/features/openai/client/components/AiAssistant/OpenDefaultAiAssistantButton.tsx

@@ -1,5 +1,7 @@
 import React, { useCallback, useMemo } from 'react';
 
+import { useTranslation } from 'react-i18next';
+
 import { NotAvailable } from '~/client/components/NotAvailable';
 import { NotAvailableForGuest } from '~/client/components/NotAvailableForGuest';
 import { useIsAiEnabled } from '~/stores-universal/context';
@@ -9,6 +11,7 @@ import { useAiAssistantChatSidebar, useSWRxAiAssistants } from '../../stores/ai-
 import styles from './OpenDefaultAiAssistantButton.module.scss';
 
 const OpenDefaultAiAssistantButton = (): JSX.Element => {
+  const { t } = useTranslation();
   const { data: isAiEnabled } = useIsAiEnabled();
   const { data: aiAssistantData } = useSWRxAiAssistants();
   const { open: openAiAssistantChatSidebar } = useAiAssistantChatSidebar();
@@ -36,7 +39,7 @@ const OpenDefaultAiAssistantButton = (): JSX.Element => {
 
   return (
     <NotAvailableForGuest>
-      <NotAvailable isDisabled={defaultAiAssistant == null} title="デフォルトアシスタントが設定されていません">
+      <NotAvailable isDisabled={defaultAiAssistant == null} title={t('default_ai_assistant.not_set')}>
         <button
           type="button"
           className={`btn btn-search ${styles['btn-open-default-ai-assistant']}`}

+ 6 - 3
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantSubstance.tsx

@@ -1,5 +1,7 @@
 import React from 'react';
 
+import { useTranslation } from 'react-i18next';
+
 import { useAiAssistantManagementModal, useSWRxAiAssistants } from '../../../stores/ai-assistant';
 
 import { AiAssistantTree } from './AiAssistantTree';
@@ -9,6 +11,7 @@ import styles from './AiAssistantSubstance.module.scss';
 const moduleClass = styles['grw-ai-assistant-substance'] ?? '';
 
 export const AiAssistantContent = (): JSX.Element => {
+  const { t } = useTranslation();
   const { open } = useAiAssistantManagementModal();
   const { data: aiAssistants, mutate: mutateAiAssistants } = useSWRxAiAssistants();
 
@@ -20,13 +23,13 @@ export const AiAssistantContent = (): JSX.Element => {
         onClick={() => open()}
       >
         <span className="material-symbols-outlined fs-5 me-2">add</span>
-        <span className="fw-normal">アシスタントを追加する</span>
+        <span className="fw-normal">{t('ai_assistant_tree.add_assistant')}</span>
       </button>
 
       <div className="d-flex flex-column gap-4">
         <div>
           <h3 className="fw-bold grw-ai-assistant-substance-header">
-            マイアシスタント
+            {t('ai_assistant_tree.my_assistants')}
           </h3>
           {aiAssistants?.myAiAssistants != null && aiAssistants.myAiAssistants.length !== 0 && (
             <AiAssistantTree
@@ -39,7 +42,7 @@ export const AiAssistantContent = (): JSX.Element => {
 
         <div>
           <h3 className="fw-bold grw-ai-assistant-substance-header">
-            チームアシスタント
+            {t('ai_assistant_tree.team_assistants')}
           </h3>
           {aiAssistants?.teamAiAssistants != null && aiAssistants.teamAiAssistants.length !== 0 && (
             <AiAssistantTree

+ 13 - 9
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantTree.tsx

@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
 
 import type { IUserHasId } from '@growi/core';
 import { getIdStringForRef } from '@growi/core';
+import { useTranslation } from 'react-i18next';
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import type { IThreadRelationHasId } from '~/features/openai/interfaces/thread-relation';
@@ -36,18 +37,19 @@ type ThreadItemProps = {
 const ThreadItem: React.FC<ThreadItemProps> = ({
   threadData, aiAssistantData, onThreadClick, onThreadDelete,
 }) => {
+  const { t } = useTranslation();
 
   const deleteThreadHandler = useCallback(async() => {
     try {
       await deleteThread({ aiAssistantId: aiAssistantData._id, threadRelationId: threadData._id });
-      toastSuccess('スレッドを削除しました');
+      toastSuccess(t('ai_assistant_tree.toaster.thread_deleted_success'));
       onThreadDelete();
     }
     catch (err) {
       logger.error(err);
-      toastError('スレッドの削除に失敗しました');
+      toastError(t('ai_assistant_tree.toaster.thread_deleted_failed'));
     }
-  }, [aiAssistantData._id, onThreadDelete, threadData._id]);
+  }, [aiAssistantData._id, onThreadDelete, t, threadData._id]);
 
   const openChatHandler = useCallback(() => {
     onThreadClick(aiAssistantData, threadData);
@@ -97,10 +99,11 @@ type ThreadItemsProps = {
 };
 
 const ThreadItems: React.FC<ThreadItemsProps> = ({ aiAssistantData, onThreadClick, onThreadDelete }) => {
+  const { t } = useTranslation();
   const { data: threads } = useSWRxThreads(aiAssistantData._id);
 
   if (threads == null || threads.length === 0) {
-    return <p className="text-secondary ms-5">スレッドが存在しません</p>;
+    return <p className="text-secondary ms-5">{t('ai_assistant_tree.thread_does_not_exist')}</p>;
   }
 
   return (
@@ -155,6 +158,7 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
 }) => {
   const [isThreadsOpened, setIsThreadsOpened] = useState(false);
 
+  const { t } = useTranslation();
   const { trigger: mutateThreadData } = useSWRMUTxThreads(aiAssistant._id);
 
   const openManagementModalHandler = useCallback((aiAssistantData: AiAssistantHasId) => {
@@ -174,23 +178,23 @@ const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
     try {
       await setDefaultAiAssistant(aiAssistant._id, !aiAssistant.isDefault);
       onUpdated?.();
-      toastSuccess('デフォルトアシスタントを切り替えました');
+      toastSuccess(t('ai_assistant_tree.toaster.ai_assistant_set_default_success'));
     }
     catch (err) {
       logger.error(err);
-      toastError('デフォルトアシスタントの切り替えに失敗しました');
+      toastError(t('ai_assistant_tree.toaster.ai_assistant_set_default_failed'));
     }
-  }, [aiAssistant._id, aiAssistant.isDefault, onUpdated]);
+  }, [aiAssistant._id, aiAssistant.isDefault, onUpdated, t]);
 
   const deleteAiAssistantHandler = useCallback(async() => {
     try {
       await deleteAiAssistant(aiAssistant._id);
       onDeleted?.();
-      toastSuccess('アシスタントを削除しました');
+      toastSuccess('ai_assistant_tree.toaster.assistant_deleted_success');
     }
     catch (err) {
       logger.error(err);
-      toastError('アシスタントの削除に失敗しました');
+      toastError('ai_assistant_tree.toaster.assistant_deleted');
     }
   }, [aiAssistant._id, onDeleted]);
 

+ 2 - 2
apps/app/src/features/openai/interfaces/thread-relation.ts

@@ -1,10 +1,10 @@
 import type { IUser, Ref, HasObjectId } from '@growi/core';
 
-import type { IVectorStore } from './vector-store';
+import type { AiAssistant } from './ai-assistant';
 
 export interface IThreadRelation {
   userId: Ref<IUser>
-  vectorStore: Ref<IVectorStore>
+  aiAssistant: Ref<AiAssistant>
   threadId: string;
   title?: string;
   expiredAt: Date;

+ 2 - 2
apps/app/src/features/openai/server/models/thread-relation.ts

@@ -25,9 +25,9 @@ const schema = new Schema<ThreadRelationDocument, ThreadRelationModel>({
     ref: 'User',
     required: true,
   },
-  vectorStore: {
+  aiAssistant: {
     type: Schema.Types.ObjectId,
-    ref: 'VectorStore',
+    ref: 'AiAssistant',
     required: true,
   },
   threadId: {

+ 2 - 7
apps/app/src/features/openai/server/routes/delete-ai-assistant.ts

@@ -11,7 +11,7 @@ import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 
-import { getOpenaiService } from '../services/openai';
+import { deleteAiAssistant } from '../services/delete-ai-assistant';
 
 import { certifyAiService } from './middlewares/certify-ai-service';
 
@@ -41,13 +41,8 @@ export const deleteAiAssistantsFactory: DeleteAiAssistantsFactory = (crowi) => {
       const { id } = req.params;
       const { user } = req;
 
-      const openaiService = getOpenaiService();
-      if (openaiService == null) {
-        return res.apiv3Err(new ErrorV3('GROWI AI is not enabled'), 501);
-      }
-
       try {
-        const deletedAiAssistant = await openaiService.deleteAiAssistant(user._id, id);
+        const deletedAiAssistant = await deleteAiAssistant(user._id, id);
         return res.apiv3({ deletedAiAssistant });
       }
       catch (err) {

+ 2 - 2
apps/app/src/features/openai/server/routes/get-messages.ts

@@ -53,13 +53,13 @@ export const getMessagesFactory: GetMessagesFactory = (crowi) => {
           threadId, aiAssistantId, limit, before, after,
         } = req.params;
 
-        const isAiAssistantUsable = openaiService.isAiAssistantUsable(aiAssistantId, req.user);
+        const isAiAssistantUsable = await openaiService.isAiAssistantUsable(aiAssistantId, req.user);
         if (!isAiAssistantUsable) {
           return res.apiv3Err(new ErrorV3('The specified AI assistant is not usable'), 400);
         }
 
         const messages = await openaiService.getMessageData(threadId, req.user.lang, {
-          limit, before, after, order: 'asc',
+          limit, before, after, order: 'desc',
         });
 
         return res.apiv3({ messages });

+ 1 - 2
apps/app/src/features/openai/server/routes/get-threads.ts

@@ -48,8 +48,7 @@ export const getThreadsFactory: GetThreadsFactory = (crowi) => {
           return res.apiv3Err(new ErrorV3('The specified AI assistant is not usable'), 400);
         }
 
-        const vectorStoreRelation = await openaiService.getVectorStoreRelation(aiAssistantId);
-        const threads = await openaiService.getThreads(vectorStoreRelation._id);
+        const threads = await openaiService.getThreadsByAiAssistantId(aiAssistantId);
 
         return res.apiv3({ threads });
       }

+ 3 - 6
apps/app/src/features/openai/server/routes/middlewares/upsert-ai-assistant-validator.ts

@@ -10,20 +10,17 @@ export const upsertAiAssistantValidator: ValidationChain[] = [
     .withMessage('name must be a string')
     .not()
     .isEmpty()
-    .withMessage('name is required')
-    .escape(),
+    .withMessage('name is required'),
 
   body('description')
     .optional()
     .isString()
-    .withMessage('description must be a string')
-    .escape(),
+    .withMessage('description must be a string'),
 
   body('additionalInstruction')
     .optional()
     .isString()
-    .withMessage('additionalInstruction must be a string')
-    .escape(),
+    .withMessage('additionalInstruction must be a string'),
 
   body('pagePathPatterns')
     .isArray()

+ 1 - 2
apps/app/src/features/openai/server/routes/thread.ts

@@ -50,8 +50,7 @@ export const createThreadHandlersFactory: CreateThreadFactory = (crowi) => {
           return res.apiv3Err(new ErrorV3('The specified AI assistant is not usable'), 400);
         }
 
-        const vectorStoreRelation = await openaiService.getVectorStoreRelation(aiAssistantId);
-        const thread = await openaiService.createThread(req.user._id, vectorStoreRelation, initialUserMessage);
+        const thread = await openaiService.createThread(req.user._id, aiAssistantId, initialUserMessage);
 
         return res.apiv3(thread);
       }

+ 10 - 0
apps/app/src/features/openai/server/services/client-delegator/azure-openai-client-delegator.ts

@@ -33,6 +33,16 @@ export class AzureOpenaiClientDelegator implements IOpenaiClientDelegator {
     });
   }
 
+  async updateThread(threadId: string, vectorStoreId: string): Promise<OpenAI.Beta.Threads.Thread> {
+    return this.client.beta.threads.update(threadId, {
+      tool_resources: {
+        file_search: {
+          vector_store_ids: [vectorStoreId],
+        },
+      },
+    });
+  }
+
   async retrieveThread(threadId: string): Promise<OpenAI.Beta.Threads.Thread> {
     return this.client.beta.threads.retrieve(threadId);
   }

+ 1 - 0
apps/app/src/features/openai/server/services/client-delegator/interfaces.ts

@@ -5,6 +5,7 @@ import type { MessageListParams } from '../../../interfaces/message';
 
 export interface IOpenaiClientDelegator {
   createThread(vectorStoreId: string): Promise<OpenAI.Beta.Threads.Thread>
+  updateThread(threadId: string, vectorStoreId: string): Promise<OpenAI.Beta.Threads.Thread>
   retrieveThread(threadId: string): Promise<OpenAI.Beta.Threads.Thread>
   deleteThread(threadId: string): Promise<OpenAI.Beta.Threads.ThreadDeleted>
   getMessages(threadId: string, options?: MessageListParams): Promise<OpenAI.Beta.Threads.Messages.MessagesPage>

+ 10 - 0
apps/app/src/features/openai/server/services/client-delegator/openai-client-delegator.ts

@@ -38,6 +38,16 @@ export class OpenaiClientDelegator implements IOpenaiClientDelegator {
     return this.client.beta.threads.retrieve(threadId);
   }
 
+  async updateThread(threadId: string, vectorStoreId: string): Promise<OpenAI.Beta.Threads.Thread> {
+    return this.client.beta.threads.update(threadId, {
+      tool_resources: {
+        file_search: {
+          vector_store_ids: [vectorStoreId],
+        },
+      },
+    });
+  }
+
   async deleteThread(threadId: string): Promise<OpenAI.Beta.Threads.ThreadDeleted> {
     return this.client.beta.threads.del(threadId);
   }

+ 1 - 1
apps/app/src/features/openai/server/services/cron/vector-store-file-deletion-cron.ts

@@ -47,7 +47,7 @@ export class VectorStoreFileDeletionCronService {
   }
 
   private async executeJob(): Promise<void> {
-    await this.openaiService.deleteObsolatedVectorStoreRelations();
+    await this.openaiService.deleteObsoletedVectorStoreRelations();
     await this.openaiService.deleteObsoleteVectorStoreFile(this.vectorStoreFileDeletionBarchSize, this.vectorStoreFileDeletionApiCallInterval);
   }
 

+ 51 - 0
apps/app/src/features/openai/server/services/delete-ai-assistant.ts

@@ -0,0 +1,51 @@
+import {
+  getIdStringForRef, type IUserHasId,
+} from '@growi/core';
+import createError from 'http-errors';
+
+import loggerFactory from '~/utils/logger';
+
+import type { AiAssistantDocument } from '../models/ai-assistant';
+import AiAssistantModel from '../models/ai-assistant';
+
+import { isAiEnabled } from './is-ai-enabled';
+import { getOpenaiService } from './openai';
+
+const logger = loggerFactory('growi:service:openai:delete-ai-assistant');
+
+
+export const deleteAiAssistant = async(ownerId: string, aiAssistantId: string): Promise<AiAssistantDocument> => {
+  const openaiService = getOpenaiService();
+  if (openaiService == null) {
+    throw createError(500, 'openaiService is not initialized');
+  }
+
+  const aiAssistant = await AiAssistantModel.findOne({ owner: ownerId, _id: aiAssistantId });
+  if (aiAssistant == null) {
+    throw createError(404, 'AiAssistant document does not exist');
+  }
+
+  const vectorStoreRelationId = getIdStringForRef(aiAssistant.vectorStore);
+  await openaiService.deleteVectorStore(vectorStoreRelationId);
+
+  const deletedAiAssistant = await aiAssistant.remove();
+  return deletedAiAssistant;
+};
+
+export const deleteUserAiAssistant = async(user: IUserHasId): Promise<void> => {
+  if (isAiEnabled()) {
+    const aiAssistants = await AiAssistantModel.find({ owner: user });
+    for await (const aiAssistant of aiAssistants) {
+      try {
+        await deleteAiAssistant(user._id, aiAssistant._id);
+      }
+      catch (err) {
+        logger.error(`Failed to delete AiAssistant ${aiAssistant._id}`);
+      }
+    }
+  }
+
+  // Cannot delete OpenAI VectorStore entities without enabling openaiService.
+  // Delete OpenAI VectorStore entities through "deleteVectorStoresOrphanedFromAiAssistant" when app starts with openaiService enabled
+  await AiAssistantModel.deleteMany({ owner: user });
+};

+ 3 - 3
apps/app/src/features/openai/server/services/normalize-data/normalize-thread-relation-expired-at/normalize-thread-relation-expired-at.integ.ts

@@ -15,7 +15,7 @@ describe('normalizeExpiredAtForThreadRelations', () => {
     const threadRelation = new ThreadRelation({
       userId: new Types.ObjectId(),
       threadId: 'test-thread',
-      vectorStore: new Types.ObjectId(),
+      aiAssistant: new Types.ObjectId(),
       expiredAt: expiredDate,
     });
     await threadRelation.save();
@@ -37,7 +37,7 @@ describe('normalizeExpiredAtForThreadRelations', () => {
     const threadRelation = new ThreadRelation({
       userId: new Types.ObjectId(),
       threadId: 'test-thread-2',
-      vectorStore: new Types.ObjectId(),
+      aiAssistant: new Types.ObjectId(),
       expiredAt: nonExpiredDate,
     });
     await threadRelation.save();
@@ -57,7 +57,7 @@ describe('normalizeExpiredAtForThreadRelations', () => {
     const threadRelation = new ThreadRelation({
       userId: new Types.ObjectId(),
       threadId: 'test-thread-3',
-      vectorStore: new Types.ObjectId(),
+      aiAssistant: new Types.ObjectId(),
       expiredAt: nonExpiredDate,
     });
     await threadRelation.save();

+ 26 - 27
apps/app/src/features/openai/server/services/openai.ts

@@ -65,17 +65,13 @@ const convertPathPatternsToRegExp = (pagePathPatterns: string[]): Array<string |
 };
 
 export interface IOpenaiService {
-  createThread(
-    userId: string, vectorStoreRelation: VectorStoreDocument, initialUserMessage: string
-  ): Promise<ThreadRelationDocument>;
-  getThreads(vectorStoreRelationId: string): Promise<ThreadRelationDocument[]>
+  createThread(userId: string, aiAssistantId: string, initialUserMessage: string): Promise<ThreadRelationDocument>;
+  getThreadsByAiAssistantId(aiAssistantId: string): Promise<ThreadRelationDocument[]>
   deleteThread(threadRelationId: string): Promise<ThreadRelationDocument>;
   deleteExpiredThreads(limit: number, apiCallInterval: number): Promise<void>; // for CronJob
-  deleteObsolatedVectorStoreRelations(): Promise<void> // for CronJob
+  deleteObsoletedVectorStoreRelations(): Promise<void> // for CronJob
   deleteVectorStore(vectorStoreRelationId: string): Promise<void>;
   getMessageData(threadId: string, lang?: Lang, options?: MessageListParams): Promise<OpenAI.Beta.Threads.Messages.MessagesPage>;
-  getVectorStoreRelation(aiAssistantId: string): Promise<VectorStoreDocument>
-  getVectorStoreRelationsByPageIds(pageId: Types.ObjectId[]): Promise<VectorStoreDocument[]>;
   createVectorStoreFile(vectorStoreRelation: VectorStoreDocument, pages: PageDocument[]): Promise<void>;
   createVectorStoreFileOnPageCreate(pages: PageDocument[]): Promise<void>;
   updateVectorStoreFileOnPageUpdate(page: HydratedDocument<PageDocument>): Promise<void>;
@@ -86,7 +82,6 @@ export interface IOpenaiService {
   createAiAssistant(data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
   updateAiAssistant(aiAssistantId: string, data: Omit<AiAssistant, 'vectorStore'>): Promise<AiAssistantDocument>;
   getAccessibleAiAssistants(user: IUserHasId): Promise<AccessibleAiAssistants>
-  deleteAiAssistant(ownerId: string, aiAssistantId: string): Promise<AiAssistantDocument>
   isLearnablePageLimitExceeded(user: IUserHasId, pagePathPatterns: string[]): Promise<boolean>;
 }
 class OpenaiService implements IOpenaiService {
@@ -122,7 +117,9 @@ class OpenaiService implements IOpenaiService {
     return threadTitle;
   }
 
-  async createThread(userId: string, vectorStoreRelation: VectorStoreDocument, initialUserMessage: string): Promise<ThreadRelationDocument> {
+  async createThread(userId: string, aiAssistantId: string, initialUserMessage: string): Promise<ThreadRelationDocument> {
+    const vectorStoreRelation = await this.getVectorStoreRelationByAiAssistantId(aiAssistantId);
+
     let threadTitle: string | null = null;
     if (initialUserMessage != null) {
       try {
@@ -137,8 +134,8 @@ class OpenaiService implements IOpenaiService {
       const thread = await this.client.createThread(vectorStoreRelation.vectorStoreId);
       const threadRelation = await ThreadRelationModel.create({
         userId,
+        aiAssistant: aiAssistantId,
         threadId: thread.id,
-        vectorStore: vectorStoreRelation._id,
         title: threadTitle,
       });
       return threadRelation;
@@ -148,8 +145,21 @@ class OpenaiService implements IOpenaiService {
     }
   }
 
-  async getThreads(vectorStoreRelationId: string): Promise<ThreadRelationDocument[]> {
-    const threadRelations = await ThreadRelationModel.find({ vectorStore: vectorStoreRelationId });
+  async updateThreads(aiAssistantId: string, vectorStoreId: string): Promise<void> {
+    const threadRelations = await this.getThreadsByAiAssistantId(aiAssistantId);
+    for await (const threadRelation of threadRelations) {
+      try {
+        const updatedThreadResponse = await this.client.updateThread(threadRelation.threadId, vectorStoreId);
+        logger.debug('Update thread', updatedThreadResponse);
+      }
+      catch (err) {
+        logger.error(err);
+      }
+    }
+  }
+
+  async getThreadsByAiAssistantId(aiAssistantId: string): Promise<ThreadRelationDocument[]> {
+    const threadRelations = await ThreadRelationModel.find({ aiAssistant: aiAssistantId });
     return threadRelations;
   }
 
@@ -211,7 +221,7 @@ class OpenaiService implements IOpenaiService {
   }
 
 
-  async getVectorStoreRelation(aiAssistantId: string): Promise<VectorStoreDocument> {
+  async getVectorStoreRelationByAiAssistantId(aiAssistantId: string): Promise<VectorStoreDocument> {
     const aiAssistant = await AiAssistantModel.findById({ _id: aiAssistantId }).populate('vectorStore');
     if (aiAssistant == null) {
       throw createError(404, 'AiAssistant document does not exist');
@@ -376,7 +386,7 @@ class OpenaiService implements IOpenaiService {
   }
 
   // Deletes all VectorStore documents that are marked as deleted (isDeleted: true) and have no associated VectorStoreFileRelation documents
-  async deleteObsolatedVectorStoreRelations(): Promise<void> {
+  async deleteObsoletedVectorStoreRelations(): Promise<void> {
     const deletedVectorStoreRelations = await VectorStoreModel.find({ isDeleted: true });
     if (deletedVectorStoreRelations.length === 0) {
       return;
@@ -812,6 +822,8 @@ class OpenaiService implements IOpenaiService {
 
       newVectorStoreRelation = await this.createVectorStore(data.name);
 
+      this.updateThreads(aiAssistantId, newVectorStoreRelation.vectorStoreId);
+
       // VectorStore creation process does not await
       this.createVectorStoreFileWithStream(newVectorStoreRelation, conditions);
     }
@@ -865,19 +877,6 @@ class OpenaiService implements IOpenaiService {
     };
   }
 
-  async deleteAiAssistant(ownerId: string, aiAssistantId: string): Promise<AiAssistantDocument> {
-    const aiAssistant = await AiAssistantModel.findOne({ owner: ownerId, _id: aiAssistantId });
-    if (aiAssistant == null) {
-      throw createError(404, 'AiAssistant document does not exist');
-    }
-
-    const vectorStoreRelationId = getIdStringForRef(aiAssistant.vectorStore);
-    await this.deleteVectorStore(vectorStoreRelationId);
-
-    const deletedAiAssistant = await aiAssistant.remove();
-    return deletedAiAssistant;
-  }
-
   async isLearnablePageLimitExceeded(user: IUserHasId, pagePathPatterns: string[]): Promise<boolean> {
     const normalizedPagePathPatterns = removeGlobPath(pagePathPatterns);
 

+ 3 - 0
apps/app/src/server/routes/apiv3/users.js

@@ -9,6 +9,7 @@ import { body, query } from 'express-validator';
 import { isEmail } from 'validator';
 
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
+import { deleteUserAiAssistant } from '~/features/openai/server/services/delete-ai-assistant';
 import { SupportedAction } from '~/interfaces/activity';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import Activity from '~/server/models/activity';
@@ -809,6 +810,8 @@ module.exports = (crowi) => {
       await user.statusDelete();
       await ExternalAccount.remove({ user });
 
+      deleteUserAiAssistant(user);
+
       const serializedUser = serializeUserSecurely(user);
 
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE });

+ 3 - 3
apps/app/src/server/service/config-manager/config-definition.ts

@@ -599,15 +599,15 @@ export const CONFIG_DEFINITIONS = {
     defaultValue: undefined,
   }),
   'security:passport-saml:entryPoint': defineConfig<string | undefined>({
-    envVarName: 'SECURITY_PASSPORT_SAML_ENTRY_POINT',
+    envVarName: 'SAML_ENTRY_POINT',
     defaultValue: undefined,
   }),
   'security:passport-saml:issuer': defineConfig<string | undefined>({
-    envVarName: 'SECURITY_PASSPORT_SAML_ISSUER',
+    envVarName: 'SAML_ISSUER',
     defaultValue: undefined,
   }),
   'security:passport-saml:cert': defineConfig<string | undefined>({
-    envVarName: 'SECURITY_PASSPORT_SAML_CERT',
+    envVarName: 'SAML_CERT',
     defaultValue: undefined,
   }),
   'security:passport-oidc:timeoutMultiplier': defineConfig<number>({

+ 11 - 3
apps/app/src/server/service/normalize-data/delete-legacy-knowledge-assistant-vector-store.ts → apps/app/src/server/service/normalize-data/delete-vector-stores-orphaned-from-ai-assistant.ts

@@ -2,8 +2,11 @@ import AiAssistantModel from '~/features/openai/server/models/ai-assistant';
 import VectorStoreRelationModel from '~/features/openai/server/models/vector-store';
 import { isAiEnabled } from '~/features/openai/server/services/is-ai-enabled';
 import { getOpenaiService } from '~/features/openai/server/services/openai';
+import loggerFactory from '~/utils/logger';
 
-export const deleteLegacyKnowledgeAssistantVectorStore = async(): Promise<void> => {
+const logger = loggerFactory('growi:service:normalize-data:delete-vector-stores-orphaned-from-ai-assistant');
+
+export const deleteVectorStoresOrphanedFromAiAssistant = async(): Promise<void> => {
   if (!isAiEnabled()) {
     return;
   }
@@ -20,7 +23,12 @@ export const deleteLegacyKnowledgeAssistantVectorStore = async(): Promise<void>
   // Logically delete only the VectorStore entities, leaving related documents to be automatically deleted by cron job
   const openaiService = getOpenaiService();
   for await (const vectorStoreRelation of nonDeletedLegacyKnowledgeAssistantVectorStoreRelations) {
-    const vectorStoreFileRelationId = vectorStoreRelation._id;
-    await openaiService?.deleteVectorStore(vectorStoreFileRelationId);
+    try {
+      const vectorStoreFileRelationId = vectorStoreRelation._id;
+      await openaiService?.deleteVectorStore(vectorStoreFileRelationId);
+    }
+    catch (err) {
+      logger.error(err);
+    }
   }
 };

+ 2 - 2
apps/app/src/server/service/normalize-data/index.ts

@@ -3,7 +3,7 @@ import loggerFactory from '~/utils/logger';
 
 import { convertNullToEmptyGrantedArrays } from './convert-null-to-empty-granted-arrays';
 import { convertRevisionPageIdToObjectId } from './convert-revision-page-id-to-objectid';
-import { deleteLegacyKnowledgeAssistantVectorStore } from './delete-legacy-knowledge-assistant-vector-store';
+import { deleteVectorStoresOrphanedFromAiAssistant } from './delete-vector-stores-orphaned-from-ai-assistant';
 import { renameDuplicateRootPages } from './rename-duplicate-root-pages';
 
 const logger = loggerFactory('growi:service:NormalizeData');
@@ -13,7 +13,7 @@ export const normalizeData = async(): Promise<void> => {
   await convertRevisionPageIdToObjectId();
   await normalizeExpiredAtForThreadRelations();
   await convertNullToEmptyGrantedArrays();
-  await deleteLegacyKnowledgeAssistantVectorStore();
+  await deleteVectorStoresOrphanedFromAiAssistant();
 
   logger.info('normalizeData has been executed');
   return;