💬 feat: assistant conversation starter (#3699)

* feat: initial UI convoStart

* fix: ConvoStarter UI

* fix: convoStarters bug

* feat: Add input field focus on conversation starters

* style: conversation starter UI update

* feat: apply fixes for starters

* style: update conversationStarters UI and fixed typo

* general UI update

* feat: Add onClick functionality to ConvoStarter component

* fix: quick fix test

* fix(AssistantSelect): remove object check

* fix: updateAssistant `conversation_starters` var

* chore: remove starter autofocus

* fix: no empty conversation starters, always show input, use Constants value for max count

* style: Update defaultTextPropsLabel styles, for a11y placeholder

* refactor: Update ConvoStarter component styles and class names for a11y and theme

* refactor: convostarter, move plus button to within persistent element

* fix: types

* chore: Update landing page assistant description styling with theming

* chore: assistant types

* refactor: documents routes

* refactor: optimize conversation starter mutations/queries

* refactor: Update listAllAssistants return type to Promise<Array<Assistant>>

* feat: edit existing starters

* feat(convo-starters): enhance ConvoStarter component and add animations

    - Update ConvoStarter component styling for better visual appeal
    - Implement fade-in animation for smoother appearance
    - Add hover effect with background color change
    - Improve text overflow handling with line-clamp and text-balance
    - Ensure responsive design for various screen sizes

* feat(assistant): add conversation starters to assistant builder

- Add localization strings for conversation starters
- Update mobile.css with shake animation for max starters reached
- Enhance user experience with tooltips and dynamic input handling

* refactor: select specific fields for assistant documents fetch

* refactor: remove endpoint query key, fetch all assistant docs for now, add conversation_starters to v1 methods

* refactor: add document filters based on endpoint config

* fix: starters not applied during creation

* refactor: update AssistantSelect component to handle undefined lastSelectedModels

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2024-08-31 13:42:20 -04:00 committed by GitHub
parent 63b80c3067
commit 79f9cd5a4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 602 additions and 214 deletions

View file

@ -11,7 +11,12 @@ import {
} from 'librechat-data-provider';
import type { UseFormReset } from 'react-hook-form';
import type { UseMutationResult } from '@tanstack/react-query';
import type { Assistant, AssistantCreateParams, AssistantsEndpoint } from 'librechat-data-provider';
import type {
Assistant,
AssistantCreateParams,
AssistantDocument,
AssistantsEndpoint,
} from 'librechat-data-provider';
import type {
Actions,
ExtendedFile,
@ -19,13 +24,20 @@ import type {
TAssistantOption,
LastSelectedModels,
} from '~/common';
import { useListAssistantsQuery, useGetAssistantDocsQuery } from '~/data-provider';
import SelectDropDown from '~/components/ui/SelectDropDown';
import { useListAssistantsQuery } from '~/data-provider';
import { useLocalize, useLocalStorage } from '~/hooks';
import { useFileMapContext } from '~/Providers';
import { cn } from '~/utils';
const keys = new Set(['name', 'id', 'description', 'instructions', 'model']);
const keys = new Set([
'name',
'id',
'description',
'instructions',
'conversation_starters',
'model',
]);
export default function AssistantSelect({
reset,
@ -45,11 +57,18 @@ export default function AssistantSelect({
const localize = useLocalize();
const fileMap = useFileMapContext();
const lastSelectedAssistant = useRef<string | null>(null);
const [lastSelectedModels] = useLocalStorage<LastSelectedModels>(
const [lastSelectedModels] = useLocalStorage<LastSelectedModels | undefined>(
LocalStorageKeys.LAST_MODEL,
{} as LastSelectedModels,
);
const { data: documentsMap = new Map<string, AssistantDocument>() } = useGetAssistantDocsQuery(
endpoint,
{
select: (data) => new Map(data.map((dbA) => [dbA.assistant_id, dbA])),
},
);
const assistants = useListAssistantsQuery(endpoint, undefined, {
select: (res) =>
res.data.map((_assistant) => {
@ -57,10 +76,10 @@ export default function AssistantSelect({
endpoint === EModelEndpoint.assistants ? FileSources.openai : FileSources.azure;
const assistant = {
..._assistant,
label: _assistant?.name ?? '',
label: _assistant.name ?? '',
value: _assistant.id,
files: _assistant?.file_ids ? ([] as Array<[string, ExtendedFile]>) : undefined,
code_files: _assistant?.tool_resources?.code_interpreter?.file_ids
files: _assistant.file_ids ? ([] as Array<[string, ExtendedFile]>) : undefined,
code_files: _assistant.tool_resources?.code_interpreter?.file_ids
? ([] as Array<[string, ExtendedFile]>)
: undefined,
};
@ -104,11 +123,17 @@ export default function AssistantSelect({
}
if (assistant.code_files && _assistant.tool_resources?.code_interpreter?.file_ids) {
_assistant.tool_resources?.code_interpreter?.file_ids?.forEach((file_id) =>
_assistant.tool_resources.code_interpreter.file_ids.forEach((file_id) =>
handleFile(file_id, assistant.code_files),
);
}
const assistantDoc = documentsMap.get(_assistant.id);
/* If no user updates, use the latest assistant docs */
if (assistantDoc && !assistant.conversation_starters) {
assistant.conversation_starters = assistantDoc.conversation_starters;
}
return assistant;
}),
});
@ -128,8 +153,8 @@ export default function AssistantSelect({
const update = {
...assistant,
label: assistant?.name ?? '',
value: assistant?.id ?? '',
label: assistant.name ?? '',
value: assistant.id ?? '',
};
const actions: Actions = {
@ -138,9 +163,9 @@ export default function AssistantSelect({
[Capabilities.retrieval]: false,
};
assistant?.tools
?.filter((tool) => tool.type !== 'function' || isImageVisionTool(tool))
?.map((tool) => tool?.function?.name || tool.type)
(assistant.tools ?? [])
.filter((tool) => tool.type !== 'function' || isImageVisionTool(tool))
.map((tool) => tool.function?.name || tool.type)
.forEach((tool) => {
if (tool === Tools.file_search) {
actions[Capabilities.retrieval] = true;
@ -148,10 +173,9 @@ export default function AssistantSelect({
actions[tool] = true;
});
const functions =
assistant?.tools
?.filter((tool) => tool.type === 'function' && !isImageVisionTool(tool))
?.map((tool) => tool.function?.name ?? '') ?? [];
const functions = (assistant.tools ?? [])
.filter((tool) => tool.type === 'function' && !isImageVisionTool(tool))
.map((tool) => tool.function?.name ?? '');
const formValues: Partial<AssistantForm & Actions> = {
functions,
@ -161,18 +185,26 @@ export default function AssistantSelect({
};
Object.entries(assistant).forEach(([name, value]) => {
if (typeof value === 'number') {
return;
} else if (typeof value === 'object') {
if (!keys.has(name)) {
return;
}
if (keys.has(name)) {
if (
name === 'conversation_starters' &&
Array.isArray(value) &&
value.every((item) => typeof item === 'string')
) {
formValues[name] = value;
return;
}
if (typeof value !== 'number' && typeof value !== 'object') {
formValues[name] = value;
}
});
reset(formValues);
setCurrentAssistantId(assistant?.id);
setCurrentAssistantId(assistant.id);
},
[assistants.data, reset, setCurrentAssistantId, createMutation, endpoint, lastSelectedModels],
);
@ -184,7 +216,7 @@ export default function AssistantSelect({
return;
}
if (selectedAssistant && assistants.data) {
if (selectedAssistant !== '' && selectedAssistant != null && assistants.data) {
timerId = setTimeout(() => {
lastSelectedAssistant.current = selectedAssistant;
onSelect(selectedAssistant);