Merge branch 'master' into dependabot/npm_and_yarn/vite-4.5.13
This commit is contained in:
commit
39148fae11
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -104,6 +104,7 @@
|
|||
on:focus
|
||||
on:input
|
||||
on:keyup
|
||||
on:keydown
|
||||
on:blur={onBlur}
|
||||
on:focus={onFocus}
|
||||
on:input={onInput}
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
on:blur
|
||||
on:focus
|
||||
on:keyup
|
||||
on:keydown
|
||||
>
|
||||
<slot />
|
||||
</TextField>
|
||||
|
|
|
@ -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 |
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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 ||
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
const oneDayInSeconds = 86400
|
||||
|
||||
const EXCLUDE_QUOTAS = {
|
||||
["Day Passes"]: () => true,
|
||||
Queries: () => true,
|
||||
Users: license => {
|
||||
return license.plan.model !== PlanModel.PER_USER
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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}`}>
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { ResponseFormat } from "@budibase/types"
|
||||
import { Scope } from "nock"
|
||||
|
||||
export interface MockLLMResponseOpts {
|
||||
host?: string
|
||||
format?: ResponseFormat
|
||||
}
|
||||
|
||||
export type MockLLMResponseFn = (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
88
yarn.lock
88
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue