Browse Source

Merge branch 'master' into support/migrate-to-pino

Yuki Takei 11 hours ago
parent
commit
439e11fa80

+ 108 - 0
.kiro/specs/news-inappnotification/requirements.md

@@ -0,0 +1,108 @@
+# Requirements Document
+
+## Introduction
+
+GROWI の InAppNotification にニュース配信・表示機能を追加する。外部の静的 JSON フィード(GitHub Pages)を GROWI 本体が cron で定期取得し、ローカル MongoDB にキャッシュした上で、InAppNotificationパネルおよび通知一覧ページにニュースとして表示する。
+
+ニュースは既存の InAppNotification とは別モデル(NewsItem)として管理する。InAppNotification はユーザーアクション起因で関係者のみに配信されるのに対し、ニュースは全ユーザー(またはロール単位)に配信されるため、1件のニュースを全ユーザーで共有する設計が SaaS 規模で効率的である。UI ではクライアント側で両データを時系列マージして統合表示する。
+
+## Requirements
+
+### Requirement 1: ニュースフィードの定期取得
+
+**Objective:** As a GROWI 運営者, I want GROWI が外部フィードからニュースを自動取得する, so that 各 GROWI インスタンスに最新のニュースが配信される
+
+#### Acceptance Criteria
+
+1. When cron スケジュールの実行時刻に達した場合, the News Cron Service shall 設定された URL から JSON フィードを HTTP GET で取得する
+2. When フィードの取得に成功した場合, the News Cron Service shall 取得したニュースアイテムをローカル MongoDB に upsert(`externalId` で重複排除)する
+3. When フィードに含まれなくなったニュースアイテムがある場合, the News Cron Service shall 該当アイテムをローカル DB から削除する
+4. When 複数の GROWI インスタンスが同時に取得を試みる場合, the News Cron Service shall ランダムスリープにより配信元へのリクエストを時間分散する
+5. If フィードの取得に失敗した場合, then the News Cron Service shall エラーをログに記録し、既存のキャッシュデータを維持する
+6. Where `NEWS_FEED_URL` が未設定または空の場合, the News Cron Service shall フィード取得をスキップしエラーなく動作する
+7. When ニュースアイテムに `growiVersionRegExps` 条件が設定されている場合, the News Cron Service shall 現在の GROWI バージョンと照合し、一致しないアイテムを除外する
+
+### Requirement 2: ニュースアイテムのローカルキャッシュ
+
+**Objective:** As a GROWI システム, I want 取得したニュースをローカル DB にキャッシュする, so that フィード配信元に障害が起きてもニュースを表示できる
+
+**Note:** NewsItem を既存の InAppNotification モデルで代替できない理由:①外部フィード由来コンテンツの重複排除に必要な `externalId`(ユニークインデックス)が InAppNotification に存在しない。②InAppNotification は per-user ドキュメント設計のため、ニュースに適用すると配信時点で全ユーザー分のドキュメントを強制生成する必要がある(例: 1000ユーザー × 10件 = 10,000件、さらに `snapshot` にニュース本文がユーザー数分コピーされる)。NewsItem は全ユーザーで1件を共有するため、SaaS規模で効率的である。③TTL管理(90日)はニュース固有の要件。
+
+#### Acceptance Criteria
+
+1. The NewsItem モデル shall `externalId` にユニークインデックスを持ち、重複登録を防止する
+2. The NewsItem モデル shall `publishedAt` にインデックスを持ち、公開日時順のソートを効率的に行う
+3. The NewsItem モデル shall `fetchedAt` に TTL インデックス(90日)を持ち、古いニュースを自動削除する
+4. The NewsItem モデル shall 多言語対応のタイトル・本文(`ja_JP`, `en_US`)を格納できる
+
+### Requirement 3: 既読/未読管理
+
+**Objective:** As a GROWI ユーザー, I want ニュースの既読/未読状態を管理したい, so that 新しいニュースを見逃さない
+
+**Note:** NewsReadStatus を既存の InAppNotification モデルで代替できない理由:InAppNotification の `status` フィールドは per-user ドキュメントに依存しており、ニュースの既読状態を管理するには配信時に全ユーザー分のドキュメントを作成しなければならない(1000ユーザー × 10件 = 配信時点で強制的に 10,000件)。NewsReadStatus はユーザーが実際に既読アクションを起こした時のみ作成される(未読はレコードなし)。全員が全件読まない限り実際のレコード数は常に 10,000件を下回り、SaaS規模でのストレージ効率が高い。
+
+#### Acceptance Criteria
+
+1. When ユーザーがニュースアイテムをクリックした場合, the News API shall 該当ユーザーとニュースアイテムの組み合わせで `NewsReadStatus` レコードを作成する
+2. While `NewsReadStatus` レコードが存在しない場合, the News API shall 該当ニュースを未読として扱う
+3. The NewsReadStatus モデル shall `userId + newsItemId` の複合ユニークインデックスにより重複登録を防止する
+4. When ニュース一覧を取得する場合, the News API shall 各ニュースアイテムに `isRead: true/false` を付与して返却する
+5. The News API shall ログインユーザーの未読ニュース数を返却するエンドポイントを提供する
+
+### Requirement 4: ロール別表示制御
+
+**Objective:** As a GROWI 運営者, I want ニュースの表示対象をロールで制御したい, so that 管理者向け情報を一般ユーザーに見せない
+
+**Note:** 表示制御はニュース配信側(GROWI運営)がフィードJSON内の `conditions.targetRoles` で指定する。インスタンス側(GROWI管理者)による制御は設けない。
+
+#### Acceptance Criteria
+
+1. When ニュースアイテムに `conditions.targetRoles` が設定されている場合, the News API shall ユーザーのロール(admin/general)に基づいてフィルタリングする
+2. When ニュースアイテムに `conditions.targetRoles` が未設定の場合, the News API shall 全ユーザーにニュースを表示する
+
+### Requirement 5: InAppNotification UI 統合表示
+
+**Objective:** As a GROWI ユーザー, I want 既存の InAppNotification UI でニュースを確認したい, so that 通知と同じ導線でニュースにアクセスできる
+
+**Note:** NewsItem と InAppNotification は別モデルとして維持する。UI のみクライアント側で両データを時系列マージして表示する。
+
+#### Acceptance Criteria
+
+1. The InAppNotificationパネル shall 通知とニュースを公開日時/作成日時の降順で混合した1つのリストとして表示する
+2. The InAppNotificationパネル shall 上部にフィルタボタン(「すべて」「通知」「お知らせ」)を配置し、デフォルトは「すべて」とする。「お知らせ」選択時はニュースのみ、「通知」選択時はニュース以外のすべての通知を表示する
+3. The InAppNotificationパネル shall 既存の「未読のみ」トグルスイッチを維持し、種別フィルタと組み合わせた2重フィルタリングを提供する。種別フィルタ(すべて/通知/お知らせ)で表示対象を絞り込んだ上で、トグルON時は未読アイテムのみをさらに絞り込む
+4. The InAppNotificationパネル shall リスト領域に最大高さを設定し、超過分はスクロールで表示する。スクロールが末端に達した場合は次のページを自動で読み込む無限スクロールとする
+5. The InAppNotificationパネル shall ニュースアイテムの `type` に応じた絵文字アイコンをタイトル前に表示する(`release`→🎉, `security`→⚠️, `tips`→💡, `maintenance`→🔧, `announcement`→📢, 未設定→📢)
+6. When ユーザーがニュースアイテムをクリックした場合, the InAppNotification UI shall ニュースの詳細 URL を新しいタブで開く
+7. When ユーザーがニュースアイテムをクリックした場合, the InAppNotification UI shall 該当ニュースを既読としてマークし、未読インジケータを更新する
+
+### Requirement 6: 既読/未読の視覚表示
+
+**Objective:** As a GROWI ユーザー, I want 未読のニュース・通知を視覚的に区別したい, so that 未確認の項目をすぐに見分けられる
+
+#### Acceptance Criteria
+
+1. The 未読アイテム shall タイトルを太字(`fw-bold`)で表示する
+2. The 未読アイテム shall 左端に青色の丸ドット(8px, `bg-primary`)を表示する
+3. The 既読アイテム shall タイトルを通常ウェイト(`fw-normal`)で表示する
+4. The 既読アイテム shall ドットと同じ幅の透明スペーサーを配置し、インデントを統一する
+
+### Requirement 7: 未読バッジ表示
+
+**Objective:** As a GROWI ユーザー, I want 未読ニュースの存在をバッジで把握したい, so that 新しいニュースがあることに気づける
+
+#### Acceptance Criteria
+
+1. The サイドバー通知アイコン shall 通知の未読数とニュースの未読数を合算してバッジに表示する
+2. When 全てのニュースが既読の場合, the バッジ shall ニュース分のカウントを含めない
+
+### Requirement 8: 多言語対応
+
+**Objective:** As a GROWI ユーザー, I want ニュースを自分の言語で読みたい, so that 内容を正しく理解できる
+
+#### Acceptance Criteria
+
+1. When ニュースアイテムに複数言語のテキストが含まれる場合, the NewsItem コンポーネント shall ブラウザの言語設定に応じたテキストを表示する
+2. If ブラウザの言語に対応するテキストが存在しない場合, then the NewsItem コンポーネント shall `ja_JP` → `en_US` の順にフォールバックする
+3. The UI ラベル(「ニュース」「ニュースはありません。」等)shall `ja_JP`, `en_US`, `zh_CN`, `ko_KR`, `fr_FR` の i18n ロケールファイルで提供する
+4. The フィルタボタン用ラベル(「通知」「お知らせ」)shall 全対応言語のロケールファイルに追加する

