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">
import { Body, Label, Icon } from "@budibase/bbui"
import BudibaseLogo from "./logos/Budibase.svelte"
import { Body, ActionButton } from "@budibase/bbui"
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 { Providers } from "./constants"
import BudibaseAILogo from "./logos/BBAI.svelte"
import type { ProviderConfig } from "@budibase/types"
const logos = {
["Budibase AI"]: BudibaseLogo,
[Providers.OpenAI.name]: OpenAILogo,
[Providers.Anthropic.name]: AnthropicLogo,
[Providers.TogetherAI.name]: TogetherAILogo,
[Providers.AzureOpenAI.name]: AzureOpenAILogo,
import { Providers } from "./constants"
const logos: Record<string, any> = {
[Providers.BudibaseAI]: BudibaseAILogo,
[Providers.OpenAI]: OpenAILogo,
[Providers.AzureOpenAI]: AzureOpenAILogo,
}
export let config: ProviderConfig
export let disabled: boolean | null = 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>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div on:click class:disabled class="option">
<div class="option">
<div class="details">
<div class="icon">
<svelte:component
this={logos[config.name || config.provider]}
height="18"
width="18"
/>
<svelte:component this={selectedLogo} height="26" width="26" />
</div>
<div class="header">
<Body>{config.provider}</Body>
<Label>{config.name}</Label>
<Body size="S" weight={"600"}>{config.name}</Body>
</div>
<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}
<div class="tag active">Activated</div>
{:else if !config.active}
<div class="tag active">Enabled</div>
{:else}
<div class="tag disabled">Disabled</div>
{/if}
{#if config.isDefault}
<div class="tag default">Default</div>
</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}
</div>
</div>
@ -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);
}
</style>

View File

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

View File

@ -1,76 +1,42 @@
<script>
import { ModalContent, Label, Input, Select, Toggle } from "@budibase/bbui"
import { ConfigMap, Providers } from "./constants"
<script lang="ts">
import { ModalContent, Label, Input, Select } from "@budibase/bbui"
import { ConfigMap, Models } from "./constants"
import type { ProviderConfig } from "@budibase/types"
export let config = {
active: false,
isDefault: false,
}
export let config: ProviderConfig
export let updateHandler: (_config: ProviderConfig) => void
export let enableHandler: (_config: ProviderConfig) => void
export let disableHandler: (_config: ProviderConfig) => void
export let saveHandler
export let deleteHandler
let complete: boolean
let validation
$: isEnabled = config.active && config.isDefault
$: {
const { provider, defaultModel, name, apiKey } = config
validation = provider && defaultModel && name && apiKey
complete = Boolean(provider && name && defaultModel && apiKey)
}
$: canEditBaseUrl =
config.provider && ConfigMap[config.provider].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
}
}
config.provider &&
ConfigMap[config.provider as keyof typeof ConfigMap].baseUrl === ""
</script>
<ModalContent
confirmText={"Save"}
cancelText={"Delete"}
onConfirm={saveHandler}
onCancel={deleteHandler}
disabled={!validation}
cancelText={isEnabled ? "Disable" : "Update"}
confirmText={isEnabled ? "Update" : "Enable"}
onConfirm={isEnabled
? () => updateHandler(config)
: () => enableHandler(config)}
onCancel={isEnabled
? () => disableHandler(config)
: () => updateHandler(config)}
disabled={!complete}
size="M"
title="Custom AI Configuration"
title={`Set up ${config.name}`}
>
<div class="form-row">
<Label size="M">Provider</Label>
<Select
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}
<Label size="M">API Key</Label>
<Input type="password" bind:value={config.apiKey} />
</div>
<div class="form-row">
<Label size="M">Base URL</Label>
@ -80,13 +46,14 @@
bind:value={config.baseUrl}
/>
</div>
<div class="form-row">
<Label size="M">API Key</Label>
<Input type="password" bind:value={config.apiKey} />
</div>
<div class="form-row">
<Toggle text="Active" bind:value={config.active} />
<Toggle text="Set as default" bind:value={config.isDefault} />
<Label size="M">Default Model</Label>
<Select
placeholder={config.provider ? "Choose an option" : "Select a provider"}
bind:value={config.defaultModel}
options={Models}
/>
</div>
</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,
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<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
$: 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<ProviderConfig> | 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,
},
if (enable) {
if (key === BBAI_KEY) {
updated = {
...details.defaultConfig,
...existing,
provider: details.provider as AIProvider,
name: details.name,
active: true,
isDefault: true,
}
} else {
// We don't store the default BB AI config in the DB
delete fullAIConfig.config.budibase_ai
updated = {
...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
if (editingAIConfig?.isDefault) {
for (let key in fullAIConfig.config) {
if (key !== id) {
fullAIConfig.config[key].isDefault = false
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,
}
}
}
// Add new or update existing custom AI Config
if (editingAIConfig) {
fullAIConfig.config[id] = editingAIConfig
}
fullAIConfig.type = ConfigType.AI
})
}
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()
})
</script>
<Modal bind:this={modal}>
<AIConfigModal
saveHandler={saveConfig}
deleteHandler={deleteConfig}
bind:config={editingAIConfig}
/>
</Modal>
<Layout noPadding>
<Layout gap="XS" noPadding>
<div class="header">
<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>
<Body
>Connect an LLM to enable AI features. You can only enable one LLM at a
time.</Body
>
<Body>
Connect an LLM to enable AI features. You can only enable one LLM at a
time.
</Body>
</Layout>
<Divider />
<div style={`opacity: ${customAIConfigsEnabled ? 1 : 0.5}`}>
<Layout noPadding>
<div class="config-heading">
<Heading size="S">AI Configurations</Heading>
<Button
size="S"
cta={customAIConfigsEnabled}
secondary={!customAIConfigsEnabled}
on:click={customAIConfigsEnabled ? newConfig : null}
>
Add configuration
</Button>
{#if !enabled.length && !$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 class="banner-buttons">
<Button primary cta size="S" on:click={() => handleEnable(BBAI_KEY)}>
Enable BB AI
</Button>
<Icon
hoverable
name="Close"
on:click={() => {
setBannerLocalStorageKey()
bannerStore.set(true)
}}
/>
</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 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
config={fullAIConfig.config[key]}
editHandler={customAIConfigsEnabled ? () => editConfig(key) : null}
deleteHandler={customAIConfigsEnabled
? () => deleteConfig(key)
: null}
config={cfg}
editHandler={() => handleEnable(key)}
disableHandler={() => handleDisable(key)}
/>
{/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}
</Layout>
</div>
</Layout>
<style>
.config-heading {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: -18px;
}
<Modal bind:this={portalModal}>
<PortalModal
confirmHandler={() => {
window.open($admin.accountPortalUrl, "_blank")
portalModal.hide()
}}
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 {
display: flex;
align-items: center;
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>

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"
| "BudibaseAI"
export type AIProviderPartial = "BudibaseAI" | "OpenAI" | "AzureOpenAI"
export interface ProviderConfig {
provider: AIProvider
isDefault: boolean