diff --git a/lerna.json b/lerna.json index 87f0731cda..b957585f7b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.9.5", + "version": "3.10.0", "npmClient": "yarn", "concurrency": 20, "command": { diff --git a/packages/builder/src/components/common/CodeEditor/AIGen.svelte b/packages/builder/src/components/common/CodeEditor/AIGen.svelte index 1e2593bb62..84edc6cbdf 100644 --- a/packages/builder/src/components/common/CodeEditor/AIGen.svelte +++ b/packages/builder/src/components/common/CodeEditor/AIGen.svelte @@ -10,8 +10,8 @@ export let bindings: EnrichedBinding[] = [] export let value: string | null = "" export let expandedOnly: boolean = false - export let parentWidth: number | null = null + const dispatch = createEventDispatcher<{ update: { code: string } accept: void @@ -26,11 +26,11 @@ const thresholdExpansionWidth = 350 - $: expanded = + $: shouldAlwaysBeExpanded = expandedOnly || (parentWidth !== null && parentWidth > thresholdExpansionWidth) - ? true - : expanded + + $: expanded = shouldAlwaysBeExpanded || expanded async function generateJs(prompt: string) { promptText = "" @@ -108,7 +108,7 @@ bind:expanded bind:value={inputValue} readonly={!!suggestedCode} - {expandedOnly} + expandedOnly={shouldAlwaysBeExpanded} /> diff --git a/packages/builder/src/components/common/ai/AIInput.svelte b/packages/builder/src/components/common/ai/AIInput.svelte index 3d1a2e25c2..fc28aa20c9 100644 --- a/packages/builder/src/components/common/ai/AIInput.svelte +++ b/packages/builder/src/components/common/ai/AIInput.svelte @@ -13,7 +13,6 @@ export let value: string = "" export const submit = onPromptSubmit - $: expanded = expandedOnly || expanded const dispatch = createEventDispatcher() let promptInput: HTMLInputElement @@ -22,6 +21,7 @@ let switchOnAIModal: Modal let addCreditsModal: Modal + $: expanded = expandedOnly || expanded $: accountPortalAccess = $auth?.user?.accountPortalAccess $: accountPortal = $admin.accountPortalUrl $: aiEnabled = $auth?.user?.llm @@ -92,9 +92,12 @@ class="ai-icon" class:loading={promptLoading} class:disabled={expanded && disabled} + class:no-toggle={expandedOnly} on:click={e => { - e.stopPropagation() - toggleExpand() + if (!expandedOnly) { + e.stopPropagation() + toggleExpand() + } }} /> {#if expanded} @@ -290,6 +293,10 @@ z-index: 2; } + .ai-icon.no-toggle { + cursor: default; + } + .ai-gen-text { white-space: nowrap; overflow: hidden; diff --git a/packages/builder/src/pages/builder/portal/settings/ai/AISettings.spec.js b/packages/builder/src/pages/builder/portal/settings/ai/AISettings.spec.js index f6e579f7fc..4b3a8404c8 100644 --- a/packages/builder/src/pages/builder/portal/settings/ai/AISettings.spec.js +++ b/packages/builder/src/pages/builder/portal/settings/ai/AISettings.spec.js @@ -1,11 +1,23 @@ import { it, expect, describe, vi } from "vitest" import AISettings from "./index.svelte" -import { render, fireEvent } from "@testing-library/svelte" +import { render, waitFor } from "@testing-library/svelte" import { admin, licensing, featureFlags } from "@/stores/portal" import { notifications } from "@budibase/bbui" +import { API } from "@/api" vi.spyOn(notifications, "error").mockImplementation(vi.fn) vi.spyOn(notifications, "success").mockImplementation(vi.fn) +vi.mock("@/api", () => ({ + API: { + getConfig: vi.fn().mockResolvedValue({ + config: {}, + }), + getLicenseKey: vi.fn().mockResolvedValue({ + licenseKey: "abc-123", + }), + saveConfig: vi.fn(), + }, +})) const Hosting = { Cloud: "cloud", @@ -15,7 +27,6 @@ const Hosting = { function setupEnv(hosting, features = {}, flags = {}) { const defaultFeatures = { budibaseAIEnabled: false, - customAIConfigsEnabled: false, ...features, } const defaultFlags = { @@ -41,33 +52,123 @@ describe("AISettings", () => { let instance = null const setupDOM = () => { - instance = render(AISettings, {}) + instance = render(AISettings) const modalContainer = document.createElement("div") modalContainer.classList.add("modal-container") instance.baseElement.appendChild(modalContainer) } + beforeEach(() => { + setupEnv(Hosting.Self) + }) + afterEach(() => { vi.restoreAllMocks() }) - it("that the AISettings is rendered", () => { - setupDOM() - expect(instance).toBeDefined() + describe("Basic rendering", () => { + it("should render the AI header", async () => { + setupDOM() + await waitFor(() => { + const header = instance.getByText("AI") + expect(header).toBeInTheDocument() + }) + }) + it("should show 'No LLMs are enabled' when no providers are active", async () => { + API.getConfig.mockResolvedValueOnce({ config: {} }) + setupDOM() + + await waitFor(() => { + const noEnabledText = instance.getByText("No LLMs are enabled") + expect(noEnabledText).toBeInTheDocument() + }) + }) + + it("should display the 'Enable BB AI' button", async () => { + API.getConfig.mockResolvedValueOnce({ config: {} }) + setupDOM() + await waitFor(() => { + const enableButton = instance.getByText("Enable BB AI") + expect(enableButton).toBeInTheDocument() + }) + }) }) - describe("DOM Render tests", () => { - it("the enable bb ai button should not do anything if the user doesn't have the correct license on self host", async () => { - let addAiButton - let configModal + describe("Provider rendering", () => { + it("should display active provider with active status tag", async () => { + API.getConfig.mockResolvedValueOnce({ + config: { + BudibaseAI: { + provider: "BudibaseAI", + active: true, + isDefault: true, + name: "Budibase AI", + }, + }, + }) - setupEnv(Hosting.Self, { customAIConfigsEnabled: false }) setupDOM() - addAiButton = instance.queryByText("Enable BB AI") - expect(addAiButton).toBeInTheDocument() - await fireEvent.click(addAiButton) - configModal = instance.queryByText("Custom AI Configuration") - expect(configModal).not.toBeInTheDocument() + + await waitFor(() => { + const providerName = instance.getByText("Budibase AI") + expect(providerName).toBeInTheDocument() + + const statusTags = instance.baseElement.querySelectorAll(".tag.active") + expect(statusTags.length).toBeGreaterThan(0) + + let foundEnabledTag = false + statusTags.forEach(tag => { + if (tag.textContent === "Enabled") { + foundEnabledTag = true + } + }) + expect(foundEnabledTag).toBe(true) + }) + }) + + it("should display disabled provider with disabled status tag", async () => { + API.getConfig.mockResolvedValueOnce({ + config: { + BudibaseAI: { + provider: "BudibaseAI", + active: true, + isDefault: true, + name: "Budibase AI", + }, + OpenAI: { + provider: "OpenAI", + active: false, + isDefault: false, + name: "OpenAI", + }, + }, + }) + + setupDOM() + + await waitFor(async () => { + const disabledProvider = instance.getByText("OpenAI") + expect(disabledProvider).toBeInTheDocument() + + const disabledTags = + instance.baseElement.querySelectorAll(".tag.disabled") + expect(disabledTags.length).toBeGreaterThan(0) + + let foundDisabledTag = false + disabledTags.forEach(tag => { + if (tag.textContent === "Disabled") { + foundDisabledTag = true + } + }) + expect(foundDisabledTag).toBe(true) + + const openAIOption = disabledProvider.closest(".option") + expect(openAIOption).not.toBeNull() + const disabledTagNearOpenAI = + openAIOption.querySelector(".tag.disabled") + expect(disabledTagNearOpenAI).not.toBeNull() + expect(disabledTagNearOpenAI.textContent).toBe("Disabled") + }) }) }) }) diff --git a/packages/builder/src/pages/builder/portal/settings/ai/index.svelte b/packages/builder/src/pages/builder/portal/settings/ai/index.svelte index 1b23c81ad9..efa2642c43 100644 --- a/packages/builder/src/pages/builder/portal/settings/ai/index.svelte +++ b/packages/builder/src/pages/builder/portal/settings/ai/index.svelte @@ -159,74 +159,76 @@ }) - - -
- AI -
- - Connect an LLM to enable AI features. You can only enable one LLM at a - time. - -
- +{#if aiConfig} + + +
+ AI +
+ + Connect an LLM to enable AI features. You can only enable one LLM at a + time. + +
+ - {#if !activeProvider && !$bannerStore} -