Merge branch 'master' into dependabot/npm_and_yarn/vite-4.5.13

This commit is contained in:
Adria Navarro 2025-04-22 15:55:26 +02:00 committed by GitHub
commit 39148fae11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 997 additions and 289 deletions

View File

@ -10,7 +10,7 @@
},
"dependencies": {
"bulma": "^0.9.3",
"next": "14.2.25",
"next": "14.2.26",
"node-fetch": "^3.2.10",
"sass": "^1.52.3",
"react": "17.0.2",

View File

@ -46,10 +46,10 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@next/env@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.25.tgz#936d10b967e103e49a4bcea1e97292d5605278dd"
integrity sha512-JnzQ2cExDeG7FxJwqAksZ3aqVJrHjFwZQAEJ9gQZSoEhIow7SNoKZzju/AwQ+PLIR4NY8V0rhcVozx/2izDO0w==
"@next/env@14.2.26":
version "14.2.26"
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.26.tgz#5d55f72d2edb7246607c78f61e7d3ff21516bc2e"
integrity sha512-vO//GJ/YBco+H7xdQhzJxF7ub3SUwft76jwaeOyVVQFHCi5DCnkP16WHB+JBylo4vOKPoZBlR94Z8xBxNBdNJA==
"@next/eslint-plugin-next@12.1.0":
version "12.1.0"
@ -58,50 +58,50 @@
dependencies:
glob "7.1.7"
"@next/swc-darwin-arm64@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.25.tgz#7bcccfda0c0ff045c45fbe34c491b7368e373e3d"
integrity sha512-09clWInF1YRd6le00vt750s3m7SEYNehz9C4PUcSu3bAdCTpjIV4aTYQZ25Ehrr83VR1rZeqtKUPWSI7GfuKZQ==
"@next/swc-darwin-arm64@14.2.26":
version "14.2.26"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.26.tgz#84b31a22149b2c49f5c5b29cddd7acb3a84d7e1c"
integrity sha512-zDJY8gsKEseGAxG+C2hTMT0w9Nk9N1Sk1qV7vXYz9MEiyRoF5ogQX2+vplyUMIfygnjn9/A04I6yrUTRTuRiyQ==
"@next/swc-darwin-x64@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.25.tgz#b489e209d7b405260b73f69a38186ed150fb7a08"
integrity sha512-V+iYM/QR+aYeJl3/FWWU/7Ix4b07ovsQ5IbkwgUK29pTHmq+5UxeDr7/dphvtXEq5pLB/PucfcBNh9KZ8vWbug==
"@next/swc-darwin-x64@14.2.26":
version "14.2.26"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.26.tgz#50a5eb37972d313951f76f36f1f0b7100d063ebd"
integrity sha512-U0adH5ryLfmTDkahLwG9sUQG2L0a9rYux8crQeC92rPhi3jGQEY47nByQHrVrt3prZigadwj/2HZ1LUUimuSbg==
"@next/swc-linux-arm64-gnu@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.25.tgz#ba064fabfdce0190d9859493d8232fffa84ef2e2"
integrity sha512-LFnV2899PJZAIEHQ4IMmZIgL0FBieh5keMnriMY1cK7ompR+JUd24xeTtKkcaw8QmxmEdhoE5Mu9dPSuDBgtTg==
"@next/swc-linux-arm64-gnu@14.2.26":
version "14.2.26"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.26.tgz#c4278c157623b05886e37ff17194811aca1c2d00"
integrity sha512-SINMl1I7UhfHGM7SoRiw0AbwnLEMUnJ/3XXVmhyptzriHbWvPPbbm0OEVG24uUKhuS1t0nvN/DBvm5kz6ZIqpg==
"@next/swc-linux-arm64-musl@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.25.tgz#bf0018267e4e0fbfa1524750321f8cae855144a3"
integrity sha512-QC5y5PPTmtqFExcKWKYgUNkHeHE/z3lUsu83di488nyP0ZzQ3Yse2G6TCxz6nNsQwgAx1BehAJTZez+UQxzLfw==
"@next/swc-linux-arm64-musl@14.2.26":
version "14.2.26"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.26.tgz#5751132764b7a1f13a5a3fe447b03d564eb29705"
integrity sha512-s6JaezoyJK2DxrwHWxLWtJKlqKqTdi/zaYigDXUJ/gmx/72CrzdVZfMvUc6VqnZ7YEvRijvYo+0o4Z9DencduA==
"@next/swc-linux-x64-gnu@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.25.tgz#64f5a6016a7148297ee80542e0fd788418a32472"
integrity sha512-y6/ML4b9eQ2D/56wqatTJN5/JR8/xdObU2Fb1RBidnrr450HLCKr6IJZbPqbv7NXmje61UyxjF5kvSajvjye5w==
"@next/swc-linux-x64-gnu@14.2.26":
version "14.2.26"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.26.tgz#74312cac45704762faa73e0880be6549027303af"
integrity sha512-FEXeUQi8/pLr/XI0hKbe0tgbLmHFRhgXOUiPScz2hk0hSmbGiU8aUqVslj/6C6KA38RzXnWoJXo4FMo6aBxjzg==
"@next/swc-linux-x64-musl@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.25.tgz#58dc636d7c55828478159546f7b95ab1e902301c"
integrity sha512-sPX0TSXHGUOZFvv96GoBXpB3w4emMqKeMgemrSxI7A6l55VBJp/RKYLwZIB9JxSqYPApqiREaIIap+wWq0RU8w==
"@next/swc-linux-x64-musl@14.2.26":
version "14.2.26"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.26.tgz#5d96464d71d2000ec704e650a1a86bb9d73f760d"
integrity sha512-BUsomaO4d2DuXhXhgQCVt2jjX4B4/Thts8nDoIruEJkhE5ifeQFtvW5c9JkdOtYvE5p2G0hcwQ0UbRaQmQwaVg==
"@next/swc-win32-arm64-msvc@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.25.tgz#93562d447c799bded1e89c1a62d5195a2a8c6c0d"
integrity sha512-ReO9S5hkA1DU2cFCsGoOEp7WJkhFzNbU/3VUF6XxNGUCQChyug6hZdYL/istQgfT/GWE6PNIg9cm784OI4ddxQ==
"@next/swc-win32-arm64-msvc@14.2.26":
version "14.2.26"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.26.tgz#859472b532b11499b8f5c2237f54401456286913"
integrity sha512-5auwsMVzT7wbB2CZXQxDctpWbdEnEW/e66DyXO1DcgHxIyhP06awu+rHKshZE+lPLIGiwtjo7bsyeuubewwxMw==
"@next/swc-win32-ia32-msvc@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.25.tgz#ad85a33466be1f41d083211ea21adc0d2c6e6554"
integrity sha512-DZ/gc0o9neuCDyD5IumyTGHVun2dCox5TfPQI/BJTYwpSNYM3CZDI4i6TOdjeq1JMo+Ug4kPSMuZdwsycwFbAw==
"@next/swc-win32-ia32-msvc@14.2.26":
version "14.2.26"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.26.tgz#e52e9bd0c43b7a469b03eda6d7a07c3d0c28f549"
integrity sha512-GQWg/Vbz9zUGi9X80lOeGsz1rMH/MtFO/XqigDznhhhTfDlDoynCM6982mPCbSlxJ/aveZcKtTlwfAjwhyxDpg==
"@next/swc-win32-x64-msvc@14.2.25":
version "14.2.25"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.25.tgz#3969c66609e683ec63a6a9f320a855f7be686a08"
integrity sha512-KSznmS6eFjQ9RJ1nEc66kJvtGIL1iZMYmGEXsZPh2YtnLtqrgdVvKXJY2ScjjoFnG6nGLyPFR0UiEvDwVah4Tw==
"@next/swc-win32-x64-msvc@14.2.26":
version "14.2.26"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.26.tgz#6f42a3ae16ae15c5c5e36efa9b7e291c86ab1275"
integrity sha512-2rdB3T1/Gp7bv1eQTTm9d1Y1sv9UuJ2LAwOE0Pe2prHKe32UNscj7YS13fRB37d0GAiGNR+Y7ZcW8YjDI8Ns0w==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@ -1253,12 +1253,12 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
next@14.2.25:
version "14.2.25"
resolved "https://registry.yarnpkg.com/next/-/next-14.2.25.tgz#0657551fde6a97f697cf9870e9ccbdaa465c6008"
integrity sha512-N5M7xMc4wSb4IkPvEV5X2BRRXUmhVHNyaXwEM86+voXthSZz8ZiRyQW4p9mwAoAPIm6OzuVZtn7idgEJeAJN3Q==
next@14.2.26:
version "14.2.26"
resolved "https://registry.yarnpkg.com/next/-/next-14.2.26.tgz#b918b3fc5c55e1a67aada1347907675713687721"
integrity sha512-b81XSLihMwCfwiUVRRja3LphLo4uBBMZEzBBWMaISbKTwOmq3wPknIETy/8000tr7Gq4WmbuFYPS7jOYIf+ZJw==
dependencies:
"@next/env" "14.2.25"
"@next/env" "14.2.26"
"@swc/helpers" "0.5.5"
busboy "1.6.0"
caniuse-lite "^1.0.30001579"
@ -1266,15 +1266,15 @@ next@14.2.25:
postcss "8.4.31"
styled-jsx "5.1.1"
optionalDependencies:
"@next/swc-darwin-arm64" "14.2.25"
"@next/swc-darwin-x64" "14.2.25"
"@next/swc-linux-arm64-gnu" "14.2.25"
"@next/swc-linux-arm64-musl" "14.2.25"
"@next/swc-linux-x64-gnu" "14.2.25"
"@next/swc-linux-x64-musl" "14.2.25"
"@next/swc-win32-arm64-msvc" "14.2.25"
"@next/swc-win32-ia32-msvc" "14.2.25"
"@next/swc-win32-x64-msvc" "14.2.25"
"@next/swc-darwin-arm64" "14.2.26"
"@next/swc-darwin-x64" "14.2.26"
"@next/swc-linux-arm64-gnu" "14.2.26"
"@next/swc-linux-arm64-musl" "14.2.26"
"@next/swc-linux-x64-gnu" "14.2.26"
"@next/swc-linux-x64-musl" "14.2.26"
"@next/swc-win32-arm64-msvc" "14.2.26"
"@next/swc-win32-ia32-msvc" "14.2.26"
"@next/swc-win32-x64-msvc" "14.2.26"
node-domexception@^1.0.0:
version "1.0.0"

View File

@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.9.0",
"version": "3.9.1",
"npmClient": "yarn",
"concurrency": 20,
"command": {

View File

@ -44,7 +44,7 @@
"bcryptjs": "2.4.3",
"bull": "4.10.1",
"correlation-id": "4.0.0",
"dd-trace": "5.43.0",
"dd-trace": "5.47.0",
"dotenv": "16.0.1",
"google-auth-library": "^8.0.1",
"google-spreadsheet": "npm:@budibase/google-spreadsheet@4.1.5",

View File

@ -104,6 +104,7 @@
on:focus
on:input
on:keyup
on:keydown
on:blur={onBlur}
on:focus={onFocus}
on:input={onInput}

View File

@ -41,6 +41,7 @@
on:blur
on:focus
on:keyup
on:keydown
>
<slot />
</TextField>

View File

@ -0,0 +1,24 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.360872 11.5905L4.90819 16.139C5.10754 16.3384 5.43058 16.3384 5.62993 16.139L7.61005 14.1583C7.78825 13.9801 7.87247 13.7282 7.83748 13.4783L7.30573 9.71929C7.26667 9.44582 7.05186 9.23095 6.77886 9.19228L3.02083 8.6604C2.77143 8.62499 2.5196 8.70923 2.34099 8.88788L0.360872 10.8685C0.161518 11.0679 0.161518 11.3911 0.360872 11.5905Z" fill="url(#paint0_linear_1216_26668)"/>
<path d="M11.0826 16.139L15.6299 11.5905C15.8292 11.3911 15.8292 11.068 15.6299 10.8686L13.6498 8.88794C13.4716 8.70969 13.2197 8.62545 12.9699 8.66045L9.21188 9.19234C8.93847 9.23141 8.72366 9.44628 8.68501 9.71934L8.15326 13.4784C8.11787 13.7278 8.20208 13.9797 8.38069 14.1584L10.3608 16.139C10.5602 16.3384 10.8832 16.3384 11.0826 16.139Z" fill="url(#paint1_linear_1216_26668)"/>
<path d="M15.6391 5.40954L11.0918 0.861023C10.8925 0.661616 10.5694 0.661614 10.3701 0.861021L8.38995 2.84166C8.21175 3.01991 8.12753 3.27181 8.16252 3.52168L8.69427 7.28071C8.73333 7.55418 8.94814 7.76905 9.22114 7.80772L12.9792 8.3396C13.2286 8.37501 13.4804 8.29077 13.659 8.11212L15.6391 6.13148C15.8385 5.93207 15.8385 5.60895 15.6391 5.40954Z" fill="url(#paint2_linear_1216_26668)"/>
<path d="M4.91745 0.860967L0.370132 5.40948C0.170778 5.60889 0.170776 5.93201 0.370131 6.13142L2.35025 8.11206C2.52845 8.29031 2.78028 8.37455 3.03009 8.33955L6.78812 7.80766C7.06152 7.76859 7.27634 7.55372 7.31499 7.28066L7.84674 3.52163C7.88213 3.27217 7.79792 3.02026 7.61931 2.84161L5.63919 0.860967C5.43984 0.66156 5.1168 0.66156 4.91745 0.860967Z" fill="url(#paint3_linear_1216_26668)"/>
<defs>
<linearGradient id="paint0_linear_1216_26668" x1="4.94623" y1="15.6582" x2="7.43064" y2="9.91942" gradientUnits="userSpaceOnUse">
<stop stop-color="#6E56FF"/>
<stop offset="1" stop-color="#9F8FFF"/>
</linearGradient>
<linearGradient id="paint1_linear_1216_26668" x1="15.1492" y1="11.5525" x2="9.411" y2="9.06961" gradientUnits="userSpaceOnUse">
<stop stop-color="#6E56FF"/>
<stop offset="1" stop-color="#9F8FFF"/>
</linearGradient>
<linearGradient id="paint2_linear_1216_26668" x1="11.0538" y1="1.34184" x2="8.56936" y2="7.08058" gradientUnits="userSpaceOnUse">
<stop stop-color="#6E56FF"/>
<stop offset="1" stop-color="#9F8FFF"/>
</linearGradient>
<linearGradient id="paint3_linear_1216_26668" x1="0.850819" y1="5.44754" x2="6.589" y2="7.93039" gradientUnits="userSpaceOnUse">
<stop stop-color="#6E56FF"/>
<stop offset="1" stop-color="#9F8FFF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -3,6 +3,8 @@ export const Events = {
COMPONENT_UPDATED: "component:updated",
APP_VIEW_PUBLISHED: "app:view_published",
BLOCK_EJECTED: "block:ejected",
AI_JS_ACCEPTED: "ai_js:accepted",
AI_JS_REJECTED: "ai_js:rejected",
}
export const EventSource = {

View File

@ -1,4 +1,4 @@
import { FieldType } from "@budibase/types"
import { FieldType, FormulaType } from "@budibase/types"
import { FIELDS } from "@/constants/backend"
import { tables } from "@/stores/builder"
import { get as svelteGet } from "svelte/store"
@ -8,7 +8,6 @@ import { makeReadableKeyPropSafe } from "@/dataBinding"
const MAX_DEPTH = 1
const TYPES_TO_SKIP = [
FieldType.FORMULA,
FieldType.AI,
FieldType.LONGFORM,
FieldType.SIGNATURE_SINGLE,
@ -17,6 +16,18 @@ const TYPES_TO_SKIP = [
FieldType.INTERNAL,
]
const shouldSkipFieldSchema = fieldSchema => {
// Skip some types always
if (TYPES_TO_SKIP.includes(fieldSchema.type)) {
return true
}
// Skip dynamic formula fields
return (
fieldSchema.type === FieldType.FORMULA &&
fieldSchema.formulaType === FormulaType.DYNAMIC
)
}
export function getBindings({
table,
path = null,
@ -32,7 +43,7 @@ export function getBindings({
// skip relationships after a certain depth and types which
// can't bind to
if (
TYPES_TO_SKIP.includes(schema.type) ||
shouldSkipFieldSchema(schema) ||
(isRelationship && depth >= MAX_DEPTH)
) {
continue

View File

@ -0,0 +1,153 @@
<script lang="ts">
import { ActionButton, notifications } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { API } from "@/api"
import type { EnrichedBinding } from "@budibase/types"
import analytics, { Events } from "@/analytics"
import AiInput from "../ai/AIInput.svelte"
export let bindings: EnrichedBinding[] = []
export let value: string | null = ""
export let expandedOnly: boolean = false
export let parentWidth: number | null = null
const dispatch = createEventDispatcher<{
update: { code: string }
accept: void
reject: { code: string | null }
}>()
let suggestedCode: string | null = null
let previousContents: string | null = null
let expanded = false
let promptText = ""
const thresholdExpansionWidth = 350
$: expanded =
expandedOnly ||
(parentWidth !== null && parentWidth > thresholdExpansionWidth)
? true
: expanded
$: containerWidth = expanded ? calculateExpandedWidth() : "auto"
async function generateJs(prompt: string) {
promptText = ""
if (!prompt.trim()) return
previousContents = value
promptText = prompt
try {
const resp = await API.generateJs({ prompt, bindings })
const code = resp.code
if (code === "") {
throw new Error("We didn't understand your prompt. Please rephrase it.")
}
suggestedCode = code
dispatch("update", { code })
} catch (e) {
console.error(e)
notifications.error(
e instanceof Error
? `Unable to generate code: ${e.message}`
: "Unable to generate code. Please try again later."
)
}
}
function acceptSuggestion() {
analytics.captureEvent(Events.AI_JS_ACCEPTED, {
code: suggestedCode,
prompt: promptText,
})
dispatch("accept")
reset()
}
function rejectSuggestion() {
analytics.captureEvent(Events.AI_JS_REJECTED, {
code: suggestedCode,
prompt: promptText,
})
dispatch("reject", { code: previousContents })
reset()
}
function reset() {
suggestedCode = null
previousContents = null
}
function calculateExpandedWidth() {
return parentWidth
? `${Math.min(Math.max(parentWidth * 0.8, 300), 600)}px`
: "300px"
}
</script>
<div class="ai-gen-container" style="--container-width: {containerWidth}">
{#if suggestedCode !== null}
<div class="floating-actions">
<ActionButton size="S" icon="CheckmarkCircle" on:click={acceptSuggestion}>
Accept
</ActionButton>
<ActionButton size="S" icon="Delete" on:click={rejectSuggestion}>
Reject
</ActionButton>
</div>
{/if}
<AiInput
placeholder="Generate with AI"
onSubmit={generateJs}
bind:expanded
on:collapse={rejectSuggestion}
readonly={!!suggestedCode}
{expandedOnly}
/>
</div>
<style>
.ai-gen-container {
height: 40px;
--container-width: auto;
position: absolute;
right: 10px;
bottom: 10px;
width: var(--container-width);
display: flex;
overflow: visible;
}
@keyframes border-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.floating-actions {
position: absolute;
display: flex;
gap: var(--spacing-s);
bottom: calc(100% + 5px);
left: 5px;
z-index: 2;
animation: fade-in 0.2s ease-out forwards;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@ -6,13 +6,7 @@
</script>
<script lang="ts">
import {
Button,
Label,
notifications,
Popover,
TextArea,
} from "@budibase/bbui"
import { Label } from "@budibase/bbui"
import { onMount, createEventDispatcher, onDestroy } from "svelte"
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
@ -69,8 +63,7 @@
import { validateHbsTemplate } from "./validator/hbs"
import { validateJsTemplate } from "./validator/js"
import { featureFlag } from "@/helpers"
import { API } from "@/api"
import Spinner from "../Spinner.svelte"
import AIGen from "./AIGen.svelte"
export let label: string | undefined = undefined
export let completions: BindingCompletion[] = []
@ -94,15 +87,19 @@
let mounted = false
let isEditorInitialised = false
let queuedRefresh = false
let editorWidth: number | null = null
// Theming!
let currentTheme = $themeStore?.theme
let isDark = !currentTheme.includes("light")
let themeConfig = new Compartment()
let popoverAnchor: HTMLElement
let popover: Popover
let promptInput: TextArea
const updateEditorWidth = () => {
if (editorEle) {
editorWidth = editorEle.offsetWidth
}
}
$: aiGenEnabled =
featureFlag.isEnabled(FeatureFlag.AI_JS_GENERATION) &&
mode.name === "javascript" &&
@ -161,68 +158,6 @@
}
}
$: promptLoading = false
let popoverWidth = 300
let suggestedCode: string | null = null
let previousContents: string | null = null
const generateJs = async (prompt: string) => {
previousContents = editor.state.doc.toString()
promptLoading = true
popoverWidth = 30
let code = ""
try {
const resp = await API.generateJs({ prompt, bindings })
code = resp.code
if (code === "") {
throw new Error(
"we didn't understand your prompt, please phrase your request in another way"
)
}
} catch (e) {
console.error(e)
if (e instanceof Error) {
notifications.error(`Unable to generate code: ${e.message}`)
} else {
notifications.error("Unable to generate code, please try again later.")
}
code = previousContents
promptLoading = false
resetPopover()
return
}
value = code
editor.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: code },
})
suggestedCode = code
popoverWidth = 100
promptLoading = false
}
const acceptSuggestion = () => {
suggestedCode = null
previousContents = null
resetPopover()
dispatch("change", editor.state.doc.toString())
dispatch("blur", editor.state.doc.toString())
}
const rejectSuggestion = () => {
suggestedCode = null
value = previousContents || ""
editor.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: value },
})
previousContents = null
resetPopover()
}
const resetPopover = () => {
popover.hide()
popoverWidth = 300
}
// Export a function to expose caret position
export const getCaretPosition = () => {
const selection_range = editor.state.selection.ranges[0]
@ -487,12 +422,31 @@
})
}
onMount(async () => {
// Handle AI generation code updates
const handleAICodeUpdate = (event: CustomEvent<{ code: string }>) => {
const { code } = event.detail
value = code
editor.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: code },
})
}
onMount(() => {
mounted = true
// Capture scrolling
editorEle.addEventListener("wheel", e => {
e.stopPropagation()
})
// Need to get the width of the drawer to pass to the prompt component
updateEditorWidth()
const resizeObserver = new ResizeObserver(() => {
updateEditorWidth()
})
resizeObserver.observe(editorEle)
return () => {
resizeObserver.disconnect()
}
})
onDestroy(() => {
@ -513,52 +467,23 @@
</div>
{#if aiGenEnabled}
<button
bind:this={popoverAnchor}
class="ai-gen"
on:click={() => {
popover.show()
setTimeout(() => {
promptInput.focus()
}, 100)
<AIGen
{bindings}
{value}
parentWidth={editorWidth}
on:update={handleAICodeUpdate}
on:accept={() => {
dispatch("change", editor.state.doc.toString())
dispatch("blur", editor.state.doc.toString())
}}
>
Generate with AI ✨
</button>
<Popover
bind:this={popover}
minWidth={popoverWidth}
anchor={popoverAnchor}
on:close={() => {
if (suggestedCode) {
acceptSuggestion()
}
on:reject={event => {
const { code } = event.detail
value = code || ""
editor.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: code || "" },
})
}}
align="left-outside"
>
{#if promptLoading}
<div class="prompt-spinner">
<Spinner size="20" color="white" />
</div>
{:else if suggestedCode !== null}
<Button on:click={acceptSuggestion}>Accept</Button>
<Button on:click={rejectSuggestion}>Reject</Button>
{:else}
<TextArea
bind:this={promptInput}
placeholder="Type your prompt then press enter..."
on:keypress={event => {
if (event.getModifierState("Shift")) {
return
}
if (event.key === "Enter") {
generateJs(promptInput.contents())
}
}}
/>
{/if}
</Popover>
/>
{/if}
<style>
@ -766,34 +691,4 @@
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
.ai-gen {
right: 1px;
bottom: 1px;
position: absolute;
justify-content: center;
align-items: center;
display: flex;
flex-direction: row;
box-sizing: border-box;
padding: var(--spacing-s);
border-left: 1px solid var(--spectrum-alias-border-color);
border-top: 1px solid var(--spectrum-alias-border-color);
border-top-left-radius: var(--spectrum-alias-border-radius-regular);
color: var(--spectrum-global-color-blue-700);
background-color: var(--spectrum-global-color-gray-75);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms),
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
border-color var(--spectrum-global-animation-duration-100, 130ms);
height: calc(var(--spectrum-alias-item-height-m) - 2px);
}
.ai-gen:hover {
cursor: pointer;
color: var(--spectrum-alias-text-color-hover);
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-alias-border-color-hover);
}
.prompt-spinner {
padding: var(--spacing-m);
}
</style>

View File

@ -0,0 +1,327 @@
<script lang="ts">
import { Icon, Button, Modal, ModalContent, Body, Link } from "@budibase/bbui"
import { auth, admin, licensing } from "@/stores/portal"
import { createEventDispatcher } from "svelte"
import BBAI from "assets/bb-ai.svg"
export let onSubmit: (_prompt: string) => Promise<void>
export let placeholder: string = ""
export let expanded: boolean = false
export let expandedOnly: boolean = false
export let readonly: boolean = false
export let value: string = ""
export const submit = onPromptSubmit
$: expanded = expandedOnly || expanded
const dispatch = createEventDispatcher()
let promptInput: HTMLInputElement
let buttonElement: HTMLButtonElement
let promptLoading = false
let switchOnAIModal: Modal
let addCreditsModal: Modal
$: accountPortalAccess = $auth?.user?.accountPortalAccess
$: accountPortal = $admin.accountPortalUrl
$: aiEnabled = $auth?.user?.llm
$: creditsExceeded = $licensing.aiCreditsExceeded
$: disabled = !aiEnabled || creditsExceeded || readonly || promptLoading
$: animateBorder = !disabled && expanded
function collapse() {
dispatch("collapse")
expanded = expandedOnly
value = ""
animateBorder = false
}
function toggleExpand() {
if (!expanded) {
expanded = true
animateBorder = true
setTimeout(() => {
promptInput?.focus()
}, 250)
} else {
collapse()
}
}
function handleKeyPress(event: KeyboardEvent) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault()
onPromptSubmit()
} else if (event.key === "Escape") {
collapse()
} else {
event.stopPropagation()
}
}
async function onPromptSubmit() {
if (readonly) {
return
}
promptLoading = true
try {
await onSubmit(value)
} finally {
promptLoading = false
}
}
</script>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<button
bind:this={buttonElement}
class="spectrum-ActionButton fade"
class:expanded
class:animate-border={animateBorder}
on:click={!expanded ? toggleExpand : undefined}
>
<div class="button-content-wrapper">
<img
src={BBAI}
alt="AI"
class="ai-icon"
class:disabled={expanded && disabled}
on:click={e => {
e.stopPropagation()
toggleExpand()
}}
/>
{#if expanded}
<input
type="text"
bind:this={promptInput}
bind:value
class="prompt-input"
{placeholder}
on:keydown={handleKeyPress}
{disabled}
/>
{:else}
<span class="spectrum-ActionButton-label ai-gen-text">
{placeholder}
</span>
{/if}
</div>
{#if expanded}
<div class="action-buttons">
{#if !aiEnabled}
<Button cta size="S" on:click={() => switchOnAIModal.show()}>
Switch on AI
</Button>
<Modal bind:this={switchOnAIModal}>
<ModalContent title="Switch on AI" showConfirmButton={false}>
<div class="enable-ai">
<p>To enable BB AI:</p>
<ul>
<li>
Add your Budibase license key:
<Link href={accountPortal}>Budibase account portal</Link>
</li>
<li>
Go to the portal settings page, click AI and switch on BB AI
</li>
</ul>
</div>
</ModalContent>
</Modal>
{:else if creditsExceeded}
<Button cta size="S" on:click={() => addCreditsModal.show()}>
Add AI credits
</Button>
<Modal bind:this={addCreditsModal}>
<ModalContent title="Add AI credits" showConfirmButton={false}>
<Body size="S">
{#if accountPortalAccess}
<Link href={"https://budibase.com/contact/"}>Contact sales</Link
> to unlock additional BB AI credits
{:else}
Contact your account holder to unlock additional BB AI credits
{/if}
</Body>
</ModalContent>
</Modal>
{:else}
<Icon
color={promptLoading
? "#6E56FF"
: "var(--spectrum-global-color-gray-600)"}
size="S"
hoverable={!readonly}
hoverColor="#6E56FF"
name={promptLoading ? "StopCircle" : "PlayCircle"}
on:click={onPromptSubmit}
/>
{/if}
</div>
{/if}
</button>
<style>
.spectrum-ActionButton {
--offset: 1px;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: var(--spacing-s);
border: 1px solid var(--spectrum-alias-border-color);
border-radius: 30px;
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
width: 100%;
height: 40px;
overflow: hidden;
cursor: pointer;
background-color: var(--spectrum-global-color-gray-75);
}
.spectrum-ActionButton::before {
content: "";
position: absolute;
top: -1px;
left: -1px;
width: calc(100% + 2px);
height: calc(100% + 2px);
border-radius: inherit;
background: linear-gradient(
125deg,
transparent -10%,
#6e56ff 2%,
#9f8fff 15%,
#9f8fff 25%,
transparent 35%,
transparent 110%
);
pointer-events: none;
z-index: 0;
}
.spectrum-ActionButton:not(.animate-border)::before {
content: none;
}
.animate-border::before {
animation: border-fade-in 1s cubic-bezier(0.17, 0.67, 0.83, 0.67);
animation-fill-mode: forwards;
}
@keyframes border-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.spectrum-ActionButton::after {
content: "";
background: inherit;
position: absolute;
top: 50%;
left: 50%;
inset: var(--offset);
height: calc(100% - 2 * var(--offset));
width: calc(100% - 2 * var(--offset));
border-radius: inherit;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.spectrum-ActionButton:hover {
cursor: pointer;
background-color: var(--spectrum-global-color-gray-75);
}
.spectrum-ActionButton.expanded {
border-radius: 30px;
overflow: hidden;
text-overflow: ellipsis;
transition: opacity 0.2s ease-out;
}
.fade {
transition: all 2s ease-in;
}
.ai-icon {
width: 18px;
height: 18px;
margin-right: 8px;
flex-shrink: 0;
cursor: var(--ai-icon-cursor, pointer);
}
.ai-gen-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: opacity 0.2s ease-out;
margin-right: var(--spacing-xs);
}
.prompt-input {
font-size: 14px;
flex: 1;
border: none;
background: transparent;
outline: none;
font-family: var(--font-sans);
color: var(--spectrum-alias-text-color);
min-width: 0;
resize: none;
overflow: hidden;
}
.prompt-input::placeholder {
color: var(--spectrum-global-color-gray-600);
font-family: var(--font-sans);
}
.action-buttons {
display: flex;
gap: var(--spacing-s);
z-index: 4;
flex-shrink: 0;
margin-right: var(--spacing-s);
}
.button-content-wrapper {
position: relative;
z-index: 1;
display: flex;
align-items: center;
overflow: hidden;
flex-grow: 1;
min-width: 0;
margin-right: var(--spacing-s);
}
.prompt-input:disabled {
color: var(--spectrum-global-color-gray-500);
cursor: not-allowed;
}
.ai-icon.disabled {
filter: grayscale(1) brightness(1.5);
opacity: 0.5;
}
</style>

View File

@ -1317,6 +1317,25 @@ const shouldReplaceBinding = (currentValue, from, convertTo, binding) => {
return !invalids.find(invalid => noSpaces?.includes(invalid))
}
// If converting readable to runtime we need to ensure we don't replace words
// which are substrings of other words - e.g. a binding of `a` would turn
// `hah` into `h[a]h` which is obviously wrong. To avoid this we can remove all
// expanded versions of the binding to be replaced.
const excludeReadableExtensions = (string, binding) => {
// Escape any special chars in the binding so we can treat it as a literal
// string match in the regexes below
const escaped = binding.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
// Regex to find prefixed bindings (e.g. exclude xfoo for foo)
const regex1 = new RegExp(`[a-zA-Z0-9-_]+${escaped}[a-zA-Z0-9-_]*`, "g")
// Regex to find prefixed bindings (e.g. exclude foox for foo)
const regex2 = new RegExp(`[a-zA-Z0-9-_]*${escaped}[a-zA-Z0-9-_]+`, "g")
const matches = [...string.matchAll(regex1), ...string.matchAll(regex2)]
for (const match of matches) {
string = string.replace(match[0], new Array(match[0].length + 1).join("*"))
}
return string
}
/**
* Utility function which replaces a string between given indices.
*/
@ -1361,6 +1380,11 @@ const bindingReplacement = (
// in the search, working from longest to shortest so always use best match first
let searchString = newBoundValue
for (let from of convertFromProps) {
// If converting readable > runtime, blank out all extensions of this
// string to avoid partial matches
if (convertTo === "runtimeBinding") {
searchString = excludeReadableExtensions(searchString, from)
}
const binding = bindableProperties.find(el => el[convertFrom] === from)
if (
isJS ||

View File

@ -72,6 +72,27 @@ describe("Builder dataBinding", () => {
runtimeBinding: "count",
type: "context",
},
{
category: "Bindings",
icon: "Brackets",
readableBinding: "location",
runtimeBinding: "[location]",
type: "context",
},
{
category: "Bindings",
icon: "Brackets",
readableBinding: "foo.[bar]",
runtimeBinding: "[foo].[qwe]",
type: "context",
},
{
category: "Bindings",
icon: "Brackets",
readableBinding: "foo.baz",
runtimeBinding: "[foo].[baz]",
type: "context",
},
]
it("should convert a readable binding to a runtime one", () => {
const textWithBindings = `Hello {{ Current User.firstName }}! The count is {{ Binding.count }}.`
@ -83,6 +104,28 @@ describe("Builder dataBinding", () => {
)
).toEqual(`Hello {{ [user].[firstName] }}! The count is {{ count }}.`)
})
it("should not convert a partial match", () => {
const textWithBindings = `location {{ _location Zlocation location locationZ _location_ }}`
expect(
readableToRuntimeBinding(
bindableProperties,
textWithBindings,
"runtimeBinding"
)
).toEqual(
`location {{ _location Zlocation [location] locationZ _location_ }}`
)
})
it("should handle special characters in the readable binding", () => {
const textWithBindings = `{{ foo.baz }}`
expect(
readableToRuntimeBinding(
bindableProperties,
textWithBindings,
"runtimeBinding"
)
).toEqual(`{{ [foo].[baz] }}`)
})
})
describe("updateReferencesInObject", () => {

View File

@ -0,0 +1,65 @@
<script lang="ts">
import { API } from "@/api"
import AiInput from "@/components/common/ai/AIInput.svelte"
import { auth, licensing } from "@/stores/portal"
import { ActionButton, notifications } from "@budibase/bbui"
let promptText = ""
$: isEnabled = $auth?.user?.llm && !$licensing.aiCreditsExceeded
async function submitPrompt(message: string) {
await API.generateTables(message)
notifications.success("Tables created successfully!")
}
const examplePrompts = [
"Create me a table for managing IT tickets",
"Create a table called students with name and address fields",
]
</script>
<div class="ai-generation">
<div class="ai-generation-prompt">
<AiInput
bind:value={promptText}
placeholder="Generate data using AI..."
onSubmit={submitPrompt}
expandedOnly
/>
</div>
<div class="ai-generation-examples">
{#if isEnabled}
{#each examplePrompts as prompt}
<ActionButton on:click={() => (promptText = prompt)}
>{prompt}</ActionButton
>
{/each}
{/if}
</div>
</div>
<style>
.ai-generation {
margin-bottom: 24px;
display: flex;
flex-direction: column;
gap: 10px;
}
.ai-generation-examples {
display: grid;
gap: 10px;
}
@media (min-width: 833px) {
.ai-generation-examples {
grid-auto-flow: column;
}
}
.ai-generation :global(.spectrum-Textfield-input),
.ai-generation :global(.spectrum-ActionButton) {
background: #1d1d1d;
border-radius: 20px;
}
</style>

View File

@ -15,6 +15,9 @@
import IntegrationIcon from "@/components/backend/DatasourceNavigator/IntegrationIcon.svelte"
import CreationPage from "@/components/common/CreationPage.svelte"
import ICONS from "@/components/backend/DatasourceNavigator/icons/index.js"
import AiTableGeneration from "./_components/AITableGeneration.svelte"
import { featureFlag } from "@/helpers"
import { FeatureFlag } from "@budibase/types"
let internalTableModal
let externalDatasourceModal
@ -24,6 +27,10 @@
$: disabled = sampleDataLoading || externalDatasourceLoading
$: aiTableGenerationEnabled = featureFlag.isEnabled(
FeatureFlag.AI_TABLE_GENERATION
)
const createSampleData = async () => {
sampleDataLoading = true
@ -58,7 +65,12 @@
</AbsTooltip>
</div>
<div class="options">
<div class="options bb-options">
{#if aiTableGenerationEnabled}
<div class="ai-generation">
<AiTableGeneration />
</div>
{/if}
<DatasourceOption
on:click={internalTableModal.show}
title="Create new table"
@ -111,7 +123,7 @@
display: flex;
align-items: center;
margin-top: 12px;
margin-bottom: 36px;
margin-bottom: 24px;
gap: 8px;
}
.subHeading :global(p) {
@ -127,4 +139,10 @@
margin-bottom: 48px;
max-width: 1050px;
}
.bb-options {
max-width: calc(3 * 235px + 2 * 24px); /* 3 columns + 2 gaps */
}
.options .ai-generation {
grid-column: 1 / -1;
}
</style>

View File

@ -32,6 +32,7 @@
const oneDayInSeconds = 86400
const EXCLUDE_QUOTAS = {
["Day Passes"]: () => true,
Queries: () => true,
Users: license => {
return license.plan.model !== PlanModel.PER_USER

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { Body, Label, Icon } from "@budibase/bbui"
import BudibaseLogo from "./logos/Budibase.svelte"
import OpenAILogo from "./logos/OpenAI.svelte"
@ -6,7 +6,7 @@
import TogetherAILogo from "./logos/TogetherAI.svelte"
import AzureOpenAILogo from "./logos/AzureOpenAI.svelte"
import { Providers } from "./constants"
import type { ProviderConfig } from "@budibase/types"
const logos = {
["Budibase AI"]: BudibaseLogo,
[Providers.OpenAI.name]: OpenAILogo,
@ -15,11 +15,11 @@
[Providers.AzureOpenAI.name]: AzureOpenAILogo,
}
export let config
export let disabled
export let config: ProviderConfig
export let disabled: boolean | null = null
export let editHandler
export let deleteHandler
export let editHandler: (() => void) | null
export let deleteHandler: (() => void) | null
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { onMount } from "svelte"
import {
Button,
@ -16,22 +16,23 @@
import { API } from "@/api"
import AIConfigModal from "./ConfigModal.svelte"
import AIConfigTile from "./AIConfigTile.svelte"
import {
type AIConfig,
ConfigType,
type ProviderConfig,
} from "@budibase/types"
const ConfigTypes = {
AI: "ai",
}
let modal
let fullAIConfig
let editingAIConfig = {}
let editingUuid
let modal: Modal
let fullAIConfig: AIConfig
let editingAIConfig: ProviderConfig | undefined
let editingUuid: string | undefined
$: isCloud = $admin.cloud
$: customAIConfigsEnabled = $licensing.customAIConfigsEnabled
async function fetchAIConfig() {
try {
fullAIConfig = await API.getConfig(ConfigTypes.AI)
fullAIConfig = (await API.getConfig(ConfigType.AI)) as AIConfig
} catch (error) {
notifications.error("Error fetching AI config")
}
@ -42,9 +43,9 @@
const id = editingUuid || Helpers.uuid()
// Creating first custom AI Config
if (!fullAIConfig) {
if (!fullAIConfig && editingAIConfig) {
fullAIConfig = {
type: ConfigTypes.AI,
type: ConfigType.AI,
config: {
[id]: editingAIConfig,
},
@ -54,7 +55,7 @@
delete fullAIConfig.config.budibase_ai
// unset the default value from other configs if default is set
if (editingAIConfig.isDefault) {
if (editingAIConfig?.isDefault) {
for (let key in fullAIConfig.config) {
if (key !== id) {
fullAIConfig.config[key].isDefault = false
@ -62,8 +63,10 @@
}
}
// Add new or update existing custom AI Config
fullAIConfig.config[id] = editingAIConfig
fullAIConfig.type = ConfigTypes.AI
if (editingAIConfig) {
fullAIConfig.config[id] = editingAIConfig
}
fullAIConfig.type = ConfigType.AI
}
try {
@ -72,7 +75,7 @@
} catch (error) {
notifications.error(
`Failed to save AI Configuration, reason: ${
error?.message || "Unknown"
error instanceof Error ? error.message : "Unknown"
}`
)
} finally {
@ -80,7 +83,7 @@
}
}
async function deleteConfig(key) {
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
@ -91,14 +94,16 @@
notifications.success(`Deleted config`)
} catch (error) {
notifications.error(
`Failed to delete config, reason: ${error?.message || "Unknown"}`
`Failed to delete config, reason: ${
error instanceof Error ? error.message : "Unknown"
}`
)
} finally {
await fetchAIConfig()
}
}
function editConfig(uuid) {
function editConfig(uuid: string) {
editingUuid = uuid
editingAIConfig = fullAIConfig?.config[editingUuid]
modal.show()
@ -136,7 +141,10 @@
</Tags>
{/if}
</div>
<Body>Configure your AI settings within this section:</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}`}>

View File

@ -56,7 +56,9 @@ interface LicensingState {
// user limits
userCount?: number
userLimit?: number
aiCreditsLimit?: number
userLimitReached: boolean
aiCreditsExceeded: boolean
errUserLimit: boolean
}
@ -102,6 +104,8 @@ class LicensingStore extends BudiStore<LicensingState> {
userLimit: undefined,
userLimitReached: false,
errUserLimit: false,
// AI Limits
aiCreditsExceeded: false,
})
}
@ -119,6 +123,16 @@ class LicensingStore extends BudiStore<LicensingState> {
return userCount > userLimit
}
aiCreditsExceeded(
aiCredits: number,
aiCreditsLimit = get(this.store).aiCreditsLimit
) {
if (aiCreditsLimit === UNLIMITED || aiCreditsLimit === undefined) {
return false
}
return aiCredits > aiCreditsLimit
}
async isCloud() {
let adminStore = get(admin)
if (!adminStore.loaded) {
@ -291,9 +305,15 @@ class LicensingStore extends BudiStore<LicensingState> {
const userQuota = license.quotas.usage.static.users
const userLimit = userQuota.value
const aiCreditsQuota = license.quotas.usage.monthly.budibaseAICredits
const aiCreditsLimit = aiCreditsQuota.value
const userCount = usage.usageQuota.users
const userLimitReached = this.usersLimitReached(userCount, userLimit)
const userLimitExceeded = this.usersLimitExceeded(userCount, userLimit)
const aiCreditsExceeded = this.aiCreditsExceeded(
usage.monthly.current.budibaseAICredits,
aiCreditsLimit
)
const isCloudAccount = await this.isCloud()
const errUserLimit =
isCloudAccount &&
@ -315,6 +335,8 @@ class LicensingStore extends BudiStore<LicensingState> {
userLimit,
userLimitReached,
errUserLimit,
aiCreditsLimit,
aiCreditsExceeded,
}
})
}

View File

@ -1,9 +1,11 @@
import { GenerateJsRequest, GenerateJsResponse } from "@budibase/types"
import { BaseAPIClient } from "./types"
import { sleep } from "../utils/utils"
export interface AIEndpoints {
generateCronExpression: (prompt: string) => Promise<{ message: string }>
generateJs: (req: GenerateJsRequest) => Promise<GenerateJsResponse>
generateTables: (prompt: string) => Promise<void>
}
export const buildAIEndpoints = (API: BaseAPIClient): AIEndpoints => ({
@ -23,4 +25,8 @@ export const buildAIEndpoints = (API: BaseAPIClient): AIEndpoints => ({
body: req,
})
},
generateTables: async prompt => {
console.warn({ prompt })
await sleep(1000)
},
})

@ -1 +1 @@
Subproject commit af8e1f1e53b9cb9a7d4b773e68b5f0344a895788
Subproject commit 6c9ccbb8a5737733448f6b0e23696de1ed343015

View File

@ -86,7 +86,7 @@
"csvtojson": "2.0.10",
"curlconverter": "3.21.0",
"dayjs": "^1.10.8",
"dd-trace": "5.43.0",
"dd-trace": "5.47.0",
"dotenv": "8.2.0",
"extract-zip": "^2.0.1",
"form-data": "4.0.0",

View File

@ -1,3 +1,4 @@
import { z } from "zod"
import { mockChatGPTResponse } from "../../../tests/utilities/mocks/ai/openai"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import nock from "nock"
@ -10,12 +11,13 @@ import {
PlanModel,
PlanType,
ProviderConfig,
StructuredOutput,
} from "@budibase/types"
import { context } from "@budibase/backend-core"
import { mocks } from "@budibase/backend-core/tests"
import { generator, mocks } from "@budibase/backend-core/tests"
import { ai, quotas } from "@budibase/pro"
import { MockLLMResponseFn } from "../../../tests/utilities/mocks/ai"
import { mockAnthropicResponse } from "../../../tests/utilities/mocks/ai/anthropic"
import { quotas } from "@budibase/pro"
function dedent(str: string) {
return str
@ -285,7 +287,8 @@ describe("BudibaseAI", () => {
envCleanup()
})
beforeEach(() => {
beforeEach(async () => {
await config.newTenant()
nock.cleanAll()
const license: License = {
plan: {
@ -366,5 +369,66 @@ describe("BudibaseAI", () => {
}
)
})
it("handles text format", async () => {
let usage = await getQuotaUsage()
expect(usage._id).toBe(`quota_usage_${config.getTenantId()}`)
expect(usage.monthly.current.budibaseAICredits).toBe(0)
const gptResponse = generator.word()
mockChatGPTResponse(gptResponse, { format: "text" })
const { message } = await config.api.ai.chat({
messages: [{ role: "user", content: "Hello!" }],
format: "text",
licenseKey: licenseKey,
})
expect(message).toBe(gptResponse)
usage = await getQuotaUsage()
expect(usage.monthly.current.budibaseAICredits).toBeGreaterThan(0)
})
it("handles json format", async () => {
let usage = await getQuotaUsage()
expect(usage._id).toBe(`quota_usage_${config.getTenantId()}`)
expect(usage.monthly.current.budibaseAICredits).toBe(0)
const gptResponse = JSON.stringify({
[generator.word()]: generator.word(),
})
mockChatGPTResponse(gptResponse, { format: "json" })
const { message } = await config.api.ai.chat({
messages: [{ role: "user", content: "Hello!" }],
format: "json",
licenseKey: licenseKey,
})
expect(message).toBe(gptResponse)
usage = await getQuotaUsage()
expect(usage.monthly.current.budibaseAICredits).toBeGreaterThan(0)
})
it("handles structured outputs", async () => {
let usage = await getQuotaUsage()
expect(usage._id).toBe(`quota_usage_${config.getTenantId()}`)
expect(usage.monthly.current.budibaseAICredits).toBe(0)
const gptResponse = generator.guid()
const structuredOutput = generator.word() as unknown as StructuredOutput
ai.structuredOutputs[structuredOutput] = {
key: generator.word(),
validator: z.object({ name: z.string() }),
}
mockChatGPTResponse(gptResponse, { format: structuredOutput })
const { message } = await config.api.ai.chat({
messages: [{ role: "user", content: "Hello!" }],
format: structuredOutput,
licenseKey: licenseKey,
})
expect(message).toBe(gptResponse)
usage = await getQuotaUsage()
expect(usage.monthly.current.budibaseAICredits).toBeGreaterThan(0)
})
})
})

View File

@ -1,7 +1,9 @@
import { ResponseFormat } from "@budibase/types"
import { Scope } from "nock"
export interface MockLLMResponseOpts {
host?: string
format?: ResponseFormat
}
export type MockLLMResponseFn = (

View File

@ -1,5 +1,7 @@
import nock from "nock"
import { MockLLMResponseFn, MockLLMResponseOpts } from "."
import _ from "lodash"
import { ai } from "@budibase/pro"
let chatID = 1
const SPACE_REGEX = /\s+/g
@ -48,8 +50,15 @@ export const mockChatGPTResponse: MockLLMResponseFn = (
answer: string | ((prompt: string) => string),
opts?: MockLLMResponseOpts
) => {
let body: any = undefined
if (opts?.format) {
body = _.matches({
response_format: ai.openai.parseResponseFormat(opts.format),
})
}
return nock(opts?.host || "https://api.openai.com")
.post("/v1/chat/completions")
.post("/v1/chat/completions", body)
.reply((uri: string, body: nock.Body) => {
const req = body as ChatCompletionRequest
const messages = req.messages

View File

@ -5,12 +5,18 @@ export interface Message {
content: string
}
export enum StructuredOutput {}
export type ResponseFormat = "text" | "json" | StructuredOutput
export interface ChatCompletionRequest {
messages: Message[]
format?: ResponseFormat
}
export interface ChatCompletionResponse {
message?: string
tokensUsed?: number
}
export interface GenerateJsRequest {

View File

@ -1,4 +1,4 @@
import { License } from "../../../sdk"
import { License, LLMProviderConfig } from "../../../sdk"
import { Account, DevInfo, User } from "../../../documents"
import { FeatureFlags } from "@budibase/types"
@ -11,6 +11,7 @@ export interface FetchAPIKeyResponse extends DevInfo {}
export interface GetGlobalSelfResponse extends User {
flags?: FeatureFlags
llm?: Omit<LLMProviderConfig, "apiKey">
account?: Account
license: License
budibaseAccess: boolean

View File

@ -1,3 +1,5 @@
import { AIProvider } from "../documents"
export enum AIOperationEnum {
SUMMARISE_TEXT = "SUMMARISE_TEXT",
CLEAN_DATA = "CLEAN_DATA",
@ -89,3 +91,13 @@ export type AIColumnSchema =
| SentimentAnalysisSchema
| PromptSchema
| SearchWebSchema
export interface LLMConfigOptions {
model: string
apiKey: string
measureUsage: boolean
}
export interface LLMProviderConfig extends LLMConfigOptions {
provider: AIProvider
}

View File

@ -1,6 +1,7 @@
export enum FeatureFlag {
USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR",
AI_JS_GENERATION = "AI_JS_GENERATION",
AI_TABLE_GENERATION = "AI_TABLE_GENERATION",
// Account-portal
DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL",
@ -9,6 +10,7 @@ export enum FeatureFlag {
export const FeatureFlagDefaults: Record<FeatureFlag, boolean> = {
[FeatureFlag.USE_ZOD_VALIDATOR]: false,
[FeatureFlag.AI_JS_GENERATION]: false,
[FeatureFlag.AI_TABLE_GENERATION]: false,
// Account-portal
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,

View File

@ -50,7 +50,7 @@
"bcrypt": "5.1.0",
"bcryptjs": "2.4.3",
"bull": "4.10.1",
"dd-trace": "5.43.0",
"dd-trace": "5.47.0",
"dotenv": "8.6.0",
"email-validator": "^2.0.4",
"global-agent": "3.0.0",

View File

@ -8,7 +8,7 @@ import {
auth as authCore,
} from "@budibase/backend-core"
import env from "../../../environment"
import { groups } from "@budibase/pro"
import { ai, groups } from "@budibase/pro"
import {
DevInfo,
FetchAPIKeyResponse,
@ -115,11 +115,20 @@ export async function getSelf(ctx: UserCtx<void, GetGlobalSelfResponse>) {
// add the feature flags for this tenant
const flags = await features.flags.fetch()
const llmConfig = await ai.getLLMConfig()
const sanitisedLLMConfig = llmConfig
? {
provider: llmConfig.provider,
model: llmConfig.model,
measureUsage: llmConfig.measureUsage,
}
: undefined
ctx.body = {
...enrichedUser,
...sessionAttributes,
flags,
llm: sanitisedLLMConfig,
}
}

View File

@ -3028,45 +3028,37 @@
resolved "https://registry.yarnpkg.com/@dagrejs/graphlib/-/graphlib-2.2.4.tgz#d77bfa9ff49e2307c0c6e6b8b26b5dd3c05816c4"
integrity sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==
"@datadog/libdatadog@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@datadog/libdatadog/-/libdatadog-0.5.0.tgz#0ef2a2a76bb9505a0e7e5bc9be1415b467dbf368"
integrity sha512-YvLUVOhYVjJssm0f22/RnDQMc7ZZt/w1bA0nty1vvjyaDz5EWaHfWaaV4GYpCt5MRvnGjCBxIwwbRivmGseKeQ==
"@datadog/libdatadog@^0.5.1":
version "0.5.1"
resolved "https://registry.yarnpkg.com/@datadog/libdatadog/-/libdatadog-0.5.1.tgz#fe5c101c457998b74cb66f555f63197b34cad4ba"
integrity sha512-KsdOxTUmtjoygaZInSS5U0+KnqoxPKGpcBjGgOHR9NDKfXzmbpy5AmoaPL7JxmMxQzwknpxSi7qzBOSB3yMoJg==
"@datadog/native-appsec@8.5.0":
version "8.5.0"
resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-8.5.0.tgz#cf4eea74a07085a0dc9f3e98c130736b38cd61c9"
integrity sha512-95y+fm7jd+3iknzuu57pWEPw9fcK9uSBCPiB4kSPHszHu3bESlZM553tc4ANsz+X3gMkYGVg2pgSydG77nSDJw==
"@datadog/native-appsec@8.5.2":
version "8.5.2"
resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-8.5.2.tgz#93a2c15c71c2a90e19e12506fbbdec9ccbc91541"
integrity sha512-lETBaVhBk+9o0pc+LDnXvp2ImDyT8K2deuqLf8A6q4/QjzCCXyR/yZO9R5+Kdoc93jZMRTWV9Pr4pBwHEdJSVA==
dependencies:
node-gyp-build "^3.9.0"
"@datadog/native-iast-rewriter@2.8.0":
version "2.8.0"
resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.8.0.tgz#8a7eddf5e33266643afcdfb920ff5ccb30e1894a"
integrity sha512-DKmtvlmCld9RIJwDcPKWNkKYWYQyiuOrOtynmBppJiUv/yfCOuZtsQV4Zepj40H33sLiQyi5ct6dbWl53vxqkA==
dependencies:
lru-cache "^7.14.0"
node-gyp-build "^4.5.0"
"@datadog/native-iast-taint-tracking@3.3.0":
version "3.3.0"
resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.3.0.tgz#5a9c87e07376e7c5a4b4d4985f140a60388eee00"
integrity sha512-OzmjOncer199ATSYeCAwSACCRyQimo77LKadSHDUcxa/n9FYU+2U/bYQTYsK3vquSA2E47EbSVq9rytrlTdvnA==
"@datadog/native-iast-taint-tracking@3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.3.1.tgz#71d2c9bdb102b4482fea145d3f22ed5453628500"
integrity sha512-TgXpoX/CDgPfYAKu9qLmEyb9UXvRVC00D71islcSb70MCFmxQwkgXGl/gAk6YA6/NmZ4j8+cgY1lSNqStGvOMg==
dependencies:
node-gyp-build "^3.9.0"
"@datadog/native-metrics@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-3.1.0.tgz#c2378841accd9fdd6866d0e49bdf6e3d76e79f22"
integrity sha512-yOBi4x0OQRaGNPZ2bx9TGvDIgEdQ8fkudLTFAe7gEM1nAlvFmbE5YfpH8WenEtTSEBwojSau06m2q7axtEEmCg==
"@datadog/native-metrics@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-3.1.1.tgz#4e5c9775751af13e353e64e573ab724104538cee"
integrity sha512-MU1gHrolwryrU4X9g+fylA1KPH3S46oqJPEtVyrO+3Kh29z80fegmtyrU22bNt8LigPUK/EdPCnSbMe88QbnxQ==
dependencies:
node-addon-api "^6.1.0"
node-gyp-build "^3.9.0"
"@datadog/pprof@5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.6.0.tgz#b6f5c566512ba5e55c6dbf46e9f0f020cfd5c6b5"
integrity sha512-x7yN0s4wMnRqv3PWQ6eXKH5XE5qvCOwWbOsXqpT2Irbsc7Wcl5w5JrJUcbPCdSJGihpIh6kAeIrS6w/ZCcHy2Q==
"@datadog/pprof@5.7.1":
version "5.7.1"
resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.7.1.tgz#3ed62372af7331c37de401319bde9e3d4dc5a8c0"
integrity sha512-D5XTxsaPG36x41vZZn8hsAeC7QQDx0rv1a1Uhxo5xCXUB/9rc19+I7iCnjgJS5aH0ShXdPVOWRClo16hOSKKSw==
dependencies:
delay "^5.0.0"
node-gyp-build "<4.0"
@ -3079,6 +3071,16 @@
resolved "https://registry.yarnpkg.com/@datadog/sketches-js/-/sketches-js-2.1.0.tgz#8c7e8028a5fc22ad102fa542b0a446c956830455"
integrity sha512-smLocSfrt3s53H/XSVP3/1kP42oqvrkjUPtyaFd1F79ux24oE31BKt+q0c6lsa6hOYrFzsIwyc5GXAI5JmfOew==
"@datadog/wasm-js-rewriter@4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@datadog/wasm-js-rewriter/-/wasm-js-rewriter-4.0.0.tgz#46963ffa39365f4bc0aff6114bba6545635f9c69"
integrity sha512-atw1uocrNlG3Fi7usLG5irTpasoz519YTZUyAxp7ZCppDNA+iLHpK1NksV9E2uCGMzF6dGifGmDaui0nK12nYg==
dependencies:
js-yaml "^4.1.0"
lru-cache "^7.14.0"
module-details-from-path "^1.0.3"
node-gyp-build "^4.5.0"
"@elastic/elasticsearch@7.10.0":
version "7.10.0"
resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.10.0.tgz#da105a9c1f14146f9f2cab4e7026cb7949121b8d"
@ -10000,28 +10002,28 @@ dayjs@^1.10.8:
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
dc-polyfill@^0.1.4:
version "0.1.6"
resolved "https://registry.yarnpkg.com/dc-polyfill/-/dc-polyfill-0.1.6.tgz#c2940fa68ffb24a7bf127cc6cfdd15b39f0e7f02"
integrity sha512-UV33cugmCC49a5uWAApM+6Ev9ZdvIUMTrtCO9fj96TPGOQiea54oeO3tiEVdVeo3J9N2UdJEmbS4zOkkEA35uQ==
dc-polyfill@0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/dc-polyfill/-/dc-polyfill-0.1.8.tgz#2d91dd4dd0f2e3575ce038d013f346161f5a413a"
integrity sha512-F9+06papa9GOFUMjxGiqM1bS98pOkinZpBF3Sygb46owrXaHdR2uLkftE6nygrqNcAurdwKjLAtX+0GJkSwIFQ==
dd-trace@5.43.0:
version "5.43.0"
resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-5.43.0.tgz#f321debd74c01d9e1e6d63b99539e7247d89f10f"
integrity sha512-WtPUSZfEosSHYVBFR48FqfYBFor8QchKwAKo+LYtbgTPtFzYKyBV/FJUqYE6sDF15Raf4sJVt/LOscywgj2zEw==
dd-trace@5.47.0:
version "5.47.0"
resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-5.47.0.tgz#c9da1469f752c002ea9947fa6535538eb496d0f5"
integrity sha512-1IrJ3LYPS/ylQdeOKpc8VscshgfngK1Ht7ZWJpNI6KlSZ6JB1rQmuDdGmhUYt7tXTTKRStTmaC4uSMJKADu8Xg==
dependencies:
"@datadog/libdatadog" "^0.5.0"
"@datadog/native-appsec" "8.5.0"
"@datadog/native-iast-rewriter" "2.8.0"
"@datadog/native-iast-taint-tracking" "3.3.0"
"@datadog/native-metrics" "^3.1.0"
"@datadog/pprof" "5.6.0"
"@datadog/libdatadog" "^0.5.1"
"@datadog/native-appsec" "8.5.2"
"@datadog/native-iast-taint-tracking" "3.3.1"
"@datadog/native-metrics" "^3.1.1"
"@datadog/pprof" "5.7.1"
"@datadog/sketches-js" "^2.1.0"
"@datadog/wasm-js-rewriter" "4.0.0"
"@isaacs/ttlcache" "^1.4.1"
"@opentelemetry/api" ">=1.0.0 <1.9.0"
"@opentelemetry/core" "^1.14.0"
crypto-randomuuid "^1.0.0"
dc-polyfill "^0.1.4"
dc-polyfill "0.1.8"
ignore "^5.2.4"
import-in-the-middle "1.13.1"
istanbul-lib-coverage "3.2.0"