Skip to content

Commit

Permalink
feat(assistants): add prompt suggestions (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
PetrBulanek authored Oct 22, 2024
1 parent d094b01 commit 3ea5402
Show file tree
Hide file tree
Showing 11 changed files with 702 additions and 36 deletions.
14 changes: 13 additions & 1 deletion src/modules/assistants/builder/AssistantBuilderProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ import {
} from '../icons/AssistantBaseIcon';
import { readAssistantQuery } from '../queries';
import { Assistant, AssistantMetadata } from '../types';
import { getAssistantFromAssistantResult } from '../utils';
import {
decodeStarterQuestionsMetadata,
encodeStarterQuestionsMetadata,
getAssistantFromAssistantResult,
} from '../utils';
import { useSaveAssistant } from './useSaveAssistant';

export type AssistantFormValues = {
Expand All @@ -55,8 +59,11 @@ export type AssistantFormValues = {
tools: { type: ToolType; id: string }[];
vectorStoreId?: string;
model?: AssistantModel;
starterQuestions?: StarterQuestion[];
};

export type StarterQuestion = { id: string; question: string };

export interface AssistantBuilderContextValue {
assistant: Assistant | null;
formReturn: UseFormReturn<AssistantFormValues>;
Expand Down Expand Up @@ -173,6 +180,7 @@ export function AssistantBuilderProvider({
icon,
vectorStoreId,
model,
starterQuestions,
}: AssistantFormValues) => {
const tools: AssistantTools = toolsValue
.map(({ type, id }) => {
Expand Down Expand Up @@ -201,6 +209,9 @@ export function AssistantBuilderProvider({
metadata: encodeMetadata<AssistantMetadata>({
icon: icon.name,
color: icon.color,
...(starterQuestions
? encodeStarterQuestionsMetadata(starterQuestions)
: {}),
}),
model,
},
Expand Down Expand Up @@ -293,6 +304,7 @@ function formValuesFromAssistant(
vectorStoreId:
assistant?.tool_resources?.file_search?.vector_store_ids?.at(0),
model: assistant?.model as AssistantModel,
starterQuestions: decodeStarterQuestionsMetadata(assistant?.metadata),
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/modules/assistants/builder/AssistantForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function AssistantForm() {
<SettingsFormGroup>
<InstructionsTextArea />

{/* <StarterQuestionsTextArea /> */}
<StarterQuestionsTextArea />
</SettingsFormGroup>
</TabPanel>
<TabPanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,25 @@
}
}

.addHolder {
display: flex;
align-items: stretch;
}

.textarea {
position: relative;
flex-grow: 1;
z-index: 1;
min-inline-size: 0;
&::after,
> textarea {
@include type-style(body-01);
padding: rem(13px) rem(63px) rem(13px) rem(15px);
padding: rem(13px) rem(15px);
border-radius: $spacing-03;
border: 1px solid $border-subtle-00;
min-inline-size: 0;
}
> textarea {
color: $text-primary;
background-color: transparent;
&::placeholder {
color: $text-placeholder;
}
Expand All @@ -42,3 +50,79 @@
}
}
}

.addTextarea {
&::after,
> textarea {
border-start-end-radius: 0;
border-end-end-radius: 0;
border: 1px solid $border-subtle-00;
}
> textarea {
background-color: transparent;
}
}

.button {
:global(.#{$prefix}--tooltip-trigger__wrapper) {
block-size: 100%;
}
:global(.#{$prefix}--btn) {
border-start-start-radius: 0;
border-end-start-radius: 0;
block-size: 100%;
padding-block: rem(15px);
> svg {
margin: 0;
}
}
}

.addButton {
:global(.#{$prefix}--btn) {
border-inline-start: 0;
border-color: $border-subtle-00;
align-items: flex-end;
color: $text-dark;
&:hover {
background-color: $border-subtle-00;
color: $text-muted;
}
&:disabled {
color: $border-disabled;
}
}
}

.list {
display: flex;
flex-direction: column;
row-gap: $spacing-03;
.addHolder + & {
margin-block-start: $spacing-03;
}
}

.item {
position: relative;
}

.itemTextarea {
&::after,
> textarea {
border: 1px solid $border-subtle-01;
padding-inline-end: rem(47px);
}
> textarea {
background-color: $border-subtle-01;
}
}

.removeButton {
&:global(.#{$prefix}--popover-container) {
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
inset-block-end: 0;
}
}
153 changes: 145 additions & 8 deletions src/modules/assistants/builder/StarterQuestionsTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,156 @@
*/

import { TextAreaAutoHeight } from '@/components/TextAreaAutoHeight/TextAreaAutoHeight';
import { FormLabel } from '@carbon/react';
import {
dispatchChangeEventOnFormInputs,
submitFormOnEnter,
} from '@/utils/formUtils';
import { isNotNull } from '@/utils/helpers';
import { FormLabel, IconButton } from '@carbon/react';
import { Add, Close } from '@carbon/react/icons';
import clsx from 'clsx';
import { FormEvent, useCallback, useRef } from 'react';
import { useController, useForm, useFormContext } from 'react-hook-form';
import { v4 as uuid } from 'uuid';
import {
AssistantFormValues,
StarterQuestion,
} from './AssistantBuilderProvider';
import classes from './StarterQuestionsTextArea.module.scss';

interface FormValues {
input: string;
}

export function StarterQuestionsTextArea() {
const formRef = useRef<HTMLFormElement>(null);
const { register, watch, handleSubmit, reset } = useForm<FormValues>({
mode: 'onChange',
defaultValues: {
input: '',
},
});
const { setValue } = useFormContext<AssistantFormValues>();
const {
field: { value: questions, onChange },
} = useController<AssistantFormValues, 'starterQuestions'>({
name: 'starterQuestions',
});

const inputValue = watch('input');

const hasMaxQuestions = questions && questions.length >= MAX_QUESTIONS;

const setQuestions = (questions?: StarterQuestion[]) => {
if (isNotNull(questions)) {
setValue('starterQuestions', questions);
onChange(questions);
}
};

const addQuestion = (question: string) => {
const newQuestion = {
id: uuid(),
question,
};

setQuestions(questions ? [...questions, newQuestion] : [newQuestion]);
};

const removeQuestion = (id: string) => {
setQuestions(questions?.filter((question) => question.id !== id));
};

const updateQuestion = (id: string, value: string) => {
setQuestions(
questions?.map((question) =>
question.id === id ? { ...question, question: value } : question,
),
);
};

const submitForm = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();

handleSubmit(({ input }) => {
if (hasMaxQuestions || !input.trim()) {
return;
}

addQuestion(input);
resetForm();
})();
};

const resetForm = useCallback(() => {
{
const formElem = formRef.current;

if (!formElem) {
return;
}

reset();
dispatchChangeEventOnFormInputs(formElem);
}
}, [reset]);

return (
<div className={classes.root}>
<form className={classes.root} onSubmit={submitForm} ref={formRef}>
<FormLabel>Starter questions</FormLabel>

<TextAreaAutoHeight
className={classes.textarea}
placeholder="Type and add a question that users can select at the start of a new session"
rows={1}
/>
</div>
{!hasMaxQuestions && (
<div className={classes.addHolder}>
<TextAreaAutoHeight
className={clsx(classes.textarea, classes.addTextarea)}
placeholder="Type and add a question that users can select at the start of a new session"
rows={1}
maxLength={MAX_QUESTION_LENGTH}
onKeyDown={submitFormOnEnter}
{...register('input', { required: true })}
/>

<IconButton
wrapperClasses={clsx(classes.button, classes.addButton)}
label="Enter the starter question to add"
kind="tertiary"
align="top-end"
disabled={!inputValue.trim()}
type="submit"
>
<Add />
</IconButton>
</div>
)}

{questions && (
<ul className={classes.list}>
{questions.map(({ id, question }) => (
<li key={id} className={classes.item}>
<TextAreaAutoHeight
className={clsx(classes.textarea, classes.itemTextarea)}
rows={1}
defaultValue={question}
maxLength={MAX_QUESTION_LENGTH}
onChange={(event) => updateQuestion(id, event.target.value)}
/>

<IconButton
wrapperClasses={clsx(classes.button, classes.removeButton)}
label="Remove"
kind="ghost"
align="top-end"
onClick={() => removeQuestion(id)}
>
<Close />
</IconButton>
</li>
))}
</ul>
)}
</form>
);
}

const MAX_QUESTIONS = 3;
const MAX_QUESTION_LENGTH = 512;
10 changes: 8 additions & 2 deletions src/modules/assistants/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@

import { AssistantResult } from '@/app/api/assistants/types';
import {
AssitantIconName,
AssistantIconColor,
AssitantIconName,
} from './icons/AssistantBaseIcon';

export interface AssistantMetadata {
export const STARTER_QUESTION_KEY_PREFIX = 'starterQuestion_';

export interface AssistantMetadata extends StarterQuestionsMetadata {
icon?: AssitantIconName;
color?: AssistantIconColor;
}

export type Assistant = Omit<AssistantResult, 'metadata'> & {
metadata: AssistantMetadata;
};

export interface StarterQuestionsMetadata {
[key: `${typeof STARTER_QUESTION_KEY_PREFIX}${string}`]: string;
}
Loading

0 comments on commit 3ea5402

Please sign in to comment.