💬 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

@ -12,7 +12,7 @@ const Assistant = mongoose.model('assistant', assistantSchema);
* @param {string} searchParams.user - The user ID of the assistant's author.
* @param {Object} updateData - An object containing the properties to update.
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional).
* @returns {Promise<Object>} The updated or newly created assistant document as a plain object.
* @returns {Promise<AssistantDocument>} The updated or newly created assistant document as a plain object.
*/
const updateAssistantDoc = async (searchParams, updateData, session = null) => {
const options = { new: true, upsert: true, session };
@ -25,7 +25,7 @@ const updateAssistantDoc = async (searchParams, updateData, session = null) => {
* @param {Object} searchParams - The search parameters to find the assistant to update.
* @param {string} searchParams.assistant_id - The ID of the assistant to update.
* @param {string} searchParams.user - The user ID of the assistant's author.
* @returns {Promise<Object|null>} The assistant document as a plain object, or null if not found.
* @returns {Promise<AssistantDocument|null>} The assistant document as a plain object, or null if not found.
*/
const getAssistant = async (searchParams) => await Assistant.findOne(searchParams).lean();
@ -33,10 +33,17 @@ const getAssistant = async (searchParams) => await Assistant.findOne(searchParam
* Retrieves all assistants that match the given search parameters.
*
* @param {Object} searchParams - The search parameters to find matching assistants.
* @returns {Promise<Array<Object>>} A promise that resolves to an array of action documents as plain objects.
* @param {Object} [select] - Optional. Specifies which document fields to include or exclude.
* @returns {Promise<Array<AssistantDocument>>} A promise that resolves to an array of assistant documents as plain objects.
*/
const getAssistants = async (searchParams) => {
return await Assistant.find(searchParams).lean();
const getAssistants = async (searchParams, select = null) => {
let query = Assistant.find(searchParams);
if (select) {
query = query.select(select);
}
return await query.lean();
};
/**

View file

@ -19,6 +19,10 @@ const assistantSchema = mongoose.Schema(
},
default: undefined,
},
conversation_starters: {
type: [String],
default: [],
},
access_level: {
type: Number,
},

View file

@ -64,7 +64,7 @@ const _listAssistants = async ({ req, res, version, query }) => {
* @param {object} params.res - The response object, used for initializing the client.
* @param {string} params.version - The API version to use.
* @param {Omit<AssistantListParams, 'endpoint'>} params.query - The query parameters to list assistants (e.g., limit, order).
* @returns {Promise<object>} A promise that resolves to the response from the `openai.beta.assistants.list` method call.
* @returns {Promise<Array<Assistant>>} A promise that resolves to the response from the `openai.beta.assistants.list` method call.
*/
const listAllAssistants = async ({ req, res, version, query }) => {
/** @type {{ openai: OpenAIClient }} */

View file

@ -18,7 +18,9 @@ const createAssistant = async (req, res) => {
try {
const { openai } = await getOpenAIClient({ req, res });
const { tools = [], endpoint, ...assistantData } = req.body;
const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body;
delete assistantData.conversation_starters;
assistantData.tools = tools
.map((tool) => {
if (typeof tool !== 'string') {
@ -41,11 +43,22 @@ const createAssistant = async (req, res) => {
};
const assistant = await openai.beta.assistants.create(assistantData);
const promise = updateAssistantDoc({ assistant_id: assistant.id }, { user: req.user.id });
const createData = { user: req.user.id };
if (conversation_starters) {
createData.conversation_starters = conversation_starters;
}
const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData);
if (azureModelIdentifier) {
assistant.model = azureModelIdentifier;
}
await promise;
if (document.conversation_starters) {
assistant.conversation_starters = document.conversation_starters;
}
logger.debug('/assistants/', assistant);
res.status(201).json(assistant);
} catch (error) {
@ -88,7 +101,7 @@ const patchAssistant = async (req, res) => {
await validateAuthor({ req, openai });
const assistant_id = req.params.id;
const { endpoint: _e, ...updateData } = req.body;
const { endpoint: _e, conversation_starters, ...updateData } = req.body;
updateData.tools = (updateData.tools ?? [])
.map((tool) => {
if (typeof tool !== 'string') {
@ -104,6 +117,15 @@ const patchAssistant = async (req, res) => {
}
const updatedAssistant = await openai.beta.assistants.update(assistant_id, updateData);
if (conversation_starters !== undefined) {
const conversationStartersUpdate = await updateAssistantDoc(
{ assistant_id },
{ conversation_starters },
);
updatedAssistant.conversation_starters = conversationStartersUpdate.conversation_starters;
}
res.json(updatedAssistant);
} catch (error) {
logger.error('[/assistants/:id] Error updating assistant', error);
@ -153,6 +175,32 @@ const listAssistants = async (req, res) => {
}
};
/**
* Filter assistants based on configuration.
*
* @param {object} params - The parameters object.
* @param {string} params.userId - The user ID to filter private assistants.
* @param {AssistantDocument[]} params.assistants - The list of assistants to filter.
* @param {Partial<TAssistantEndpoint>} [params.assistantsConfig] - The assistant configuration.
* @returns {AssistantDocument[]} - The filtered list of assistants.
*/
function filterAssistantDocs({ documents, userId, assistantsConfig = {} }) {
const { supportedIds, excludedIds, privateAssistants } = assistantsConfig;
const removeUserId = (doc) => {
const { user: _u, ...document } = doc;
return document;
};
if (privateAssistants) {
return documents.filter((doc) => userId === doc.user.toString()).map(removeUserId);
} else if (supportedIds?.length) {
return documents.filter((doc) => supportedIds.includes(doc.assistant_id)).map(removeUserId);
} else if (excludedIds?.length) {
return documents.filter((doc) => !excludedIds.includes(doc.assistant_id)).map(removeUserId);
}
return documents.map(removeUserId);
}
/**
* Returns a list of the user's assistant documents (metadata saved to database).
* @route GET /assistants/documents
@ -160,7 +208,25 @@ const listAssistants = async (req, res) => {
*/
const getAssistantDocuments = async (req, res) => {
try {
res.json(await getAssistants({ user: req.user.id }));
const endpoint = req.query;
const assistantsConfig = req.app.locals[endpoint];
const documents = await getAssistants(
{},
{
user: 1,
assistant_id: 1,
conversation_starters: 1,
createdAt: 1,
updatedAt: 1,
},
);
const docs = filterAssistantDocs({
documents,
userId: req.user.id,
assistantsConfig,
});
res.json(docs);
} catch (error) {
logger.error('[/assistants/documents] Error listing assistant documents', error);
res.status(500).json({ error: error.message });

View file

@ -16,7 +16,9 @@ const createAssistant = async (req, res) => {
/** @type {{ openai: OpenAIClient }} */
const { openai } = await getOpenAIClient({ req, res });
const { tools = [], endpoint, ...assistantData } = req.body;
const { tools = [], endpoint, conversation_starters, ...assistantData } = req.body;
delete assistantData.conversation_starters;
assistantData.tools = tools
.map((tool) => {
if (typeof tool !== 'string') {
@ -39,11 +41,22 @@ const createAssistant = async (req, res) => {
};
const assistant = await openai.beta.assistants.create(assistantData);
const promise = updateAssistantDoc({ assistant_id: assistant.id }, { user: req.user.id });
const createData = { user: req.user.id };
if (conversation_starters) {
createData.conversation_starters = conversation_starters;
}
const document = await updateAssistantDoc({ assistant_id: assistant.id }, createData);
if (azureModelIdentifier) {
assistant.model = azureModelIdentifier;
}
await promise;
if (document.conversation_starters) {
assistant.conversation_starters = document.conversation_starters;
}
logger.debug('/assistants/', assistant);
res.status(201).json(assistant);
} catch (error) {
@ -64,6 +77,17 @@ const createAssistant = async (req, res) => {
const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
await validateAuthor({ req, openai });
const tools = [];
let conversation_starters = null;
if (updateData?.conversation_starters) {
const conversationStartersUpdate = await updateAssistantDoc(
{ assistant_id: assistant_id },
{ conversation_starters: updateData.conversation_starters },
);
conversation_starters = conversationStartersUpdate.conversation_starters;
delete updateData.conversation_starters;
}
let hasFileSearch = false;
for (const tool of updateData.tools ?? []) {
@ -108,7 +132,13 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
updateData.model = openai.locals.azureOptions.azureOpenAIApiDeploymentName;
}
return await openai.beta.assistants.update(assistant_id, updateData);
const assistant = await openai.beta.assistants.update(assistant_id, updateData);
if (conversation_starters) {
assistant.conversation_starters = conversation_starters;
}
return assistant;
};
/**

View file

@ -0,0 +1,13 @@
const express = require('express');
const controllers = require('~/server/controllers/assistants/v1');
const router = express.Router();
/**
* Returns a list of the user's assistant documents (metadata saved to database).
* @route GET /assistants/documents
* @returns {AssistantDocument[]} 200 - success response - application/json
*/
router.get('/', controllers.getAssistantDocuments);
module.exports = router;

View file

@ -1,6 +1,7 @@
const multer = require('multer');
const express = require('express');
const controllers = require('~/server/controllers/assistants/v1');
const documents = require('./documents');
const actions = require('./actions');
const tools = require('./tools');
@ -20,6 +21,13 @@ router.use('/actions', actions);
*/
router.use('/tools', tools);
/**
* Create an assistant.
* @route GET /assistants/documents
* @returns {AssistantDocument[]} 200 - application/json
*/
router.use('/documents', documents);
/**
* Create an assistant.
* @route POST /assistants
@ -61,13 +69,6 @@ router.delete('/:id', controllers.deleteAssistant);
*/
router.get('/', controllers.listAssistants);
/**
* Returns a list of the user's assistant documents (metadata saved to database).
* @route GET /assistants/documents
* @returns {AssistantDocument[]} 200 - success response - application/json
*/
router.get('/documents', controllers.getAssistantDocuments);
/**
* Uploads and updates an avatar for a specific assistant.
* @route POST /avatar/:assistant_id

View file

@ -2,6 +2,7 @@ const multer = require('multer');
const express = require('express');
const v1 = require('~/server/controllers/assistants/v1');
const v2 = require('~/server/controllers/assistants/v2');
const documents = require('./documents');
const actions = require('./actions');
const tools = require('./tools');
@ -21,6 +22,13 @@ router.use('/actions', actions);
*/
router.use('/tools', tools);
/**
* Create an assistant.
* @route GET /assistants/documents
* @returns {AssistantDocument[]} 200 - application/json
*/
router.use('/documents', documents);
/**
* Create an assistant.
* @route POST /assistants
@ -62,13 +70,6 @@ router.delete('/:id', v1.deleteAssistant);
*/
router.get('/', v1.listAssistants);
/**
* Returns a list of the user's assistant documents (metadata saved to database).
* @route GET /assistants/documents
* @returns {AssistantDocument[]} 200 - success response - application/json
*/
router.get('/documents', v1.getAssistantDocuments);
/**
* Uploads and updates an avatar for a specific assistant.
* @route POST /avatar/:assistant_id

View file

@ -22,6 +22,7 @@ export type AssistantForm = {
name: string | null;
description: string | null;
instructions: string | null;
conversation_starters: string[];
model: string;
functions: string[];
} & Actions;

View file

@ -18,6 +18,7 @@ import type {
TConversation,
TStartupConfig,
EModelEndpoint,
ActionMetadata,
AssistantsEndpoint,
TMessageContentParts,
AuthorizationTypeEnum,
@ -146,9 +147,13 @@ export type ActionAuthForm = {
token_exchange_method: TokenExchangeMethodEnum;
};
export type ActionWithNullableMetadata = Omit<Action, 'metadata'> & {
metadata: ActionMetadata | null;
};
export type AssistantPanelProps = {
index?: number;
action?: Action;
action?: ActionWithNullableMetadata;
actions?: Action[];
assistant_id?: string;
activePanel?: string;

View file

@ -46,7 +46,7 @@ const DeleteBookmarkButton: FC<{
</Label>
}
confirm={confirmDelete}
className="transition-color flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover"
className="transition-colors flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover"
icon={<TrashIcon className="size-4" />}
tabIndex={tabIndex}
onFocus={onFocus}

View file

@ -25,7 +25,7 @@ const EditBookmarkButton: FC<{
/>
<button
type="button"
className="transition-color flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover"
className="transition-colors flex size-7 items-center justify-center rounded-lg duration-200 hover:bg-surface-hover"
tabIndex={tabIndex}
onFocus={onFocus}
onBlur={onBlur}

View file

@ -0,0 +1,17 @@
interface ConvoStarterProps {
text: string;
onClick: () => void;
}
export default function ConvoStarter({ text, onClick }: ConvoStarterProps) {
return (
<button
onClick={onClick}
className="relative flex w-40 cursor-pointer flex-col gap-2 rounded-2xl border border-border-medium px-3 pb-4 pt-3 text-start align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-colors duration-300 ease-in-out fade-in hover:bg-surface-tertiary"
>
<p className="break-word line-clamp-3 overflow-hidden text-balance break-all text-text-secondary">
{text}
</p>
</button>
);
}

View file

@ -136,7 +136,7 @@ const ChatForm = ({ index = 0 }) => {
newConversation={generateConversation}
textAreaRef={textAreaRef}
commandChar="+"
placeholder="com_ui_add"
placeholder="com_ui_add_model_preset"
includeAssistants={false}
/>
)}

View file

@ -1,12 +1,15 @@
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import { useMemo } from 'react';
import { EModelEndpoint, isAssistantsEndpoint, Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { ReactNode } from 'react';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useGetAssistantDocsQuery } from '~/data-provider';
import ConvoIcon from '~/components/Endpoints/ConvoIcon';
import { useLocalize, useSubmitMessage } from '~/hooks';
import { BirthdayIcon } from '~/components/svg';
import { getIconEndpoint, cn } from '~/utils';
import { useLocalize } from '~/hooks';
import ConvoStarter from './ConvoStarter';
export default function Landing({ Header }: { Header?: ReactNode }) {
const { conversation } = useChatContext();
@ -29,21 +32,36 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
const iconURL = conversation?.iconURL;
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
const { data: documentsMap = new Map() } = useGetAssistantDocsQuery(endpoint, {
select: (data) => new Map(data.map((dbA) => [dbA.assistant_id, dbA])),
});
const isAssistant = isAssistantsEndpoint(endpoint);
const assistant = isAssistant ? assistantMap?.[endpoint][assistant_id ?? ''] : undefined;
const assistantName = assistant && assistant.name;
const assistantDesc = assistant && assistant.description;
const avatar = assistant && (assistant.metadata?.avatar as string);
const assistantName = assistant?.name ?? '';
const assistantDesc = assistant?.description ?? '';
const avatar = assistant?.metadata?.avatar ?? '';
const conversation_starters = useMemo(() => {
/* The user made updates, use client-side cache, */
if (assistant?.conversation_starters) {
return assistant.conversation_starters;
}
/* If none in cache, we use the latest assistant docs */
const assistantDocs = documentsMap.get(assistant_id ?? '');
return assistantDocs?.conversation_starters ?? [];
}, [documentsMap, assistant_id, assistant?.conversation_starters]);
const containerClassName =
'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black';
const { submitMessage } = useSubmitMessage();
const sendConversationStarter = (text: string) => submitMessage({ text });
return (
<TooltipProvider delayDuration={50}>
<Tooltip>
<div className="relative h-full">
<div className="absolute left-0 right-0">{Header && Header}</div>
<div className="absolute left-0 right-0">{Header != null ? Header : null}</div>
<div className="flex h-full flex-col items-center justify-center">
<div className={cn('relative h-12 w-12', assistantName && avatar ? 'mb-0' : 'mb-3')}>
<ConvoIcon
@ -55,7 +73,7 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
className="h-2/3 w-2/3"
size={41}
/>
{!!startupConfig?.showBirthdayIcon && (
{startupConfig?.showBirthdayIcon === true ? (
<div>
<TooltipTrigger>
<BirthdayIcon className="absolute bottom-8 right-2.5" />
@ -64,14 +82,14 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
{localize('com_ui_happy_birthday')}
</TooltipContent>
</div>
)}
) : null}
</div>
{assistantName ? (
<div className="flex flex-col items-center gap-0 p-2">
<div className="text-center text-2xl font-medium dark:text-white">
{assistantName}
</div>
<div className="text-token-text-secondary max-w-md text-center text-xl font-normal ">
<div className="max-w-md text-center text-sm font-normal text-text-primary ">
{assistantDesc ? assistantDesc : localize('com_nav_welcome_message')}
</div>
{/* <div className="mt-1 flex items-center gap-1 text-token-text-tertiary">
@ -85,6 +103,18 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
: conversation?.greeting ?? localize('com_nav_welcome_message')}
</h2>
)}
<div className="mt-8 flex flex-wrap justify-center gap-3 px-4">
{conversation_starters.length > 0 &&
conversation_starters
.slice(0, Constants.MAX_CONVO_STARTERS)
.map((text, index) => (
<ConvoStarter
key={index}
text={text}
onClick={() => sendConversationStarter(text)}
/>
))}
</div>
</div>
</div>
</Tooltip>

View file

@ -3,7 +3,7 @@ import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
export default function PromptCard({ promptGroup }: { promptGroup: TPromptGroup }) {
return (
<div className="hover:bg-token-main-surface-secondary relative flex w-40 cursor-pointer flex-col gap-2 rounded-2xl border px-3 pb-4 pt-3 text-start align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition transition-colors duration-300 ease-in-out fade-in hover:bg-slate-100 dark:border-gray-600 dark:hover:bg-gray-700">
<div className="hover:bg-token-main-surface-secondary relative flex w-40 cursor-pointer flex-col gap-2 rounded-2xl border px-3 pb-4 pt-3 text-start align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-colors duration-300 ease-in-out fade-in hover:bg-slate-100 dark:border-gray-600 dark:hover:bg-gray-700">
<div className="">
<CategoryIcon className="size-4" category={promptGroup.category || ''} />
</div>

View file

@ -1,62 +0,0 @@
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type { ReactNode } from 'react';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { TooltipProvider, Tooltip } from '~/components/ui';
import ConvoIcon from '~/components/Endpoints/ConvoIcon';
import { getIconEndpoint, cn } from '~/utils';
import Prompts from './Prompts';
export default function Landing({ Header }: { Header?: ReactNode }) {
const { conversation } = useChatContext();
const assistantMap = useAssistantsMapContext();
const { data: endpointsConfig } = useGetEndpointsQuery();
let { endpoint = '' } = conversation ?? {};
const { assistant_id = null } = conversation ?? {};
if (
endpoint === EModelEndpoint.chatGPTBrowser ||
endpoint === EModelEndpoint.azureOpenAI ||
endpoint === EModelEndpoint.gptPlugins
) {
endpoint = EModelEndpoint.openAI;
}
const iconURL = conversation?.iconURL;
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
const isAssistant = isAssistantsEndpoint(endpoint);
const assistant = isAssistant && assistantMap?.[endpoint]?.[assistant_id ?? ''];
const assistantName = (assistant && assistant?.name) || '';
const avatar = (assistant && (assistant?.metadata?.avatar as string)) || '';
const containerClassName =
'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black';
return (
<TooltipProvider delayDuration={50}>
<Tooltip>
<div className="relative h-full">
<div className="absolute left-0 right-0">{Header && Header}</div>
<div className="flex h-full flex-col items-center justify-center">
<div className={cn('relative h-12 w-12', assistantName && avatar ? 'mb-0' : 'mb-3')}>
<ConvoIcon
conversation={conversation}
assistantMap={assistantMap}
endpointsConfig={endpointsConfig}
containerClassName={containerClassName}
context="landing"
className="h-2/3 w-2/3"
size={41}
/>
</div>
<div className="h-3/5">
<Prompts />
</div>
</div>
</div>
</Tooltip>
</TooltipProvider>
);
}

View file

@ -149,10 +149,10 @@ export default function Conversation({
/>
<div className="flex gap-1">
<button onClick={cancelRename}>
<X className="transition-color h-4 w-4 duration-200 ease-in-out hover:opacity-70" />
<X className="transition-colors h-4 w-4 duration-200 ease-in-out hover:opacity-70" />
</button>
<button onClick={onRename}>
<Check className="transition-color h-4 w-4 duration-200 ease-in-out hover:opacity-70" />
<Check className="transition-colors h-4 w-4 duration-200 ease-in-out hover:opacity-70" />
</button>
</div>
</div>

View file

@ -13,7 +13,7 @@ import type {
ValidationResult,
AssistantsEndpoint,
} from 'librechat-data-provider';
import type { ActionAuthForm } from '~/common';
import type { ActionAuthForm, ActionWithNullableMetadata } from '~/common';
import type { Spec } from './ActionsTable';
import { useAssistantsMapContext, useToastContext } from '~/Providers';
import { ActionsTable, columns } from './ActionsTable';
@ -37,7 +37,7 @@ export default function ActionsInput({
version,
setAction,
}: {
action?: Action;
action?: ActionWithNullableMetadata;
assistant_id?: string;
endpoint: AssistantsEndpoint;
version: number | string;
@ -62,12 +62,13 @@ export default function ActionsInput({
const [functions, setFunctions] = useState<FunctionTool[] | null>(null);
useEffect(() => {
if (!action?.metadata.raw_spec) {
const rawSpec = action?.metadata?.raw_spec ?? '';
if (!rawSpec) {
return;
}
setInputValue(action.metadata.raw_spec);
debouncedValidation(action.metadata.raw_spec, handleResult);
}, [action?.metadata.raw_spec]);
setInputValue(rawSpec);
debouncedValidation(rawSpec, handleResult);
}, [action?.metadata?.raw_spec]);
useEffect(() => {
if (!validationResult || !validationResult.status || !validationResult.spec) {
@ -100,7 +101,8 @@ export default function ActionsInput({
},
onError(error) {
showToast({
message: (error as Error).message ?? localize('com_assistants_update_actions_error'),
message:
(error as Error | undefined)?.message ?? localize('com_assistants_update_actions_error'),
status: 'error',
});
},
@ -108,7 +110,8 @@ export default function ActionsInput({
const saveAction = handleSubmit((authFormData) => {
console.log('authFormData', authFormData);
if (!assistant_id) {
const currentAssistantId = assistant_id ?? '';
if (!currentAssistantId) {
// alert user?
return;
}
@ -121,7 +124,10 @@ export default function ActionsInput({
return;
}
let { metadata = {} } = action ?? {};
let { metadata } = action ?? {};
if (!metadata) {
metadata = {};
}
const action_id = action?.action_id;
metadata.raw_spec = inputValue;
const parsedUrl = new URL(data[0].domain);
@ -177,10 +183,10 @@ export default function ActionsInput({
action_id,
metadata,
functions,
assistant_id,
assistant_id: currentAssistantId,
endpoint,
version,
model: assistantMap?.[endpoint][assistant_id].model ?? '',
model: assistantMap?.[endpoint][currentAssistantId].model ?? '',
});
});

View file

@ -40,7 +40,8 @@ export default function ActionsPanel({
},
onError(error) {
showToast({
message: (error as Error)?.message ?? localize('com_assistants_delete_actions_error'),
message:
(error as Error | undefined)?.message ?? localize('com_assistants_delete_actions_error'),
status: 'error',
});
},
@ -127,7 +128,7 @@ export default function ActionsPanel({
<div className="absolute right-0 top-6">
<button
type="button"
disabled={!assistant_id || !action.action_id}
disabled={!(assistant_id ?? '') || !action.action_id}
className="btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium"
>
<TrashIcon className="text-red-500" />
@ -145,16 +146,17 @@ export default function ActionsPanel({
}
selection={{
selectHandler: () => {
if (!assistant_id) {
const currentId = assistant_id ?? '';
if (!currentId) {
return showToast({
message: 'No assistant_id found, is the assistant created?',
status: 'error',
});
}
deleteAction.mutate({
model: assistantMap[endpoint][assistant_id].model,
model: assistantMap?.[endpoint][currentId].model ?? '',
action_id: action.action_id,
assistant_id,
assistant_id: currentId,
endpoint,
});
},

View file

@ -28,7 +28,7 @@ export default function AssistantAction({
{isHovering && (
<button
type="button"
className="transition-color flex h-9 w-9 min-w-9 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
className="transition-colors flex h-9 w-9 min-w-9 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
>
<GearIcon className="icon-sm" />
</button>

View file

@ -0,0 +1,169 @@
import React, { useRef, useState } from 'react';
import { Plus, X } from 'lucide-react';
import { Transition } from 'react-transition-group';
import { Constants } from 'librechat-data-provider';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '~/components/ui';
import { useLocalize } from '~/hooks';
interface AssistantConversationStartersProps {
field: {
value: string[];
onChange: (value: string[]) => void;
};
inputClass: string;
labelClass: string;
}
const AssistantConversationStarters: React.FC<AssistantConversationStartersProps> = ({
field,
inputClass,
labelClass,
}) => {
const localize = useLocalize();
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const nodeRef = useRef(null);
const [newStarter, setNewStarter] = useState('');
const handleAddStarter = () => {
if (newStarter.trim() && field.value.length < Constants.MAX_CONVO_STARTERS) {
const newValues = [newStarter, ...field.value];
field.onChange(newValues);
setNewStarter('');
}
};
const handleDeleteStarter = (index: number) => {
const newValues = field.value.filter((_, i) => i !== index);
field.onChange(newValues);
};
const defaultStyle = {
transition: 'opacity 200ms ease-in-out',
opacity: 0,
};
const triggerShake = (element: HTMLElement) => {
element.classList.remove('shake');
void element.offsetWidth;
element.classList.add('shake');
setTimeout(() => {
element.classList.remove('shake');
}, 200);
};
const transitionStyles = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 },
};
const hasReachedMax = field.value.length >= Constants.MAX_CONVO_STARTERS;
return (
<div className="relative">
<label className={labelClass} htmlFor="conversation_starters">
{localize('com_assistants_conversation_starters')}
</label>
<div className="mt-4 space-y-2">
{/* Persistent starter, used for creating only */}
<div className="relative">
<input
ref={(el) => (inputRefs.current[0] = el)}
value={newStarter}
maxLength={64}
className={`${inputClass} pr-10`}
type="text"
placeholder={
hasReachedMax
? localize('com_assistants_max_starters_reached')
: localize('com_assistants_conversation_starters_placeholder')
}
onChange={(e) => setNewStarter(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (hasReachedMax) {
triggerShake(e.currentTarget);
} else {
handleAddStarter();
}
}
}}
/>
<Transition
nodeRef={nodeRef}
in={field.value.length < Constants.MAX_CONVO_STARTERS}
timeout={200}
unmountOnExit
>
{(state: string) => (
<div
ref={nodeRef}
style={{
...defaultStyle,
...transitionStyles[state as keyof typeof transitionStyles],
transition: state === 'entering' ? 'none' : defaultStyle.transition,
}}
className="absolute right-1 top-1"
>
<TooltipProvider delayDuration={1000}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={handleAddStarter}
disabled={hasReachedMax}
>
<Plus className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{hasReachedMax
? localize('com_assistants_max_starters_reached')
: localize('com_ui_add')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
</Transition>
</div>
{field.value.map((starter, index) => (
<div key={index} className="relative">
<input
ref={(el) => (inputRefs.current[index + 1] = el)}
value={starter}
onChange={(e) => {
const newValue = [...field.value];
newValue[index] = e.target.value;
field.onChange(newValue);
}}
className={`${inputClass} pr-10`}
type="text"
maxLength={64}
/>
<TooltipProvider delayDuration={1000}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="absolute right-1 top-1 flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={() => handleDeleteStarter(index)}
>
<X className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={0}>
{localize('com_ui_delete')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
))}
</div>
</div>
);
};
export default AssistantConversationStarters;

View file

@ -14,6 +14,7 @@ import type { FunctionTool, TConfig, TPlugin } from 'librechat-data-provider';
import type { AssistantForm, AssistantPanelProps } from '~/common';
import { useCreateAssistantMutation, useUpdateAssistantMutation } from '~/data-provider';
import { cn, cardStyle, defaultTextProps, removeFocusOutlines } from '~/utils';
import AssistantConversationStarters from './AssistantConversationStarters';
import { useAssistantsMapContext, useToastContext } from '~/Providers';
import { useSelectAssistant, useLocalize } from '~/hooks';
import { ToolSelectDialog } from '~/components/Tools';
@ -31,7 +32,7 @@ import { Panel } from '~/common';
const labelClass = 'mb-2 text-token-text-primary block font-medium';
const inputClass = cn(
defaultTextProps,
'flex w-full px-3 py-2 dark:border-gray-800 dark:bg-gray-800',
'flex w-full px-3 py-2 dark:border-gray-800 dark:bg-gray-800 rounded-xl mb-2',
removeFocusOutlines,
);
@ -106,6 +107,7 @@ export default function AssistantPanel({
});
},
});
const create = useCreateAssistantMutation({
onSuccess: (data) => {
setCurrentAssistantId(data.id);
@ -139,7 +141,7 @@ export default function AssistantPanel({
return functionName;
} else {
const assistant = assistantMap?.[endpoint]?.[assistant_id];
const tool = assistant?.tools.find((tool) => tool.function?.name === functionName);
const tool = assistant?.tools?.find((tool) => tool.function?.name === functionName);
if (assistant && tool) {
return tool;
}
@ -148,7 +150,6 @@ export default function AssistantPanel({
return functionName;
});
console.log(data);
if (data.code_interpreter) {
tools.push({ type: Tools.code_interpreter });
}
@ -163,6 +164,7 @@ export default function AssistantPanel({
name,
description,
instructions,
conversation_starters: starters,
model,
// file_ids, // TODO: add file handling here
} = data;
@ -174,6 +176,7 @@ export default function AssistantPanel({
name,
description,
instructions,
conversation_starters: starters.filter((starter) => starter.trim() !== ''),
model,
tools,
endpoint,
@ -186,6 +189,7 @@ export default function AssistantPanel({
name,
description,
instructions,
conversation_starters: starters.filter((starter) => starter.trim() !== ''),
model,
tools,
endpoint,
@ -239,12 +243,12 @@ export default function AssistantPanel({
</button>
)}
</div>
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
<div className="bg-surface-50 h-auto px-4 pb-8 pt-3 dark:bg-transparent">
{/* Avatar & Name */}
<div className="mb-4">
<AssistantAvatar
createMutation={create}
assistant_id={assistant_id ?? null}
assistant_id={assistant_id}
metadata={assistant['metadata'] ?? null}
endpoint={endpoint}
version={version}
@ -271,7 +275,7 @@ export default function AssistantPanel({
name="id"
control={control}
render={({ field }) => (
<p className="h-3 text-xs italic text-text-secondary">{field.value ?? ''}</p>
<p className="h-3 text-xs italic text-text-secondary">{field.value}</p>
)}
/>
</div>
@ -318,6 +322,23 @@ export default function AssistantPanel({
)}
/>
</div>
{/* Conversation Starters */}
<div className="relative mb-6">
{/* the label of conversation starters is in the component */}
<Controller
name="conversation_starters"
control={control}
defaultValue={[]}
render={({ field }) => (
<AssistantConversationStarters
field={field}
inputClass={inputClass}
labelClass={labelClass}
/>
)}
/>
</div>
{/* Model */}
<div className="mb-6">
<label className={labelClass} htmlFor="model">

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);

View file

@ -78,7 +78,7 @@ export default function AssistantTool({
<OGDialogTrigger asChild>
<button
type="button"
className="transition-color flex h-9 w-9 min-w-9 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
className="transition-colors flex h-9 w-9 min-w-9 items-center justify-center rounded-lg duration-200 hover:bg-gray-200 dark:hover:bg-gray-700"
>
<TrashIcon />
</button>
@ -97,7 +97,7 @@ export default function AssistantTool({
selection={{
selectHandler: () => removeTool(currentTool.pluginKey),
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-colors duration-200 text-white',
selectText: localize('com_ui_delete'),
}}
/>

View file

@ -39,11 +39,11 @@ function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled }: ToolItemProps)
{!isInstalled ? (
<button
className="btn btn-primary relative"
aria-label={`${localize('com_nav_tool_add')} ${tool.name}`}
aria-label={`${localize('com_ui_add')} ${tool.name}`}
onClick={handleClick}
>
<div className="flex w-full items-center justify-center gap-2">
{localize('com_nav_tool_add')}
{localize('com_ui_add')}
<PlusCircleIcon className="flex h-4 w-4 items-center stroke-2" />
</div>
</button>

View file

@ -622,7 +622,7 @@ export const useUploadFileMutation = (
const update = {};
if (!tool_resource) {
update['file_ids'] = [...assistant.file_ids, data.file_id];
update['file_ids'] = [...(assistant.file_ids ?? []), data.file_id];
}
if (tool_resource === EToolResources.code_interpreter) {
const prevResources = assistant.tool_resources ?? {};
@ -884,6 +884,24 @@ export const useUpdateAssistantMutation = (
return options?.onSuccess?.(updatedAssistant, variables, context);
}
queryClient.setQueryData<t.AssistantDocument[]>(
[QueryKeys.assistantDocs, variables.data.endpoint],
(prev) => {
if (!prev) {
return prev;
}
prev.map((doc) => {
if (doc.assistant_id === variables.assistant_id) {
return {
...doc,
conversation_starters: updatedAssistant.conversation_starters,
};
}
return doc;
});
},
);
queryClient.setQueryData<t.AssistantListResponse>(
[QueryKeys.assistants, variables.data.endpoint, defaultOrderQuery],
{
@ -1070,7 +1088,7 @@ export const useDeleteAction = (
if (assistant.id === variables.assistant_id) {
return {
...assistant,
tools: assistant.tools.filter(
tools: (assistant.tools ?? []).filter(
(tool) => !tool.function?.name.includes(domain ?? ''),
),
};

View file

@ -249,8 +249,8 @@ export const useListAssistantsQuery = <TData = AssistantListResponse>(
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const userProvidesKey = !!(endpointsConfig?.[endpoint]?.userProvide ?? false);
const keyProvided = userProvidesKey ? !!(keyExpiry?.expiresAt ?? '') : true;
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return useQuery<AssistantListResponse, unknown, TData>(
@ -368,22 +368,24 @@ export const useGetActionsQuery = <TData = Action[]>(
},
);
};
/**
* Hook for retrieving user's saved Assistant Documents (metadata saved to Database)
*/
export const useGetAssistantDocsQuery = (
endpoint: t.AssistantsEndpoint,
config?: UseQueryOptions<AssistantDocument[]>,
): QueryObserverResult<AssistantDocument[], unknown> => {
export const useGetAssistantDocsQuery = <TData = AssistantDocument[]>(
endpoint: t.AssistantsEndpoint | string,
config?: UseQueryOptions<AssistantDocument[], unknown, TData>,
): QueryObserverResult<TData> => {
const queryClient = useQueryClient();
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const keyExpiry = queryClient.getQueryData<TCheckUserKeyResponse>([QueryKeys.name, endpoint]);
const userProvidesKey = !!endpointsConfig?.[endpoint]?.userProvide;
const keyProvided = userProvidesKey ? !!keyExpiry?.expiresAt : true;
const userProvidesKey = !!(endpointsConfig?.[endpoint]?.userProvide ?? false);
const keyProvided = userProvidesKey ? !!(keyExpiry?.expiresAt ?? '') : true;
const enabled = !!endpointsConfig?.[endpoint] && keyProvided;
const version = endpointsConfig?.[endpoint]?.version ?? defaultAssistantsVersion[endpoint];
return useQuery<AssistantDocument[]>(
[QueryKeys.assistantDocs],
return useQuery<AssistantDocument[], unknown, TData>(
[QueryKeys.assistantDocs, endpoint],
() =>
dataService.getAssistantDocs({
endpoint,

View file

@ -549,7 +549,7 @@ export default {
com_nav_change_picture: 'تغيير الصورة',
com_nav_plugin_install: 'تثبيت',
com_nav_plugin_uninstall: 'إلغاء تثبيت',
com_nav_tool_add: 'إضافة',
com_ui_add: 'إضافة',
com_nav_tool_remove: 'إزالة',
com_nav_tool_dialog: 'أدوات المساعد',
com_nav_tool_dialog_description: 'يجب حفظ المساعد لإبقاء اختيارات الأدوات.',
@ -2594,7 +2594,7 @@ export const comparisons = {
english: 'Uninstall',
translated: 'إلغاء تثبيت',
},
com_nav_tool_add: {
com_ui_add: {
english: 'Add',
translated: 'إضافة',
},

View file

@ -425,7 +425,7 @@ export default {
com_nav_plugin_store: 'Loja de plugins',
com_nav_plugin_install: 'Instalar',
com_nav_plugin_uninstall: 'Desinstalar',
com_nav_tool_add: 'Adicionar',
com_ui_add: 'Adicionar',
com_nav_tool_remove: 'Remover',
com_nav_tool_dialog: 'Ferramentas do Assistente',
com_nav_tool_dialog_description:
@ -2021,7 +2021,7 @@ export const comparisons = {
english: 'Uninstall',
translated: 'Desinstalar',
},
com_nav_tool_add: {
com_ui_add: {
english: 'Add',
translated: 'Adicionar',
},

View file

@ -216,7 +216,7 @@ export default {
com_ui_fork_from_message: 'Wähle eine Abzweigungsoption',
com_ui_mention:
'Erwähne einen Endpunkt, Assistenten oder eine Voreinstellung, um schnell dorthin zu wechseln',
com_ui_add: 'Ein KI-Modell oder eine Voreinstellung für eine zusätzliche Antwort hinzufügen',
com_ui_add_model_preset: 'Ein KI-Modell oder eine Voreinstellung für eine zusätzliche Antwort hinzufügen',
com_ui_regenerate: 'Neu generieren',
com_ui_continue: 'Fortfahren',
com_ui_edit: 'Bearbeiten',
@ -592,7 +592,7 @@ export default {
com_nav_plugin_store: 'Plugin-Store',
com_nav_plugin_install: 'Installieren',
com_nav_plugin_uninstall: 'Deinstallieren',
com_nav_tool_add: 'Hinzufügen',
com_ui_add: 'Hinzufügen',
com_nav_tool_remove: 'Entfernen',
com_nav_tool_dialog: 'Assistenten-Werkzeuge',
com_ui_misc: 'Sonstiges',
@ -2283,7 +2283,7 @@ export const comparisons = {
english: 'Uninstall',
translated: 'Deinstallieren',
},
com_nav_tool_add: {
com_ui_add: {
english: 'Add',
translated: 'Hinzufügen',
},

View file

@ -78,6 +78,8 @@ export default {
com_assistants_update_error: 'There was an error updating your assistant.',
com_assistants_create_success: 'Successfully created',
com_assistants_create_error: 'There was an error creating your assistant.',
com_assistants_conversation_starters: 'Conversation Starters',
com_assistants_conversation_starters_placeholder: 'Enter a conversation starter',
com_ui_date_today: 'Today',
com_ui_date_yesterday: 'Yesterday',
com_ui_date_previous_7_days: 'Previous 7 days',
@ -221,7 +223,8 @@ export default {
com_ui_fork_visible: 'Visible messages only',
com_ui_fork_from_message: 'Select a fork option',
com_ui_mention: 'Mention an endpoint, assistant, or preset to quickly switch to it',
com_ui_add: 'Add a model or preset for an additional response',
com_ui_add_model_preset: 'Add a model or preset for an additional response',
com_assistants_max_starters_reached: 'Max number of conversation starters reached',
com_ui_regenerate: 'Regenerate',
com_ui_continue: 'Continue',
com_ui_edit: 'Edit',
@ -603,7 +606,7 @@ export default {
com_nav_plugin_store: 'Plugin store',
com_nav_plugin_install: 'Install',
com_nav_plugin_uninstall: 'Uninstall',
com_nav_tool_add: 'Add',
com_ui_add: 'Add',
com_nav_tool_remove: 'Remove',
com_nav_tool_dialog: 'Assistant Tools',
com_ui_misc: 'Misc.',

View file

@ -431,7 +431,7 @@ export default {
com_nav_plugin_store: 'Tienda de plugins',
com_nav_plugin_install: 'Instalar',
com_nav_plugin_uninstall: 'Desinstalar',
com_nav_tool_add: 'Agregar',
com_ui_add: 'Agregar',
com_nav_tool_remove: 'Eliminar',
com_nav_tool_dialog: 'Herramientas del asistente',
com_nav_tool_dialog_description:
@ -2153,7 +2153,7 @@ export const comparisons = {
english: 'Uninstall',
translated: 'Desinstalar',
},
com_nav_tool_add: {
com_ui_add: {
english: 'Add',
translated: 'Agregar',
},

View file

@ -202,7 +202,7 @@ export default {
com_ui_fork_visible: 'Vain näkyvät viestit',
com_ui_fork_from_message: 'Valitse haarautustapa',
com_ui_mention: 'Mainitse päätepiste, Avustaja tai asetus vaihtaaksesi siihen pikana',
com_ui_add: 'Lisää malli tai esiasetus lisävastausta varten',
com_ui_add_model_preset: 'Lisää malli tai esiasetus lisävastausta varten',
com_ui_regenerate: 'Luo uudestaan',
com_ui_continue: 'Jatka',
com_ui_edit: 'Muokkaa',
@ -571,7 +571,7 @@ export default {
com_nav_plugin_store: 'Lisäosakauppa',
com_nav_plugin_install: 'Asenna',
com_nav_plugin_uninstall: 'Poista',
com_nav_tool_add: 'Lisää',
com_ui_add: 'Lisää',
com_nav_tool_remove: 'Poista',
com_nav_tool_dialog: 'Avustajatyökalut',
com_ui_misc: 'Muu',

View file

@ -418,7 +418,7 @@ export default {
com_ui_date_december: 'Décembre',
com_ui_nothing_found: 'Aucun résultat trouvé',
com_ui_go_to_conversation: 'Aller à la conversation',
com_nav_tool_add: 'Ajouter',
com_ui_add: 'Ajouter',
com_nav_tool_remove: 'Supprimer',
com_nav_tool_dialog: 'Outils de l\'assistant',
com_nav_tool_dialog_description:
@ -624,7 +624,7 @@ export default {
com_ui_upload_invalid_var:
'Fichier non valide pour le téléchargement. L\'image ne doit pas dépasser {0} Mo',
com_ui_read_aloud: 'Lire à haute voix',
com_ui_add: 'Ajouter un modèle ou un préréglage pour une réponse supplémentaire',
com_ui_add_model_preset: 'Ajouter un modèle ou un préréglage pour une réponse supplémentaire',
com_ui_loading: 'Chargement...',
com_ui_all_proper: 'Tout',
com_ui_chat: 'Discussion',
@ -2208,7 +2208,7 @@ export const comparisons = {
english: 'Go to conversation',
translated: 'Aller à la conversation',
},
com_nav_tool_add: {
com_ui_add: {
english: 'Add',
translated: 'Ajouter',
},

View file

@ -354,7 +354,7 @@ export default {
com_nav_plugin_store: 'חנות פלאגין',
com_nav_plugin_install: 'התקן',
com_nav_plugin_uninstall: 'הסר התקנה',
com_nav_tool_add: 'הוסף',
com_ui_add: 'הוסף',
com_nav_tool_remove: 'הסר',
com_nav_tool_dialog: 'כלי סייען',
com_nav_tool_dialog_description: 'יש לשמור את האסיסטנט כדי להמשיך בבחירת הכלים.',
@ -1728,7 +1728,7 @@ export const comparisons = {
english: 'Uninstall',
translated: 'הסר התקנה',
},
com_nav_tool_add: {
com_ui_add: {
english: 'Add',
translated: 'הוסף',
},

View file

@ -483,7 +483,7 @@ export default {
com_nav_plugin_store: 'Store plugin',
com_nav_plugin_install: 'Installa',
com_nav_plugin_uninstall: 'Disinstalla',
com_nav_tool_add: 'Aggiungi',
com_ui_add: 'Aggiungi',
com_nav_tool_remove: 'Rimuovi',
com_nav_tool_dialog: 'Strumenti Assistente',
com_nav_tool_dialog_description:
@ -2326,7 +2326,7 @@ export const comparisons = {
english: 'Uninstall',
translated: 'Disinstalla',
},
com_nav_tool_add: {
com_ui_add: {
english: 'Add',
translated: 'Aggiungi',
},

View file

@ -431,7 +431,7 @@ export default {
com_nav_plugin_store: 'プラグインストア',
com_nav_plugin_install: 'インストール',
com_nav_plugin_uninstall: 'アンインストール',
com_nav_tool_add: '追加',
com_ui_add: '追加',
com_nav_tool_dialog: 'アシスタントツール',
com_nav_tool_dialog_description:
'ツールの選択を維持するには、アシスタントを保存する必要があります。',
@ -2165,7 +2165,7 @@ export const comparisons = {
english: 'Uninstall',
translated: 'アンインストール',
},
com_nav_tool_add: {
com_ui_add: {
english: 'Add',
translated: '追加',
},

View file

@ -544,7 +544,7 @@ export default {
com_nav_change_picture: '프로필 사진 변경',
com_nav_plugin_install: '플러그인 설치',
com_nav_plugin_uninstall: '플러그인 제거',
com_nav_tool_add: '추가',
com_ui_add: '추가',
com_nav_tool_remove: '제거',
com_nav_tool_dialog: '어시스턴트 도구',
com_nav_tool_dialog_description: 'Assistant를 저장해야 도구 선택이 유지됩니다.',
@ -2595,7 +2595,7 @@ export const comparisons = {
english: 'Uninstall',
translated: '플러그인 제거',
},
com_nav_tool_add: {
com_ui_add: {
english: 'Add',
translated: '추가',
},

View file

@ -567,7 +567,7 @@ export default {
com_nav_welcome_assistant: 'Выберите ассистента',
com_nav_plugin_install: 'Установить',
com_nav_plugin_uninstall: 'Удалить',
com_nav_tool_add: 'Добавить',
com_ui_add: 'Добавить',
com_nav_tool_remove: 'Удалить',
com_nav_tool_dialog: 'Инструменты ассистента',
com_nav_tool_dialog_description:
@ -2656,7 +2656,7 @@ export const comparisons = {
english: 'Uninstall',
translated: 'Удалить',
},
com_nav_tool_add: {
com_ui_add: {
english: 'Add',
translated: 'Добавить',
},

View file

@ -530,7 +530,7 @@ export default {
com_nav_plugin_store: 'Eklenti mağazası',
com_nav_plugin_install: 'Yükle',
com_nav_plugin_uninstall: 'Kaldır',
com_nav_tool_add: 'Ekle',
com_ui_add: 'Ekle',
com_nav_tool_remove: 'Kaldır',
com_nav_tool_dialog: 'Asistan Araçları',
com_nav_tool_dialog_description: 'Araç seçimlerinin kalıcı olması için asistan kaydedilmelidir.',

View file

@ -392,7 +392,7 @@ export default {
com_nav_plugin_store: '插件商店',
com_nav_plugin_install: '安装',
com_nav_plugin_uninstall: '卸载',
com_nav_tool_add: '添加',
com_ui_add: '添加',
com_nav_tool_remove: '移除',
com_nav_tool_dialog: '助手工具',
com_nav_tool_dialog_description: '必须保存助手才能保留工具选择。',
@ -2071,7 +2071,7 @@ export const comparisons = {
english: 'Uninstall',
translated: '卸载',
},
com_nav_tool_add: {
com_ui_add: {
english: 'Add',
translated: '添加',
},

View file

@ -522,7 +522,7 @@ export default {
com_nav_change_picture: '更換圖片',
com_nav_plugin_install: '安裝',
com_nav_plugin_uninstall: '解除安裝',
com_nav_tool_add: '新增',
com_ui_add: '新增',
com_nav_tool_remove: '移除',
com_nav_tool_dialog: 'AI 工具',
com_nav_tool_dialog_description: '必須儲存 Assistant 才能保留工具選擇。',
@ -2563,7 +2563,7 @@ export const comparisons = {
english: 'Uninstall',
translated: '解除安裝',
},
com_nav_tool_add: {
com_ui_add: {
english: 'Add',
translated: '新增',
},

View file

@ -1355,7 +1355,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Uninstall
- **translated**: Desinstalar
- **com_nav_tool_add**:
- **com_ui_add**:
- **english**: Add
- **translated**: Adicionar

View file

@ -1379,7 +1379,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Uninstall
- **translated**: Deinstallieren
- **com_nav_tool_add**:
- **com_ui_add**:
- **english**: Add
- **translated**: Hinzufügen

View file

@ -1355,7 +1355,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Uninstall
- **translated**: Desinstalar
- **com_nav_tool_add**:
- **com_ui_add**:
- **english**: Add
- **translated**: Agregar

View file

@ -1155,7 +1155,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Uninstall
- **translated**: הסר התקנה
- **com_nav_tool_add**:
- **com_ui_add**:
- **english**: Add
- **translated**: הוסף

View file

@ -1507,7 +1507,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Uninstall
- **translated**: Disinstalla
- **com_nav_tool_add**:
- **com_ui_add**:
- **english**: Add
- **translated**: Aggiungi

View file

@ -1407,7 +1407,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Uninstall
- **translated**: アンインストール
- **com_nav_tool_add**:
- **com_ui_add**:
- **english**: Add
- **translated**: 追加

View file

@ -1359,7 +1359,7 @@ Write a prompt that is mindful of the nuances in the language with respect to it
- **english**: Uninstall
- **translated**: 卸载
- **com_nav_tool_add**:
- **com_ui_add**:
- **english**: Add
- **translated**: 添加

View file

@ -328,3 +328,13 @@
.sp-wrapper {
@apply flex h-full w-full grow flex-col justify-center;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-3px); }
20%, 40%, 60%, 80% { transform: translateX(3px); }
}
.shake {
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
}

View file

@ -1172,7 +1172,7 @@ button {
.btn-neutral {
--tw-bg-opacity: 1;
--tw-text-opacity: 1;
background-color: rgba(255, 255, 255, var(--tw-bg-opacity));
background-color: var(--surface-secondary);
border-color: rgba(0, 0, 0, 0.1);
border-width: 1px;
color: rgba(64, 65, 79, var(--tw-text-opacity));

View file

@ -60,13 +60,13 @@ export const cardStyle =
'transition-colors rounded-md min-w-[75px] border font-normal bg-white hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-700 dark:bg-gray-800 text-black dark:text-gray-600 focus:outline-none data-[state=open]:bg-gray-50 dark:data-[state=open]:bg-gray-700';
export const defaultTextProps =
'rounded-md border border-gray-200 focus:border-gray-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none focus-within:placeholder:text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:focus:bg-gray-600 dark:focus:border-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:outline-none';
'rounded-md border border-gray-200 focus:border-gray-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none focus-within:placeholder:text-text-primary focus:placeholder:text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:focus:bg-gray-600 dark:focus:border-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:outline-none';
export const optionText =
'p-0 shadow-none text-right pr-1 h-8 border-transparent hover:bg-gray-800/10 dark:hover:bg-white/10 dark:focus:bg-white/10 transition-colors';
export const defaultTextPropsLabel =
'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none focus-within:placeholder:text-text-primary placeholder:text-text-secondary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-600 dark:focus:outline-none';
'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none focus-within:placeholder:text-text-primary focus:placeholder:text-text-primary placeholder:text-text-secondary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-600 dark:focus:outline-none';
export function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);

1
package-lock.json generated
View file

@ -30357,6 +30357,7 @@
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",

View file

@ -972,6 +972,8 @@ export enum Constants {
DEFAULT_STREAM_RATE = 1,
/** Saved Tag */
SAVED_TAG = 'Saved',
/** Max number of Conversation starters for Agents/Assistants */
MAX_CONVO_STARTERS = 4,
}
export enum LocalStorageKeys {

View file

@ -255,14 +255,18 @@ export function getAssistantDocs({
endpoint,
version,
}: {
endpoint: s.AssistantsEndpoint;
endpoint: s.AssistantsEndpoint | string;
version: number | string;
}): Promise<a.AssistantDocument[]> {
if (!s.isAssistantsEndpoint(endpoint)) {
return Promise.resolve([]);
}
return request.get(
endpoints.assistants({
path: 'documents',
version,
endpoint,
options: { endpoint },
endpoint: endpoint as s.AssistantsEndpoint,
}),
);
}

View file

@ -61,6 +61,7 @@ export const defaultAssistantFormValues = {
name: '',
description: '',
instructions: '',
conversation_starters: [],
model: '',
functions: [],
code_interpreter: false,

View file

@ -70,13 +70,14 @@ export type Assistant = {
id: string;
created_at: number;
description: string | null;
file_ids: string[];
file_ids?: string[];
instructions: string | null;
conversation_starters?: string[];
metadata: Metadata | null;
model: string;
name: string | null;
object: string;
tools: FunctionTool[];
tools?: FunctionTool[];
tool_resources?: ToolResources;
};
@ -87,6 +88,7 @@ export type AssistantCreateParams = {
description?: string | null;
file_ids?: string[];
instructions?: string | null;
conversation_starters?: string[];
metadata?: Metadata | null;
name?: string | null;
tools?: Array<FunctionTool | string>;
@ -99,6 +101,7 @@ export type AssistantUpdateParams = {
description?: string | null;
file_ids?: string[];
instructions?: string | null;
conversation_starters?: string[] | null;
metadata?: Metadata | null;
name?: string | null;
tools?: Array<FunctionTool | string>;
@ -392,6 +395,7 @@ export type AssistantAvatar = {
export type AssistantDocument = {
user: string;
assistant_id: string;
conversation_starters?: string[];
avatar?: AssistantAvatar;
access_level?: number;
file_ids?: string[];