Merge branch 'master' into BUDI-9294/dry-code
This commit is contained in:
commit
a072bb090c
|
@ -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": {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"),
|
||||
|
|
Loading…
Reference in New Issue