Add auto filtering in prop-picker

This commit is contained in:
Guilhem
2024-11-04 22:21:48 +01:00
parent a56e37564b
commit 72102b7b1c
6 changed files with 274 additions and 140 deletions
@@ -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<string, any>; required?: string[] }
export let arg: InputTransform | any
@@ -59,6 +60,8 @@
const { shouldUpdatePropertyType, exprsToSet } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
const { inputMatches } = getContext<PropPickerWrapperContext>('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<PropPickerWrapperContext>('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)}
>
<span class="font-normal"
>Javascript expression detected - press
<span class="font-bold">TAB</span> to switch
>JavaScript expression detected - press
<span class="font-bold">TAB</span> to exit static mode
</span>
</Button>
{/if}
@@ -30,6 +30,7 @@
const propPickerConfig = writable<PropPickerConfig | undefined>(undefined)
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
propPickerConfig,
inputMatches: writable(undefined),
focusProp: (propName, insertionMode, onSelect) => {
propPickerConfig.set({
propName,
@@ -79,6 +79,7 @@
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
focusProp: () => {},
propPickerConfig: writable(undefined),
inputMatches: writable(undefined),
clearFocus: () => {},
filteredPickableProperties: writable(undefined)
})
@@ -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
}
@@ -14,6 +14,7 @@ export type PropPickerConfig = {
export type PropPickerWrapperContext = {
propPickerConfig: Writable<PropPickerConfig | undefined>
filteredPickableProperties: Writable<PickableProperties | undefined>
inputMatches: Writable<{ word: string; value: string }[] | undefined>
focusProp: (propName: string, insertionMode: InsertionMode, onSelect: SelectCallback) => void
clearFocus: () => void
}
@@ -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<string, any> = {}
let resultByIdFiltered: Record<string, any> = {}
let collapsableInitialState:
| {
allResultsCollapsed: boolean
displayVariable: boolean
displayResources: boolean
}
| undefined
const EMPTY_STRING = ''
let search = ''
const { propPickerConfig, filteredPickableProperties } =
const { propPickerConfig, filteredPickableProperties, inputMatches } =
getContext<PropPickerWrapperContext>('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
}
</script>
<div class="flex flex-col h-full !bg-surface rounded overflow-hidden">
@@ -94,19 +157,21 @@
class="overflow-y-auto px-2 pt-2 grow"
class:bg-surface-secondary={!$propPickerConfig && !notSelectable}
>
<div class="flex justify-between items-center space-x-1">
<span class="font-normal text-sm text-secondary">Flow Input</span>
<div class="flex space-x-2 items-center" />
</div>
<div class="overflow-y-auto mb-2">
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
json={flowInputsFiltered}
prefix="flow_input"
on:select
/>
</div>
{#if flowInputsFiltered && Object.keys(flowInputsFiltered).length > 0}
<div class="flex justify-between items-center space-x-1">
<span class="font-normal text-sm text-secondary">Flow Input</span>
<div class="flex space-x-2 items-center" />
</div>
<div class="overflow-y-auto mb-2">
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
json={flowInputsFiltered}
prefix="flow_input"
on:select
/>
</div>
{/if}
{#if error}
<span class="font-normal text-sm text-secondary">Error</span>
<div class="overflow-y-auto mb-2">
@@ -151,15 +216,16 @@
</div>
{/if}
{:else}
{#if previousId}
{@const json = Object.fromEntries(
Object.entries(resultByIdFiltered).filter(([k, v]) => k == previousId)
)}
{#if previousId && Object.keys(json).length > 0}
<span class="font-normal text-sm text-secondary">Previous Result</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
json={Object.fromEntries(
Object.entries(resultByIdFiltered).filter(([k, v]) => k == previousId)
)}
{json}
prefix="results"
on:select
/>
@@ -194,112 +260,117 @@
/>
</div>
{/if}
{#if Object.keys(resultByIdFiltered).length > 0}
<div class="overflow-y-auto mb-2">
<span class="font-normal text-sm text-tertiary">All Results :</span>
{#if !allResultsCollapsed}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
allResultsCollapsed = true
}}
wrapperClasses="inline-flex w-fit h-4"
btnClasses="font-normal text-primary border-nord-300 rounded-[0.275rem]">-</Button
>
{/if}
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
bind:collapsed={allResultsCollapsed}
json={resultByIdFiltered}
prefix="results"
on:select
/>
</div>
{/if}
{/if}
{/if}
{#if displayContext}
{#if !$inputMatches || $inputMatches.some((match) => match.word === 'variable')}
<div class="overflow-y-auto mb-2">
<span class="font-normal text-sm text-tertiary">All Results :</span>
{#if !allResultsCollapsed}
<span class="font-normal text-sm text-secondary">Variables :</span>
{#if displayVariable}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
allResultsCollapsed = true
displayVariable = false
}}
wrapperClasses="inline-flex w-fit h-4"
btnClasses="font-normal text-primary border-nord-300 rounded-[0.275rem]">-</Button
>
{/if}
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
bind:collapsed={allResultsCollapsed}
json={resultByIdFiltered}
prefix="results"
on:select
/>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
rawKey={true}
json={variables}
prefix="variable"
on:select
/>
{:else}
<Button
color="light"
size="xs2"
variant="border"
on:click={async () => {
await loadVariables()
displayVariable = true
}}
wrapperClasses="inline-flex w-fit h-5"
btnClasses="font-semibold border-nord-300 rounded-[0.275rem] p-1"
>
{'{...}'}
</Button>
{/if}
</div>
{/if}
{#if !$inputMatches || $inputMatches.some((match) => match.word === 'resource')}
<div class="overflow-y-auto mb-2">
<span class="font-normal text-sm text-secondary">Resources :</span>
{#if displayResources}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
displayResources = false
}}
wrapperClasses="inline-flex w-fit h-5"
btnClasses="font-semibold text-primary border-nord-300 rounded-[0.275rem]">-</Button
>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
rawKey={true}
json={resources}
prefix="resource"
on:select
/>
{:else}
<Button
color="light"
size="xs2"
variant="border"
on:click={async () => {
await loadResources()
displayResources = true
}}
wrapperClasses="inline-flex w-fit h-5"
btnClasses="font-semibold border-nord-300 rounded-[0.275rem] p-1"
>
{'{...}'}
</Button>
{/if}
</div>
{/if}
{/if}
{#if displayContext}
<div class="overflow-y-auto mb-2">
<span class="font-normal text-sm text-secondary">Variables :</span>
{#if displayVariable}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
displayVariable = false
}}
wrapperClasses="inline-flex w-fit h-4"
btnClasses="font-normal text-primary border-nord-300 rounded-[0.275rem]">-</Button
>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
rawKey={true}
json={variables}
prefix="variable"
on:select
/>
{:else}
<Button
color="light"
size="xs2"
variant="border"
on:click={async () => {
await loadVariables()
displayVariable = true
}}
wrapperClasses="inline-flex w-fit h-5"
btnClasses="font-semibold border-nord-300 rounded-[0.275rem] p-1"
>
{'{...}'}
</Button>
{/if}
</div>
<div class="overflow-y-auto mb-2">
<span class="font-normal text-sm text-secondary">Resources :</span>
{#if displayResources}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
displayResources = false
}}
wrapperClasses="inline-flex w-fit h-5"
btnClasses="font-semibold text-primary border-nord-300 rounded-[0.275rem]">-</Button
>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
rawKey={true}
json={resources}
prefix="resource"
on:select
/>
{:else}
<Button
color="light"
size="xs2"
variant="border"
on:click={async () => {
await loadResources()
displayResources = true
}}
wrapperClasses="inline-flex w-fit h-5"
btnClasses="font-semibold border-nord-300 rounded-[0.275rem] p-1"
>
{'{...}'}
</Button>
{/if}
</div>
{/if}
</div>
</div>