Skip to content

Commit

Permalink
Merge pull request #19 from rebeccaalpert/questions
Browse files Browse the repository at this point in the history
Add sample questions field and a11y improvements
  • Loading branch information
rebeccaalpert authored Dec 5, 2024
2 parents e246116 + 703debd commit 414391a
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 9 deletions.
119 changes: 119 additions & 0 deletions src/app/FlyoutForm/ExpandingTextInputs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as React from 'react';
import { Button, TextInput } from '@patternfly/react-core';
import { CheckIcon, CloseIcon, PencilAltIcon } from '@patternfly/react-icons';

interface ExpandingTextInputsProps {
handleInputChange: (newQuestion: string, index?: number) => void;
values: string[];
/** Unique id for expanding text inputs */
fieldId: string;
onDeleteValue: (index: number) => void;
isDisabled?: boolean;
}

export const ExpandingTextInputs: React.FunctionComponent<ExpandingTextInputsProps> = ({
values,
handleInputChange,
onDeleteValue,
fieldId,
isDisabled = false,
}: ExpandingTextInputsProps) => {
const [inputValue, setInputValue] = React.useState('');
const [editingIndex, setEditingIndex] = React.useState<number>();
const [editingInputValue, setEditingInputValue] = React.useState('');
const handleClick = () => {
handleInputChange(inputValue);
setInputValue('');
document.getElementById(`${fieldId}-text-input`)?.focus();
};
const onEdit = (index: number) => {
setEditingInputValue(values[index] ?? '');
setEditingIndex(index);
document
.getElementById(`${fieldId}-edit-text-input-${index}`)
?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
document.getElementById(`${fieldId}-edit-text-input-${index}`)?.focus();
};

return (
<div className="expanding-text-inputs">
<div className="expanding-text-inputs__row">
<TextInput
value={inputValue}
type="text"
id={`${fieldId}-text-input`}
name={`${fieldId}-text-input`}
onChange={(e, value) => {
setInputValue(value);
}}
aria-label="Enter example question"
/>
<Button isDisabled={inputValue === '' || isDisabled} variant="secondary" onClick={handleClick}>
Add
</Button>
</div>
{values.length > 0 && (
<section aria-label="Example questions" tabIndex={-1}>
{values.map((value, index) => {
return (
<div key={index} className="expanding-text-inputs__row expanding-text-input__row-with-background">
<div className={`expanding-text-inputs__row-value ${editingIndex === index ? '' : 'hidden'}`}>
<TextInput
value={editingInputValue}
type="text"
id={`${fieldId}-edit-text-input-${index}`}
name={`${fieldId}-edit-text-input-${index}`}
onChange={(e, value) => setEditingInputValue(value)}
tabIndex={editingIndex === index ? 0 : -1}
aria-label={
editingInputValue === '' ? 'Edit example question' : `Edit example question ${editingInputValue}`
}
/>
</div>
<div className={`expanding-text-inputs__row-value ${editingIndex === index ? 'hidden' : ''}`}>
{value}
</div>
<Button
tabIndex={editingIndex === index ? 0 : -1}
onClick={() => {
handleInputChange(editingInputValue, index);
setEditingIndex(undefined);
setEditingInputValue('');
document.getElementById(`${fieldId}-text-input`)?.focus();
}}
variant="plain"
className={editingIndex === index ? '' : 'hidden'}
aria-label={`Save question ${editingInputValue}`}
>
<CheckIcon />
</Button>
<Button
tabIndex={editingIndex === index ? -1 : 0}
variant="plain"
onClick={() => onEdit(index)}
className={editingIndex === index ? 'hidden' : ''}
aria-label={`Edit question ${value}`}
>
<PencilAltIcon />
</Button>
<Button
variant="plain"
onClick={() => {
onDeleteValue(index);
document.getElementById(`${fieldId}-text-input`)?.focus();
if (editingIndex === index) {
setEditingIndex(undefined);
}
}}
aria-label={`Delete question ${value}`}
>
<CloseIcon />
</Button>
</div>
);
})}
</section>
)}
</div>
);
};
67 changes: 62 additions & 5 deletions src/app/FlyoutForm/FlyoutForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '@patternfly/react-core';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import * as React from 'react';
import { ExpandingTextInputs } from './ExpandingTextInputs';

