Skip to content

Commit

Permalink
feat: add error message below prompt editor (#284)
Browse files Browse the repository at this point in the history
  • Loading branch information
audrey-kho authored Jun 11, 2024
1 parent f9b9d39 commit eb92ef0
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 13 deletions.
4 changes: 2 additions & 2 deletions web-src/src/components/GenerateButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const createWaitMessagesController = (intlFn) => {
};
};

export function GenerateButton() {
export function GenerateButton({ isDisabled }) {
const { runMode, firefallService } = useApplicationContext();
const prompt = useRecoilValue(promptState);
const parameters = useRecoilValue(parametersState);
Expand Down Expand Up @@ -154,7 +154,7 @@ export function GenerateButton() {
variant="cta"
style="fill"
onPress={handleGenerate}
isDisabled={generationInProgress}>
isDisabled={isDisabled || generationInProgress}>
{generationInProgress ? <ProgressCircle size="S" aria-label="Generate" isIndeterminate right="8px" /> : <GenAIIcon marginEnd={'8px'} color={'white'}/>}
{formatMessage(intlMessages.promptSessionSideView.generateButtonLabel)}
</Button>
Expand Down
41 changes: 39 additions & 2 deletions web-src/src/components/PromptEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { css, injectGlobal } from '@emotion/css';
import { Global } from '@emotion/react';
import { motion, AnimatePresence } from 'framer-motion';
import Close from '@spectrum-icons/workflow/Close';
import Alert from '@spectrum-icons/workflow/Alert';
import { useIntl } from 'react-intl';

import { intlMessages } from './PromptSessionSideView.l10n.js';
Expand Down Expand Up @@ -75,6 +76,15 @@ const style = {
border-radius: 4px;
padding: 12px;
`,
containerError: css`
width: 100%;
height: 100%;
position: relative;
overflow: auto;
border: 1px solid var(--spectrum-red-900);
border-radius: 4px;
padding: 12px;
`,
editor: css`
font-family: Monospaced, monospace;
font-size: 12px;
Expand All @@ -86,12 +96,27 @@ const style = {
textarea: css`
outline: none;
`,
errorHelpText: css`
margin-top: 15px;
color: var(--spectrum-red-900);
`,
hidden: css`
display: none;
`,
};

function PromptEditor({ isOpen, onClose, ...props }) {
export function findSyntaxError(prompt) {
const matches = [...prompt.matchAll(/=".*[{}"]+.*"/g)];
return matches.length > 0;
}

function PromptEditor({
isOpen, onClose, setPromptValidationError, ...props
}) {
const [prompt, setPrompt] = useRecoilState(promptState);
const [promptText, setPromptText] = useState(prompt);
const [viewSource, setViewSource] = useState(false);
const [showErrorMsg, setShowErrorMsg] = useState(false);

const parameters = useRecoilValue(parametersState);
const contentFragment = useRecoilValue(contentFragmentState);
Expand All @@ -104,6 +129,10 @@ function PromptEditor({ isOpen, onClose, ...props }) {
if (promptEditorTextArea) {
promptEditorTextArea.setAttribute('title', 'Prompt Editor');
}

const isErrorFound = findSyntaxError(promptText);
setShowErrorMsg(isErrorFound);
setPromptValidationError(isErrorFound);
}, [promptText, setPrompt]);

useEffect(() => {
Expand Down Expand Up @@ -181,7 +210,7 @@ function PromptEditor({ isOpen, onClose, ...props }) {
</Flex>
</Flex>

<div className={style.container}>
<div className={showErrorMsg ? style.containerError : style.container}>
<SimpleEditor
className={style.editor}
textareaClassName={style.textarea}
Expand All @@ -196,6 +225,14 @@ function PromptEditor({ isOpen, onClose, ...props }) {
readOnly={!viewSource}
/>
</div>

<Flex gap="size-100" UNSAFE_className={showErrorMsg ? style.errorHelpText : style.hidden}>
<Alert aria-label="Negative Alert" color="negative" />
<Text>
The characters <b>&#123;</b>, <b>&#125;</b>, and <b>&quot;</b> are reserved and can&apos;t
be used within quoted text values. Please remove or replace these characters and try again.
</Text>
</Flex>
</motion.div>
)}
</AnimatePresence>
Expand Down
200 changes: 200 additions & 0 deletions web-src/src/components/PromptEditor.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* Copyright 2023 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { findSyntaxError } from './PromptEditor.js';

describe('findSyntaxError', () => {
it('empty prompt', () => {
const prompt = '';
expect(findSyntaxError(prompt)).toBe(false);
});

it('simple prompt with valid syntax', () => {
const prompt = 'This is a valid prompt.';
expect(findSyntaxError(prompt)).toBe(false);
});

it('prompt with valid syntax', () => {
const prompt = `{{# -------------------------------------------------------------------------- }}
{{# REQUIREMENTS }}
{{# Specify any and all conditions your content must comply with to meet your }}
{{# brand writing guidelines. }}
{{# -------------------------------------------------------------------------- }}
Requirements: \`\`\`
- The text must consist of three parts: a Title, a Body and an "AI Rationale".
- The text must be brief, such that:
* In 20 words (100 characters) or less, compose the "AI Rationale" text first and use it to explain your reasoning for composing the copy, before composing the other parts.
* The Title text must not exceed 6 words or 30 characters, including spaces.
* The Body text must not exceed 13 words or 65 characters, including spaces.
- The tone of the text needs to be: {{tone_of_voice}}.
- The product name must appear once either in the Title text or Body text.
- Never abbreviate or shorten the name of the product in the text.
- Compose the text without using the same adjective more than once.
- Do not use exclamation marks or points at the end of sentences in the text.
- Do not use exclamation marks or points in the text.
- Format the response as an array of valid, iterable RFC8259 compliant JSON. Always list the "AI Rationale" attribute last.
\`\`\`
{{# -------------------------------------------------------------------------- }}
{{# DOMAIN KNOWLEDGE AND TRUSTED SOURCE DOCUMENTS }}
{{# Here you will Provide more background information or specific details to }}
{{# guide the creation of the content. }}
{{# -------------------------------------------------------------------------- }}
Additional Context: [[
{{domain_knowledge_and_trusted_source_documents}}
]]
{{# -------------------------------------------------------------------------- }}
{{# METADATA }}
{{# -------------------------------------------------------------------------- }}
{{@explain_interaction,
label="Explain user interaction",
description="Elaborate on the user's interaction with the element, emphasizing the results and impacts of the interaction",
default="the user is taken to a product page displaying a detailed view of our bestselling wireless headphones. Here, they can read product specifications, customer reviews, and make a purchase if they so choose",
type=text
}};`;

expect(findSyntaxError(prompt)).toBe(false);
});

it('prompt with invalid syntax', () => {
const prompt = `{{# -------------------------------------------------------------------------- }}
{{# REQUIREMENTS }}
{{# Specify any and all conditions your content must comply with to meet your }}
{{# brand writing guidelines. }}
{{# -------------------------------------------------------------------------- }}
Requirements: \`\`\`
- The text must consist of three parts: a Title, a Body and an "AI Rationale".
- The text must be brief, such that:
* In 20 words (100 characters) or less, compose the "AI Rationale" text first and use it to explain your reasoning for composing the copy, before composing the other parts.
* The Title text must not exceed 6 words or 30 characters, including spaces.
* The Body text must not exceed 13 words or 65 characters, including spaces.
- The tone of the text needs to be: {{tone_of_voice}}.
- The product name must appear once either in the Title text or Body text.
- Never abbreviate or shorten the name of the product in the text.
- Compose the text without using the same adjective more than once.
- Do not use exclamation marks or points at the end of sentences in the text.
- Do not use exclamation marks or points in the text.
- Format the response as an array of valid, iterable RFC8259 compliant JSON. Always list the "AI Rationale" attribute last.
\`\`\`
{{# -------------------------------------------------------------------------- }}
{{# DOMAIN KNOWLEDGE AND TRUSTED SOURCE DOCUMENTS }}
{{# Here you will Provide more background information or specific details to }}
{{# guide the creation of the content. }}
{{# -------------------------------------------------------------------------- }}
Additional Context: [[
{{domain_knowledge_and_trusted_source_documents}}
]]
{{# -------------------------------------------------------------------------- }}
{{# METADATA }}
{{# -------------------------------------------------------------------------- }}
{{@explain_interaction,
label="Explain user interaction",
description="Elaborate on the user's interaction with the element, emphasizing the results and impacts of the interaction",
default="the user is taken to a {product page displaying a detailed view of our bestselling wireless headphones. Here, they can read product specifications, customer reviews, and make a purchase if they so choose",
type=text
}};`;

expect(findSyntaxError(prompt)).toBe(true);
});

it('prompt with invalid syntax', () => {
const prompt = `{{# -------------------------------------------------------------------------- }}
{{# REQUIREMENTS }}
{{# Specify any and all conditions your content must comply with to meet your }}
{{# brand writing guidelines. }}
{{# -------------------------------------------------------------------------- }}
Requirements: \`\`\`
- The text must consist of three parts: a Title, a Body and an "AI Rationale".
- The text must be brief, such that:
* In 20 words (100 characters) or less, compose the "AI Rationale" text first and use it to explain your reasoning for composing the copy, before composing the other parts.
* The Title text must not exceed 6 words or 30 characters, including spaces.
* The Body text must not exceed 13 words or 65 characters, including spaces.
- The tone of the text needs to be: {{tone_of_voice}}.
- The product name must appear once either in the Title text or Body text.
- Never abbreviate or shorten the name of the product in the text.
- Compose the text without using the same adjective more than once.
- Do not use exclamation marks or points at the end of sentences in the text.
- Do not use exclamation marks or points in the text.
- Format the response as an array of valid, iterable RFC8259 compliant JSON. Always list the "AI Rationale" attribute last.
\`\`\`
{{# -------------------------------------------------------------------------- }}
{{# DOMAIN KNOWLEDGE AND TRUSTED SOURCE DOCUMENTS }}
{{# Here you will Provide more background information or specific details to }}
{{# guide the creation of the content. }}
{{# -------------------------------------------------------------------------- }}
Additional Context: [[
{{domain_knowledge_and_trusted_source_documents}}
]]
{{# -------------------------------------------------------------------------- }}
{{# METADATA }}
{{# -------------------------------------------------------------------------- }}
{{@explain_interaction,
label="Explain user interaction",
description="Elaborate on the user's interaction with the element, emphasizing the results and impacts of the interaction",
default="the user is taken to a {product page displaying a detailed view of our bestselling wireless headphones. Here, they can read product specifications, customer reviews, and make a purchase if they so choose",
type=text
}};`;

expect(findSyntaxError(prompt)).toBe(true);
});

it('prompt with invalid syntax', () => {
const prompt = `{{# -------------------------------------------------------------------------- }}
{{# DOMAIN KNOWLEDGE AND TRUSTED SOURCE DOCUMENTS }}
{{# Here you will Provide more background information or specific details to }}
{{# guide the creation of the content. }}
{{# -------------------------------------------------------------------------- }}
Additional Context: [[
{{domain_knowledge_and_trusted_source_documents}}
]]
{{# -------------------------------------------------------------------------- }}
{{# METADATA }}
{{# -------------------------------------------------------------------------- }}
{{@explain_interaction,
label="Explain user interaction",
description="Elaborate on the user's interaction}} with the element, emphasizing the results and impacts of the interaction",
default="the user is taken to a product page displaying a detailed view of our bestselling wireless headphones. Here, they can read product specifications, customer reviews, and make a purchase if they so choose",
type=text
}};`;

expect(findSyntaxError(prompt)).toBe(true);
});

it('prompt with invalid syntax', () => {
const prompt = `{{# -------------------------------------------------------------------------- }}
{{# DOMAIN KNOWLEDGE AND TRUSTED SOURCE DOCUMENTS }}
{{# Here you will Provide more background information or specific details to }}
{{# guide the creation of the content. }}
{{# -------------------------------------------------------------------------- }}
Additional Context: [[
{{domain_knowledge_and_trusted_source_documents}}
]]
{{# -------------------------------------------------------------------------- }}
{{# METADATA }}
{{# -------------------------------------------------------------------------- }}
{{@explain_interaction,
label="Explain user i"nteraction",
description="Elaborate on the user's interaction with the element, emphasizing the results and impacts of the interaction",
default="the user is taken to a product page displaying a detailed view of our bestselling wireless headphones. Here, they can read product specifications, customer reviews, and make a purchase if they so choose",
type=text
}};`;

expect(findSyntaxError(prompt)).toBe(true);
});
});
17 changes: 12 additions & 5 deletions web-src/src/components/PromptSessionPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/
import { Flex, Grid } from '@adobe/react-spectrum';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
import { PromptSessionSideView } from './PromptSessionSideView.js';
import { PromptResultListView } from './PromptResultListView.js';
Expand All @@ -20,6 +20,7 @@ import { log } from '../helpers/MetricsHelper.js';

export function PromptSessionPanel() {
const [isOpenPromptEditor, setIsOpenPromptEditor] = useRecoilState(promptEditorState);
const [promptEditorError, setPromptEditorError] = useState(false);

useEffect(() => {
if (isOpenPromptEditor) {
Expand All @@ -36,8 +37,11 @@ export function PromptSessionPanel() {
gap={'size-300'}
height={'100%'}>

<PromptSessionSideView isOpenPromptEditor={isOpenPromptEditor}
onTogglePrompt={() => setIsOpenPromptEditor(!isOpenPromptEditor)} />
<PromptSessionSideView
isOpenPromptEditor={isOpenPromptEditor}
onTogglePrompt={() => setIsOpenPromptEditor(!isOpenPromptEditor)}
promptEditorError={promptEditorError}
/>

<Grid UNSAFE_style={{ padding: '20px 20px 0 0' }}
columns={'1fr'}
Expand All @@ -51,9 +55,12 @@ export function PromptSessionPanel() {
}}>
<PromptResultListView style={{ height: '100%' }} onClick={() => setIsOpenPromptEditor(false)} />
</Flex>
<PromptEditor isOpen={isOpenPromptEditor} onClose={() => setIsOpenPromptEditor(false)} />
<PromptEditor
isOpen={isOpenPromptEditor}
onClose={() => setIsOpenPromptEditor(false)}
setPromptValidationError={setPromptEditorError}
/>
</Grid>

</Grid>
);
}
9 changes: 5 additions & 4 deletions web-src/src/components/PromptSessionSideView.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ const styles = {
`,
};

export function PromptSessionSideView({ isOpenPromptEditor, onTogglePrompt, ...props }) {
export function PromptSessionSideView({
isOpenPromptEditor, onTogglePrompt, promptEditorError, ...props
}) {
const currentSession = useRecoilValue(sessionState);
const [viewType, setViewType] = useRecoilState(viewTypeState);
const { formatMessage } = useIntl();
Expand Down Expand Up @@ -113,12 +115,11 @@ export function PromptSessionSideView({ isOpenPromptEditor, onTogglePrompt, ...p

<div className={styles.actions}>
<Flex direction={'column'} alignItems={'flex-start'} justifyContent={'center'} flexShrink={0} gap={'4px'}>
<SavePromptButton />
<SavePromptButton isDisabled={promptEditorError} />
<ResetButton />
</Flex>
<GenerateButton />
<GenerateButton isDisabled={promptEditorError} />
</div>

</Grid>
);
}

0 comments on commit eb92ef0

Please sign in to comment.