mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-06 11:20:56 -05:00
feat: gate AI chart generation behind all 3 AI checks
- Server-side: Replace hardcoded OpenAI with provider-agnostic `getAiModel(env)` and enforce `assertOrganizationAIConfigured(organizationId, "dataAnalysis")` which validates license entitlement, org-level toggle, and instance configuration - Client-side: Instead of hiding AI section when unavailable, show it disabled with a tooltip explaining the reason (not in plan / not enabled / instance not configured), following the same pattern as AI translate - Thread `isAIAvailable` and `aiUnavailableReason` through the component chain from server pages down to `AIQuerySection` - Update test mocks to match new provider-agnostic AI imports
This commit is contained in:
@@ -1597,6 +1597,10 @@ checksums:
|
||||
workspace/analysis/charts/add_filter: ed5d8e9bfcb05cd1e10e4c403befbae6
|
||||
workspace/analysis/charts/add_to_dashboard: 9941c3d30895bb8e25ce8d4e03d33a08
|
||||
workspace/analysis/charts/advanced_chart_builder_config_prompt: c2fe2c1a076f27d3ae62a4db75474b0a
|
||||
workspace/analysis/charts/ai_instance_not_configured: 6deeb8aeaff3982d07e1d5a045e06d2d
|
||||
workspace/analysis/charts/ai_not_available: 173abfcd32dd45edcc258dfdaaed494b
|
||||
workspace/analysis/charts/ai_not_enabled: 8651fdac58cd311d17a48001a880318d
|
||||
workspace/analysis/charts/ai_not_in_plan: 60bb0792a1ed98c07d8694029cdfdb43
|
||||
workspace/analysis/charts/ai_query_placeholder: 24c3d18f514cb3a9953f04c3b04503a2
|
||||
workspace/analysis/charts/ai_query_section_description: 66d06342f29bf6658793403856521fd7
|
||||
workspace/analysis/charts/ai_query_section_title: c0e450a47af7c2a516b77f73cf54db1b
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "Filter hinzufügen",
|
||||
"add_to_dashboard": "Zum Dashboard hinzufügen",
|
||||
"advanced_chart_builder_config_prompt": "Konfiguriere dein Diagramm und klicke auf \"Abfrage ausführen\", um eine Vorschau zu sehen",
|
||||
"ai_instance_not_configured": "KI ist auf dieser Instanz nicht konfiguriert. Kontaktiere deinen Administrator.",
|
||||
"ai_not_available": "KI-Datenanalyse ist nicht verfügbar.",
|
||||
"ai_not_enabled": "KI-Datenanalyse ist für diese Organisation deaktiviert. Aktiviere sie in den Organisationseinstellungen.",
|
||||
"ai_not_in_plan": "KI-Datenanalyse ist in deinem aktuellen Tarif nicht verfügbar. Führe ein Upgrade durch, um diese Funktion freizuschalten.",
|
||||
"ai_query_placeholder": "z.B. Wie viele Nutzer haben sich letzte Woche angemeldet?",
|
||||
"ai_query_section_description": "Beschreibe, was du sehen möchtest, und lass die KI das Diagramm erstellen.",
|
||||
"ai_query_section_title": "Frag deine Daten",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "Add filter",
|
||||
"add_to_dashboard": "Add to Dashboard",
|
||||
"advanced_chart_builder_config_prompt": "Configure your chart and click \"Run Query\" to preview",
|
||||
"ai_instance_not_configured": "AI is not configured on this instance. Contact your administrator.",
|
||||
"ai_not_available": "AI data analysis is not available.",
|
||||
"ai_not_enabled": "AI data analysis is disabled for this organization. Enable it in organization settings.",
|
||||
"ai_not_in_plan": "AI data analysis is not available on your current plan. Upgrade to unlock this feature.",
|
||||
"ai_query_placeholder": "e.g. How many users signed up last week?",
|
||||
"ai_query_section_description": "Describe what you want to see and let AI build the chart.",
|
||||
"ai_query_section_title": "Ask your data",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "Añadir filtro",
|
||||
"add_to_dashboard": "Añadir al panel de control",
|
||||
"advanced_chart_builder_config_prompt": "Configura tu gráfico y haz clic en \"Ejecutar consulta\" para previsualizar",
|
||||
"ai_instance_not_configured": "La IA no está configurada en esta instancia. Contacta con tu administrador.",
|
||||
"ai_not_available": "El análisis de datos con IA no está disponible.",
|
||||
"ai_not_enabled": "El análisis de datos con IA está desactivado para esta organización. Actívalo en la configuración de la organización.",
|
||||
"ai_not_in_plan": "El análisis de datos con IA no está disponible en tu plan actual. Actualiza para desbloquear esta función.",
|
||||
"ai_query_placeholder": "p. ej. ¿Cuántos usuarios se registraron la semana pasada?",
|
||||
"ai_query_section_description": "Describe lo que quieres ver y deja que la IA construya el gráfico.",
|
||||
"ai_query_section_title": "Pregunta a tus datos",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "Ajouter un filtre",
|
||||
"add_to_dashboard": "Ajouter au tableau de bord",
|
||||
"advanced_chart_builder_config_prompt": "Configurez votre graphique et cliquez sur « Exécuter la requête » pour prévisualiser",
|
||||
"ai_instance_not_configured": "L'IA n'est pas configurée sur cette instance. Contacte ton administrateur.",
|
||||
"ai_not_available": "L'analyse de données par IA n'est pas disponible.",
|
||||
"ai_not_enabled": "L'analyse de données par IA est désactivée pour cette organisation. Active-la dans les paramètres de l'organisation.",
|
||||
"ai_not_in_plan": "L'analyse de données par IA n'est pas disponible sur ton forfait actuel. Passe à un forfait supérieur pour débloquer cette fonctionnalité.",
|
||||
"ai_query_placeholder": "ex. Combien d'utilisateurs se sont inscrits la semaine dernière ?",
|
||||
"ai_query_section_description": "Décrivez ce que vous souhaitez voir et laissez l'IA créer le graphique.",
|
||||
"ai_query_section_title": "Interrogez vos données",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "Szűrő hozzáadása",
|
||||
"add_to_dashboard": "Hozzáadás a vezérlőpulthoz",
|
||||
"advanced_chart_builder_config_prompt": "Állítsd be a diagramot, és kattints a \"Lekérdezés futtatása\" gombra az előnézethez",
|
||||
"ai_instance_not_configured": "Az AI nincs konfigurálva ezen a példányon. Kérjük, lépjen kapcsolatba a rendszergazdával.",
|
||||
"ai_not_available": "Az AI adatelemzés nem elérhető.",
|
||||
"ai_not_enabled": "Az AI adatelemzés le van tiltva ezen szervezet számára. Kérjük, engedélyezze a szervezeti beállításokban.",
|
||||
"ai_not_in_plan": "Az AI adatelemzés nem elérhető az Ön jelenlegi csomagjában. Kérjük, frissítsen magasabb csomagra ezen funkció feloldásához.",
|
||||
"ai_query_placeholder": "pl. Hány felhasználó regisztrált a múlt héten?",
|
||||
"ai_query_section_description": "Írd le, mit szeretnél látni, és hagyd, hogy az AI elkészítse a diagramot.",
|
||||
"ai_query_section_title": "Kérdezd meg az adataidat",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "フィルターを追加",
|
||||
"add_to_dashboard": "ダッシュボードに追加",
|
||||
"advanced_chart_builder_config_prompt": "チャートを設定して「クエリを実行」をクリックしてプレビューを表示",
|
||||
"ai_instance_not_configured": "このインスタンスではAIが設定されていません。管理者にお問い合わせください。",
|
||||
"ai_not_available": "AIデータ分析は利用できません。",
|
||||
"ai_not_enabled": "この組織ではAIデータ分析が無効になっています。組織設定で有効にしてください。",
|
||||
"ai_not_in_plan": "AIデータ分析は現在のプランではご利用いただけません。この機能を利用するにはアップグレードしてください。",
|
||||
"ai_query_placeholder": "例: 先週何人のユーザーが登録しましたか?",
|
||||
"ai_query_section_description": "表示したい内容を説明すると、AIがチャートを作成します。",
|
||||
"ai_query_section_title": "データに質問する",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "Filter toevoegen",
|
||||
"add_to_dashboard": "Toevoegen aan dashboard",
|
||||
"advanced_chart_builder_config_prompt": "Configureer je grafiek en klik op \"Query uitvoeren\" om een voorbeeld te zien",
|
||||
"ai_instance_not_configured": "AI is niet geconfigureerd op deze instantie. Neem contact op met je beheerder.",
|
||||
"ai_not_available": "AI-data-analyse is niet beschikbaar.",
|
||||
"ai_not_enabled": "AI-data-analyse is uitgeschakeld voor deze organisatie. Schakel het in bij de organisatie-instellingen.",
|
||||
"ai_not_in_plan": "AI-data-analyse is niet beschikbaar in je huidige abonnement. Upgrade om deze functie te ontgrendelen.",
|
||||
"ai_query_placeholder": "bijv. Hoeveel gebruikers hebben zich vorige week aangemeld?",
|
||||
"ai_query_section_description": "Beschrijf wat je wilt zien en laat AI de grafiek bouwen.",
|
||||
"ai_query_section_title": "Vraag het aan je data",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "Adicionar filtro",
|
||||
"add_to_dashboard": "Adicionar ao painel",
|
||||
"advanced_chart_builder_config_prompt": "Configure seu gráfico e clique em \"Executar consulta\" para visualizar",
|
||||
"ai_instance_not_configured": "A IA não está configurada nesta instância. Entre em contato com seu administrador.",
|
||||
"ai_not_available": "A análise de dados com IA não está disponível.",
|
||||
"ai_not_enabled": "A análise de dados com IA está desabilitada para esta organização. Habilite nas configurações da organização.",
|
||||
"ai_not_in_plan": "A análise de dados com IA não está disponível no seu plano atual. Faça upgrade para desbloquear este recurso.",
|
||||
"ai_query_placeholder": "ex: Quantos usuários se cadastraram na semana passada?",
|
||||
"ai_query_section_description": "Descreva o que você quer ver e deixe a IA construir o gráfico.",
|
||||
"ai_query_section_title": "Pergunte aos seus dados",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "Adicionar filtro",
|
||||
"add_to_dashboard": "Adicionar ao painel",
|
||||
"advanced_chart_builder_config_prompt": "Configura o teu gráfico e clica em \"Executar consulta\" para pré-visualizar",
|
||||
"ai_instance_not_configured": "A IA não está configurada nesta instância. Contacta o teu administrador.",
|
||||
"ai_not_available": "A análise de dados por IA não está disponível.",
|
||||
"ai_not_enabled": "A análise de dados por IA está desativada para esta organização. Ativa-a nas definições da organização.",
|
||||
"ai_not_in_plan": "A análise de dados por IA não está disponível no teu plano atual. Faz upgrade para desbloquear esta funcionalidade.",
|
||||
"ai_query_placeholder": "ex: Quantos utilizadores se registaram na semana passada?",
|
||||
"ai_query_section_description": "Descreve o que queres ver e deixa a IA construir o gráfico.",
|
||||
"ai_query_section_title": "Pergunta aos teus dados",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "Adaugă filtru",
|
||||
"add_to_dashboard": "Adaugă la Tablou de Bord",
|
||||
"advanced_chart_builder_config_prompt": "Configurează graficul și apasă pe \"Rulează interogarea\" pentru previzualizare",
|
||||
"ai_instance_not_configured": "AI nu este configurat pe această instanță. Contactează administratorul.",
|
||||
"ai_not_available": "Analiza datelor cu AI nu este disponibilă.",
|
||||
"ai_not_enabled": "Analiza datelor cu AI este dezactivată pentru această organizație. Activează-o în setările organizației.",
|
||||
"ai_not_in_plan": "Analiza datelor cu AI nu este disponibilă în planul tău actual. Treci la un plan superior pentru a debloca această funcție.",
|
||||
"ai_query_placeholder": "ex: Câți utilizatori s-au înscris săptămâna trecută?",
|
||||
"ai_query_section_description": "Descrie ce vrei să vezi și lasă AI-ul să construiască graficul.",
|
||||
"ai_query_section_title": "Întreabă-ți datele",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "Добавить фильтр",
|
||||
"add_to_dashboard": "Добавить на панель",
|
||||
"advanced_chart_builder_config_prompt": "Настрой график и нажми «Выполнить запрос», чтобы посмотреть предварительный просмотр",
|
||||
"ai_instance_not_configured": "ИИ не настроен на этом экземпляре. Свяжитесь с администратором.",
|
||||
"ai_not_available": "Анализ данных с помощью ИИ недоступен.",
|
||||
"ai_not_enabled": "Анализ данных с помощью ИИ отключён для этой организации. Включите его в настройках организации.",
|
||||
"ai_not_in_plan": "Анализ данных с помощью ИИ недоступен в вашем текущем тарифе. Обновите тариф, чтобы получить эту функцию.",
|
||||
"ai_query_placeholder": "например: Сколько пользователей зарегистрировались на прошлой неделе?",
|
||||
"ai_query_section_description": "Опиши, что хочешь увидеть, и AI построит график.",
|
||||
"ai_query_section_title": "Спроси свои данные",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "Lägg till filter",
|
||||
"add_to_dashboard": "Lägg till på instrumentpanelen",
|
||||
"advanced_chart_builder_config_prompt": "Konfigurera ditt diagram och klicka på \"Kör fråga\" för att förhandsgranska",
|
||||
"ai_instance_not_configured": "AI är inte konfigurerad på denna instans. Kontakta din administratör.",
|
||||
"ai_not_available": "AI-dataanalys är inte tillgänglig.",
|
||||
"ai_not_enabled": "AI-dataanalys är inaktiverad för denna organisation. Aktivera det i organisationsinställningarna.",
|
||||
"ai_not_in_plan": "AI-dataanalys är inte tillgänglig i din nuvarande plan. Uppgradera för att låsa upp denna funktion.",
|
||||
"ai_query_placeholder": "t.ex. Hur många användare registrerade sig förra veckan?",
|
||||
"ai_query_section_description": "Beskriv vad du vill se så bygger AI diagrammet åt dig.",
|
||||
"ai_query_section_title": "Fråga din data",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "Filtre ekle",
|
||||
"add_to_dashboard": "Panoya Ekle",
|
||||
"advanced_chart_builder_config_prompt": "Grafiğini yapılandır ve önizleme için \"Sorguyu Çalıştır\"a tıkla",
|
||||
"ai_instance_not_configured": "Bu örnekte AI yapılandırılmamış. Yöneticinle iletişime geç.",
|
||||
"ai_not_available": "AI veri analizi mevcut değil.",
|
||||
"ai_not_enabled": "Bu organizasyon için AI veri analizi devre dışı. Organizasyon ayarlarından etkinleştir.",
|
||||
"ai_not_in_plan": "AI veri analizi mevcut planında bulunmuyor. Bu özelliğin kilidini açmak için yükselt.",
|
||||
"ai_query_placeholder": "örn. Geçen hafta kaç kullanıcı kaydoldu?",
|
||||
"ai_query_section_description": "Ne görmek istediğini anlat, AI grafiği oluştursun.",
|
||||
"ai_query_section_title": "Verilerine sor",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "添加过滤器",
|
||||
"add_to_dashboard": "添加到 Dashboard",
|
||||
"advanced_chart_builder_config_prompt": "配置你的图表,然后点击“运行查询”预览",
|
||||
"ai_instance_not_configured": "此实例未配置 AI。请联系您的管理员。",
|
||||
"ai_not_available": "AI 数据分析不可用。",
|
||||
"ai_not_enabled": "此组织已禁用 AI 数据分析。请在组织设置中启用。",
|
||||
"ai_not_in_plan": "您当前的套餐不包含 AI 数据分析。升级以解锁此功能。",
|
||||
"ai_query_placeholder": "例如:上周有多少用户注册?",
|
||||
"ai_query_section_description": "描述你想要看到的内容,让 AI 帮你生成图表。",
|
||||
"ai_query_section_title": "向你的数据提问",
|
||||
|
||||
@@ -1662,6 +1662,10 @@
|
||||
"add_filter": "新增篩選器",
|
||||
"add_to_dashboard": "新增到儀表板",
|
||||
"advanced_chart_builder_config_prompt": "設定你的圖表,然後點擊「執行查詢」預覽",
|
||||
"ai_instance_not_configured": "此執行個體未設定 AI。請聯絡您的管理員。",
|
||||
"ai_not_available": "AI 資料分析無法使用。",
|
||||
"ai_not_enabled": "此組織已停用 AI 資料分析。請在組織設定中啟用。",
|
||||
"ai_not_in_plan": "您目前的方案不包含 AI 資料分析。請升級以解鎖此功能。",
|
||||
"ai_query_placeholder": "例如:上週有多少用戶註冊?",
|
||||
"ai_query_section_description": "描述你想看到的內容,讓 AI 幫你建立圖表。",
|
||||
"ai_query_section_title": "詢問你的數據",
|
||||
|
||||
@@ -11,7 +11,8 @@ const mocks = vi.hoisted(() => {
|
||||
checkFeedbackDirectoryAccess: vi.fn(),
|
||||
getIsDashboardsEnabled: vi.fn(),
|
||||
createChart: vi.fn(),
|
||||
createOpenAI: vi.fn(),
|
||||
getAiModel: vi.fn(),
|
||||
assertOrganizationAIConfigured: vi.fn(),
|
||||
executeTenantScopedQuery: vi.fn(),
|
||||
generateText: vi.fn(),
|
||||
updateChart: vi.fn(),
|
||||
@@ -64,8 +65,20 @@ vi.mock("@/modules/ee/audit-logs/lib/handler", () => ({
|
||||
withAuditLogging: vi.fn((_eventName, _objectType, fn) => fn),
|
||||
}));
|
||||
|
||||
vi.mock("@ai-sdk/openai", () => ({
|
||||
createOpenAI: mocks.createOpenAI,
|
||||
vi.mock("@formbricks/ai", () => ({
|
||||
getAiModel: mocks.getAiModel,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ai/service", () => ({
|
||||
assertOrganizationAIConfigured: mocks.assertOrganizationAIConfigured,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/env", () => ({
|
||||
env: {},
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/analysis/lib/ai-schema-context", () => ({
|
||||
generateSchemaContext: vi.fn(() => "schema context"),
|
||||
}));
|
||||
|
||||
vi.mock("ai", () => ({
|
||||
@@ -83,7 +96,6 @@ const ctx = {
|
||||
describe("chart Cube actions", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubEnv("OPENAI_API_KEY", "openai-key");
|
||||
mocks.actionClientAction.mockImplementation((fn) => fn);
|
||||
mocks.actionClientInputSchema.mockReturnValue({ action: mocks.actionClientAction });
|
||||
mocks.getIsDashboardsEnabled.mockResolvedValue(true);
|
||||
@@ -94,7 +106,8 @@ describe("chart Cube actions", () => {
|
||||
mocks.checkFeedbackDirectoryAccess.mockResolvedValue({
|
||||
feedbackDirectoryId: "frd-1",
|
||||
});
|
||||
mocks.createOpenAI.mockReturnValue(() => "model");
|
||||
mocks.assertOrganizationAIConfigured.mockResolvedValue(undefined);
|
||||
mocks.getAiModel.mockReturnValue("model");
|
||||
mocks.createChart.mockResolvedValue({
|
||||
id: "chart-1",
|
||||
name: "Chart",
|
||||
@@ -110,7 +123,7 @@ describe("chart Cube actions", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test("executeQueryAction delegates to the tenant-scoped Cube helper after authorization", async () => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use server";
|
||||
|
||||
import { createOpenAI } from "@ai-sdk/openai";
|
||||
import { Output, generateText } from "ai";
|
||||
import { z } from "zod";
|
||||
import { getAiModel } from "@formbricks/ai";
|
||||
import { type TChartQuery, ZChartQuery } from "@formbricks/types/analysis";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { assertOrganizationAIConfigured } from "@/lib/ai/service";
|
||||
import { env } from "@/lib/env";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
import { executeTenantScopedQuery } from "@/modules/ee/analysis/api/lib/cube-client";
|
||||
@@ -344,6 +346,9 @@ export const generateAIChartAction = authenticatedActionClient
|
||||
|
||||
await checkDashboardsEnabled(organizationId);
|
||||
|
||||
// Verify AI is entitled, enabled at org level, and configured at instance level
|
||||
await assertOrganizationAIConfigured(organizationId, "dataAnalysis");
|
||||
|
||||
const { feedbackDirectoryId } = await checkFeedbackDirectoryAccess({
|
||||
feedbackDirectoryId: parsedInput.feedbackDirectoryId,
|
||||
organizationId,
|
||||
@@ -352,15 +357,10 @@ export const generateAIChartAction = authenticatedActionClient
|
||||
source: "charts.generateAIChartAction",
|
||||
});
|
||||
|
||||
if (!process.env.OPENAI_API_KEY) {
|
||||
throw new Error("OPENAI_API_KEY is not configured");
|
||||
}
|
||||
|
||||
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
const schemaContext = generateSchemaContext();
|
||||
|
||||
const { output } = await generateText({
|
||||
model: openai("gpt-4o-mini"),
|
||||
model: getAiModel(env),
|
||||
output: Output.object({ schema: ZGenerateAIQueryResponse }),
|
||||
system: schemaContext,
|
||||
prompt: `User request: "${parsedInput.prompt}"`,
|
||||
|
||||
@@ -9,17 +9,22 @@ import { generateAIChartAction } from "@/modules/ee/analysis/charts/actions";
|
||||
import type { AnalyticsResponse } from "@/modules/ee/analysis/types/analysis";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
|
||||
interface AIQuerySectionProps {
|
||||
workspaceId: string;
|
||||
onChartGenerated: (data: AnalyticsResponse) => void;
|
||||
feedbackDirectoryId: string;
|
||||
isAIAvailable?: boolean;
|
||||
aiUnavailableReason?: string;
|
||||
}
|
||||
|
||||
export function AIQuerySection({
|
||||
workspaceId,
|
||||
onChartGenerated,
|
||||
feedbackDirectoryId,
|
||||
isAIAvailable = true,
|
||||
aiUnavailableReason,
|
||||
}: Readonly<AIQuerySectionProps>) {
|
||||
const [userQuery, setUserQuery] = useState("");
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
@@ -70,16 +75,34 @@ export function AIQuerySection({
|
||||
value={userQuery}
|
||||
onChange={(e) => setUserQuery(e.target.value)}
|
||||
maxLength={2000}
|
||||
disabled={isGenerating}
|
||||
disabled={!isAIAvailable || isGenerating}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="default"
|
||||
disabled={!userQuery.trim() || isGenerating}
|
||||
loading={isGenerating}>
|
||||
<WandSparklesIcon className="h-4 w-4" />
|
||||
{t("workspace.analysis.charts.create_chart_with_ai")}
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="default"
|
||||
className="w-full"
|
||||
disabled={!isAIAvailable || !userQuery.trim() || isGenerating}
|
||||
loading={isGenerating}>
|
||||
<WandSparklesIcon className="h-4 w-4" />
|
||||
{t("workspace.analysis.charts.create_chart_with_ai")}
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{!isAIAvailable && (
|
||||
<TooltipContent>
|
||||
{{
|
||||
not_in_plan: t("workspace.analysis.charts.ai_not_in_plan"),
|
||||
not_enabled: t("workspace.analysis.charts.ai_not_enabled"),
|
||||
instance_not_configured: t("workspace.analysis.charts.ai_instance_not_configured"),
|
||||
}[aiUnavailableReason ?? ""] ?? t("workspace.analysis.charts.ai_not_available")}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { use } from "react";
|
||||
import { getOrganizationAIConfig } from "@/lib/ai/service";
|
||||
import type { TOrganizationAIConfig } from "@/lib/ai/service";
|
||||
import { getConnectorsWithMappings } from "@/lib/connector/service";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
@@ -14,6 +16,15 @@ import { getIsDashboardsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
||||
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
|
||||
|
||||
const getAIUnavailableReason = (
|
||||
aiConfig: TOrganizationAIConfig
|
||||
): "not_in_plan" | "not_enabled" | "instance_not_configured" | undefined => {
|
||||
if (!aiConfig.isAIDataAnalysisEntitled) return "not_in_plan";
|
||||
if (!aiConfig.isAIDataAnalysisEnabled) return "not_enabled";
|
||||
if (!aiConfig.isInstanceConfigured) return "instance_not_configured";
|
||||
return undefined;
|
||||
};
|
||||
|
||||
interface ChartsListContentProps {
|
||||
chartsPromise: Promise<TChartWithCreator[]>;
|
||||
workspaceId: string;
|
||||
@@ -71,10 +82,13 @@ export async function ChartsListPage({ workspaceId }: Readonly<ChartsListPagePro
|
||||
);
|
||||
}
|
||||
|
||||
const [directories, connectors] = await Promise.all([
|
||||
const [directories, connectors, aiConfig] = await Promise.all([
|
||||
getFeedbackDirectoriesByWorkspaceId(workspaceId),
|
||||
getConnectorsWithMappings(workspaceId),
|
||||
getOrganizationAIConfig(organization.id),
|
||||
]);
|
||||
const aiUnavailableReason = getAIUnavailableReason(aiConfig);
|
||||
const isAIAvailable = !aiUnavailableReason;
|
||||
const hasFeedbackRecords = await hasFeedbackRecordsInDirectories(
|
||||
directories.map((directory) => directory.id)
|
||||
);
|
||||
@@ -90,6 +104,8 @@ export async function ChartsListPage({ workspaceId }: Readonly<ChartsListPagePro
|
||||
workspaceId={workspaceId}
|
||||
directories={directories}
|
||||
buttonProps={{ disabled: !hasFeedbackRecords }}
|
||||
isAIAvailable={isAIAvailable}
|
||||
aiUnavailableReason={aiUnavailableReason}
|
||||
/>
|
||||
)
|
||||
}>
|
||||
|
||||
@@ -14,6 +14,8 @@ interface CreateChartButtonProps {
|
||||
onSuccess?: () => void;
|
||||
showIcon?: boolean;
|
||||
buttonProps?: Omit<ButtonProps, "onClick" | "children">;
|
||||
isAIAvailable?: boolean;
|
||||
aiUnavailableReason?: string;
|
||||
}
|
||||
|
||||
export function CreateChartButton({
|
||||
@@ -24,6 +26,8 @@ export function CreateChartButton({
|
||||
onSuccess,
|
||||
showIcon = true,
|
||||
buttonProps,
|
||||
isAIAvailable,
|
||||
aiUnavailableReason,
|
||||
}: Readonly<CreateChartButtonProps>) {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
@@ -41,6 +45,8 @@ export function CreateChartButton({
|
||||
autoAddToDashboardId={autoAddToDashboardId}
|
||||
directories={directories}
|
||||
onSuccess={onSuccess}
|
||||
isAIAvailable={isAIAvailable}
|
||||
aiUnavailableReason={aiUnavailableReason}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface CreateChartDialogProps {
|
||||
initialChart?: TChartWithCreator;
|
||||
onSuccess?: () => void;
|
||||
directories: { id: string; name: string }[];
|
||||
isAIAvailable?: boolean;
|
||||
aiUnavailableReason?: string;
|
||||
}
|
||||
|
||||
export function CreateChartDialog({
|
||||
@@ -23,6 +25,8 @@ export function CreateChartDialog({
|
||||
initialChart,
|
||||
onSuccess,
|
||||
directories,
|
||||
isAIAvailable,
|
||||
aiUnavailableReason,
|
||||
}: Readonly<CreateChartDialogProps>) {
|
||||
return (
|
||||
<CreateChartView
|
||||
@@ -34,6 +38,8 @@ export function CreateChartDialog({
|
||||
autoAddToDashboardId={autoAddToDashboardId}
|
||||
onSuccess={onSuccess}
|
||||
directories={directories}
|
||||
isAIAvailable={isAIAvailable}
|
||||
aiUnavailableReason={aiUnavailableReason}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ interface CreateChartViewProps {
|
||||
autoAddToDashboardId?: string;
|
||||
onSuccess?: () => void;
|
||||
directories: { id: string; name: string }[];
|
||||
isAIAvailable?: boolean;
|
||||
aiUnavailableReason?: string;
|
||||
}
|
||||
|
||||
export function CreateChartView({
|
||||
@@ -45,6 +47,8 @@ export function CreateChartView({
|
||||
autoAddToDashboardId,
|
||||
onSuccess,
|
||||
directories,
|
||||
isAIAvailable,
|
||||
aiUnavailableReason,
|
||||
}: Readonly<CreateChartViewProps>) {
|
||||
const { t } = useTranslation();
|
||||
const isEditing = !!chartId;
|
||||
@@ -150,6 +154,8 @@ export function CreateChartView({
|
||||
workspaceId={workspaceId}
|
||||
onChartGenerated={handleChartGenerated}
|
||||
feedbackDirectoryId={selectedDirectoryId}
|
||||
isAIAvailable={isAIAvailable}
|
||||
aiUnavailableReason={aiUnavailableReason}
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
|
||||
@@ -30,6 +30,8 @@ interface AddExistingChartsDialogProps {
|
||||
directories: { id: string; name: string }[];
|
||||
existingChartIds: string[];
|
||||
onSuccess: () => void;
|
||||
isAIAvailable?: boolean;
|
||||
aiUnavailableReason?: string;
|
||||
}
|
||||
|
||||
interface ChartOption {
|
||||
@@ -45,6 +47,8 @@ export function AddExistingChartsDialog({
|
||||
directories,
|
||||
existingChartIds,
|
||||
onSuccess,
|
||||
isAIAvailable,
|
||||
aiUnavailableReason,
|
||||
}: Readonly<AddExistingChartsDialogProps>) {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
@@ -160,6 +164,8 @@ export function AddExistingChartsDialog({
|
||||
onSuccess();
|
||||
}}
|
||||
buttonProps={{ variant: "secondary", size: "default", disabled: isAdding }}
|
||||
isAIAvailable={isAIAvailable}
|
||||
aiUnavailableReason={aiUnavailableReason}
|
||||
/>
|
||||
<div className="flex flex-col-reverse gap-2 sm:flex-row">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isAdding}>
|
||||
|
||||
@@ -21,6 +21,8 @@ interface DashboardControlBarProps {
|
||||
isSaving: boolean;
|
||||
hasChanges: boolean;
|
||||
isReadOnly: boolean;
|
||||
isAIAvailable?: boolean;
|
||||
aiUnavailableReason?: string;
|
||||
onRefresh: () => void;
|
||||
onEditToggle: () => void;
|
||||
onSave: () => void;
|
||||
@@ -36,6 +38,8 @@ export const DashboardControlBar = ({
|
||||
isSaving,
|
||||
hasChanges,
|
||||
isReadOnly,
|
||||
isAIAvailable,
|
||||
aiUnavailableReason,
|
||||
onRefresh,
|
||||
onEditToggle,
|
||||
onSave,
|
||||
@@ -139,6 +143,8 @@ export const DashboardControlBar = ({
|
||||
setIsAddExistingDialogOpen(false);
|
||||
router.refresh();
|
||||
}}
|
||||
isAIAvailable={isAIAvailable}
|
||||
aiUnavailableReason={aiUnavailableReason}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -34,6 +34,8 @@ interface DashboardDetailClientProps {
|
||||
>;
|
||||
directories: { id: string; name: string }[];
|
||||
isReadOnly: boolean;
|
||||
isAIAvailable: boolean;
|
||||
aiUnavailableReason?: string;
|
||||
}
|
||||
|
||||
const widgetsToLayout = (widgets: TDashboardWidget[]): LayoutItem[] => {
|
||||
@@ -151,6 +153,8 @@ export function DashboardDetailClient({
|
||||
widgetDataPromises,
|
||||
directories,
|
||||
isReadOnly,
|
||||
isAIAvailable,
|
||||
aiUnavailableReason,
|
||||
}: Readonly<DashboardDetailClientProps>) {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
@@ -297,6 +301,8 @@ export function DashboardDetailClient({
|
||||
isSaving={isSaving}
|
||||
hasChanges={hasChanges}
|
||||
isReadOnly={isReadOnly}
|
||||
isAIAvailable={isAIAvailable}
|
||||
aiUnavailableReason={aiUnavailableReason}
|
||||
onRefresh={() => router.refresh()}
|
||||
onEditToggle={handleEnterEditMode}
|
||||
onSave={handleSave}
|
||||
@@ -363,6 +369,8 @@ export function DashboardDetailClient({
|
||||
router.refresh();
|
||||
}}
|
||||
directories={directories}
|
||||
isAIAvailable={isAIAvailable}
|
||||
aiUnavailableReason={aiUnavailableReason}
|
||||
/>
|
||||
)}
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { notFound } from "next/navigation";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import type { TChartQuery } from "@formbricks/types/analysis";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { type TOrganizationAIConfig, getOrganizationAIConfig } from "@/lib/ai/service";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { executeTenantScopedQuery } from "@/modules/ee/analysis/api/lib/cube-client";
|
||||
@@ -16,6 +17,15 @@ import { DashboardDetailClient } from "../components/dashboard-detail-client";
|
||||
import { getDashboard } from "../lib/dashboards";
|
||||
import { DASHBOARD_WIDGET_LOAD_ERROR, type TDashboardWidgetError } from "../lib/widget-errors";
|
||||
|
||||
const getAIUnavailableReason = (
|
||||
aiConfig: TOrganizationAIConfig
|
||||
): "not_in_plan" | "not_enabled" | "instance_not_configured" | undefined => {
|
||||
if (!aiConfig.isAIDataAnalysisEntitled) return "not_in_plan";
|
||||
if (!aiConfig.isAIDataAnalysisEnabled) return "not_enabled";
|
||||
if (!aiConfig.isInstanceConfigured) return "instance_not_configured";
|
||||
return undefined;
|
||||
};
|
||||
|
||||
type TDashboardDetail = Awaited<ReturnType<typeof getDashboard>>;
|
||||
type TDashboardWidget = TDashboardDetail["widgets"][number];
|
||||
type TDashboardWidgetWithChart = TDashboardWidget & { chart: NonNullable<TDashboardWidget["chart"]> };
|
||||
@@ -96,7 +106,12 @@ export async function DashboardDetailPage({
|
||||
);
|
||||
}
|
||||
|
||||
const directories = await getFeedbackDirectoriesByWorkspaceId(workspaceId);
|
||||
const [directories, aiConfig] = await Promise.all([
|
||||
getFeedbackDirectoriesByWorkspaceId(workspaceId),
|
||||
getOrganizationAIConfig(organization.id),
|
||||
]);
|
||||
const aiUnavailableReason = getAIUnavailableReason(aiConfig);
|
||||
const isAIAvailable = !aiUnavailableReason;
|
||||
|
||||
let dashboard;
|
||||
try {
|
||||
@@ -132,6 +147,8 @@ export async function DashboardDetailPage({
|
||||
widgetDataPromises={widgetDataPromises}
|
||||
directories={directories}
|
||||
isReadOnly={isReadOnly}
|
||||
isAIAvailable={isAIAvailable}
|
||||
aiUnavailableReason={aiUnavailableReason}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user