icons, styling of AI configs and further simplification

This commit is contained in:
Martin McKeaveney 2024-09-03 15:11:52 +01:00
parent f328ae4bf9
commit ca4b17bc9b
10 changed files with 276 additions and 102 deletions

View File

@ -19,7 +19,6 @@ import { DocumentType, SEPARATOR } from "../constants"
import { CacheKey, TTL, withCache } from "../cache"
import * as context from "../context"
import env from "../environment"
import { getConfigParams } from "@budibase/server/src/migrations/functions/backfill/global/configs"
// UTILS
@ -262,37 +261,37 @@ export async function getSCIMConfig(): Promise<SCIMInnerConfig | undefined> {
// AI
// TODO: Can we assume that you are licensed when you hit this endpoint?
export async function getAIConfig(): Promise<AIConfig | undefined> {
if (!env.SELF_HOSTED) {
// always use the env vars in cloud
// TODO: Licensing stuff - make this right
if (env.OPENAI_API_KEY) {
return getDefaultBudibaseAIConfig()
}
}
// export async function getAIConfig(): Promise<AIConfig | undefined> {
// if (!env.SELF_HOSTED) {
// // always use the env vars in cloud
// // TODO: Licensing stuff - make this right
// if (env.OPENAI_API_KEY) {
// return getDefaultBudibaseAIConfig()
// }
// }
// prefer the config in self-host
let config = await getConfig<AIConfig>(ConfigType.AI)
// let config = await getConfig<AIConfig>(ConfigType.AI)
// fallback to env vars
if (!config || !config.activated) {
config = getDefaultBudibaseAIConfig()
}
// if (!config || !config.activated) {
// config = getDefaultBudibaseAIConfig()
// }
return config
}
// return config
// }
export function getDefaultBudibaseAIConfig(): AIInnerConfig | undefined {
if (env.OPENAI_API_KEY) {
return {
provider: "",
isDefault: true,
name: "Budibase AI",
active: true,
baseUrl: "",
apiKey: env.OPENAI_API_KEY,
// TODO: should be enum
defaultModel: ""
}
}
}
// export function getDefaultBudibaseAIConfig(): AIInnerConfig | undefined {
// if (env.OPENAI_API_KEY) {
// return {
// provider: "",
// isDefault: true,
// name: "Budibase AI",
// active: true,
// baseUrl: "",
// apiKey: env.OPENAI_API_KEY,
// // TODO: should be enum
// defaultModel: ""
// }
// }
// }

View File

@ -0,0 +1,90 @@
<script>
import {Body, Label, Icon, Tag} from "@budibase/bbui"
import OpenAILogo from "./logos/OpenAI.svelte"
import AnthropicLogo from "./logos/Anthropic.svelte"
import { Providers } from "./constants"
export let config
export let disabled
export let editHandler
export let deleteHandler
</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="icon">
{#if config.provider === Providers.OpenAI.name}
<OpenAILogo height="30" width="30"/>
{:else if config.provider === Providers.Anthropic.name}
<AnthropicLogo height="30" width="30"/>
{/if}
</div>
<div class="header">
<Body>{config.name}</Body>
<Label>{config.name}</Label>
</div>
<div class="controls">
<Icon
on:click={editHandler}
size="S"
hoverable
name="Edit"
/>
<Icon
on:click={deleteHandler}
size="S"
hoverable
name="Delete"
/>
<div>Activated</div>
</div>
</div>
<style>
.option {
background-color: var(--background);
border: 1px solid var(--grey-4);
padding: 10px 16px 14px;
border-radius: 4px;
cursor: pointer;
display: grid;
grid-template-columns: 6% 1fr 20%;
grid-gap: 20px;
align-items: center;
}
.option :global(label) {
cursor: pointer;
}
.option:hover {
background-color: var(--background-alt);
}
.header {
align-items: center;
}
.icon {
background-color: white;
height: 38px;
width: 38px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
}
.disabled {
pointer-events: none;
}
.controls {
display: grid;
grid-auto-flow: column;
grid-gap: 10px;
align-items: center;
}
</style>

View File

@ -8,27 +8,17 @@
Body,
notifications,
} from "@budibase/bbui"
import { ConfigMap } from "./constants"
import { ConfigMap, Providers } from "./constants"
import { API } from "api"
// TODO: Update these
const providers = [
"OpenAI",
"Anthropic",
"Together AI",
"Azure Open AI",
"Custom"
]
const models = [
"gpt4o-mini",
]
const defaultConfig = {
export let defaultConfig = {
active: false,
isDefault: false,
}
export let saveHandler
export let deleteHandler
let aiConfig = defaultConfig
let validation
@ -43,48 +33,15 @@
if (ConfigMap[provider]) {
aiConfig = {
...aiConfig,
...ConfigMap[provider]
}
} else {
aiConfig = {
...aiConfig,
...ConfigMap[provider],
provider
}
}
}
async function saveConfig() {
const config = {
type: "ai",
config: [
// TODO: include the ones that are already there, or just handle this in the backend
aiConfig,
]
}
try {
const savedConfig = await API.saveConfig(config)
aiConfig._rev = savedConfig._rev
aiConfig._id = savedConfig._id
notifications.success(`Configuration saved`)
} catch (error) {
notifications.error(
`Failed to save AI Configuration, reason: ${error?.message || "Unknown"}`
)
}
}
async function deleteConfig() {
// Delete a configuration
try {
// await API.deleteConfig({
// id: smtpConfig._id,
// rev: smtpConfig._rev,
// })
notifications.success(`Deleted config`)
} catch (error) {
notifications.error(
`Failed to clear email settings, reason: ${error?.message || "Unknown"}`
)
} else {
aiConfig.provider = provider
// aiConfig = {
// ...aiConfig,
// provider
// }
}
}
@ -93,8 +50,8 @@
<ModalContent
confirmText={"Save"}
cancelText={"Delete"}
onConfirm={saveConfig}
onCancel={deleteConfig}
onConfirm={saveHandler}
onCancel={deleteHandler}
disabled={!validation}
size="M"
title="Custom AI Configuration"
@ -104,16 +61,16 @@
<Select
placeholder={null}
bind:value={aiConfig.provider}
options={providers}
options={Object.keys(Providers)}
on:change={prefillConfig}
/>
</div>
<div class="form-row">
<Label size="M">Default Model</Label>
<Select
placeholder={null}
placeholder={aiConfig.provider ? "Choose an option" : "Select a provider first"}
bind:value={aiConfig.defaultModel}
options={models}
options={aiConfig.provider ? Providers[aiConfig.provider].models : []}
/>
</div>
<div class="form-row">
@ -126,7 +83,7 @@
</div>
<div class="form-row">
<Label size="M">API Key</Label>
<Input bind:value={aiConfig.apiKey}/>
<Input type="password" bind:value={aiConfig.apiKey}/>
</div>
<Toggle text="Active" bind:value={aiConfig.active}/>
<Toggle text="Set as default" bind:value={aiConfig.isDefault}/>

View File

@ -1,8 +1,47 @@
export const Providers = {
OpenAI: {
name: "OpenAI",
models: [
"gpt-4o-mini",
"gpt-4o",
"gpt-3.5-turbo",
"chatgpt-4o-latest",
"gpt-4-turbo",
"gpt-4",
]
},
Anthropic: {
name: "Anthropic",
models: [
"claude-3-5-sonnet-20240620",
"claude-3-sonnet-20240229",
"claude-3-opus-20240229",
"claude-3-haiku-20240307"
]
},
TogetherAI: {
name: "Together AI",
// TODO: too many - probably need to use an autocomplete for this
models: [""]
},
Custom: {
name: "Custom",
// TODO: too many - probably need to use an autocomplete for this
models: [""]
},
}
export const ConfigMap = {
OpenAI: {
baseUrl: "https://api.openai.com"
},
Anthropic: {
baseUrl: ""
},
TogetherAI: {
baseUrl: "https://api.together.xyz/v1"
},
Custom: {
baseUrl: ""
}
}

View File

@ -1,4 +1,5 @@
<script>
import { onMount } from "svelte"
import {
Button,
Layout,
@ -14,8 +15,7 @@
import { admin, licensing } from "stores/portal"
import { API } from "api"
import AIConfigModal from "./ConfigModal.svelte"
import { onMount } from "svelte"
import { sdk } from "@budibase/shared-core"
import AIConfigTile from "./AIConfigTile.svelte"
const ConfigTypes = {
AI: "ai",
@ -23,26 +23,75 @@
let modal
let aiConfig
let loading = false
let currentlyEditingConfig
$: isCloud = $admin.cloud
$: budibaseAIEnabled = $licensing.budibaseAIEnabled
$: customAIConfigsEnabled = $licensing.customAIConfigsEnabled
async function fetchAIConfig() {
loading = true
try {
// Fetch the configs for smtp
// Fetch the AI configs
const aiDoc = await API.getConfig(ConfigTypes.AI)
if (aiDoc._id) {
aiConfig = aiDoc
}
loading = false
} catch (error) {
notifications.error("Error fetching AI config")
}
}
async function saveConfig() {
// Update the config that was changed
const updateConfigs = aiConfig.config
const config = {
type: ConfigTypes.AI,
config: [
// TODO: include the ones that are already there, or just handle this in the backend
aiConfig,
]
}
try {
const isNew = !!aiConfig._rev
const savedConfig = await API.saveConfig(config)
aiConfig._rev = savedConfig._rev
aiConfig._id = savedConfig._id
notifications.success(`Successfully saved and activated ${isNew ? "new" : ""} AI Configuration`)
} catch (error) {
notifications.error(
`Failed to save AI Configuration, reason: ${error?.message || "Unknown"}`
)
}
}
async function deleteConfig(name) {
// Delete a configuration
const idx = aiConfig.config.findIndex(config => config.name === currentlyEditingConfig?.name || name)
aiConfig.config.splice(idx, 1)
try {
const savedConfig = await API.saveConfig(aiConfig)
aiConfig._rev = savedConfig._rev
aiConfig._id = savedConfig._id
notifications.success(`Deleted config`)
} catch (error) {
notifications.error(
`Failed to delete config, reason: ${error?.message || "Unknown"}`
)
}
}
function editConfig(config) {
currentlyEditingConfig = config
modal.show()
}
function newConfig() {
currentlyEditingConfig = undefined
modal.show()
}
onMount(async () => {
await fetchAIConfig()
})
@ -51,7 +100,11 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<Modal bind:this={modal}>
<AIConfigModal />
<AIConfigModal
saveHandler={saveConfig}
deleteHandler={deleteConfig}
defaultConfig={currentlyEditingConfig}
/>
</Modal>
<Layout noPadding>
<Layout gap="XS" noPadding>
@ -76,11 +129,20 @@
<Tag icon="LockClosed">Enterprise</Tag>
</Tags>
{:else}
<Button size="S" cta on:click={modal.show}>Add configuration</Button>
<Button size="S" cta on:click={newConfig}>Add configuration</Button>
{/if}
</div>
<Body size="S">Use the following interface to select your preferred AI configuration.</Body>
<Body size="S">Select your AI Model:</Body>
{#if aiConfig}
{#each aiConfig.config as config}
<AIConfigTile
{config}
editHandler={() => editConfig(config)}
deleteHandler={modal.show}
/>
{/each}
{/if}
</Layout>
</Layout>

View File

@ -0,0 +1,15 @@
<script>
export let height
export let width
</script>
<svg id="katman_1" {height} {width} data-name="katman 1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 841.89 595.28">
<defs>
<style>
.cls-1 {
fill: #000;
stroke-width: 0px;
}
</style>
</defs>
<path class="cls-1" d="M552.13,91.34h-90.99l162.93,412.61h88.87l-160.81-412.61ZM289.76,91.34l-160.81,412.61h90.99l35.97-86.75h169.28l33.86,84.64h90.99L384.97,91.34h-95.22ZM281.29,341.02l55.01-146,57.13,146h-112.14Z"/>
</svg>

View File

@ -0,0 +1,5 @@
<script>
export let height
export let width
</script>
<svg fill="#000000" {width} {height} viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>OpenAI icon</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>

View File

@ -123,7 +123,7 @@ export interface AIInnerConfig {
defaultModel: string
}
export interface AIConfig extends Config<AIInnerConfig> {}
export interface AIConfig extends Config<AIInnerConfig[]> {}
export const isSettingsConfig = (config: Config): config is SettingsConfig =>
config.type === ConfigType.SETTINGS

View File

@ -28,7 +28,7 @@ import {
SSOConfig,
SSOConfigType,
UserCtx,
OIDCLogosConfig,
OIDCLogosConfig, AIConfig,
} from "@budibase/types"
import * as pro from "@budibase/pro"
@ -313,6 +313,13 @@ function enrichOIDCLogos(oidcLogos: OIDCLogosConfig) {
)
}
function sanitizeAIConfig(aiConfig: AIConfig) {
for (let providerConfig of aiConfig.config) {
delete providerConfig.apiKey
}
return aiConfig
}
export async function find(ctx: UserCtx) {
try {
// Find the config with the most granular scope based on context
@ -325,8 +332,8 @@ export async function find(ctx: UserCtx) {
}
if (type === ConfigType.AI) {
// TODO: strip the keys from the configs here
// TODO: do the licensing checks here and return the right things based on the license
sanitizeAIConfig(scopedConfig)
}
ctx.body = scopedConfig
} else {

View File

@ -73,7 +73,7 @@ function aiValidation() {
isDefault: Joi.boolean().required(),
name: Joi.string().required(),
active: Joi.boolean().required(),
baseUrl: Joi.string().optional(),
baseUrl: Joi.string().optional().allow("", null),
apiKey: Joi.string().required(),
// TODO: should be enum
defaultModel: Joi.string().optional(),