interface RetrieverAPIResponse {
id: string;
Expand All @@ -41,6 +42,7 @@ interface FlyoutFormProps {
}

type validate = 'success' | 'error' | 'default';
type questionsValidate = 'error' | 'default';

export const FlyoutForm: React.FunctionComponent<FlyoutFormProps> = ({ header, hideFlyout }: FlyoutFormProps) => {
const [isLoading, setIsLoading] = React.useState(true);
Expand All @@ -57,6 +59,8 @@ export const FlyoutForm: React.FunctionComponent<FlyoutFormProps> = ({ header, h
const [validated, setValidated] = React.useState<validate>('default');
const [selectedLLM, setSelectedLLM] = React.useState<LLMAPIResponse>();
const [prompt, setPrompt] = React.useState<string>();
const [questions, setQuestions] = React.useState<string[]>([]);
const [questionsValidated, setQuestionsValidated] = React.useState<questionsValidate>('default');
const { nextStep, prevStep, setReloadList } = useFlyoutWizard();
const { chatbots } = useAppData();

Expand Down Expand Up @@ -171,6 +175,35 @@ export const FlyoutForm: React.FunctionComponent<FlyoutFormProps> = ({ header, h
setPrompt(newPrompt);
};

const handleQuestionsChange = (newQuestion: string, index?: number) => {
const newQuestions = [...questions];
if (index !== undefined && index !== null) {
newQuestions[index] = newQuestion;
setQuestions(newQuestions);
} else {
newQuestions.push(newQuestion);
if (newQuestions.length > 3) {
setQuestionsValidated('error');
return;
}
setQuestions(newQuestions);
if (newQuestions.length === 3) {
setQuestionsValidated('error');
} else {
setQuestionsValidated('default');
}
}
};

const onDeleteQuestion = (index: number) => {
const newQuestions = [...questions];
newQuestions.splice(index, 1);
if (newQuestions.length < 3) {
setQuestionsValidated('default');
}
setQuestions(newQuestions);
};

const onRetrieverToggleClick = () => {
setIsRetrieverDropdownOpen(!isRetrieverDropdownOpen);
};
Expand All @@ -189,6 +222,7 @@ export const FlyoutForm: React.FunctionComponent<FlyoutFormProps> = ({ header, h
llmConnectionId: selectedLLM?.id,
retrieverConnectionId: selectedRetriever?.id,
userPrompt: prompt,
exampleQuestions: questions,
};

try {
Expand Down Expand Up @@ -241,7 +275,7 @@ export const FlyoutForm: React.FunctionComponent<FlyoutFormProps> = ({ header, h
) : (
<>
<FlyoutHeader title={header} hideFlyout={hideFlyout} />
<div className="flyout-form-container">
<section className="flyout-form-container" aria-label={title} tabIndex={-1}>
{error ? (
<FlyoutError title={error.title} subtitle={error.body} onClick={onClick} />
) : (
Expand Down Expand Up @@ -280,8 +314,9 @@ export const FlyoutForm: React.FunctionComponent<FlyoutFormProps> = ({ header, h
</FormHelperText>
</FormGroup>
{retrieverConnections && retrieverConnections.length > 0 && (
<FormGroup label="Knowledge source" fieldId="flyout-form-model">
<FormGroup label="Knowledge source" fieldId="flyout-form-knowledge-source">
<Dropdown
id="flyout-form-knowledge-source"
isOpen={isRetrieverDropdownOpen}
onSelect={handleRetrieverChange}
onOpenChange={(isOpen: boolean) => setIsRetrieverDropdownOpen(isOpen)}
Expand Down Expand Up @@ -322,6 +357,7 @@ export const FlyoutForm: React.FunctionComponent<FlyoutFormProps> = ({ header, h
{llms && llms.length > 0 && (
<FormGroup isRequired label="Model" fieldId="flyout-form-model">
<Dropdown
id="flyout-form-model"
isOpen={isLLMDropdownOpen}
onSelect={handleLLMChange}
onOpenChange={(isOpen: boolean) => setIsLLMDropdownOpen(isOpen)}
Expand All @@ -330,7 +366,7 @@ export const FlyoutForm: React.FunctionComponent<FlyoutFormProps> = ({ header, h
onOpenChangeKeys={['Escape']}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle ref={toggleRef} onClick={onLLMToggleClick} isExpanded={isLLMDropdownOpen}>
{selectedLLM ? selectedLLM.description : 'Choose an model'}
{selectedLLM ? selectedLLM.description : 'Choose a model'}
</MenuToggle>
)}
popperProps={{ appendTo: 'inline' }}
Expand Down Expand Up @@ -365,7 +401,7 @@ export const FlyoutForm: React.FunctionComponent<FlyoutFormProps> = ({ header, h
</FormGroup>
<FormGroup fieldId="flyout-form-prompts" label="Prompt">
<TextArea
id="flyout-form-prompt"
id="flyout-form-prompts"
value={prompt}
onChange={handlePromptChange}
aria-label="Text area for sample prompt"
Expand All @@ -380,9 +416,30 @@ export const FlyoutForm: React.FunctionComponent<FlyoutFormProps> = ({ header, h
</HelperText>
</FormHelperText>
</FormGroup>
<FormGroup fieldId="flyout-form-questions" label="Example questions (Limit 3)">
<ExpandingTextInputs
fieldId="flyout-form-questions"
handleInputChange={handleQuestionsChange}
values={questions}
onDeleteValue={onDeleteQuestion}
isDisabled={questions.length === 3}
/>
<FormHelperText>
<HelperText>
<HelperTextItem
icon={questionsValidated === 'error' ? <ExclamationCircleIcon /> : undefined}
variant={questionsValidated}
>
{questionsValidated === 'error'
? 'There is a three question limit. Try deleting a question to add more.'
: 'A set of up to three example questions that a model could be asked. This will help the model generate accurate, relevant, and contextually appropriate responses.'}
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
</Form>
)}
</div>
</section>
{!error && (
<FlyoutFooter
isPrimaryButtonDisabled={title === '' || (llms.length > 0 && !selectedLLM) || validated !== 'success'}
Expand Down
2 changes: 1 addition & 1 deletion src/app/FlyoutHeader.tsx/FlyoutHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const FlyoutHeader: React.FunctionComponent<FlyoutHeaderProps> = ({ title
return (
<div className="flyout-header">
{title}
<Button onClick={hideFlyout} variant="plain" icon={<TimesIcon />} />
<Button onClick={hideFlyout} variant="plain" icon={<TimesIcon />} aria-label="Close" />
</div>
);
};
5 changes: 2 additions & 3 deletions src/app/FlyoutList/FlyoutList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,8 @@ export const FlyoutList: React.FunctionComponent<FlyoutListProps> = ({
{isLoading ? (
<FlyoutLoading />
) : (
<div className="flyout-list">
<section className="flyout-list" aria-label={title} tabIndex={-1}>
<SearchInput
aria-label={`Search ${typeWordPlural}`}
onChange={(_event, value) => handleTextInputChange(value)}
placeholder={`Search ${typeWordPlural}...`}
/>
Expand All @@ -176,7 +175,7 @@ export const FlyoutList: React.FunctionComponent<FlyoutListProps> = ({
)}
</MenuContent>
</Menu>
</div>
</section>
)}
<FlyoutFooter primaryButton={buttonText} onPrimaryButtonClick={onFooterButtonClick ?? nextStep} />
</>
Expand Down
32 changes: 32 additions & 0 deletions src/app/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -525,3 +525,35 @@ pf-v6-c-page__main-container.pf-m-fill {
.flyout-error {
height: 100%;
}

.expanding-text-inputs {
display: flex;
flex-direction: column;
gap: var(--pf-t--global--spacer--md);
}

.expanding-text-inputs__row {
display: flex;
gap: var(--pf-t--global--spacer--sm);
align-items: center;
width: 100%;

.expanding-text-inputs__row-value {
flex: 1;
}
}

.expanding-text-input__row-with-background {
background-color: var(--pf-t--global--background--color--secondary--default);
padding-inline-start: var(--pf-t--global--spacer--md);
padding-inline-end: var(--pf-t--global--spacer--sm);
padding-block-start: var(--pf-t--global--spacer--sm);
padding-block-end: var(--pf-t--global--spacer--sm);
position: relative;
}

.hidden {
position: absolute;
opacity: 0;
z-index: -1;
}

0 comments on commit 414391a

Please sign in to comment.