feat(ui): experimental dynamic prompts in js

This commit is contained in:
psychedelicious 2023-08-20 01:44:43 +10:00
parent 0f1b975d0e
commit 4878badb24
5 changed files with 185 additions and 1 deletions

View File

@ -85,6 +85,7 @@
"konva": "^9.2.0",
"lodash-es": "^4.17.21",
"nanostores": "^0.9.2",
"ohm-js": "^17.1.0",
"openapi-fetch": "^0.6.1",
"overlayscrollbars": "^2.2.0",
"overlayscrollbars-react": "^0.5.0",

View File

@ -0,0 +1,152 @@
import * as ohm from 'ohm-js';
const grammarSource = `
Prompt {
exp
= exp choicesStart multi_number multi_trigger choices choicesEnd exp --multi_choices
| exp choicesStart multi_number multi_trigger multi_joinString multi_trigger choices choicesEnd exp --multi_choicesWithJoinString
| exp choicesStart choices choicesEnd exp --choices
| text --text
choices
= listOf<choice, choicesDelimiter>
text
= (~choicesStart any)*
choice
= (~reservedChar any)*
reservedChar
= (choicesEnd | choicesStart | choicesDelimiter | multi_trigger)
choicesStart
= "{"
choicesEnd
= "}"
choicesDelimiter
= "|"
multi_trigger
= "$$"
multi_number
= digit+
multi_joinString
= (~multi_trigger choice)
}`;
const getPermutationsOfSize = (array: string[], size: number): string[][] => {
const result: string[][] = [];
function permute(arr: string[], m: string[] = []) {
if (m.length === size) {
result.push(m);
return;
}
for (let i = 0; i < arr.length; i++) {
const curr = arr.slice();
const next = curr.splice(i, 1);
permute(curr, m.concat(next));
}
}
permute(array);
return result;
};
export const dynamicPromptsGrammar = ohm.grammar(grammarSource);
export const dynamicPromptsSemantics = dynamicPromptsGrammar
.createSemantics()
.addOperation('expand', {
exp_multi_choices: function (
before,
_choicesStart,
number,
_multiTrigger,
choices,
_choicesEnd,
after
) {
const beforePermutations = before.expand();
const choicePermutations = getPermutationsOfSize(
choices.expand(),
Number(number.sourceString)
);
const afterPermutations = after.expand();
const sep = ',';
const combined = [];
for (const b of beforePermutations) {
for (const c of choicePermutations) {
for (const a of afterPermutations) {
combined.push(b + c.join(sep) + a);
}
}
}
return combined;
},
exp_multi_choicesWithJoinString: function (
before,
_choicesStart,
number,
_multiTrigger1,
_multiJoinString,
_multiTrigger2,
choices,
_choicesEnd,
after
) {
const beforePermutations = before.expand();
const choicePermutations = getPermutationsOfSize(
choices.expand(),
Number(number.sourceString)
);
const afterPermutations = after.expand();
const sep = _multiJoinString.sourceString;
const combined = [];
for (const b of beforePermutations) {
for (const c of choicePermutations) {
for (const a of afterPermutations) {
combined.push(b + c.join(sep) + a);
}
}
}
return combined;
},
exp_choices: function (before, _choicesStart, choices, _choicesEnd, after) {
const beforePermutations = before.expand();
const choicePermutations = choices.expand();
const afterPermutations = after.expand();
const combined: string[] = [];
for (const b of beforePermutations) {
for (const c of choicePermutations) {
for (const a of afterPermutations) {
combined.push(b + c + a);
}
}
}
return combined;
},
exp_text: function (text) {
return [text.sourceString];
},
choices: function (choices) {
return choices.asIteration().children.map((c) => c.sourceString);
},
_iter: function (...children) {
children.map((c) => c.expand());
},
_terminal: function () {
return this.sourceString;
},
});

View File

@ -0,0 +1,17 @@
import { useMemo } from 'react';
import { useDebounce } from 'use-debounce';
import { dynamicPromptsGrammar, dynamicPromptsSemantics } from './grammar';
const parsePrompt = (prompt: string): { prompts: string[]; error: boolean } => {
const match = dynamicPromptsGrammar.match(prompt);
if (match.failed()) {
return { prompts: [prompt], error: true };
}
return { prompts: dynamicPromptsSemantics(match).expand(), error: false };
};
export const useDynamicPrompts = (prompt: string) => {
const [debouncedPrompt] = useDebounce(prompt, 500, { leading: false });
const result = useMemo(() => parsePrompt(debouncedPrompt), [debouncedPrompt]);
return result;
};

View File

@ -1,4 +1,4 @@
import { Box, FormControl, useDisclosure } from '@chakra-ui/react';
import { Box, FormControl, Text, useDisclosure } from '@chakra-ui/react';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react';
@ -20,6 +20,7 @@ import { flushSync } from 'react-dom';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus';
import { useDynamicPrompts } from 'common/util/dynamicPrompts/useDynamicPrompts';
const promptInputSelector = createSelector(
[stateSelector, activeTabNameSelector],
@ -48,6 +49,7 @@ const ParamPositiveConditioning = () => {
const promptRef = useRef<HTMLTextAreaElement>(null);
const { isOpen, onClose, onOpen } = useDisclosure();
const { t } = useTranslation();
const parseResult = useDynamicPrompts(prompt);
const handleChangePrompt = useCallback(
(e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(setPositivePrompt(e.target.value));
@ -155,6 +157,13 @@ const ParamPositiveConditioning = () => {
<AddEmbeddingButton onClick={onOpen} />
</Box>
)}
<Box>
generated {parseResult.prompts.length} prompts:
{parseResult.error && <Text color="error.500">Parsing failed</Text>}
{parseResult.prompts.map((p, i) => (
<Text key={`${p}.${i}`}>{p}</Text>
))}
</Box>
</Box>
);
};

View File

@ -5074,6 +5074,11 @@ object.values@^1.1.6:
define-properties "^1.1.4"
es-abstract "^1.20.4"
ohm-js@^17.1.0:
version "17.1.0"
resolved "https://registry.yarnpkg.com/ohm-js/-/ohm-js-17.1.0.tgz#50d8e08f69d7909931998d75202d35e2a90c8885"
integrity sha512-xc3B5dgAjTBQGHaH7B58M2Pmv6WvzrJ/3/7LeUzXNg0/sY3jQPdSd/S2SstppaleO77rifR1tyhdfFGNIwxf2Q==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"