+
+
+
+
+
+
+ {#if config.active}
+
Enabled
+ {:else}
+
Disabled
+ {/if}
+
-
-
- {#if config.name !== "Budibase AI"}
-
-
- {/if}
- {#if config.active}
-
Activated
- {:else if !config.active}
-
Disabled
- {/if}
- {#if config.isDefault}
-
Default
+
+ {#if config.provider === "BudibaseAI"}
+ {#if config.active}
+
disableHandler && disableHandler()}>
+ Disable
+
+ {:else}
+
editHandler && editHandler()}>
+ Enable
+
+ {/if}
+ {:else}
+
+
editHandler && editHandler()}>
+ {#if config.apiKey}Edit{:else}Set up{/if}
+
{/if}
@@ -71,10 +63,16 @@
padding: 16px;
border-radius: 4px;
cursor: pointer;
- display: grid;
- grid-template-columns: 6% 1fr auto;
- grid-gap: 20px;
+ display: flex;
align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ }
+
+ .details {
+ display: flex;
+ align-items: center;
+ gap: 12px;
}
.option :global(label) {
@@ -87,12 +85,13 @@
.header {
align-items: center;
+ color: var(--spectrum-global-color-gray-900);
}
.icon {
- background-color: white;
- height: 38px;
- width: 38px;
+ background-color: var(--spectrum-global-color-gray-200);
+ height: 40px;
+ width: 40px;
display: flex;
justify-content: center;
align-items: center;
@@ -103,33 +102,21 @@
pointer-events: none;
}
- .controls {
- display: grid;
- grid-auto-flow: column;
- grid-gap: 10px;
- align-items: center;
- }
-
.tag {
- display: flex;
- color: #ffffff;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 8px;
font-size: 12px;
border-radius: 5px;
- }
-
- .default {
- background: var(--grey-6);
+ color: #fff;
}
.active {
- background: var(--spectrum-global-color-green-600);
+ background: #004c2e;
}
.disabled {
- background: var(--spectrum-global-color-red-600);
+ background: var(--grey-3);
}
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 35f8859cd5..f6e579f7fc 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
@@ -56,66 +56,18 @@ describe("AISettings", () => {
expect(instance).toBeDefined()
})
- describe("Licensing", () => {
- it("should show the premium label on self host for custom configs", async () => {
- setupEnv(Hosting.Self)
- setupDOM()
- const premiumTag = instance.queryByText("Premium")
- expect(premiumTag).toBeInTheDocument()
- })
-
- it("should show the enterprise label on cloud for custom configs", async () => {
- setupEnv(Hosting.Cloud)
- setupDOM()
- const enterpriseTag = instance.queryByText("Enterprise")
- expect(enterpriseTag).toBeInTheDocument()
- })
-
- it("the add configuration button should not do anything the user doesn't have the correct license on cloud", async () => {
- let addConfigurationButton
+ 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
- setupEnv(Hosting.Cloud)
+ setupEnv(Hosting.Self, { customAIConfigsEnabled: false })
setupDOM()
- addConfigurationButton = instance.queryByText("Add configuration")
- expect(addConfigurationButton).toBeInTheDocument()
- await fireEvent.click(addConfigurationButton)
+ addAiButton = instance.queryByText("Enable BB AI")
+ expect(addAiButton).toBeInTheDocument()
+ await fireEvent.click(addAiButton)
configModal = instance.queryByText("Custom AI Configuration")
expect(configModal).not.toBeInTheDocument()
})
-
- it("the add configuration button should open the config modal if the user has the correct license on cloud", async () => {
- let addConfigurationButton
- let configModal
-
- setupEnv(
- Hosting.Cloud,
- { customAIConfigsEnabled: true },
- { AI_CUSTOM_CONFIGS: true }
- )
- setupDOM()
- addConfigurationButton = instance.queryByText("Add configuration")
- expect(addConfigurationButton).toBeInTheDocument()
- await fireEvent.click(addConfigurationButton)
- configModal = instance.queryByText("Custom AI Configuration")
- expect(configModal).toBeInTheDocument()
- })
-
- it("the add configuration button should open the config modal if the user has the correct license on self host", async () => {
- let addConfigurationButton
- let configModal
-
- setupEnv(
- Hosting.Self,
- { customAIConfigsEnabled: true },
- { AI_CUSTOM_CONFIGS: true }
- )
- setupDOM()
- addConfigurationButton = instance.queryByText("Add configuration")
- expect(addConfigurationButton).toBeInTheDocument()
- await fireEvent.click(addConfigurationButton)
- configModal = instance.queryByText("Custom AI Configuration")
- expect(configModal).toBeInTheDocument()
- })
})
})
diff --git a/packages/builder/src/pages/builder/portal/settings/ai/ConfigModal.svelte b/packages/builder/src/pages/builder/portal/settings/ai/ConfigModal.svelte
index 26e7a1606c..ac6c578a1a 100644
--- a/packages/builder/src/pages/builder/portal/settings/ai/ConfigModal.svelte
+++ b/packages/builder/src/pages/builder/portal/settings/ai/ConfigModal.svelte
@@ -1,76 +1,42 @@
-
updateHandler(config)
+ : () => enableHandler(config)}
+ onCancel={isEnabled
+ ? () => disableHandler(config)
+ : () => updateHandler(config)}
+ disabled={!complete}
size="M"
- title="Custom AI Configuration"
+ title={`Set up ${config.name}`}
>
- Provider
-
-
-
- Name
-
-
-
- Default Model
- {#if config.provider !== Providers.Custom.name}
-
- {:else}
-
- {/if}
+ API Key
+
Base URL
@@ -80,13 +46,14 @@
bind:value={config.baseUrl}
/>
+
- API Key
-
-
-
-
-
+ Default Model
+
diff --git a/packages/builder/src/pages/builder/portal/settings/ai/PortalModal.svelte b/packages/builder/src/pages/builder/portal/settings/ai/PortalModal.svelte
new file mode 100644
index 0000000000..98a7f4b736
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/settings/ai/PortalModal.svelte
@@ -0,0 +1,21 @@
+
+
+
+ To setup BB AI you must add a Budibase license key.
+
+ To get your license key, account admins must sign up to the
+ Budibase Account Portal
+
+
diff --git a/packages/builder/src/pages/builder/portal/settings/ai/constants.js b/packages/builder/src/pages/builder/portal/settings/ai/constants.js
deleted file mode 100644
index 76db5a1394..0000000000
--- a/packages/builder/src/pages/builder/portal/settings/ai/constants.js
+++ /dev/null
@@ -1,60 +0,0 @@
-export const Providers = {
- OpenAI: {
- name: "OpenAI",
- models: [
- { label: "GPT 4o Mini", value: "gpt-4o-mini" },
- { label: "GPT 4o", value: "gpt-4o" },
- { label: "GPT 4 Turbo", value: "gpt-4-turbo" },
- { label: "GPT 4", value: "gpt-4" },
- { label: "GPT 3.5 Turbo", value: "gpt-3.5-turbo" },
- ],
- },
- Anthropic: {
- name: "Anthropic",
- models: [
- { label: "Claude 3.5 Sonnet", value: "claude-3-5-sonnet-20240620" },
- { label: "Claude 3 Sonnet", value: "claude-3-sonnet-20240229" },
- { label: "Claude 3 Opus", value: "claude-3-opus-20240229" },
- { label: "Claude 3 Haiku", value: "claude-3-haiku-20240307" },
- ],
- },
- TogetherAI: {
- name: "Together AI",
- models: [{ label: "Llama 3 8B", value: "meta-llama/Meta-Llama-3-8B" }],
- },
- AzureOpenAI: {
- name: "Azure OpenAI",
- models: [
- { label: "GPT 4o Mini", value: "gpt-4o-mini" },
- { label: "GPT 4o", value: "gpt-4o" },
- { label: "GPT 4 Turbo", value: "gpt-4-turbo" },
- { label: "GPT 4", value: "gpt-4" },
- { label: "GPT 3.5 Turbo", value: "gpt-3.5-turbo" },
- ],
- },
- Custom: {
- name: "Custom",
- },
-}
-
-export const ConfigMap = {
- OpenAI: {
- name: "OpenAI",
- baseUrl: "https://api.openai.com",
- },
- Anthropic: {
- name: "Anthropic",
- baseUrl: "https://api.anthropic.com/v1",
- },
- TogetherAI: {
- name: "TogetherAI",
- baseUrl: "https://api.together.xyz/v1",
- },
- AzureOpenAI: {
- name: "Azure OpenAI",
- baseUrl: "",
- },
- Custom: {
- baseUrl: "",
- },
-}
diff --git a/packages/builder/src/pages/builder/portal/settings/ai/constants.ts b/packages/builder/src/pages/builder/portal/settings/ai/constants.ts
new file mode 100644
index 0000000000..557af3e7ab
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/settings/ai/constants.ts
@@ -0,0 +1,89 @@
+export const BBAI_KEY = "BudibaseAI"
+export const OPENAI_KEY = "OpenAI"
+export const AZURE_KEY = "AzureOpenAI"
+
+export const Providers = {
+ BudibaseAI: "BudibaseAI",
+ OpenAI: "OpenAI",
+ AzureOpenAI: "AzureOpenAI",
+}
+
+export const Models = [
+ { label: "GPT 4o Mini", value: "gpt-4o-mini" },
+ { label: "GPT 4o", value: "gpt-4o" },
+ { label: "GPT 4 Turbo", value: "gpt-4-turbo" },
+ { label: "GPT 4", value: "gpt-4" },
+ { label: "GPT 3.5 Turbo", value: "gpt-3.5-turbo" },
+]
+
+export const ProviderDetails = {
+ [BBAI_KEY]: {
+ provider: "BudibaseAI",
+ name: "BB AI",
+ defaultConfig: {
+ active: false,
+ isDefault: false,
+ },
+ models: [],
+ },
+ [OPENAI_KEY]: {
+ provider: "OpenAI",
+ name: "OpenAI",
+ baseUrl: "https://api.openai.com",
+ defaultConfig: {
+ active: false,
+ isDefault: false,
+ apiKey: "",
+ baseUrl: "https://api.openai.com",
+ defaultModel: "",
+ },
+ models: [
+ { label: "GPT 4o Mini", value: "gpt-4o-mini" },
+ { label: "GPT 4o", value: "gpt-4o" },
+ { label: "GPT 4 Turbo", value: "gpt-4-turbo" },
+ { label: "GPT 4", value: "gpt-4" },
+ { label: "GPT 3.5 Turbo", value: "gpt-3.5-turbo" },
+ ],
+ },
+ [AZURE_KEY]: {
+ provider: "AzureOpenAI",
+ name: "Azure OpenAI",
+ baseUrl: "",
+ defaultConfig: {
+ active: false,
+ isDefault: false,
+ apiKey: "",
+ baseUrl: "",
+ defaultModel: "",
+ },
+ models: [
+ { label: "GPT 4o Mini", value: "gpt-4o-mini" },
+ { label: "GPT 4o", value: "gpt-4o" },
+ { label: "GPT 4 Turbo", value: "gpt-4-turbo" },
+ { label: "GPT 4", value: "gpt-4" },
+ { label: "GPT 3.5 Turbo", value: "gpt-3.5-turbo" },
+ ],
+ },
+}
+
+export const ConfigMap = {
+ OpenAI: {
+ name: "OpenAI",
+ baseUrl: "https://api.openai.com",
+ },
+ Anthropic: {
+ name: "Anthropic",
+ baseUrl: "https://api.anthropic.com/v1",
+ },
+ TogetherAI: {
+ name: "TogetherAI",
+ baseUrl: "https://api.together.xyz/v1",
+ },
+ AzureOpenAI: {
+ name: "Azure OpenAI",
+ baseUrl: "",
+ },
+ Custom: {
+ baseUrl: "",
+ },
+}
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 b569993cd1..0923e5846f 100644
--- a/packages/builder/src/pages/builder/portal/settings/ai/index.svelte
+++ b/packages/builder/src/pages/builder/portal/settings/ai/index.svelte
@@ -5,193 +5,332 @@
Layout,
Heading,
Body,
- Helpers,
Divider,
notifications,
Modal,
- Tags,
- Tag,
+ Icon,
} from "@budibase/bbui"
+ import BBAI from "assets/bb-ai.svg"
import { admin, licensing } from "@/stores/portal"
- import { API } from "@/api"
- import AIConfigModal from "./ConfigModal.svelte"
- import AIConfigTile from "./AIConfigTile.svelte"
- import {
- type AIConfig,
- ConfigType,
- type ProviderConfig,
- } from "@budibase/types"
+ import { BudiStore, PersistenceType } from "@/stores/BudiStore"
- let modal: Modal
- let fullAIConfig: AIConfig
- let editingAIConfig: ProviderConfig | undefined
- let editingUuid: string | undefined
+ import { API } from "@/api"
+ import AIConfigTile from "./AIConfigTile.svelte"
+ import ConfigModal from "./ConfigModal.svelte"
+ import PortalModal from "./PortalModal.svelte"
+ import {
+ type AIProvider,
+ ConfigType,
+ type AIConfig,
+ type ProviderConfig,
+ type AIProviderPartial,
+ } from "@budibase/types"
+ import { ProviderDetails, BBAI_KEY, OPENAI_KEY, AZURE_KEY } from "./constants"
+
+ const bannerKey = `bb-ai-configuration-banner`
+ const bannerStore = new BudiStore
(false, {
+ persistence: {
+ type: PersistenceType.LOCAL,
+ key: bannerKey,
+ },
+ })
+
+ let aiConfig: AIConfig
+ let configModal: { show: () => void; hide: () => void }
+ let portalModal: { show: () => void; hide: () => void }
+ let modalKey: AIProviderPartial
+ let modalConfig: ProviderConfig
+ let providerKeys: AIProviderPartial[]
$: isCloud = $admin.cloud
- $: customAIConfigsEnabled = $licensing.customAIConfigsEnabled
+ $: providerKeys = isCloud ? [BBAI_KEY] : [BBAI_KEY, OPENAI_KEY, AZURE_KEY]
+ $: providers = aiConfig
+ ? providerKeys.map((key: AIProviderPartial) => ({
+ key,
+ cfg: getProviderConfig(key),
+ }))
+ : []
- async function fetchAIConfig() {
- try {
- fullAIConfig = (await API.getConfig(ConfigType.AI)) as AIConfig
- } catch (error) {
- notifications.error("Error fetching AI config")
+ $: activeKey = providers.find(p => p.cfg.active)?.key
+ $: enabled = !isCloud ? providers.filter(p => p.key === activeKey) : providers
+ $: disabled = !isCloud ? providers.filter(p => p.key !== activeKey) : []
+
+ function getConfigForProvider(key: AIProviderPartial) {
+ for (const config of Object.values(aiConfig.config)) {
+ if (config.provider === key) {
+ return config
+ }
+ }
+ return undefined
+ }
+
+ function getProviderConfig(key: AIProviderPartial): ProviderConfig {
+ const details = ProviderDetails[key]
+ const config = getConfigForProvider(key) || { ...details.defaultConfig }
+
+ return {
+ ...config,
+ provider: details.provider as AIProvider,
+ name: details.name,
+ active: config.active ?? false,
+ isDefault: config.isDefault ?? false,
}
}
- async function saveConfig() {
- // Use existing key or generate new one
- const id = editingUuid || Helpers.uuid()
+ async function updateProviderConfig(
+ key: AIProviderPartial,
+ enable: boolean,
+ configData: Partial | null = null
+ ) {
+ const details = ProviderDetails[key]
+ const existing = aiConfig.config[key] || {}
+ let updated: ProviderConfig
- // Creating first custom AI Config
- if (!fullAIConfig && editingAIConfig) {
- fullAIConfig = {
- type: ConfigType.AI,
- config: {
- [id]: editingAIConfig,
- },
- }
- } else {
- // We don't store the default BB AI config in the DB
- delete fullAIConfig.config.budibase_ai
-
- // unset the default value from other configs if default is set
- if (editingAIConfig?.isDefault) {
- for (let key in fullAIConfig.config) {
- if (key !== id) {
- fullAIConfig.config[key].isDefault = false
- }
+ if (enable) {
+ if (key === BBAI_KEY) {
+ updated = {
+ ...details.defaultConfig,
+ ...existing,
+ provider: details.provider as AIProvider,
+ name: details.name,
+ active: true,
+ isDefault: true,
+ }
+ } else {
+ updated = {
+ ...details.defaultConfig,
+ ...existing,
+ ...configData,
+ active: true,
+ isDefault: true,
}
}
- // Add new or update existing custom AI Config
- if (editingAIConfig) {
- fullAIConfig.config[id] = editingAIConfig
+ } else {
+ updated = {
+ ...details.defaultConfig,
+ ...existing,
+ ...configData,
+ active: false,
+ isDefault: false,
}
- fullAIConfig.type = ConfigType.AI
+ }
+
+ const baseConfig = { ...aiConfig.config }
+ const payload = {
+ type: ConfigType.AI,
+ config: { ...baseConfig, [key]: updated },
+ }
+ if (enable) {
+ Object.keys(payload.config).forEach(providerKey => {
+ if (providerKey !== key) {
+ payload.config[providerKey] = {
+ ...payload.config[providerKey],
+ active: false,
+ isDefault: false,
+ }
+ }
+ })
}
try {
- await API.saveConfig(fullAIConfig)
- notifications.success(`Successfully saved and activated AI Configuration`)
- } catch (error) {
- notifications.error(
- `Failed to save AI Configuration, reason: ${
- error instanceof Error ? error.message : "Unknown"
- }`
- )
- } finally {
- await fetchAIConfig()
+ await API.saveConfig(payload)
+ aiConfig = (await API.getConfig(ConfigType.AI)) as AIConfig
+ notifications.success(`AI provider updated`)
+ } catch (err: any) {
+ notifications.error(err.message || "Failed to update AI provider")
}
+ configModal?.hide()
}
- async function deleteConfig(key: string) {
- // We don't store the default BB AI config in the DB
- delete fullAIConfig.config.budibase_ai
- // Delete the configuration
- delete fullAIConfig.config[key]
+ function handleEnable(key: AIProviderPartial) {
+ modalKey = key
+ if (
+ key === BBAI_KEY &&
+ !$admin.cloud &&
+ !$licensing.customAIConfigsEnabled
+ ) {
+ portalModal.show()
+ return
+ }
+ if (key === BBAI_KEY) {
+ updateProviderConfig(key, true)
+ return
+ }
+
+ const currentCfg = getProviderConfig(key)
+ modalConfig = { ...currentCfg }
+ configModal.show()
+ }
+
+ function handleDisable(key: AIProviderPartial) {
+ if (
+ key === BBAI_KEY &&
+ !$admin.cloud &&
+ !$licensing.customAIConfigsEnabled
+ ) {
+ portalModal.show()
+ return
+ }
+ updateProviderConfig(key, false)
+ }
+
+ function setBannerLocalStorageKey() {
+ localStorage.setItem(bannerKey, "true")
+ }
+
+ onMount(async () => {
try {
- await API.saveConfig(fullAIConfig)
- notifications.success(`Deleted config`)
- } catch (error) {
- notifications.error(
- `Failed to delete config, reason: ${
- error instanceof Error ? error.message : "Unknown"
- }`
- )
- } finally {
- await fetchAIConfig()
+ aiConfig = (await API.getConfig(ConfigType.AI)) as AIConfig
+ } catch {
+ notifications.error("Error fetching AI settings")
}
- }
-
- function editConfig(uuid: string) {
- editingUuid = uuid
- editingAIConfig = fullAIConfig?.config[editingUuid]
- modal.show()
- }
-
- function newConfig() {
- editingUuid = undefined
- editingAIConfig = undefined
- modal.show()
- }
-
- onMount(() => {
- fetchAIConfig()
})
-
-
-
- Connect an LLM to enable AI features. You can only enable one LLM at a
- time.
+
+ Connect an LLM to enable AI features. You can only enable one LLM at a
+ time.
+
-
-
-
-
AI Configurations
-
- Add configuration
-
+
+ {#if !enabled.length && !$bannerStore}
+
+
+
+
+
+
Try BB AI for free. 50,000 tokens included. No CC required.
- Use the following interface to select your preferred AI configuration.
- {#if customAIConfigsEnabled}
- Select your AI Model:
- {/if}
- {#if fullAIConfig?.config}
- {#each Object.keys(fullAIConfig.config) as key}
+
+ handleEnable(BBAI_KEY)}>
+ Enable BB AI
+
+ {
+ setBannerLocalStorageKey()
+ bannerStore.set(true)
+ }}
+ />
+
+
+ {/if}
+
+
+
Enabled
+ {#if enabled.length}
+ {#each enabled as { key, cfg } (key)}
+
handleEnable(key)}
+ disableHandler={() => handleDisable(key)}
+ />
+ {/each}
+ {:else}
+
+ No LLMs are enabled
+
+ {/if}
+ {#if !isCloud}
+ Disabled
+
+ {#each disabled as { key, cfg } (key)}
editConfig(key) : null}
- deleteHandler={customAIConfigsEnabled
- ? () => deleteConfig(key)
- : null}
+ config={cfg}
+ editHandler={() => handleEnable(key)}
+ disableHandler={() => handleDisable(key)}
/>
{/each}
- {/if}
-
+
+ {/if}
-
diff --git a/packages/builder/src/pages/builder/portal/settings/ai/logos/BBAI.svelte b/packages/builder/src/pages/builder/portal/settings/ai/logos/BBAI.svelte
new file mode 100644
index 0000000000..72ffeed4b1
--- /dev/null
+++ b/packages/builder/src/pages/builder/portal/settings/ai/logos/BBAI.svelte
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/types/src/documents/global/config.ts b/packages/types/src/documents/global/config.ts
index eb5b1ac00f..860118b316 100644
--- a/packages/types/src/documents/global/config.ts
+++ b/packages/types/src/documents/global/config.ts
@@ -119,6 +119,8 @@ export type AIProvider =
| "Custom"
| "BudibaseAI"
+export type AIProviderPartial = "BudibaseAI" | "OpenAI" | "AzureOpenAI"
+
export interface ProviderConfig {
provider: AIProvider
isDefault: boolean