Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sample questions field and a11y improvements #19

Merged
merged 1 commit into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
} 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 @@
}

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 @@
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 @@ -132,7 +136,7 @@

React.useEffect(() => {
loadData();
}, []);

Check warning on line 139 in src/app/FlyoutForm/FlyoutForm.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook React.useEffect has a missing dependency: 'loadData'. Either include it or remove the dependency array

const chatbotExists = (title: string) => {
return chatbots.filter((chatbot) => chatbot.name === title).length >= 1;
Expand Down Expand Up @@ -171,6 +175,35 @@
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 @@
llmConnectionId: selectedLLM?.id,
retrieverConnectionId: selectedRetriever?.id,
userPrompt: prompt,
exampleQuestions: questions,
};

try {
Expand Down Expand Up @@ -241,7 +275,7 @@
) : (
<>
<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 @@
</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 @@
{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 @@
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 @@
</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 @@
</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 @@ -99,7 +99,7 @@

React.useEffect(() => {
loadAssistants();
}, []);

Check warning on line 102 in src/app/FlyoutList/FlyoutList.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook React.useEffect has a missing dependency: 'loadAssistants'. Either include it or remove the dependency array

const buildMenu = () => {
return (
Expand Down Expand Up @@ -159,9 +159,8 @@
{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 @@
)}
</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;
}
Loading