Merge branch 'master' into BUDI-9294/dry-code

This commit is contained in:
Sam Rose 2025-05-06 14:45:43 +01:00 committed by GitHub
commit a072bb090c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 199 additions and 88 deletions

View File

@ -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": {

View File

@ -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}
/>
</div>

View File

@ -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;

View File

@ -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")
})
})
})
})

View File

@ -159,74 +159,76 @@
})
</script>
<Layout noPadding>
<Layout gap="XS" noPadding>
<div class="header">
<Heading size="M">AI</Heading>
</div>
<Body>
Connect an LLM to enable AI features. You can only enable one LLM at a
time.
</Body>
</Layout>
<Divider />
{#if aiConfig}
<Layout noPadding>
<Layout gap="XS" noPadding>
<div class="header">
<Heading size="M">AI</Heading>
</div>
<Body>
Connect an LLM to enable AI features. You can only enable one LLM at a
time.
</Body>
</Layout>
<Divider />
{#if !activeProvider && !$bannerStore}
<div class="banner">
<div class="banner-content">
<div class="banner-icon">
<img src={BBAI} alt="BB AI" width="24" height="24" />
{#if !activeProvider && !$bannerStore}
<div class="banner">
<div class="banner-content">
<div class="banner-icon">
<img src={BBAI} alt="BB AI" width="24" height="24" />
</div>
<div>Try BB AI for free. 50,000 tokens included. No CC required.</div>
</div>
<div>Try BB AI for free. 50,000 tokens included. No CC required.</div>
</div>
<div class="banner-buttons">
<Button
primary
cta
size="S"
on:click={() => handleEnable("BudibaseAI")}
>
Enable BB AI
</Button>
<Icon
hoverable
name="Close"
on:click={() => {
setBannerLocalStorageKey()
bannerStore.set(true)
}}
/>
</div>
</div>
{/if}
<div class="section">
<div class="section-title">Enabled</div>
{#if activeProvider}
<AIConfigTile
config={getProviderConfig(activeProvider).config}
editHandler={() => handleEnable(activeProvider)}
disableHandler={() => disableProvider(activeProvider)}
/>
{:else}
<div class="no-enabled">
<Body size="S">No LLMs are enabled</Body>
</div>
{/if}
{#if disabledProviders.length > 0}
<div class="section-title disabled-title">Disabled</div>
<div class="ai-list">
{#each disabledProviders as { provider, config } (provider)}
<AIConfigTile
{config}
editHandler={() => handleEnable(provider)}
disableHandler={() => disableProvider(provider)}
<div class="banner-buttons">
<Button
primary
cta
size="S"
on:click={() => handleEnable("BudibaseAI")}
>
Enable BB AI
</Button>
<Icon
hoverable
name="Close"
on:click={() => {
setBannerLocalStorageKey()
bannerStore.set(true)
}}
/>
{/each}
</div>
</div>
{/if}
</div>
</Layout>
<div class="section">
<div class="section-title">Enabled</div>
{#if activeProvider}
<AIConfigTile
config={getProviderConfig(activeProvider).config}
editHandler={() => handleEnable(activeProvider)}
disableHandler={() => disableProvider(activeProvider)}
/>
{:else}
<div class="no-enabled">
<Body size="S">No LLMs are enabled</Body>
</div>
{/if}
{#if disabledProviders.length > 0}
<div class="section-title disabled-title">Disabled</div>
<div class="ai-list">
{#each disabledProviders as { provider, config } (provider)}
<AIConfigTile
{config}
editHandler={() => handleEnable(provider)}
disableHandler={() => disableProvider(provider)}
/>
{/each}
</div>
{/if}
</div>
</Layout>
{/if}
<Modal bind:this={portalModal}>
<PortalModal

View File

@ -87,6 +87,7 @@ export default defineConfig(({ mode }) => {
exclude: ["@roxi/routify", "fsevents"],
},
resolve: {
conditions: mode === "test" ? ["browser"] : [],
dedupe: ["@roxi/routify"],
alias: {
"@budibase/types": path.resolve(__dirname, "../types/src"),