Merge master.
This commit is contained in:
commit
c552190498
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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: "",
|
||||
},
|
||||
}
|
|
@ -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: "",
|
||||
},
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -119,6 +119,8 @@ export type AIProvider =
|
|||
| "Custom"
|
||||
| "BudibaseAI"
|
||||
|
||||
export type AIProviderPartial = "BudibaseAI" | "OpenAI" | "AzureOpenAI"
|
||||
|
||||
export interface ProviderConfig {
|
||||
provider: AIProvider
|
||||
isDefault: boolean
|
||||
|
|
Loading…
Reference in New Issue