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