+ 22 - 0
.kiro/specs/news-inappnotification/spec.json

@@ -0,0 +1,22 @@
+{
+  "feature_name": "news-inappnotification",
+  "created_at": "2026-03-24T00:00:00Z",
+  "updated_at": "2026-03-24T01:00:00Z",
+  "language": "ja",
+  "phase": "requirements-generated",
+  "approvals": {
+    "requirements": {
+      "generated": true,
+      "approved": false
+    },
+    "design": {
+      "generated": false,
+      "approved": false
+    },
+    "tasks": {
+      "generated": false,
+      "approved": false
+    }
+  },
+  "ready_for_implementation": false
+}

+ 39 - 1
CHANGELOG.md

@@ -1,9 +1,47 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/growilabs/compare/v7.4.7...HEAD)
+## [Unreleased](https://github.com/growilabs/compare/v7.5.0...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v7.5.0](https://github.com/growilabs/compare/v7.4.7...v7.5.0) - 2026-04-07
+
+### 💎 Features
+
+* feat(ai): Suggest path to save (#10777) @tomoyuki-t-weseek
+* feat: Audit log bulk export (#10874) @Ryosei-Fukushima
+* feat(page-create-modal): add template help link icon (#10899) @tomoyuki-t-weseek
+* feat: add tooltips to editor toolbar (#10938) @Ryosei-Fukushima
+* feat: Add growi cloud link to audit log settings (#10881) @ryota-t0401
+
+### 🚀 Improvement
+
+* imprv: Staff credit (#10839) @yuki-takei
+* imprv(lsx): Allow spaces in attribute names (#10931) @NJisEverywhere
+* imprv: pre-fill export modal with current filter values (#10944) @NJisEverywhere
+* imprv(presentation): Decouple Marp from GrowiSlides (#10840) @yuki-takei
+* imprv(ui): Implement the improved New button. (#10937) @yuyaiwahori
+
+### 🐛 Bug Fixes
+
+* fix: re-scroll to hash target after lazy-rendered content completes (#10853) @miya
+* fix: Bulk export fails due to S3 upload complete version (#10833) @ryotaro-nagahara
+* fix: Duplicate user data is appearing in the user table at /user/admin (#10940) @miya
+* fix: Deleted users are not displayed in the user list on the user management page (/admin/users) (#10934) @miya
+* fix: Resolve React warnings during page rendering (#10913) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Upgrade Next.js to v16 (#10831) @yuki-takei
+* support(yjs): Migrate collaborative editing transport from y-socket.io to y-websocket (#10889) @yuki-takei
+* support: Upgrade vite v6, vitest v3, and related packages (#10945) @yuki-takei
+* ci(deps-dev): bump vite from 6.4.1 to 6.4.2 (#10960) @[dependabot[bot]](https://github.com/apps/dependabot)
+* support: Upgrade version-pinned packages and replace escape-string-regexp with RegExp.escape() (#10920) @yuki-takei
+* support: Migrate to Turbopack (#10838) @yuki-takei
+* support: Modernize Dockerfile (#10809) @yuki-takei
+* support: Reclassify deps (#10873) @yuki-takei
+* support(dev): Reduce modules loaded (#10822) @yuki-takei
+
 ## [v7.4.7](https://github.com/growilabs/compare/v7.4.6...v7.4.7) - 2026-03-23
 ## [v7.4.7](https://github.com/growilabs/compare/v7.4.6...v7.4.7) - 2026-03-23
 
 
 ### 💎 Features
 ### 💎 Features

+ 1 - 1
apps/app/docker/README.md

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`7.4.7`, `7.4`, `7`, `latest` (Dockerfile)](https://github.com/growilabs/growi/blob/v7.4.7/apps/app/docker/Dockerfile)
+* [`7.5.0`, `7.4`, `7`, `latest` (Dockerfile)](https://github.com/growilabs/growi/blob/v7.5.0/apps/app/docker/Dockerfile)
 * [`7.3.0`, `7.3` (Dockerfile)](https://github.com/growilabs/growi/blob/v7.3.0/apps/app/docker/Dockerfile)
 * [`7.3.0`, `7.3` (Dockerfile)](https://github.com/growilabs/growi/blob/v7.3.0/apps/app/docker/Dockerfile)
 * [`7.2.0`, `7.2` (Dockerfile)](https://github.com/growilabs/growi/blob/v7.2.0/apps/app/docker/Dockerfile)
 * [`7.2.0`, `7.2` (Dockerfile)](https://github.com/growilabs/growi/blob/v7.2.0/apps/app/docker/Dockerfile)
 
 

+ 1 - 1
apps/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "7.5.0-RC.0",
+  "version": "7.5.1-RC.0",
   "license": "MIT",
   "license": "MIT",
   "private": true,
   "private": true,
   "scripts": {
   "scripts": {

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

@@ -660,7 +660,9 @@
     }
     }
   },
   },
   "default_ai_assistant": {
   "default_ai_assistant": {
-    "not_set": "Default assistant is not set"
+    "not_set": "Default assistant is not set",
+    "open_cloud_settings_to_enable": "Please check the GROWI.cloud management screen to enable AI integration",
+    "to_cloud_settings": "Open GROWI.cloud Settings"
   },
   },
   "ai_assistant_substance": {
   "ai_assistant_substance": {
     "add_assistant": "Add Assistant",
     "add_assistant": "Add Assistant",

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

@@ -655,7 +655,9 @@
     }
     }
   },
   },
   "default_ai_assistant": {
   "default_ai_assistant": {
-    "not_set": "L'assistant par défaut n'est pas configuré"
+    "not_set": "L'assistant par défaut n'est pas configuré",
+    "open_cloud_settings_to_enable": "Veuillez consulter l'écran de gestion GROWI.cloud pour activer l'intégration AI",
+    "to_cloud_settings": "Ouvrir les paramètres GROWI.cloud"
   },
   },
   "ai_assistant_substance": {
   "ai_assistant_substance": {
     "add_assistant": "Ajouter un assistant",
     "add_assistant": "Ajouter un assistant",

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

@@ -693,7 +693,9 @@
     }
     }
   },
   },
   "default_ai_assistant": {
   "default_ai_assistant": {
-    "not_set": "デフォルトアシスタントが設定されていません"
+    "not_set": "デフォルトアシスタントが設定されていません",
+    "open_cloud_settings_to_enable": "AI 連携を有効にするには GROWI.cloud の管理画面をご確認ください",
+    "to_cloud_settings": "GROWI.cloud の管理画面へ"
   },
   },
   "ai_assistant_substance": {
   "ai_assistant_substance": {
     "add_assistant": "アシスタントを追加する",
     "add_assistant": "アシスタントを追加する",

+ 3 - 1
apps/app/public/static/locales/ko_KR/translation.json

@@ -620,7 +620,9 @@
     }
     }
   },
   },
   "default_ai_assistant": {
   "default_ai_assistant": {
-    "not_set": "기본 어시스턴트가 설정되지 않았습니다."
+    "not_set": "기본 어시스턴트가 설정되지 않았습니다.",
+    "open_cloud_settings_to_enable": "AI 통합을 활성화하려면 GROWI.cloud 관리 화면을 확인하십시오",
+    "to_cloud_settings": "GROWI.cloud 관리 화면 열기"
   },
   },
   "ai_assistant_substance": {
   "ai_assistant_substance": {
     "add_assistant": "어시스턴트 추가",
     "add_assistant": "어시스턴트 추가",

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

@@ -651,7 +651,9 @@
     }
     }
   },
   },
   "default_ai_assistant": {
   "default_ai_assistant": {
-    "not_set": "未设置默认助手"
+    "not_set": "未设置默认助手",
+    "open_cloud_settings_to_enable": "请查看 GROWI.cloud 管理界面以启用 AI 集成",
+    "to_cloud_settings": "前往 GROWI.cloud 管理界面"
   },
   },
   "ai_assistant_substance": {
   "ai_assistant_substance": {
     "add_assistant": "添加助手",
     "add_assistant": "添加助手",

+ 15 - 5
apps/app/src/client/components/NotAvailable.tsx

@@ -1,12 +1,16 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
 import { Disable } from 'react-disable';
 import { Disable } from 'react-disable';
 import type { UncontrolledTooltipProps } from 'reactstrap';
 import type { UncontrolledTooltipProps } from 'reactstrap';
-import { UncontrolledTooltip } from 'reactstrap';
+import {
+  PopoverBody,
+  UncontrolledPopover,
+  UncontrolledTooltip,
+} from 'reactstrap';
 
 
 type NotAvailableProps = {
 type NotAvailableProps = {
   children: JSX.Element;
   children: JSX.Element;
   isDisabled: boolean;
   isDisabled: boolean;
-  title: string;
+  title: string | JSX.Element;
   classNamePrefix?: string;
   classNamePrefix?: string;
   placement?: UncontrolledTooltipProps['placement'];
   placement?: UncontrolledTooltipProps['placement'];
 };
 };
@@ -29,9 +33,15 @@ export const NotAvailable = ({
       <div id={id}>
       <div id={id}>
         <Disable disabled={isDisabled}>{children}</Disable>
         <Disable disabled={isDisabled}>{children}</Disable>
       </div>
       </div>
-      <UncontrolledTooltip placement={placement} target={id}>
-        {title}
-      </UncontrolledTooltip>
+      {typeof title === 'string' ? (
+        <UncontrolledTooltip placement={placement} target={id}>
+          {title}
+        </UncontrolledTooltip>
+      ) : (
+        <UncontrolledPopover trigger="hover" placement={placement} target={id}>
+          <PopoverBody>{title}</PopoverBody>
+        </UncontrolledPopover>
+      )}
     </>
     </>
   );
   );
 };
 };

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

@@ -1,9 +1,12 @@
 import { memo } from 'react';
 import { memo } from 'react';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { useAtomValue } from 'jotai';
 import { useAtomValue } from 'jotai';
+import { useTranslation } from 'react-i18next';
 
 
+import { NotAvailable } from '~/client/components/NotAvailable';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { useIsGuestUser } from '~/states/context';
 import { useIsGuestUser } from '~/states/context';
+import { useGrowiAppIdForGrowiCloud, useGrowiCloudUri } from '~/states/global';
 import { aiEnabledAtom } from '~/states/server-configurations';
 import { aiEnabledAtom } from '~/states/server-configurations';
 import { useSidebarMode } from '~/states/ui/sidebar';
 import { useSidebarMode } from '~/states/ui/sidebar';
 
 
@@ -27,14 +30,35 @@ type Props = {
 export const PrimaryItems = memo((props: Props) => {
 export const PrimaryItems = memo((props: Props) => {
   const { onItemHover } = props;
   const { onItemHover } = props;
 
 
+  const { t } = useTranslation();
   const { sidebarMode } = useSidebarMode();
   const { sidebarMode } = useSidebarMode();
   const isAiEnabled = useAtomValue(aiEnabledAtom);
   const isAiEnabled = useAtomValue(aiEnabledAtom);
   const isGuestUser = useIsGuestUser();
   const isGuestUser = useIsGuestUser();
+  const growiCloudUri = useGrowiCloudUri();
+  const growiAppIdForGrowiCloud = useGrowiAppIdForGrowiCloud();
+  const isCloud = growiCloudUri != null && growiAppIdForGrowiCloud != null;
 
 
   if (sidebarMode == null) {
   if (sidebarMode == null) {
     return <></>;
     return <></>;
   }
   }
 
 
+  const aiAssistantNotAvailableTitle = (
+    <>
+      <p className="mb-2">
+        {t('default_ai_assistant.open_cloud_settings_to_enable')}
+      </p>
+      <a href={`${growiCloudUri}/my/apps/${growiAppIdForGrowiCloud}`}>
+        <span
+          className="material-symbols-outlined me-1"
+          style={{ fontSize: '1rem', verticalAlign: 'middle' }}
+        >
+          share
+        </span>
+        {t('default_ai_assistant.to_cloud_settings')}
+      </a>
+    </>
+  );
+
   return (
   return (
     <div className={`${styles['grw-primary-items']} mt-1`}>
     <div className={`${styles['grw-primary-items']} mt-1`}>
       <PrimaryItem
       <PrimaryItem
@@ -78,7 +102,7 @@ export const PrimaryItems = memo((props: Props) => {
           onHover={onItemHover}
           onHover={onItemHover}
         />
         />
       )}
       )}
-      {isAiEnabled && (
+      {isAiEnabled ? (
         <PrimaryItem
         <PrimaryItem
           sidebarMode={sidebarMode}
           sidebarMode={sidebarMode}
           contents={SidebarContentsType.AI_ASSISTANT}
           contents={SidebarContentsType.AI_ASSISTANT}
@@ -87,6 +111,22 @@ export const PrimaryItems = memo((props: Props) => {
           isCustomIcon
           isCustomIcon
           onHover={onItemHover}
           onHover={onItemHover}
         />
         />
+      ) : (
+        isCloud && (
+          <NotAvailable
+            isDisabled
+            title={aiAssistantNotAvailableTitle}
+            placement="right"
+          >
+            <PrimaryItem
+              sidebarMode={sidebarMode}
+              contents={SidebarContentsType.AI_ASSISTANT}
+              label="AI Assistant"
+              iconName="growi_ai"
+              isCustomIcon
+            />
+          </NotAvailable>
+        )
       )}
       )}
     </div>
     </div>
   );
   );

+ 47 - 10
apps/app/src/features/openai/client/components/AiAssistant/OpenDefaultAiAssistantButton.tsx

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
 
 
 import { NotAvailable } from '~/client/components/NotAvailable';
 import { NotAvailable } from '~/client/components/NotAvailable';
 import { NotAvailableForGuest } from '~/client/components/NotAvailableForGuest';
 import { NotAvailableForGuest } from '~/client/components/NotAvailableForGuest';
+import { useGrowiAppIdForGrowiCloud, useGrowiCloudUri } from '~/states/global';
 import { aiEnabledAtom } from '~/states/server-configurations';
 import { aiEnabledAtom } from '~/states/server-configurations';
 
 
 import { useAiAssistantSidebarActions } from '../../states';
 import { useAiAssistantSidebarActions } from '../../states';
@@ -11,6 +12,22 @@ import { useSWRxAiAssistants } from '../../stores/ai-assistant';
 
 
 import styles from './OpenDefaultAiAssistantButton.module.scss';
 import styles from './OpenDefaultAiAssistantButton.module.scss';
 
 
+const AiAssistantButton = ({
+  onClick,
+}: {
+  onClick?: () => void;
+}): JSX.Element => (
+  <button
+    type="button"
+    className={`btn btn-search ${styles['btn-open-default-ai-assistant']}`}
+    onClick={onClick}
+  >
+    <span className="growi-custom-icons fs-4 align-middle lh-1">
+      ai_assistant
+    </span>
+  </button>
+);
+
 const OpenDefaultAiAssistantButtonSubstance = (): JSX.Element => {
 const OpenDefaultAiAssistantButtonSubstance = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: aiAssistantData } = useSWRxAiAssistants();
   const { data: aiAssistantData } = useSWRxAiAssistants();
@@ -42,15 +59,7 @@ const OpenDefaultAiAssistantButtonSubstance = (): JSX.Element => {
         isDisabled={defaultAiAssistant == null}
         isDisabled={defaultAiAssistant == null}
         title={t('default_ai_assistant.not_set')}
         title={t('default_ai_assistant.not_set')}
       >
       >
-        <button
-          type="button"
-          className={`btn btn-search ${styles['btn-open-default-ai-assistant']}`}
-          onClick={openDefaultAiAssistantButtonClickHandler}
-        >
-          <span className="growi-custom-icons fs-4 align-middle lh-1">
-            ai_assistant
-          </span>
-        </button>
+        <AiAssistantButton onClick={openDefaultAiAssistantButtonClickHandler} />
       </NotAvailable>
       </NotAvailable>
     </NotAvailableForGuest>
     </NotAvailableForGuest>
   );
   );
@@ -58,9 +67,37 @@ const OpenDefaultAiAssistantButtonSubstance = (): JSX.Element => {
 
 
 const OpenDefaultAiAssistantButton = (): JSX.Element => {
 const OpenDefaultAiAssistantButton = (): JSX.Element => {
   const isAiEnabled = useAtomValue(aiEnabledAtom);
   const isAiEnabled = useAtomValue(aiEnabledAtom);
+  const { t } = useTranslation();
+  const growiCloudUri = useGrowiCloudUri();
+  const growiAppIdForGrowiCloud = useGrowiAppIdForGrowiCloud();
+  const isCloud = growiCloudUri != null && growiAppIdForGrowiCloud != null;
 
 
   if (!isAiEnabled) {
   if (!isAiEnabled) {
-    return <></>;
+    if (!isCloud) return <></>;
+
+    return (
+      <NotAvailable
+        isDisabled
+        title={
+          <>
+            <p className="mb-2">
+              {t('default_ai_assistant.open_cloud_settings_to_enable')}
+            </p>
+            <a href={`${growiCloudUri}/my/apps/${growiAppIdForGrowiCloud}`}>
+              <span
+                className="material-symbols-outlined me-1"
+                style={{ fontSize: '1rem', verticalAlign: 'middle' }}
+              >
+                share
+              </span>
+              {t('default_ai_assistant.to_cloud_settings')}
+            </a>
+          </>
+        }
+      >
+        <AiAssistantButton />
+      </NotAvailable>
+    );
   }
   }
 
 
   return <OpenDefaultAiAssistantButtonSubstance />;
   return <OpenDefaultAiAssistantButtonSubstance />;

+ 1 - 1
apps/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slackbot-proxy",
   "name": "@growi/slackbot-proxy",
-  "version": "7.5.0-slackbot-proxy.0",
+  "version": "7.5.1-slackbot-proxy.0",
   "license": "MIT",
   "license": "MIT",
   "private": true,
   "private": true,
   "scripts": {
   "scripts": {

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "7.5.0-RC.0",
+  "version": "7.5.1-RC.0",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "license": "MIT",
   "license": "MIT",
   "private": true,
   "private": true,