💬 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 {string} searchParams.user - The user ID of the assistant's author.
* @param {Object} updateData - An object containing the properties to update. * @param {Object} updateData - An object containing the properties to update.
* @param {mongoose.ClientSession} [session] - The transaction session to use (optional). * @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 updateAssistantDoc = async (searchParams, updateData, session = null) => {
const options = { new: true, upsert: true, session }; 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 {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.assistant_id - The ID of the assistant to update.
* @param {string} searchParams.user - The user ID of the assistant's author. * @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(); 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. * Retrieves all assistants that match the given search parameters.
* *
* @param {Object} searchParams - The search parameters to find matching assistants. * @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) => { const getAssistants = async (searchParams, select = null) => {
return await Assistant.find(searchParams).lean(); 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, default: undefined,
}, },
conversation_starters: {
type: [String],
default: [],
},
access_level: { access_level: {
type: Number, 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 {object} params.res - The response object, used for initializing the client.
* @param {string} params.version - The API version to use. * @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). * @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 }) => { const listAllAssistants = async ({ req, res, version, query }) => {
/** @type {{ openai: OpenAIClient }} */ /** @type {{ openai: OpenAIClient }} */

View file

@ -18,7 +18,9 @@ const createAssistant = async (req, res) => {
try { try {
const { openai } = await getOpenAIClient({ req, res }); 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 assistantData.tools = tools
.map((tool) => { .map((tool) => {
if (typeof tool !== 'string') { if (typeof tool !== 'string') {
@ -41,11 +43,22 @@ const createAssistant = async (req, res) => {
}; };
const assistant = await openai.beta.assistants.create(assistantData); 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) { if (azureModelIdentifier) {
assistant.model = azureModelIdentifier; assistant.model = azureModelIdentifier;
} }
await promise;
if (document.conversation_starters) {
assistant.conversation_starters = document.conversation_starters;
}
logger.debug('/assistants/', assistant); logger.debug('/assistants/', assistant);
res.status(201).json(assistant); res.status(201).json(assistant);
} catch (error) { } catch (error) {
@ -88,7 +101,7 @@ const patchAssistant = async (req, res) => {
await validateAuthor({ req, openai }); await validateAuthor({ req, openai });
const assistant_id = req.params.id; const assistant_id = req.params.id;
const { endpoint: _e, ...updateData } = req.body; const { endpoint: _e, conversation_starters, ...updateData } = req.body;
updateData.tools = (updateData.tools ?? []) updateData.tools = (updateData.tools ?? [])
.map((tool) => { .map((tool) => {
if (typeof tool !== 'string') { if (typeof tool !== 'string') {
@ -104,6 +117,15 @@ const patchAssistant = async (req, res) => {
} }
const updatedAssistant = await openai.beta.assistants.update(assistant_id, updateData); 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); res.json(updatedAssistant);
} catch (error) { } catch (error) {
logger.error('[/assistants/:id] Error updating assistant', 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). * Returns a list of the user's assistant documents (metadata saved to database).
* @route GET /assistants/documents * @route GET /assistants/documents
@ -160,7 +208,25 @@ const listAssistants = async (req, res) => {
*/ */
const getAssistantDocuments = async (req, res) => { const getAssistantDocuments = async (req, res) => {
try { 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) { } catch (error) {
logger.error('[/assistants/documents] Error listing assistant documents', error); logger.error('[/assistants/documents] Error listing assistant documents', error);
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });

View file

@ -16,7 +16,9 @@ const createAssistant = async (req, res) => {
/** @type {{ openai: OpenAIClient }} */ /** @type {{ openai: OpenAIClient }} */
const { openai } = await getOpenAIClient({ req, res }); 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 assistantData.tools = tools
.map((tool) => { .map((tool) => {
if (typeof tool !== 'string') { if (typeof tool !== 'string') {
@ -39,11 +41,22 @@ const createAssistant = async (req, res) => {
}; };
const assistant = await openai.beta.assistants.create(assistantData); 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) { if (azureModelIdentifier) {
assistant.model = azureModelIdentifier; assistant.model = azureModelIdentifier;
} }
await promise;
if (document.conversation_starters) {
assistant.conversation_starters = document.conversation_starters;
}
logger.debug('/assistants/', assistant); logger.debug('/assistants/', assistant);
res.status(201).json(assistant); res.status(201).json(assistant);
} catch (error) { } catch (error) {
@ -64,6 +77,17 @@ const createAssistant = async (req, res) => {
const updateAssistant = async ({ req, openai, assistant_id, updateData }) => { const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
await validateAuthor({ req, openai }); await validateAuthor({ req, openai });
const tools = []; 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; let hasFileSearch = false;
for (const tool of updateData.tools ?? []) { for (const tool of updateData.tools ?? []) {
@ -108,7 +132,13 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
updateData.model = openai.locals.azureOptions.azureOpenAIApiDeploymentName; 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 multer = require('multer');
const express = require('express'); const express = require('express');
const controllers = require('~/server/controllers/assistants/v1'); const controllers = require('~/server/controllers/assistants/v1');
const documents = require('./documents');
const actions = require('./actions'); const actions = require('./actions');
const tools = require('./tools'); const tools = require('./tools');
@ -20,6 +21,13 @@ router.use('/actions', actions);
*/ */
router.use('/tools', tools); router.use('/tools', tools);
/**
* Create an assistant.
* @route GET /assistants/documents
* @returns {AssistantDocument[]} 200 - application/json
*/
router.use('/documents', documents);
/** /**
* Create an assistant. * Create an assistant.
* @route POST /assistants * @route POST /assistants
@ -61,13 +69,6 @@ router.delete('/:id', controllers.deleteAssistant);
*/ */
router.get('/', controllers.listAssistants); 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. * Uploads and updates an avatar for a specific assistant.
* @route POST /avatar/:assistant_id * @route POST /avatar/:assistant_id

View file

@ -2,6 +2,7 @@ const multer = require('multer');
const express = require('express'); const express = require('express');
const v1 = require('~/server/controllers/assistants/v1'); const v1 = require('~/server/controllers/assistants/v1');
const v2 = require('~/server/controllers/assistants/v2'); const v2 = require('~/server/controllers/assistants/v2');
const documents = require('./documents');
const actions = require('./actions'); const actions = require('./actions');
const tools = require('./tools'); const tools = require('./tools');
@ -21,6 +22,13 @@ router.use('/actions', actions);
*/ */
router.use('/tools', tools); router.use('/tools', tools);
/**
* Create an assistant.
* @route GET /assistants/documents
* @returns {AssistantDocument[]} 200 - application/json
*/
router.use('/documents', documents);
/** /**
* Create an assistant. * Create an assistant.
* @route POST /assistants * @route POST /assistants
@ -62,13 +70,6 @@ router.delete('/:id', v1.deleteAssistant);
*/ */
router.get('/', v1.listAssistants); 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. * Uploads and updates an avatar for a specific assistant.
* @route POST /avatar/:assistant_id * @route POST /avatar/:assistant_id

View file

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

View file

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

View file

@ -46,7 +46,7 @@ const DeleteBookmarkButton: FC<{
</Label> </Label>
} }
confirm={confirmDelete} 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" />} icon={<TrashIcon className="size-4" />}
tabIndex={tabIndex} tabIndex={tabIndex}
onFocus={onFocus} onFocus={onFocus}

View file

@ -25,7 +25,7 @@ const EditBookmarkButton: FC<{
/> />
<button <button
type="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} tabIndex={tabIndex}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} 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} newConversation={generateConversation}
textAreaRef={textAreaRef} textAreaRef={textAreaRef}
commandChar="+" commandChar="+"
placeholder="com_ui_add" placeholder="com_ui_add_model_preset"
includeAssistants={false} 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 { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui'; import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '~/components/ui';
import { useChatContext, useAssistantsMapContext } from '~/Providers'; import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useGetAssistantDocsQuery } from '~/data-provider';
import ConvoIcon from '~/components/Endpoints/ConvoIcon'; import ConvoIcon from '~/components/Endpoints/ConvoIcon';
import { useLocalize, useSubmitMessage } from '~/hooks';
import { BirthdayIcon } from '~/components/svg'; import { BirthdayIcon } from '~/components/svg';
import { getIconEndpoint, cn } from '~/utils'; import { getIconEndpoint, cn } from '~/utils';
import { useLocalize } from '~/hooks'; import ConvoStarter from './ConvoStarter';
export default function Landing({ Header }: { Header?: ReactNode }) { export default function Landing({ Header }: { Header?: ReactNode }) {
const { conversation } = useChatContext(); const { conversation } = useChatContext();
@ -29,21 +32,36 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
const iconURL = conversation?.iconURL; const iconURL = conversation?.iconURL;
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint }); 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 isAssistant = isAssistantsEndpoint(endpoint);
const assistant = isAssistant ? assistantMap?.[endpoint][assistant_id ?? ''] : undefined; const assistant = isAssistant ? assistantMap?.[endpoint][assistant_id ?? ''] : undefined;
const assistantName = assistant && assistant.name; const assistantName = assistant?.name ?? '';
const assistantDesc = assistant && assistant.description; const assistantDesc = assistant?.description ?? '';
const avatar = assistant && (assistant.metadata?.avatar as string); 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 = const containerClassName =
'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black'; '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 ( return (
<TooltipProvider delayDuration={50}> <TooltipProvider delayDuration={50}>
<Tooltip> <Tooltip>
<div className="relative h-full"> <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="flex h-full flex-col items-center justify-center">
<div className={cn('relative h-12 w-12', assistantName && avatar ? 'mb-0' : 'mb-3')}> <div className={cn('relative h-12 w-12', assistantName && avatar ? 'mb-0' : 'mb-3')}>
<ConvoIcon <ConvoIcon
@ -55,7 +73,7 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
className="h-2/3 w-2/3" className="h-2/3 w-2/3"
size={41} size={41}
/> />
{!!startupConfig?.showBirthdayIcon && ( {startupConfig?.showBirthdayIcon === true ? (
<div> <div>
<TooltipTrigger> <TooltipTrigger>
<BirthdayIcon className="absolute bottom-8 right-2.5" /> <BirthdayIcon className="absolute bottom-8 right-2.5" />
@ -64,14 +82,14 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
{localize('com_ui_happy_birthday')} {localize('com_ui_happy_birthday')}
</TooltipContent> </TooltipContent>
</div> </div>
)} ) : null}
</div> </div>
{assistantName ? ( {assistantName ? (
<div className="flex flex-col items-center gap-0 p-2"> <div className="flex flex-col items-center gap-0 p-2">
<div className="text-center text-2xl font-medium dark:text-white"> <div className="text-center text-2xl font-medium dark:text-white">
{assistantName} {assistantName}
</div> </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')} {assistantDesc ? assistantDesc : localize('com_nav_welcome_message')}
</div> </div>
{/* <div className="mt-1 flex items-center gap-1 text-token-text-tertiary"> {/* <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')} : conversation?.greeting ?? localize('com_nav_welcome_message')}
</h2> </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>
</div> </div>
</Tooltip> </Tooltip>

View file

@ -3,7 +3,7 @@ import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
export default function PromptCard({ promptGroup }: { promptGroup: TPromptGroup }) { export default function PromptCard({ promptGroup }: { promptGroup: TPromptGroup }) {
return ( 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=""> <div className="">
<CategoryIcon className="size-4" category={promptGroup.category || ''} /> <CategoryIcon className="size-4" category={promptGroup.category || ''} />
</div> </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"> <div className="flex gap-1">
<button onClick={cancelRename}> <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>
<button onClick={onRename}> <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> </button>
</div> </div>
</div> </div>

View file

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

View file

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

View file

@ -28,7 +28,7 @@ export default function AssistantAction({
{isHovering && ( {isHovering && (
<button <button
type="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" /> <GearIcon className="icon-sm" />
</button> </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 type { AssistantForm, AssistantPanelProps } from '~/common';
import { useCreateAssistantMutation, useUpdateAssistantMutation } from '~/data-provider'; import { useCreateAssistantMutation, useUpdateAssistantMutation } from '~/data-provider';
import { cn, cardStyle, defaultTextProps, removeFocusOutlines } from '~/utils'; import { cn, cardStyle, defaultTextProps, removeFocusOutlines } from '~/utils';
import AssistantConversationStarters from './AssistantConversationStarters';
import { useAssistantsMapContext, useToastContext } from '~/Providers'; import { useAssistantsMapContext, useToastContext } from '~/Providers';
import { useSelectAssistant, useLocalize } from '~/hooks'; import { useSelectAssistant, useLocalize } from '~/hooks';
import { ToolSelectDialog } from '~/components/Tools'; 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 labelClass = 'mb-2 text-token-text-primary block font-medium';
const inputClass = cn( const inputClass = cn(
defaultTextProps, 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, removeFocusOutlines,
); );
@ -106,6 +107,7 @@ export default function AssistantPanel({
}); });
}, },
}); });
const create = useCreateAssistantMutation({ const create = useCreateAssistantMutation({
onSuccess: (data) => { onSuccess: (data) => {
setCurrentAssistantId(data.id); setCurrentAssistantId(data.id);
@ -139,7 +141,7 @@ export default function AssistantPanel({
return functionName; return functionName;
} else { } else {
const assistant = assistantMap?.[endpoint]?.[assistant_id]; 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) { if (assistant && tool) {
return tool; return tool;
} }
@ -148,7 +150,6 @@ export default function AssistantPanel({
return functionName; return functionName;
}); });
console.log(data);
if (data.code_interpreter) { if (data.code_interpreter) {
tools.push({ type: Tools.code_interpreter }); tools.push({ type: Tools.code_interpreter });
} }
@ -163,6 +164,7 @@ export default function AssistantPanel({
name, name,
description, description,
instructions, instructions,
conversation_starters: starters,
model, model,
// file_ids, // TODO: add file handling here // file_ids, // TODO: add file handling here
} = data; } = data;
@ -174,6 +176,7 @@ export default function AssistantPanel({
name, name,
description, description,
instructions, instructions,
conversation_starters: starters.filter((starter) => starter.trim() !== ''),
model, model,
tools, tools,
endpoint, endpoint,
@ -186,6 +189,7 @@ export default function AssistantPanel({
name, name,
description, description,
instructions, instructions,
conversation_starters: starters.filter((starter) => starter.trim() !== ''),
model, model,
tools, tools,
endpoint, endpoint,
@ -239,12 +243,12 @@ export default function AssistantPanel({
</button> </button>
)} )}
</div> </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 */} {/* Avatar & Name */}
<div className="mb-4"> <div className="mb-4">
<AssistantAvatar <AssistantAvatar
createMutation={create} createMutation={create}
assistant_id={assistant_id ?? null} assistant_id={assistant_id}
metadata={assistant['metadata'] ?? null} metadata={assistant['metadata'] ?? null}
endpoint={endpoint} endpoint={endpoint}
version={version} version={version}
@ -271,7 +275,7 @@ export default function AssistantPanel({
name="id" name="id"
control={control} control={control}
render={({ field }) => ( 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> </div>
@ -318,6 +322,23 @@ export default function AssistantPanel({
)} )}
/> />
</div> </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 */} {/* Model */}
<div className="mb-6"> <div className="mb-6">
<label className={labelClass} htmlFor="model"> <label className={labelClass} htmlFor="model">

View file

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

View file

@ -78,7 +78,7 @@ export default function AssistantTool({
<OGDialogTrigger asChild> <OGDialogTrigger asChild>
<button <button
type="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 /> <TrashIcon />
</button> </button>
@ -97,7 +97,7 @@ export default function AssistantTool({
selection={{ selection={{
selectHandler: () => removeTool(currentTool.pluginKey), selectHandler: () => removeTool(currentTool.pluginKey),
selectClasses: 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'), selectText: localize('com_ui_delete'),
}} }}
/> />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -216,7 +216,7 @@ export default {
com_ui_fork_from_message: 'Wähle eine Abzweigungsoption', com_ui_fork_from_message: 'Wähle eine Abzweigungsoption',
com_ui_mention: com_ui_mention:
'Erwähne einen Endpunkt, Assistenten oder eine Voreinstellung, um schnell dorthin zu wechseln', '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_regenerate: 'Neu generieren',
com_ui_continue: 'Fortfahren', com_ui_continue: 'Fortfahren',
com_ui_edit: 'Bearbeiten', com_ui_edit: 'Bearbeiten',
@ -592,7 +592,7 @@ export default {
com_nav_plugin_store: 'Plugin-Store', com_nav_plugin_store: 'Plugin-Store',
com_nav_plugin_install: 'Installieren', com_nav_plugin_install: 'Installieren',
com_nav_plugin_uninstall: 'Deinstallieren', com_nav_plugin_uninstall: 'Deinstallieren',
com_nav_tool_add: 'Hinzufügen', com_ui_add: 'Hinzufügen',
com_nav_tool_remove: 'Entfernen', com_nav_tool_remove: 'Entfernen',
com_nav_tool_dialog: 'Assistenten-Werkzeuge', com_nav_tool_dialog: 'Assistenten-Werkzeuge',
com_ui_misc: 'Sonstiges', com_ui_misc: 'Sonstiges',
@ -2283,7 +2283,7 @@ export const comparisons = {
english: 'Uninstall', english: 'Uninstall',
translated: 'Deinstallieren', translated: 'Deinstallieren',
}, },
com_nav_tool_add: { com_ui_add: {
english: 'Add', english: 'Add',
translated: 'Hinzufügen', translated: 'Hinzufügen',
}, },

View file

@ -78,6 +78,8 @@ export default {
com_assistants_update_error: 'There was an error updating your assistant.', com_assistants_update_error: 'There was an error updating your assistant.',
com_assistants_create_success: 'Successfully created', com_assistants_create_success: 'Successfully created',
com_assistants_create_error: 'There was an error creating your assistant.', 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_today: 'Today',
com_ui_date_yesterday: 'Yesterday', com_ui_date_yesterday: 'Yesterday',
com_ui_date_previous_7_days: 'Previous 7 days', 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_visible: 'Visible messages only',
com_ui_fork_from_message: 'Select a fork option', 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_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_regenerate: 'Regenerate',
com_ui_continue: 'Continue', com_ui_continue: 'Continue',
com_ui_edit: 'Edit', com_ui_edit: 'Edit',
@ -603,7 +606,7 @@ export default {
com_nav_plugin_store: 'Plugin store', com_nav_plugin_store: 'Plugin store',
com_nav_plugin_install: 'Install', com_nav_plugin_install: 'Install',
com_nav_plugin_uninstall: 'Uninstall', com_nav_plugin_uninstall: 'Uninstall',
com_nav_tool_add: 'Add', com_ui_add: 'Add',
com_nav_tool_remove: 'Remove', com_nav_tool_remove: 'Remove',
com_nav_tool_dialog: 'Assistant Tools', com_nav_tool_dialog: 'Assistant Tools',
com_ui_misc: 'Misc.', com_ui_misc: 'Misc.',

View file

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

View file

@ -202,7 +202,7 @@ export default {
com_ui_fork_visible: 'Vain näkyvät viestit', com_ui_fork_visible: 'Vain näkyvät viestit',
com_ui_fork_from_message: 'Valitse haarautustapa', com_ui_fork_from_message: 'Valitse haarautustapa',
com_ui_mention: 'Mainitse päätepiste, Avustaja tai asetus vaihtaaksesi siihen pikana', 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_regenerate: 'Luo uudestaan',
com_ui_continue: 'Jatka', com_ui_continue: 'Jatka',
com_ui_edit: 'Muokkaa', com_ui_edit: 'Muokkaa',
@ -571,7 +571,7 @@ export default {
com_nav_plugin_store: 'Lisäosakauppa', com_nav_plugin_store: 'Lisäosakauppa',
com_nav_plugin_install: 'Asenna', com_nav_plugin_install: 'Asenna',
com_nav_plugin_uninstall: 'Poista', com_nav_plugin_uninstall: 'Poista',
com_nav_tool_add: 'Lisää', com_ui_add: 'Lisää',
com_nav_tool_remove: 'Poista', com_nav_tool_remove: 'Poista',
com_nav_tool_dialog: 'Avustajatyökalut', com_nav_tool_dialog: 'Avustajatyökalut',
com_ui_misc: 'Muu', com_ui_misc: 'Muu',

View file

@ -418,7 +418,7 @@ export default {
com_ui_date_december: 'Décembre', com_ui_date_december: 'Décembre',
com_ui_nothing_found: 'Aucun résultat trouvé', com_ui_nothing_found: 'Aucun résultat trouvé',
com_ui_go_to_conversation: 'Aller à la conversation', 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_remove: 'Supprimer',
com_nav_tool_dialog: 'Outils de l\'assistant', com_nav_tool_dialog: 'Outils de l\'assistant',
com_nav_tool_dialog_description: com_nav_tool_dialog_description:
@ -624,7 +624,7 @@ export default {
com_ui_upload_invalid_var: com_ui_upload_invalid_var:
'Fichier non valide pour le téléchargement. L\'image ne doit pas dépasser {0} Mo', '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_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_loading: 'Chargement...',
com_ui_all_proper: 'Tout', com_ui_all_proper: 'Tout',
com_ui_chat: 'Discussion', com_ui_chat: 'Discussion',
@ -2208,7 +2208,7 @@ export const comparisons = {
english: 'Go to conversation', english: 'Go to conversation',
translated: 'Aller à la conversation', translated: 'Aller à la conversation',
}, },
com_nav_tool_add: { com_ui_add: {
english: 'Add', english: 'Add',
translated: 'Ajouter', translated: 'Ajouter',
}, },

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -530,7 +530,7 @@ export default {
com_nav_plugin_store: 'Eklenti mağazası', com_nav_plugin_store: 'Eklenti mağazası',
com_nav_plugin_install: 'Yükle', com_nav_plugin_install: 'Yükle',
com_nav_plugin_uninstall: 'Kaldır', com_nav_plugin_uninstall: 'Kaldır',
com_nav_tool_add: 'Ekle', com_ui_add: 'Ekle',
com_nav_tool_remove: 'Kaldır', com_nav_tool_remove: 'Kaldır',
com_nav_tool_dialog: 'Asistan Araçları', com_nav_tool_dialog: 'Asistan Araçları',
com_nav_tool_dialog_description: 'Araç seçimlerinin kalıcı olması için asistan kaydedilmelidir.', 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_store: '插件商店',
com_nav_plugin_install: '安装', com_nav_plugin_install: '安装',
com_nav_plugin_uninstall: '卸载', com_nav_plugin_uninstall: '卸载',
com_nav_tool_add: '添加', com_ui_add: '添加',
com_nav_tool_remove: '移除', com_nav_tool_remove: '移除',
com_nav_tool_dialog: '助手工具', com_nav_tool_dialog: '助手工具',
com_nav_tool_dialog_description: '必须保存助手才能保留工具选择。', com_nav_tool_dialog_description: '必须保存助手才能保留工具选择。',
@ -2071,7 +2071,7 @@ export const comparisons = {
english: 'Uninstall', english: 'Uninstall',
translated: '卸载', translated: '卸载',
}, },
com_nav_tool_add: { com_ui_add: {
english: 'Add', english: 'Add',
translated: '添加', translated: '添加',
}, },

View file

@ -522,7 +522,7 @@ export default {
com_nav_change_picture: '更換圖片', com_nav_change_picture: '更換圖片',
com_nav_plugin_install: '安裝', com_nav_plugin_install: '安裝',
com_nav_plugin_uninstall: '解除安裝', com_nav_plugin_uninstall: '解除安裝',
com_nav_tool_add: '新增', com_ui_add: '新增',
com_nav_tool_remove: '移除', com_nav_tool_remove: '移除',
com_nav_tool_dialog: 'AI 工具', com_nav_tool_dialog: 'AI 工具',
com_nav_tool_dialog_description: '必須儲存 Assistant 才能保留工具選擇。', com_nav_tool_dialog_description: '必須儲存 Assistant 才能保留工具選擇。',
@ -2563,7 +2563,7 @@ export const comparisons = {
english: 'Uninstall', english: 'Uninstall',
translated: '解除安裝', translated: '解除安裝',
}, },
com_nav_tool_add: { com_ui_add: {
english: 'Add', english: 'Add',
translated: '新增', 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 - **english**: Uninstall
- **translated**: Desinstalar - **translated**: Desinstalar
- **com_nav_tool_add**: - **com_ui_add**:
- **english**: Add - **english**: Add
- **translated**: Adicionar - **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 - **english**: Uninstall
- **translated**: Deinstallieren - **translated**: Deinstallieren
- **com_nav_tool_add**: - **com_ui_add**:
- **english**: Add - **english**: Add
- **translated**: Hinzufügen - **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 - **english**: Uninstall
- **translated**: Desinstalar - **translated**: Desinstalar
- **com_nav_tool_add**: - **com_ui_add**:
- **english**: Add - **english**: Add
- **translated**: Agregar - **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 - **english**: Uninstall
- **translated**: הסר התקנה - **translated**: הסר התקנה
- **com_nav_tool_add**: - **com_ui_add**:
- **english**: Add - **english**: Add
- **translated**: הוסף - **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 - **english**: Uninstall
- **translated**: Disinstalla - **translated**: Disinstalla
- **com_nav_tool_add**: - **com_ui_add**:
- **english**: Add - **english**: Add
- **translated**: Aggiungi - **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 - **english**: Uninstall
- **translated**: アンインストール - **translated**: アンインストール
- **com_nav_tool_add**: - **com_ui_add**:
- **english**: Add - **english**: Add
- **translated**: 追加 - **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 - **english**: Uninstall
- **translated**: 卸载 - **translated**: 卸载
- **com_nav_tool_add**: - **com_ui_add**:
- **english**: Add - **english**: Add
- **translated**: 添加 - **translated**: 添加

View file

@ -328,3 +328,13 @@
.sp-wrapper { .sp-wrapper {
@apply flex h-full w-full grow flex-col justify-center; @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 { .btn-neutral {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
--tw-text-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-color: rgba(0, 0, 0, 0.1);
border-width: 1px; border-width: 1px;
color: rgba(64, 65, 79, var(--tw-text-opacity)); 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'; '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 = 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 = 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'; '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 = 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) { export function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);

1
package-lock.json generated
View file

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

View file

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

View file

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

View file

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

View file

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