diff --git a/frontend/src/lib/components/InputTransformForm.svelte b/frontend/src/lib/components/InputTransformForm.svelte index 92a2d919c5..7316d0873c 100644 --- a/frontend/src/lib/components/InputTransformForm.svelte +++ b/frontend/src/lib/components/InputTransformForm.svelte @@ -25,6 +25,7 @@ import type { FlowCopilotContext } from './copilot/flow' import StepInputGen from './copilot/StepInputGen.svelte' import type { PickableProperties } from './flows/previousResults' + import { buildPrefixRegex } from './flows/previousResults' import { twMerge } from 'tailwind-merge' export let schema: Schema | { properties?: Record; required?: string[] } export let arg: InputTransform | any @@ -59,6 +60,8 @@ const { shouldUpdatePropertyType, exprsToSet } = getContext('FlowCopilotContext') || {} + const { inputMatches } = getContext('PropPickerWrapper') + function setExpr() { const newArg = $exprsToSet?.[argName] if (newArg) { @@ -135,16 +138,19 @@ let codeInjectionDetected = false + const dynamicTemplateRegexPairs = buildPrefixRegex([ + 'flow_input', + 'results', + 'resource', + 'variable' + ]) + function checkCodeInjection(rawValue: string) { - if (!arg) { - return + if (!arg || !rawValue || rawValue.length < 3 || !dynamicTemplateRegexPairs) { + return [] } - - const dynamicTemplateRegex = new RegExp( - /^(flow_input\.|results\.|flo$|flow$|flow_$|flow_i$|flow_in$|flow_inp$|flow_inpu$|flow_input$|res$|resu$|resul$|result$|results$).*/ - ) - - codeInjectionDetected = dynamicTemplateRegex.test(rawValue) + const matches = dynamicTemplateRegexPairs.filter(({ regex }) => regex.test(rawValue)) + return matches.map((m) => ({ word: m.word, value: rawValue })) } async function setJavaScriptExpr(rawValue: string) { @@ -210,9 +216,24 @@ const { focusProp, propPickerConfig } = getContext('PropPickerWrapper') - $: isStaticTemplate(inputCat) && propertyType == 'static' && setPropertyType(arg?.value) + $: updateStaticInput(inputCat, propertyType, arg) - $: isStaticTemplate(inputCat) && propertyType == 'static' && checkCodeInjection(arg?.value) + function updateStaticInput( + inputCat: InputCat, + propertyType: 'static' | 'javascript', + arg: InputTransform | any + ) { + if (!isStaticTemplate(inputCat)) { + return + } + if (propertyType == 'static') { + setPropertyType(arg?.value) + codeInjectionDetected = checkCodeInjection(arg?.value).length > 0 + } else if (propertyType == 'javascript' && focused) { + setPropertyType(arg?.expr) + $inputMatches = checkCodeInjection(arg?.expr) + } + } function setDefaultCode() { if (!arg?.value) { @@ -446,8 +467,8 @@ on:click={() => setJavaScriptExpr(arg.value)} > Javascript expression detected - press - TAB to switch + >JavaScript expression detected - press + TAB to exit static mode {/if} diff --git a/frontend/src/lib/components/flows/FlowEditor.svelte b/frontend/src/lib/components/flows/FlowEditor.svelte index af9a9eb6e2..2e78763350 100644 --- a/frontend/src/lib/components/flows/FlowEditor.svelte +++ b/frontend/src/lib/components/flows/FlowEditor.svelte @@ -30,6 +30,7 @@ const propPickerConfig = writable(undefined) setContext('PropPickerWrapper', { propPickerConfig, + inputMatches: writable(undefined), focusProp: (propName, insertionMode, onSelect) => { propPickerConfig.set({ propName, diff --git a/frontend/src/lib/components/flows/content/FlowConstants.svelte b/frontend/src/lib/components/flows/content/FlowConstants.svelte index adbb9cc075..da0039a5bf 100644 --- a/frontend/src/lib/components/flows/content/FlowConstants.svelte +++ b/frontend/src/lib/components/flows/content/FlowConstants.svelte @@ -79,6 +79,7 @@ setContext('PropPickerWrapper', { focusProp: () => {}, propPickerConfig: writable(undefined), + inputMatches: writable(undefined), clearFocus: () => {}, filteredPickableProperties: writable(undefined) }) diff --git a/frontend/src/lib/components/flows/previousResults.ts b/frontend/src/lib/components/flows/previousResults.ts index 94c028af23..218efbb12c 100644 --- a/frontend/src/lib/components/flows/previousResults.ts +++ b/frontend/src/lib/components/flows/previousResults.ts @@ -260,3 +260,42 @@ declare const approvers: string } ` } + +export function buildPrefixRegex(words: string[]): Array<{ regex: RegExp; word: string }> { + return words.map((word) => { + const prefixes: string[] = [] + for (let i = 1; i <= word.length; i++) { + prefixes.push(word.slice(0, i) + '$') + } + prefixes.push(word + '\\.') + prefixes.push(word + '\\[') + + return { + regex: new RegExp(`^(${prefixes.join('|')}).*`), + word + } + }) +} + +export function filterNestedObject(obj: any, nestedKeys: string[]) { + if (nestedKeys.length === 0) return obj + if (nestedKeys.length === 1) { + if (nestedKeys[0] === '') { + return obj + } + const regexes = buildPrefixRegex(Object.keys(obj)) + const matches = regexes.filter(({ regex }) => regex.test(nestedKeys[0])) + const filteredObj = {} + matches.forEach(({ word }) => { + if (obj.hasOwnProperty(word)) { + filteredObj[word] = obj[word] + } + }) + return filteredObj + } + const [key, ...rest] = nestedKeys + if (obj && typeof obj === 'object' && key in obj) { + return filterNestedObject(obj[key], rest) + } + return undefined +} diff --git a/frontend/src/lib/components/prop_picker.ts b/frontend/src/lib/components/prop_picker.ts index e8f06e6de6..23f5d9d164 100644 --- a/frontend/src/lib/components/prop_picker.ts +++ b/frontend/src/lib/components/prop_picker.ts @@ -14,6 +14,7 @@ export type PropPickerConfig = { export type PropPickerWrapperContext = { propPickerConfig: Writable filteredPickableProperties: Writable + inputMatches: Writable<{ word: string; value: string }[] | undefined> focusProp: (propName: string, insertionMode: InsertionMode, onSelect: SelectCallback) => void clearFocus: () => void } diff --git a/frontend/src/lib/components/propertyPicker/PropPicker.svelte b/frontend/src/lib/components/propertyPicker/PropPicker.svelte index 7abd89a440..8731691c30 100644 --- a/frontend/src/lib/components/propertyPicker/PropPicker.svelte +++ b/frontend/src/lib/components/propertyPicker/PropPicker.svelte @@ -9,6 +9,7 @@ import { keepByKey } from './utils' import type { PickableProperties } from '../flows/previousResults' import ClearableInput from '../common/clearableInput/ClearableInput.svelte' + import { filterNestedObject } from '../flows/previousResults' export let pickableProperties: PickableProperties export let displayContext = true @@ -22,37 +23,30 @@ let displayVariable = false let displayResources = false let allResultsCollapsed = true + let flowInputsFiltered: Record = {} + let resultByIdFiltered: Record = {} + let collapsableInitialState: + | { + allResultsCollapsed: boolean + displayVariable: boolean + displayResources: boolean + } + | undefined const EMPTY_STRING = '' let search = '' - const { propPickerConfig, filteredPickableProperties } = + const { propPickerConfig, filteredPickableProperties, inputMatches } = getContext('PropPickerWrapper') $filteredPickableProperties = { ...pickableProperties } - $: flowInputsFiltered = - search === EMPTY_STRING - ? pickableProperties.flow_input - : keepByKey(pickableProperties.flow_input, search) - - $: resultByIdFiltered = - search === EMPTY_STRING - ? pickableProperties.priorIds - : keepByKey(pickableProperties.priorIds, search) + $: filterPickableProperties(), updateCollapsable(), search, $inputMatches $: suggestedPropsFiltered = $propPickerConfig ? keepByKey(pickableProperties.priorIds, $propPickerConfig.propName) : undefined - $: resultByIdFiltered && - $filteredPickableProperties && - ($filteredPickableProperties.priorIds = resultByIdFiltered) - - $: flowInputsFiltered && - $filteredPickableProperties && - ($filteredPickableProperties.flow_input = flowInputsFiltered) - async function loadVariables() { variables = Object.fromEntries( ( @@ -72,6 +66,75 @@ ).map((resource) => [resource.path, resource.description ?? '']) ) } + + function filterPickableProperties() { + flowInputsFiltered = pickableProperties.flow_input + resultByIdFiltered = pickableProperties.priorIds + + if ($inputMatches) { + if (!$inputMatches.some((match) => match.word === 'flow_input')) { + flowInputsFiltered = [] + } + if (!$inputMatches.some((match) => match.word === 'results')) { + resultByIdFiltered = [] + } + if ($inputMatches.length == 1) { + if ($inputMatches[0].word === 'flow_input') { + let [, ...nestedKeys] = $inputMatches[0].value.split('.') + flowInputsFiltered = filterNestedObject(flowInputsFiltered, nestedKeys) + } else if ($inputMatches[0].word === 'results') { + let [, ...nestedKeys] = $inputMatches[0].value.split('.') + resultByIdFiltered = filterNestedObject(resultByIdFiltered, nestedKeys) + } + } + } + + if (flowInputsFiltered && search !== EMPTY_STRING) { + flowInputsFiltered = keepByKey(flowInputsFiltered, search) + } + if (resultByIdFiltered && search !== EMPTY_STRING) { + resultByIdFiltered = keepByKey(resultByIdFiltered, search) + } + + if ($filteredPickableProperties) { + resultByIdFiltered && ($filteredPickableProperties.priorIds = resultByIdFiltered) + flowInputsFiltered && ($filteredPickableProperties.flow_input = flowInputsFiltered) + } + } + + async function updateCollapsable() { + if (!$inputMatches || $inputMatches.length !== 1) { + resetCollapsable() + return + } + + if (!collapsableInitialState) { + collapsableInitialState = { allResultsCollapsed, displayVariable, displayResources } + } + + if ($inputMatches[0].word === 'variable') { + await loadVariables() + displayVariable = true + return + } + if ($inputMatches[0].word === 'resource') { + await loadResources() + displayResources = true + return + } + if ($inputMatches[0].word === 'results') { + allResultsCollapsed = false + return + } + } + + function resetCollapsable() { + if (!collapsableInitialState) { + return + } + ;({ allResultsCollapsed, displayVariable, displayResources } = collapsableInitialState) + collapsableInitialState = undefined + }
@@ -94,19 +157,21 @@ class="overflow-y-auto px-2 pt-2 grow" class:bg-surface-secondary={!$propPickerConfig && !notSelectable} > -
- Flow Input -
-
-
- -
+ {#if flowInputsFiltered && Object.keys(flowInputsFiltered).length > 0} +
+ Flow Input +
+
+
+ +
+ {/if} {#if error} Error
@@ -151,15 +216,16 @@
{/if} {:else} - {#if previousId} + {@const json = Object.fromEntries( + Object.entries(resultByIdFiltered).filter(([k, v]) => k == previousId) + )} + {#if previousId && Object.keys(json).length > 0} Previous Result
k == previousId) - )} + {json} prefix="results" on:select /> @@ -194,112 +260,117 @@ />
{/if} + {#if Object.keys(resultByIdFiltered).length > 0} +
+ All Results : + {#if !allResultsCollapsed} + + {/if} + + +
+ {/if} + {/if} + {/if} + + {#if displayContext} + {#if !$inputMatches || $inputMatches.some((match) => match.word === 'variable')}
- All Results : - {#if !allResultsCollapsed} + Variables : + + {#if displayVariable} - {/if} - + + {:else} + + {/if} +
+ {/if} + {#if !$inputMatches || $inputMatches.some((match) => match.word === 'resource')} +
+ Resources : + + {#if displayResources} + + + {:else} + + {/if}
{/if} {/if} - - {#if displayContext} -
- Variables : - - {#if displayVariable} - - - - {:else} - - {/if} -
- -
- Resources : - - {#if displayResources} - - - {:else} - - {/if} -
- {/if}