Merge master.

This commit is contained in:
Sam Rose 2025-04-29 11:10:11 +01:00
commit c552190498
No known key found for this signature in database
9 changed files with 565 additions and 393 deletions

View File

@ -1,65 +1,57 @@
<script lang="ts"> <script lang="ts">
import { Body, Label, Icon } from "@budibase/bbui" import { Body, ActionButton } from "@budibase/bbui"
import BudibaseLogo from "./logos/Budibase.svelte"
import OpenAILogo from "./logos/OpenAI.svelte" import OpenAILogo from "./logos/OpenAI.svelte"
import AnthropicLogo from "./logos/Anthropic.svelte"
import TogetherAILogo from "./logos/TogetherAI.svelte"
import AzureOpenAILogo from "./logos/AzureOpenAI.svelte" import AzureOpenAILogo from "./logos/AzureOpenAI.svelte"
import { Providers } from "./constants" import BudibaseAILogo from "./logos/BBAI.svelte"
import type { ProviderConfig } from "@budibase/types" import type { ProviderConfig } from "@budibase/types"
const logos = { import { Providers } from "./constants"
["Budibase AI"]: BudibaseLogo,
[Providers.OpenAI.name]: OpenAILogo, const logos: Record<string, any> = {
[Providers.Anthropic.name]: AnthropicLogo, [Providers.BudibaseAI]: BudibaseAILogo,
[Providers.TogetherAI.name]: TogetherAILogo, [Providers.OpenAI]: OpenAILogo,
[Providers.AzureOpenAI.name]: AzureOpenAILogo, [Providers.AzureOpenAI]: AzureOpenAILogo,
} }
export let config: ProviderConfig export let config: ProviderConfig
export let disabled: boolean | null = null
export let editHandler: (() => void) | null export let editHandler: (() => void) | null
export let deleteHandler: (() => void) | null export let disableHandler: (() => void) | null
let selectedLogo: any =
logos[Providers[config.provider as keyof typeof Providers]]
</script> </script>
<!-- svelte-ignore a11y-no-static-element-interactions --> <div class="option">
<!-- svelte-ignore a11y-click-events-have-key-events --> <div class="details">
<div on:click class:disabled class="option">
<div class="icon"> <div class="icon">
<svelte:component <svelte:component this={selectedLogo} height="26" width="26" />
this={logos[config.name || config.provider]}
height="18"
width="18"
/>
</div> </div>
<div class="header"> <div class="header">
<Body>{config.provider}</Body> <Body size="S" weight={"600"}>{config.name}</Body>
<Label>{config.name}</Label>
</div> </div>
<div class="controls"> <div class="controls">
{#if config.name !== "Budibase AI"}
<Icon
on:click={editHandler}
color="var(--grey-6)"
size="S"
hoverable
name="Edit"
/>
<Icon
on:click={deleteHandler}
color="var(--grey-6)"
size="S"
hoverable
name="Delete"
/>
{/if}
{#if config.active} {#if config.active}
<div class="tag active">Activated</div> <div class="tag active">Enabled</div>
{:else if !config.active} {:else}
<div class="tag disabled">Disabled</div> <div class="tag disabled">Disabled</div>
{/if} {/if}
{#if config.isDefault} </div>
<div class="tag default">Default</div> </div>
<div>
{#if config.provider === "BudibaseAI"}
{#if config.active}
<ActionButton on:click={() => disableHandler && disableHandler()}>
Disable
</ActionButton>
{:else}
<ActionButton on:click={() => editHandler && editHandler()}>
Enable
</ActionButton>
{/if}
{:else}
<!-- OpenAI or AzureOpenAI -->
<ActionButton on:click={() => editHandler && editHandler()}>
{#if config.apiKey}Edit{:else}Set up{/if}
</ActionButton>
{/if} {/if}
</div> </div>
</div> </div>
@ -71,10 +63,16 @@
padding: 16px; padding: 16px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
display: grid; display: flex;
grid-template-columns: 6% 1fr auto;
grid-gap: 20px;
align-items: center; align-items: center;
justify-content: space-between;
gap: 12px;
}
.details {
display: flex;
align-items: center;
gap: 12px;
} }
.option :global(label) { .option :global(label) {
@ -87,12 +85,13 @@
.header { .header {
align-items: center; align-items: center;
color: var(--spectrum-global-color-gray-900);
} }
.icon { .icon {
background-color: white; background-color: var(--spectrum-global-color-gray-200);
height: 38px; height: 40px;
width: 38px; width: 40px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -103,33 +102,21 @@
pointer-events: none; pointer-events: none;
} }
.controls {
display: grid;
grid-auto-flow: column;
grid-gap: 10px;
align-items: center;
}
.tag { .tag {
display: flex;
color: #ffffff;
padding: 4px 8px; padding: 4px 8px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 12px; font-size: 12px;
border-radius: 5px; border-radius: 5px;
} color: #fff;
.default {
background: var(--grey-6);
} }
.active { .active {
background: var(--spectrum-global-color-green-600); background: #004c2e;
} }
.disabled { .disabled {
background: var(--spectrum-global-color-red-600); background: var(--grey-3);
} }
</style> </style>

View File

@ -56,66 +56,18 @@ describe("AISettings", () => {
expect(instance).toBeDefined() expect(instance).toBeDefined()
}) })
describe("Licensing", () => { describe("DOM Render tests", () => {
it("should show the premium label on self host for custom configs", async () => { it("the enable bb ai button should not do anything if the user doesn't have the correct license on self host", async () => {
setupEnv(Hosting.Self) let addAiButton
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
let configModal let configModal
setupEnv(Hosting.Cloud) setupEnv(Hosting.Self, { customAIConfigsEnabled: false })
setupDOM() setupDOM()
addConfigurationButton = instance.queryByText("Add configuration") addAiButton = instance.queryByText("Enable BB AI")
expect(addConfigurationButton).toBeInTheDocument() expect(addAiButton).toBeInTheDocument()
await fireEvent.click(addConfigurationButton) await fireEvent.click(addAiButton)
configModal = instance.queryByText("Custom AI Configuration") configModal = instance.queryByText("Custom AI Configuration")
expect(configModal).not.toBeInTheDocument() 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()
})
}) })
}) })

View File

@ -1,76 +1,42 @@
<script> <script lang="ts">
import { ModalContent, Label, Input, Select, Toggle } from "@budibase/bbui" import { ModalContent, Label, Input, Select } from "@budibase/bbui"
import { ConfigMap, Providers } from "./constants" import { ConfigMap, Models } from "./constants"
import type { ProviderConfig } from "@budibase/types"
export let config = { export let config: ProviderConfig
active: false, export let updateHandler: (_config: ProviderConfig) => void
isDefault: false, export let enableHandler: (_config: ProviderConfig) => void
} export let disableHandler: (_config: ProviderConfig) => void
export let saveHandler let complete: boolean
export let deleteHandler
let validation $: isEnabled = config.active && config.isDefault
$: { $: {
const { provider, defaultModel, name, apiKey } = config const { provider, defaultModel, name, apiKey } = config
validation = provider && defaultModel && name && apiKey complete = Boolean(provider && name && defaultModel && apiKey)
} }
$: canEditBaseUrl = $: canEditBaseUrl =
config.provider && ConfigMap[config.provider].baseUrl === "" config.provider &&
ConfigMap[config.provider as keyof typeof ConfigMap].baseUrl === ""
function prefillConfig(evt) {
const provider = evt.detail
// grab the preset config from the constants for that provider and fill it in
if (ConfigMap[provider]) {
config = {
...config,
...ConfigMap[provider],
provider,
}
} else {
config.provider = provider
}
}
</script> </script>
<ModalContent <ModalContent
confirmText={"Save"} cancelText={isEnabled ? "Disable" : "Update"}
cancelText={"Delete"} confirmText={isEnabled ? "Update" : "Enable"}
onConfirm={saveHandler} onConfirm={isEnabled
onCancel={deleteHandler} ? () => updateHandler(config)
disabled={!validation} : () => enableHandler(config)}
onCancel={isEnabled
? () => disableHandler(config)
: () => updateHandler(config)}
disabled={!complete}
size="M" size="M"
title="Custom AI Configuration" title={`Set up ${config.name}`}
> >
<div class="form-row"> <div class="form-row">
<Label size="M">Provider</Label> <Label size="M">API Key</Label>
<Select <Input type="password" bind:value={config.apiKey} />
placeholder={null}
bind:value={config.provider}
options={Object.keys(Providers)}
on:change={prefillConfig}
/>
</div>
<div class="form-row">
<Label size="M">Name</Label>
<Input
error={config.name === "Budibase AI" ? "Cannot use this name" : null}
placeholder={"Enter a name"}
bind:value={config.name}
/>
</div>
<div class="form-row">
<Label size="M">Default Model</Label>
{#if config.provider !== Providers.Custom.name}
<Select
placeholder={config.provider ? "Choose an option" : "Select a provider"}
bind:value={config.defaultModel}
options={config.provider ? Providers[config.provider].models : []}
/>
{:else}
<Input bind:value={config.defaultModel} />
{/if}
</div> </div>
<div class="form-row"> <div class="form-row">
<Label size="M">Base URL</Label> <Label size="M">Base URL</Label>
@ -80,13 +46,14 @@
bind:value={config.baseUrl} bind:value={config.baseUrl}
/> />
</div> </div>
<div class="form-row"> <div class="form-row">
<Label size="M">API Key</Label> <Label size="M">Default Model</Label>
<Input type="password" bind:value={config.apiKey} /> <Select
</div> placeholder={config.provider ? "Choose an option" : "Select a provider"}
<div class="form-row"> bind:value={config.defaultModel}
<Toggle text="Active" bind:value={config.active} /> options={Models}
<Toggle text="Set as default" bind:value={config.isDefault} /> />
</div> </div>
</ModalContent> </ModalContent>

View File

@ -0,0 +1,21 @@
<script lang="ts">
import { ModalContent, Body, Link } from "@budibase/bbui"
import { admin } from "@/stores/portal"
export let confirmHandler: () => void
export let cancelHandler: () => void
</script>
<ModalContent
title="Setup BB AI"
confirmText="Go to Portal"
cancelText="Cancel"
onConfirm={confirmHandler}
onCancel={cancelHandler}
>
<Body>To setup BB AI you must add a Budibase license key.</Body>
<Body>
To get your license key, account admins must sign up to the
<Link href={$admin.accountPortalUrl}>Budibase Account Portal</Link>
</Body>
</ModalContent>

View File

@ -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: "",
},
}

View File

@ -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: "",
},
}

View File

@ -5,193 +5,332 @@
Layout, Layout,
Heading, Heading,
Body, Body,
Helpers,
Divider, Divider,
notifications, notifications,
Modal, Modal,
Tags, Icon,
Tag,
} from "@budibase/bbui" } from "@budibase/bbui"
import BBAI from "assets/bb-ai.svg"
import { admin, licensing } from "@/stores/portal" import { admin, licensing } from "@/stores/portal"
import { API } from "@/api" import { BudiStore, PersistenceType } from "@/stores/BudiStore"
import AIConfigModal from "./ConfigModal.svelte"
import AIConfigTile from "./AIConfigTile.svelte"
import {
type AIConfig,
ConfigType,
type ProviderConfig,
} from "@budibase/types"
let modal: Modal import { API } from "@/api"
let fullAIConfig: AIConfig import AIConfigTile from "./AIConfigTile.svelte"
let editingAIConfig: ProviderConfig | undefined import ConfigModal from "./ConfigModal.svelte"
let editingUuid: string | undefined 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<boolean>(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 $: 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() { $: activeKey = providers.find(p => p.cfg.active)?.key
try { $: enabled = !isCloud ? providers.filter(p => p.key === activeKey) : providers
fullAIConfig = (await API.getConfig(ConfigType.AI)) as AIConfig $: disabled = !isCloud ? providers.filter(p => p.key !== activeKey) : []
} catch (error) {
notifications.error("Error fetching AI config") 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() { async function updateProviderConfig(
// Use existing key or generate new one key: AIProviderPartial,
const id = editingUuid || Helpers.uuid() enable: boolean,
configData: Partial<ProviderConfig> | null = null
) {
const details = ProviderDetails[key]
const existing = aiConfig.config[key] || {}
let updated: ProviderConfig
// Creating first custom AI Config if (enable) {
if (!fullAIConfig && editingAIConfig) { if (key === BBAI_KEY) {
fullAIConfig = { updated = {
type: ConfigType.AI, ...details.defaultConfig,
config: { ...existing,
[id]: editingAIConfig, provider: details.provider as AIProvider,
}, name: details.name,
active: true,
isDefault: true,
} }
} else { } else {
// We don't store the default BB AI config in the DB updated = {
delete fullAIConfig.config.budibase_ai ...details.defaultConfig,
...existing,
...configData,
active: true,
isDefault: true,
}
}
} else {
updated = {
...details.defaultConfig,
...existing,
...configData,
active: false,
isDefault: false,
}
}
// unset the default value from other configs if default is set const baseConfig = { ...aiConfig.config }
if (editingAIConfig?.isDefault) { const payload = {
for (let key in fullAIConfig.config) { type: ConfigType.AI,
if (key !== id) { config: { ...baseConfig, [key]: updated },
fullAIConfig.config[key].isDefault = false }
if (enable) {
Object.keys(payload.config).forEach(providerKey => {
if (providerKey !== key) {
payload.config[providerKey] = {
...payload.config[providerKey],
active: false,
isDefault: false,
} }
} }
} })
// Add new or update existing custom AI Config
if (editingAIConfig) {
fullAIConfig.config[id] = editingAIConfig
}
fullAIConfig.type = ConfigType.AI
} }
try { try {
await API.saveConfig(fullAIConfig) await API.saveConfig(payload)
notifications.success(`Successfully saved and activated AI Configuration`) aiConfig = (await API.getConfig(ConfigType.AI)) as AIConfig
} catch (error) { notifications.success(`AI provider updated`)
notifications.error( } catch (err: any) {
`Failed to save AI Configuration, reason: ${ notifications.error(err.message || "Failed to update AI provider")
error instanceof Error ? error.message : "Unknown"
}`
)
} finally {
await fetchAIConfig()
} }
configModal?.hide()
} }
async function deleteConfig(key: string) { function handleEnable(key: AIProviderPartial) {
// We don't store the default BB AI config in the DB modalKey = key
delete fullAIConfig.config.budibase_ai if (
// Delete the configuration key === BBAI_KEY &&
delete fullAIConfig.config[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 { try {
await API.saveConfig(fullAIConfig) aiConfig = (await API.getConfig(ConfigType.AI)) as AIConfig
notifications.success(`Deleted config`) } catch {
} catch (error) { notifications.error("Error fetching AI settings")
notifications.error(
`Failed to delete config, reason: ${
error instanceof Error ? error.message : "Unknown"
}`
)
} finally {
await fetchAIConfig()
} }
}
function editConfig(uuid: string) {
editingUuid = uuid
editingAIConfig = fullAIConfig?.config[editingUuid]
modal.show()
}
function newConfig() {
editingUuid = undefined
editingAIConfig = undefined
modal.show()
}
onMount(() => {
fetchAIConfig()
}) })
</script> </script>
<Modal bind:this={modal}>
<AIConfigModal
saveHandler={saveConfig}
deleteHandler={deleteConfig}
bind:config={editingAIConfig}
/>
</Modal>
<Layout noPadding> <Layout noPadding>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<div class="header"> <div class="header">
<Heading size="M">AI</Heading> <Heading size="M">AI</Heading>
{#if !isCloud && !customAIConfigsEnabled}
<Tags>
<Tag icon="LockClosed">Premium</Tag>
</Tags>
{:else if isCloud && !customAIConfigsEnabled}
<Tags>
<Tag icon="LockClosed">Enterprise</Tag>
</Tags>
{/if}
</div> </div>
<Body <Body>
>Connect an LLM to enable AI features. You can only enable one LLM at a Connect an LLM to enable AI features. You can only enable one LLM at a
time.</Body time.
> </Body>
</Layout> </Layout>
<Divider /> <Divider />
<div style={`opacity: ${customAIConfigsEnabled ? 1 : 0.5}`}>
<Layout noPadding> {#if !enabled.length && !$bannerStore}
<div class="config-heading"> <div class="banner">
<Heading size="S">AI Configurations</Heading> <div class="banner-content">
<Button <div class="banner-icon">
size="S" <img src={BBAI} alt="BB AI" width="24" height="24" />
cta={customAIConfigsEnabled} </div>
secondary={!customAIConfigsEnabled} <div>Try BB AI for free. 50,000 tokens included. No CC required.</div>
on:click={customAIConfigsEnabled ? newConfig : null} </div>
> <div class="banner-buttons">
Add configuration <Button primary cta size="S" on:click={() => handleEnable(BBAI_KEY)}>
</Button> Enable BB AI
</Button>
<Icon
hoverable
name="Close"
on:click={() => {
setBannerLocalStorageKey()
bannerStore.set(true)
}}
/>
</div>
</div> </div>
<Body size="S"
>Use the following interface to select your preferred AI configuration.</Body
>
{#if customAIConfigsEnabled}
<Body size="S">Select your AI Model:</Body>
{/if} {/if}
{#if fullAIConfig?.config}
{#each Object.keys(fullAIConfig.config) as key} <div class="section">
<div class="section-title">Enabled</div>
{#if enabled.length}
{#each enabled as { key, cfg } (key)}
<AIConfigTile <AIConfigTile
config={fullAIConfig.config[key]} config={cfg}
editHandler={customAIConfigsEnabled ? () => editConfig(key) : null} editHandler={() => handleEnable(key)}
deleteHandler={customAIConfigsEnabled disableHandler={() => handleDisable(key)}
? () => deleteConfig(key)
: null}
/> />
{/each} {/each}
{:else}
<div class="no-enabled">
<Body size="S">No LLMs are enabled</Body>
</div>
{/if}
{#if !isCloud}
<div class="section-title disabled-title">Disabled</div>
<div class="ai-list">
{#each disabled as { key, cfg } (key)}
<AIConfigTile
config={cfg}
editHandler={() => handleEnable(key)}
disableHandler={() => handleDisable(key)}
/>
{/each}
</div>
{/if} {/if}
</Layout>
</div> </div>
</Layout> </Layout>
<style> <Modal bind:this={portalModal}>
.config-heading { <PortalModal
display: flex; confirmHandler={() => {
justify-content: space-between; window.open($admin.accountPortalUrl, "_blank")
align-items: center; portalModal.hide()
margin-bottom: -18px; }}
} cancelHandler={() => portalModal.hide()}
/>
</Modal>
<Modal bind:this={configModal}>
<ConfigModal
config={modalConfig}
updateHandler={updatedConfig =>
updateProviderConfig(modalKey, false, updatedConfig)}
enableHandler={updatedConfig =>
updateProviderConfig(modalKey, true, updatedConfig)}
disableHandler={() => updateProviderConfig(modalKey, false)}
/>
</Modal>
<style>
.header { .header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
} }
.banner {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #2e3851;
border-radius: var(--border-radius-m);
padding: var(--spacing-s);
}
.banner-content {
display: flex;
align-items: center;
gap: 12px;
}
.banner-icon {
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--border-radius-s);
width: 32px;
height: 32px;
}
.banner-buttons {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.ai-list {
margin-top: var(--spacing-l);
margin-bottom: var(--spacing-l);
display: flex;
flex-direction: column;
gap: 12px;
}
.no-enabled {
padding: 16px;
background-color: var(--spectrum-global-color-gray-75);
border: 1px solid var(--grey-4);
border-radius: var(--border-radius-s);
}
.section-title {
margin-bottom: var(--spacing-m);
}
.disabled-title {
margin-top: var(--spacing-xl);
}
</style> </style>

View File

@ -0,0 +1,75 @@
<script>
export let width
export let height
</script>
<svg
{width}
{height}
viewBox="0 0 636 636"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.3447 440.727L195.1 621.483C203.025 629.408 215.866 629.408 223.79 621.483L302.5 542.774C309.583 535.69 312.931 525.68 311.54 515.75L290.403 366.368C288.85 355.5 280.311 346.962 269.46 345.425L120.078 324.288C110.164 322.881 100.154 326.229 93.0544 333.328L14.3447 412.038C6.42032 419.962 6.42032 432.803 14.3447 440.727Z"
fill="url(#paint0_linear_608_138)"
/>
<path
d="M440.54 621.487L621.296 440.732C629.221 432.807 629.221 419.967 621.296 412.042L542.587 333.332C535.503 326.249 525.493 322.901 515.563 324.292L366.181 345.429C355.313 346.982 346.775 355.521 345.238 366.372L324.101 515.754C322.694 525.668 326.042 535.678 333.141 542.778L411.851 621.487C419.775 629.412 432.616 629.412 440.54 621.487Z"
fill="url(#paint1_linear_608_138)"
/>
<path
d="M621.655 195.108L440.9 14.3526C432.975 6.4283 420.135 6.42825 412.21 14.3526L333.5 93.0623C326.417 100.146 323.069 110.156 324.46 120.086L345.597 269.468C347.15 280.335 355.689 288.874 366.54 290.411L515.922 311.548C525.836 312.955 535.846 309.607 542.946 302.508L621.655 223.798C629.58 215.873 629.58 203.033 621.655 195.108Z"
fill="url(#paint2_linear_608_138)"
/>
<path
d="M195.46 14.3447L14.7037 195.1C6.77937 203.025 6.77932 215.865 14.7037 223.79L93.4134 302.5C100.497 309.583 110.507 312.931 120.437 311.54L269.819 290.403C280.687 288.85 289.225 280.311 290.762 269.46L311.899 120.078C313.306 110.164 309.958 100.154 302.859 93.0544L224.149 14.3447C216.225 6.42033 203.384 6.42032 195.46 14.3447Z"
fill="url(#paint3_linear_608_138)"
/>
<defs>
<linearGradient
id="paint0_linear_608_138"
x1="196.613"
y1="602.376"
x2="295.324"
y2="374.302"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#6E56FF" />
<stop offset="1" stop-color="#9F8FFF" />
</linearGradient>
<linearGradient
id="paint1_linear_608_138"
x1="602.189"
y1="439.219"
x2="374.115"
y2="340.508"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#6E56FF" />
<stop offset="1" stop-color="#9F8FFF" />
</linearGradient>
<linearGradient
id="paint2_linear_608_138"
x1="439.387"
y1="33.4599"
x2="340.676"
y2="261.534"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#6E56FF" />
<stop offset="1" stop-color="#9F8FFF" />
</linearGradient>
<linearGradient
id="paint3_linear_608_138"
x1="33.811"
y1="196.613"
x2="261.885"
y2="295.324"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#6E56FF" />
<stop offset="1" stop-color="#9F8FFF" />
</linearGradient>
</defs>
</svg>

View File

@ -119,6 +119,8 @@ export type AIProvider =
| "Custom" | "Custom"
| "BudibaseAI" | "BudibaseAI"
export type AIProviderPartial = "BudibaseAI" | "OpenAI" | "AzureOpenAI"
export interface ProviderConfig { export interface ProviderConfig {
provider: AIProvider provider: AIProvider
isDefault: boolean isDefault: boolean