mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-21 19:00:13 +01:00
🤖 Assistants V2 Support: Part 2
🎹 fix: Autocompletion Chrome Bug on Action API Key Input
chore: remove `useOriginNavigate`
chore: set correct OpenAI Storage Source
fix: azure file deletions, instantiate clients by source for deletion
update code interpret files info
feat: deleteResourceFileId
chore: increase poll interval as azure easily rate limits
fix: openai file deletions, TODO: evaluate rejected deletion settled promises to determine which to delete from db records
file source icons
update table file filters
chore: file search info and versioning
fix: retrieval update with necessary tool_resources if specified
fix(useMentions): add optional chaining in case listMap value is undefined
fix: force assistant avatar roundedness
fix: azure assistants, check correct flag
chore: bump data-provider
This commit is contained in:
parent
2bdbff5141
commit
bc46ccdcad
44 changed files with 420 additions and 174 deletions
|
|
@ -3,11 +3,11 @@ const {
|
|||
Constants,
|
||||
RunStatus,
|
||||
CacheKeys,
|
||||
FileSources,
|
||||
ContentTypes,
|
||||
EModelEndpoint,
|
||||
ViolationTypes,
|
||||
ImageVisionTool,
|
||||
checkOpenAIStorage,
|
||||
AssistantStreamEvents,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
|
|
@ -361,10 +361,7 @@ const chatV2 = async (req, res) => {
|
|||
|
||||
/** @type {MongoFile[]} */
|
||||
const attachments = await req.body.endpointOption.attachments;
|
||||
if (
|
||||
attachments &&
|
||||
attachments.every((attachment) => attachment.source === FileSources.openai)
|
||||
) {
|
||||
if (attachments && attachments.every((attachment) => checkOpenAIStorage(attachment.source))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -422,7 +419,7 @@ const chatV2 = async (req, res) => {
|
|||
|
||||
if (processedFiles) {
|
||||
for (const file of processedFiles) {
|
||||
if (file.source !== FileSources.openai) {
|
||||
if (!checkOpenAIStorage(file.source)) {
|
||||
attachedFileIds.delete(file.file_id);
|
||||
const index = file_ids.indexOf(file.file_id);
|
||||
if (index > -1) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
const {
|
||||
EModelEndpoint,
|
||||
FileSources,
|
||||
CacheKeys,
|
||||
defaultAssistantsVersion,
|
||||
} = require('librechat-data-provider');
|
||||
const { EModelEndpoint, CacheKeys, defaultAssistantsVersion } = require('librechat-data-provider');
|
||||
const {
|
||||
initializeClient: initAzureClient,
|
||||
} = require('~/server/services/Endpoints/azureAssistants');
|
||||
|
|
@ -121,13 +116,8 @@ const listAssistantsForAzure = async ({ req, res, version, azureConfig = {}, que
|
|||
};
|
||||
};
|
||||
|
||||
async function getOpenAIClient({ req, res, endpointOption, initAppClient }) {
|
||||
let endpoint = req.body.endpoint ?? req.query.endpoint;
|
||||
if (!endpoint && req.baseUrl.includes('files') && req.body.files) {
|
||||
const source = req.body.files[0]?.source;
|
||||
endpoint =
|
||||
source === FileSources.openai ? EModelEndpoint.assistants : EModelEndpoint.azureAssistants;
|
||||
}
|
||||
async function getOpenAIClient({ req, res, endpointOption, initAppClient, overrideEndpoint }) {
|
||||
let endpoint = overrideEndpoint ?? req.body.endpoint ?? req.query.endpoint;
|
||||
const version = await getCurrentVersion(req, endpoint);
|
||||
if (!endpoint) {
|
||||
throw new Error(`[${req.baseUrl}] Endpoint is required`);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
const { ToolCallTypes } = require('librechat-data-provider');
|
||||
const { validateAndUpdateTool } = require('~/server/services/ActionService');
|
||||
const { getOpenAIClient } = require('./helpers');
|
||||
const { logger } = require('~/config');
|
||||
|
|
@ -54,6 +55,7 @@ const createAssistant = async (req, res) => {
|
|||
const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
|
||||
const tools = [];
|
||||
|
||||
let hasFileSearch = false;
|
||||
for (const tool of updateData.tools ?? []) {
|
||||
let actualTool = typeof tool === 'string' ? req.app.locals.availableTools[tool] : tool;
|
||||
|
||||
|
|
@ -61,6 +63,10 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (actualTool.type === ToolCallTypes.FILE_SEARCH) {
|
||||
hasFileSearch = true;
|
||||
}
|
||||
|
||||
if (!actualTool.function) {
|
||||
tools.push(actualTool);
|
||||
continue;
|
||||
|
|
@ -72,6 +78,20 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (hasFileSearch && !updateData.tool_resources) {
|
||||
const assistant = await openai.beta.assistants.retrieve(assistant_id);
|
||||
updateData.tool_resources = assistant.tool_resources ?? null;
|
||||
}
|
||||
|
||||
if (hasFileSearch && !updateData.tool_resources?.file_search) {
|
||||
updateData.tool_resources = {
|
||||
...(updateData.tool_resources ?? {}),
|
||||
file_search: {
|
||||
vector_store_ids: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
updateData.tools = tools;
|
||||
|
||||
if (openai.locals?.azureOptions && updateData.model) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const fs = require('fs').promises;
|
||||
const express = require('express');
|
||||
const { isUUID, FileSources } = require('librechat-data-provider');
|
||||
const { isUUID, checkOpenAIStorage } = require('librechat-data-provider');
|
||||
const {
|
||||
filterFile,
|
||||
processFileUpload,
|
||||
|
|
@ -89,7 +89,7 @@ router.get('/download/:userId/:file_id', async (req, res) => {
|
|||
return res.status(403).send('Forbidden');
|
||||
}
|
||||
|
||||
if (file.source === FileSources.openai && !file.model) {
|
||||
if (checkOpenAIStorage(file.source) && !file.model) {
|
||||
logger.warn(`${errorPrefix} has no associated model: ${file_id}`);
|
||||
return res.status(400).send('The model used when creating this file is not available');
|
||||
}
|
||||
|
|
@ -110,7 +110,8 @@ router.get('/download/:userId/:file_id', async (req, res) => {
|
|||
let passThrough;
|
||||
/** @type {ReadableStream | undefined} */
|
||||
let fileStream;
|
||||
if (file.source === FileSources.openai) {
|
||||
|
||||
if (checkOpenAIStorage(file.source)) {
|
||||
req.body = { model: file.model };
|
||||
const { openai } = await initializeClient({ req, res });
|
||||
logger.debug(`Downloading file ${file_id} from OpenAI`);
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ async function createOnTextProgress({
|
|||
* @return {Promise<OpenAIAssistantFinish | OpenAIAssistantAction[] | ThreadMessage[] | RequiredActionFunctionToolCall[]>}
|
||||
*/
|
||||
async function getResponse({ openai, run_id, thread_id }) {
|
||||
const run = await waitForRun({ openai, run_id, thread_id, pollIntervalMs: 500 });
|
||||
const run = await waitForRun({ openai, run_id, thread_id, pollIntervalMs: 2000 });
|
||||
|
||||
if (run.status === RunStatus.COMPLETED) {
|
||||
const messages = await openai.beta.threads.messages.list(thread_id, defaultOrderQuery);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,44 @@ const OpenAIClient = require('~/app/clients/OpenAIClient');
|
|||
const { isUserProvided } = require('~/server/utils');
|
||||
const { constructAzureURL } = require('~/utils');
|
||||
|
||||
class Files {
|
||||
constructor(client) {
|
||||
this._client = client;
|
||||
}
|
||||
/**
|
||||
* Create an assistant file by attaching a
|
||||
* [File](https://platform.openai.com/docs/api-reference/files) to an
|
||||
* [assistant](https://platform.openai.com/docs/api-reference/assistants).
|
||||
*/
|
||||
create(assistantId, body, options) {
|
||||
return this._client.post(`/assistants/${assistantId}/files`, {
|
||||
body,
|
||||
...options,
|
||||
headers: { 'OpenAI-Beta': 'assistants=v1', ...options?.headers },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an AssistantFile.
|
||||
*/
|
||||
retrieve(assistantId, fileId, options) {
|
||||
return this._client.get(`/assistants/${assistantId}/files/${fileId}`, {
|
||||
...options,
|
||||
headers: { 'OpenAI-Beta': 'assistants=v1', ...options?.headers },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an assistant file.
|
||||
*/
|
||||
del(assistantId, fileId, options) {
|
||||
return this._client.delete(`/assistants/${assistantId}/files/${fileId}`, {
|
||||
...options,
|
||||
headers: { 'OpenAI-Beta': 'assistants=v1', ...options?.headers },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const initializeClient = async ({ req, res, version, endpointOption, initAppClient = false }) => {
|
||||
const { PROXY, OPENAI_ORGANIZATION, AZURE_ASSISTANTS_API_KEY, AZURE_ASSISTANTS_BASE_URL } =
|
||||
process.env;
|
||||
|
|
@ -130,6 +168,8 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie
|
|||
...opts,
|
||||
});
|
||||
|
||||
openai.beta.assistants.files = new Files(openai);
|
||||
|
||||
openai.req = req;
|
||||
openai.res = res;
|
||||
|
||||
|
|
|
|||
|
|
@ -180,7 +180,15 @@ const deleteFirebaseFile = async (req, file) => {
|
|||
if (!fileName.includes(req.user.id)) {
|
||||
throw new Error('Invalid file path');
|
||||
}
|
||||
await deleteFile('', fileName);
|
||||
try {
|
||||
await deleteFile('', fileName);
|
||||
} catch (error) {
|
||||
logger.error('Error deleting file from Firebase:', error);
|
||||
if (error.code === 'storage/object-not-found') {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,21 +10,19 @@ const {
|
|||
EModelEndpoint,
|
||||
mergeFileConfig,
|
||||
hostImageIdSuffix,
|
||||
checkOpenAIStorage,
|
||||
hostImageNamePrefix,
|
||||
isAssistantsEndpoint,
|
||||
} = require('librechat-data-provider');
|
||||
const { addResourceFileId, deleteResourceFileId } = require('~/server/controllers/assistants/v2');
|
||||
const { convertImage, resizeAndConvert } = require('~/server/services/Files/images');
|
||||
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
|
||||
const { createFile, updateFileUsage, deleteFiles } = require('~/models/File');
|
||||
const { addResourceFileId } = require('~/server/controllers/assistants/v2');
|
||||
const { LB_QueueAsyncCall } = require('~/server/utils/queue');
|
||||
const { getStrategyFunctions } = require('./strategies');
|
||||
const { determineFileType } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const checkOpenAIStorage = (source) =>
|
||||
source === FileSources.openai || source === FileSources.azure;
|
||||
|
||||
const processFiles = async (files) => {
|
||||
const promises = [];
|
||||
for (let file of files) {
|
||||
|
|
@ -39,13 +37,15 @@ const processFiles = async (files) => {
|
|||
/**
|
||||
* Enqueues the delete operation to the leaky bucket queue if necessary, or adds it directly to promises.
|
||||
*
|
||||
* @param {Express.Request} req - The express request object.
|
||||
* @param {MongoFile} file - The file object to delete.
|
||||
* @param {Function} deleteFile - The delete file function.
|
||||
* @param {Promise[]} promises - The array of promises to await.
|
||||
* @param {OpenAI | undefined} [openai] - If an OpenAI file, the initialized OpenAI client.
|
||||
* @param {object} params - The passed parameters.
|
||||
* @param {Express.Request} params.req - The express request object.
|
||||
* @param {MongoFile} params.file - The file object to delete.
|
||||
* @param {Function} params.deleteFile - The delete file function.
|
||||
* @param {Promise[]} params.promises - The array of promises to await.
|
||||
* @param {string[]} params.resolvedFileIds - The array of promises to await.
|
||||
* @param {OpenAI | undefined} [params.openai] - If an OpenAI file, the initialized OpenAI client.
|
||||
*/
|
||||
function enqueueDeleteOperation(req, file, deleteFile, promises, openai) {
|
||||
function enqueueDeleteOperation({ req, file, deleteFile, promises, resolvedFileIds, openai }) {
|
||||
if (checkOpenAIStorage(file.source)) {
|
||||
// Enqueue to leaky bucket
|
||||
promises.push(
|
||||
|
|
@ -58,6 +58,7 @@ function enqueueDeleteOperation(req, file, deleteFile, promises, openai) {
|
|||
logger.error('Error deleting file from OpenAI source', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolvedFileIds.push(file.file_id);
|
||||
resolve(result);
|
||||
}
|
||||
},
|
||||
|
|
@ -67,10 +68,12 @@ function enqueueDeleteOperation(req, file, deleteFile, promises, openai) {
|
|||
} else {
|
||||
// Add directly to promises
|
||||
promises.push(
|
||||
deleteFile(req, file).catch((err) => {
|
||||
logger.error('Error deleting file', err);
|
||||
return Promise.reject(err);
|
||||
}),
|
||||
deleteFile(req, file)
|
||||
.then(() => resolvedFileIds.push(file.file_id))
|
||||
.catch((err) => {
|
||||
logger.error('Error deleting file', err);
|
||||
return Promise.reject(err);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -85,35 +88,71 @@ function enqueueDeleteOperation(req, file, deleteFile, promises, openai) {
|
|||
* @param {Express.Request} params.req - The express request object.
|
||||
* @param {DeleteFilesBody} params.req.body - The request body.
|
||||
* @param {string} [params.req.body.assistant_id] - The assistant ID if file uploaded is associated to an assistant.
|
||||
* @param {string} [params.req.body.tool_resource] - The tool resource if assistant file uploaded is associated to a tool resource.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const processDeleteRequest = async ({ req, files }) => {
|
||||
const file_ids = files.map((file) => file.file_id);
|
||||
|
||||
const resolvedFileIds = [];
|
||||
const deletionMethods = {};
|
||||
const promises = [];
|
||||
promises.push(deleteFiles(file_ids));
|
||||
|
||||
/** @type {OpenAI | undefined} */
|
||||
let openai;
|
||||
if (req.body.assistant_id) {
|
||||
({ openai } = await getOpenAIClient({ req }));
|
||||
/** @type {Record<string, OpenAI | undefined>} */
|
||||
const client = { [FileSources.openai]: undefined, [FileSources.azure]: undefined };
|
||||
const initializeClients = async () => {
|
||||
const openAIClient = await getOpenAIClient({
|
||||
req,
|
||||
overrideEndpoint: EModelEndpoint.assistants,
|
||||
});
|
||||
client[FileSources.openai] = openAIClient.openai;
|
||||
|
||||
if (!req.app.locals[EModelEndpoint.azureOpenAI]?.assistants) {
|
||||
return;
|
||||
}
|
||||
|
||||
const azureClient = await getOpenAIClient({
|
||||
req,
|
||||
overrideEndpoint: EModelEndpoint.azureAssistants,
|
||||
});
|
||||
client[FileSources.azure] = azureClient.openai;
|
||||
};
|
||||
|
||||
if (req.body.assistant_id !== undefined) {
|
||||
await initializeClients();
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const source = file.source ?? FileSources.local;
|
||||
|
||||
if (checkOpenAIStorage(source) && !openai) {
|
||||
({ openai } = await getOpenAIClient({ req }));
|
||||
if (checkOpenAIStorage(source) && !client[source]) {
|
||||
await initializeClients();
|
||||
}
|
||||
|
||||
if (req.body.assistant_id) {
|
||||
const openai = client[source];
|
||||
|
||||
if (req.body.assistant_id && req.body.tool_resource) {
|
||||
promises.push(
|
||||
deleteResourceFileId({
|
||||
req,
|
||||
openai,
|
||||
file_id: file.file_id,
|
||||
assistant_id: req.body.assistant_id,
|
||||
tool_resource: req.body.tool_resource,
|
||||
}),
|
||||
);
|
||||
} else if (req.body.assistant_id) {
|
||||
promises.push(openai.beta.assistants.files.del(req.body.assistant_id, file.file_id));
|
||||
}
|
||||
|
||||
if (deletionMethods[source]) {
|
||||
enqueueDeleteOperation(req, file, deletionMethods[source], promises, openai);
|
||||
enqueueDeleteOperation({
|
||||
req,
|
||||
file,
|
||||
deleteFile: deletionMethods[source],
|
||||
promises,
|
||||
resolvedFileIds,
|
||||
openai,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -123,10 +162,11 @@ const processDeleteRequest = async ({ req, files }) => {
|
|||
}
|
||||
|
||||
deletionMethods[source] = deleteFile;
|
||||
enqueueDeleteOperation(req, file, deleteFile, promises, openai);
|
||||
enqueueDeleteOperation({ req, file, deleteFile, promises, resolvedFileIds, openai });
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
await deleteFiles(resolvedFileIds);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -381,7 +421,10 @@ const processOpenAIFile = async ({
|
|||
originalName ? `/${originalName}` : ''
|
||||
}`;
|
||||
const type = mime.getType(originalName ?? file_id);
|
||||
|
||||
const source =
|
||||
openai.req.body.endpoint === EModelEndpoint.azureAssistants
|
||||
? FileSources.azure
|
||||
: FileSources.openai;
|
||||
const file = {
|
||||
..._file,
|
||||
type,
|
||||
|
|
@ -390,7 +433,7 @@ const processOpenAIFile = async ({
|
|||
usage: 1,
|
||||
user: userId,
|
||||
context: _file.purpose,
|
||||
source: FileSources.openai,
|
||||
source,
|
||||
model: openai.req.body.model,
|
||||
filename: originalName ?? file_id,
|
||||
};
|
||||
|
|
@ -435,12 +478,14 @@ const processOpenAIImageOutput = async ({ req, buffer, file_id, filename, fileEx
|
|||
filename: `${hostImageNamePrefix}${filename}`,
|
||||
};
|
||||
createFile(file, true);
|
||||
const source =
|
||||
req.body.endpoint === EModelEndpoint.azureAssistants ? FileSources.azure : FileSources.openai;
|
||||
createFile(
|
||||
{
|
||||
...file,
|
||||
file_id,
|
||||
filename,
|
||||
source: FileSources.openai,
|
||||
source,
|
||||
type: mime.getType(fileExt),
|
||||
},
|
||||
true,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ async function createRun({ openai, thread_id, body }) {
|
|||
* @param {string} params.run_id - The ID of the run to wait for.
|
||||
* @param {string} params.thread_id - The ID of the thread associated with the run.
|
||||
* @param {RunManager} params.runManager - The RunManager instance to manage run steps.
|
||||
* @param {number} [params.pollIntervalMs=750] - The interval for polling the run status; default is 750 milliseconds.
|
||||
* @param {number} [params.pollIntervalMs=2000] - The interval for polling the run status; default is 2000 milliseconds.
|
||||
* @param {number} [params.timeout=180000] - The period to wait until timing out polling; default is 3 minutes (in ms).
|
||||
* @return {Promise<Run>} A promise that resolves to the last fetched run object.
|
||||
*/
|
||||
|
|
@ -64,7 +64,7 @@ async function waitForRun({
|
|||
run_id,
|
||||
thread_id,
|
||||
runManager,
|
||||
pollIntervalMs = 750,
|
||||
pollIntervalMs = 2000,
|
||||
timeout = 60000 * 3,
|
||||
}) {
|
||||
let timeElapsed = 0;
|
||||
|
|
@ -233,7 +233,7 @@ async function _handleRun({ openai, run_id, thread_id }) {
|
|||
run_id,
|
||||
thread_id,
|
||||
runManager,
|
||||
pollIntervalMs: 750,
|
||||
pollIntervalMs: 2000,
|
||||
timeout: 60000,
|
||||
});
|
||||
const actions = [];
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ export type IconProps = Pick<TMessage, 'isCreatedByUser' | 'model'> &
|
|||
iconURL?: string;
|
||||
message?: boolean;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
endpoint?: EModelEndpoint | string | null;
|
||||
endpointType?: EModelEndpoint | null;
|
||||
assistantName?: string;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { TFile } from 'librechat-data-provider';
|
|||
import type { ExtendedFile } from '~/common';
|
||||
import FileIcon from '~/components/svg/Files/FileIcon';
|
||||
import ProgressCircle from './ProgressCircle';
|
||||
import SourceIcon from './SourceIcon';
|
||||
import { useProgress } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
|
|
@ -21,7 +22,6 @@ const FilePreview = ({
|
|||
const radius = 55; // Radius of the SVG circle
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const progress = useProgress(file?.['progress'] ?? 1, 0.001, (file as ExtendedFile)?.size ?? 1);
|
||||
console.log(progress);
|
||||
|
||||
// Calculate the offset based on the loading progress
|
||||
const offset = circumference - progress * circumference;
|
||||
|
|
@ -32,6 +32,7 @@ const FilePreview = ({
|
|||
return (
|
||||
<div className={cn('h-10 w-10 shrink-0 overflow-hidden rounded-md', className)}>
|
||||
<FileIcon file={file} fileType={fileType} />
|
||||
<SourceIcon source={file?.source} />
|
||||
{progress < 1 && (
|
||||
<ProgressCircle
|
||||
circumference={circumference}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect } from 'react';
|
||||
import { EToolResources } from 'librechat-data-provider';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import { useDeleteFilesMutation } from '~/data-provider';
|
||||
import { useFileDeletion } from '~/hooks/Files';
|
||||
|
|
@ -10,6 +11,7 @@ export default function FileRow({
|
|||
setFiles,
|
||||
setFilesLoading,
|
||||
assistant_id,
|
||||
tool_resource,
|
||||
fileFilter,
|
||||
Wrapper,
|
||||
}: {
|
||||
|
|
@ -18,6 +20,7 @@ export default function FileRow({
|
|||
setFilesLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
fileFilter?: (file: ExtendedFile) => boolean;
|
||||
assistant_id?: string;
|
||||
tool_resource?: EToolResources;
|
||||
Wrapper?: React.FC<{ children: React.ReactNode }>;
|
||||
}) {
|
||||
const files = Array.from(_files.values()).filter((file) =>
|
||||
|
|
@ -25,7 +28,8 @@ export default function FileRow({
|
|||
);
|
||||
|
||||
const { mutateAsync } = useDeleteFilesMutation({
|
||||
onMutate: async () => console.log('Deleting files: assistant_id', assistant_id),
|
||||
onMutate: async () =>
|
||||
console.log('Deleting files: assistant_id, tool_resource', assistant_id, tool_resource),
|
||||
onSuccess: () => {
|
||||
console.log('Files deleted');
|
||||
},
|
||||
|
|
@ -34,7 +38,7 @@ export default function FileRow({
|
|||
},
|
||||
});
|
||||
|
||||
const { deleteFile } = useFileDeletion({ mutateAsync, assistant_id });
|
||||
const { deleteFile } = useFileDeletion({ mutateAsync, assistant_id, tool_resource });
|
||||
|
||||
useEffect(() => {
|
||||
if (!files) {
|
||||
|
|
@ -82,6 +86,7 @@ export default function FileRow({
|
|||
url={file.preview}
|
||||
onDelete={handleDelete}
|
||||
progress={file.progress}
|
||||
source={file.source}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,16 +12,9 @@ export default function Files({ open, onOpenChange }) {
|
|||
const { data: files = [] } = useGetFiles<TFile[]>({
|
||||
select: (files) =>
|
||||
files.map((file) => {
|
||||
if (file.source === FileSources.local || file.source === FileSources.openai) {
|
||||
file.context = file.context ?? FileContext.unknown;
|
||||
return file;
|
||||
} else {
|
||||
return {
|
||||
...file,
|
||||
context: file.context ?? FileContext.unknown,
|
||||
source: FileSources.local,
|
||||
};
|
||||
}
|
||||
file.context = file.context ?? FileContext.unknown;
|
||||
file.filterSource = file.source === FileSources.firebase ? FileSources.local : file.source;
|
||||
return file;
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { FileSources } from 'librechat-data-provider';
|
||||
import ImagePreview from './ImagePreview';
|
||||
import RemoveFile from './RemoveFile';
|
||||
|
||||
|
|
@ -6,16 +7,18 @@ const Image = ({
|
|||
url,
|
||||
onDelete,
|
||||
progress = 1,
|
||||
source = FileSources.local,
|
||||
}: {
|
||||
imageBase64?: string;
|
||||
url?: string;
|
||||
onDelete: () => void;
|
||||
progress: number; // between 0 and 1
|
||||
source?: FileSources;
|
||||
}) => {
|
||||
return (
|
||||
<div className="group relative inline-block text-sm text-black/70 dark:text-white/90">
|
||||
<div className="relative overflow-hidden rounded-xl border border-gray-200 dark:border-gray-600">
|
||||
<ImagePreview imageBase64={imageBase64} url={url} progress={progress} />
|
||||
<ImagePreview source={source} imageBase64={imageBase64} url={url} progress={progress} />
|
||||
</div>
|
||||
<RemoveFile onRemove={onDelete} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { FileSources } from 'librechat-data-provider';
|
||||
import ProgressCircle from './ProgressCircle';
|
||||
import SourceIcon from './SourceIcon';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
type styleProps = {
|
||||
|
|
@ -13,11 +15,13 @@ const ImagePreview = ({
|
|||
url,
|
||||
progress = 1,
|
||||
className = '',
|
||||
source,
|
||||
}: {
|
||||
imageBase64?: string;
|
||||
url?: string;
|
||||
progress?: number; // between 0 and 1
|
||||
className?: string;
|
||||
source?: FileSources;
|
||||
}) => {
|
||||
let style: styleProps = {
|
||||
backgroundSize: 'cover',
|
||||
|
|
@ -65,6 +69,7 @@ const ImagePreview = ({
|
|||
circleCSSProperties={circleCSSProperties}
|
||||
/>
|
||||
)}
|
||||
<SourceIcon source={source} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
45
client/src/components/Chat/Input/Files/SourceIcon.tsx
Normal file
45
client/src/components/Chat/Input/Files/SourceIcon.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { EModelEndpoint, FileSources } from 'librechat-data-provider';
|
||||
import { MinimalIcon } from '~/components/Endpoints';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
const sourceToEndpoint = {
|
||||
[FileSources.openai]: EModelEndpoint.openAI,
|
||||
[FileSources.azure]: EModelEndpoint.azureOpenAI,
|
||||
};
|
||||
const sourceToClassname = {
|
||||
[FileSources.openai]: 'bg-black/65',
|
||||
[FileSources.azure]: 'azure-bg-color opacity-85',
|
||||
};
|
||||
|
||||
const defaultClassName =
|
||||
'absolute right-0 bottom-0 rounded-full p-[0.15rem] text-gray-600 transition-colors';
|
||||
|
||||
export default function SourceIcon({
|
||||
source,
|
||||
className = defaultClassName,
|
||||
}: {
|
||||
source?: FileSources;
|
||||
className?: string;
|
||||
}) {
|
||||
if (source === FileSources.local || source === FileSources.firebase) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const endpoint = sourceToEndpoint[source ?? ''];
|
||||
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<button type="button" className={cn(className, sourceToClassname[source ?? ''] ?? '')}>
|
||||
<span className="flex items-center justify-center">
|
||||
<MinimalIcon
|
||||
endpoint={endpoint}
|
||||
size={14}
|
||||
isCreatedByUser={false}
|
||||
iconClassName="h-3 w-3"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import ImagePreview from '~/components/Chat/Input/Files/ImagePreview';
|
|||
import FilePreview from '~/components/Chat/Input/Files/FilePreview';
|
||||
import { SortFilterHeader } from './SortFilterHeader';
|
||||
import { OpenAIMinimalIcon } from '~/components/svg';
|
||||
import { AzureMinimalIcon } from '~/components/svg';
|
||||
import { Button, Checkbox } from '~/components/ui';
|
||||
import { formatDate, getFileType } from '~/utils';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
|
|
@ -71,10 +72,11 @@ export const columns: ColumnDef<TFile>[] = [
|
|||
const file = row.original;
|
||||
if (file.type?.startsWith('image')) {
|
||||
return (
|
||||
<div className="flex gap-2 ">
|
||||
<div className="flex gap-2">
|
||||
<ImagePreview
|
||||
url={file.filepath}
|
||||
className="h-10 w-10 shrink-0 overflow-hidden rounded-md"
|
||||
className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md"
|
||||
source={file?.source}
|
||||
/>
|
||||
<span className="self-center truncate ">{file.filename}</span>
|
||||
</div>
|
||||
|
|
@ -84,7 +86,7 @@ export const columns: ColumnDef<TFile>[] = [
|
|||
const fileType = getFileType(file.type);
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{fileType && <FilePreview fileType={fileType} />}
|
||||
{fileType && <FilePreview fileType={fileType} className="relative" file={file} />}
|
||||
<span className="self-center truncate">{file.filename}</span>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -108,7 +110,7 @@ export const columns: ColumnDef<TFile>[] = [
|
|||
cell: ({ row }) => formatDate(row.original.updatedAt),
|
||||
},
|
||||
{
|
||||
accessorKey: 'source',
|
||||
accessorKey: 'filterSource',
|
||||
header: ({ column }) => {
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
|
|
@ -117,10 +119,14 @@ export const columns: ColumnDef<TFile>[] = [
|
|||
title={localize('com_ui_storage')}
|
||||
filters={{
|
||||
Storage: Object.values(FileSources).filter(
|
||||
(value) => value === FileSources.local || value === FileSources.openai,
|
||||
(value) =>
|
||||
value === FileSources.local ||
|
||||
value === FileSources.openai ||
|
||||
value === FileSources.azure,
|
||||
),
|
||||
}}
|
||||
valueMap={{
|
||||
[FileSources.azure]: 'Azure',
|
||||
[FileSources.openai]: 'OpenAI',
|
||||
[FileSources.local]: 'com_ui_host',
|
||||
}}
|
||||
|
|
@ -137,6 +143,13 @@ export const columns: ColumnDef<TFile>[] = [
|
|||
{'OpenAI'}
|
||||
</div>
|
||||
);
|
||||
} else if (source === FileSources.azure) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<AzureMinimalIcon className="icon-sm text-cyan-700" />
|
||||
{'Azure'}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -48,7 +48,12 @@ const contextMap = {
|
|||
[FileContext.bytes]: 'com_ui_size',
|
||||
};
|
||||
|
||||
type Style = { width?: number | string; maxWidth?: number | string; minWidth?: number | string };
|
||||
type Style = {
|
||||
width?: number | string;
|
||||
maxWidth?: number | string;
|
||||
minWidth?: number | string;
|
||||
zIndex?: number;
|
||||
};
|
||||
|
||||
export default function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -142,7 +147,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header, index) => {
|
||||
const style: Style = { maxWidth: '32px', minWidth: '125px' };
|
||||
const style: Style = { maxWidth: '32px', minWidth: '125px', zIndex: 50 };
|
||||
if (header.id === 'filename') {
|
||||
style.maxWidth = '50%';
|
||||
style.width = '50%';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { cn } from '~/utils';
|
|||
import { IconProps } from '~/common';
|
||||
|
||||
const MinimalIcon: React.FC<IconProps> = (props) => {
|
||||
const { size = 30, error } = props;
|
||||
const { size = 30, iconClassName, error } = props;
|
||||
|
||||
let endpoint = 'default'; // Default value for endpoint
|
||||
|
||||
|
|
@ -25,10 +25,13 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
|
|||
|
||||
const endpointIcons = {
|
||||
[EModelEndpoint.azureOpenAI]: {
|
||||
icon: <AzureMinimalIcon />,
|
||||
icon: <AzureMinimalIcon className={iconClassName} />,
|
||||
name: props.chatGptLabel || 'ChatGPT',
|
||||
},
|
||||
[EModelEndpoint.openAI]: {
|
||||
icon: <OpenAIMinimalIcon className={iconClassName} />,
|
||||
name: props.chatGptLabel || 'ChatGPT',
|
||||
},
|
||||
[EModelEndpoint.openAI]: { icon: <OpenAIMinimalIcon />, name: props.chatGptLabel || 'ChatGPT' },
|
||||
[EModelEndpoint.gptPlugins]: { icon: <MinimalPlugin />, name: 'Plugins' },
|
||||
[EModelEndpoint.google]: { icon: <GoogleMinimalIcon />, name: props.modelLabel || 'Google' },
|
||||
[EModelEndpoint.anthropic]: {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
|||
}}
|
||||
placeholder={localize('com_nav_search_placeholder')}
|
||||
onKeyUp={handleKeyUp}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<X
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ const ApiKey = () => {
|
|||
<input
|
||||
placeholder="<HIDDEN>"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
autoComplete="new-password"
|
||||
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
|
||||
{...register('api_key', { required: type === AuthTypeEnum.ServiceHttp })}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
Tools,
|
||||
FileSources,
|
||||
Capabilities,
|
||||
EModelEndpoint,
|
||||
LocalStorageKeys,
|
||||
isImageVisionTool,
|
||||
defaultAssistantFormValues,
|
||||
|
|
@ -52,6 +53,8 @@ export default function AssistantSelect({
|
|||
const assistants = useListAssistantsQuery(endpoint, undefined, {
|
||||
select: (res) =>
|
||||
res.data.map((_assistant) => {
|
||||
const source =
|
||||
endpoint === EModelEndpoint.assistants ? FileSources.openai : FileSources.azure;
|
||||
const assistant = {
|
||||
..._assistant,
|
||||
label: _assistant?.name ?? '',
|
||||
|
|
@ -77,7 +80,7 @@ export default function AssistantSelect({
|
|||
size: file.bytes,
|
||||
preview: file.filepath,
|
||||
progress: 1,
|
||||
source: FileSources.openai,
|
||||
source,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
|
|
@ -90,7 +93,7 @@ export default function AssistantSelect({
|
|||
size: 1,
|
||||
progress: 1,
|
||||
filepath: endpoint,
|
||||
source: FileSources.openai,
|
||||
source,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ export default function Code({
|
|||
version={version}
|
||||
endpoint={endpoint}
|
||||
files={files}
|
||||
tool_resource={Capabilities.code_interpreter}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import { mergeFileConfig, fileConfig as defaultFileConfig } from 'librechat-data-provider';
|
||||
import {
|
||||
EToolResources,
|
||||
mergeFileConfig,
|
||||
fileConfig as defaultFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { AssistantsEndpoint } from 'librechat-data-provider';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||
|
|
@ -8,17 +12,17 @@ import { useFileHandling } from '~/hooks/Files';
|
|||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useChatContext } from '~/Providers';
|
||||
|
||||
export default function Knowledge({
|
||||
const tool_resource = EToolResources.code_interpreter;
|
||||
|
||||
export default function CodeFiles({
|
||||
endpoint,
|
||||
assistant_id,
|
||||
files: _files,
|
||||
tool_resource,
|
||||
}: {
|
||||
version: number | string;
|
||||
endpoint: AssistantsEndpoint;
|
||||
assistant_id: string;
|
||||
files?: [string, ExtendedFile][];
|
||||
tool_resource?: string;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { setFilesLoading } = useChatContext();
|
||||
|
|
@ -57,13 +61,14 @@ export default function Knowledge({
|
|||
<div className={'mb-2'}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-token-text-tertiary rounded-lg text-xs">
|
||||
{assistant_id ? localize('com_assistants_knowledge_info') : ''}
|
||||
{localize('com_assistants_code_interpreter_files')}
|
||||
</div>
|
||||
<FileRow
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
setFilesLoading={setFilesLoading}
|
||||
assistant_id={assistant_id}
|
||||
tool_resource={tool_resource}
|
||||
setFilesLoading={setFilesLoading}
|
||||
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
|
||||
/>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ export const AssistantAvatar = ({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="relative overflow-hidden rounded-full">
|
||||
<div className="relative h-20 w-20 overflow-hidden rounded-full">
|
||||
<img
|
||||
src={url}
|
||||
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full"
|
||||
className="bg-token-surface-secondary dark:bg-token-surface-tertiary h-full w-full rounded-full object-cover"
|
||||
alt="GPT"
|
||||
width="80"
|
||||
height="80"
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import { useFormContext, Controller, useWatch } from 'react-hook-form';
|
|||
import { Capabilities } from 'librechat-data-provider';
|
||||
import type { AssistantsEndpoint } from 'librechat-data-provider';
|
||||
import type { AssistantForm } from '~/common';
|
||||
import { Checkbox } from '~/components/ui';
|
||||
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
|
||||
import { Checkbox, HoverCard, HoverCardTrigger } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { ESide } from '~/common';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
export default function Retrieval({
|
||||
|
|
@ -21,50 +23,72 @@ export default function Retrieval({
|
|||
const model = useWatch({ control, name: 'model' });
|
||||
const assistant = useWatch({ control, name: 'assistant' });
|
||||
|
||||
const files = useMemo(() => {
|
||||
const vectorStores = useMemo(() => {
|
||||
if (typeof assistant === 'string') {
|
||||
return [];
|
||||
}
|
||||
return assistant.tool_resources?.file_search;
|
||||
}, [assistant]);
|
||||
|
||||
const isDisabled = useMemo(() => !retrievalModels.has(model), [model, retrievalModels]);
|
||||
|
||||
useEffect(() => {
|
||||
if (model && !retrievalModels.has(model)) {
|
||||
if (model && isDisabled) {
|
||||
setValue(Capabilities.retrieval, false);
|
||||
}
|
||||
}, [model, setValue, retrievalModels]);
|
||||
}, [model, setValue, isDisabled]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
name={Capabilities.retrieval}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
disabled={!retrievalModels.has(model)}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
name={Capabilities.retrieval}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
disabled={isDisabled}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
|
||||
value={field?.value?.toString()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<HoverCard openDelay={50}>
|
||||
<HoverCardTrigger asChild>
|
||||
<label
|
||||
className={cn(
|
||||
'form-check-label text-token-text-primary w-full select-none',
|
||||
isDisabled ? 'cursor-no-drop opacity-50' : 'cursor-pointer',
|
||||
)}
|
||||
htmlFor={Capabilities.retrieval}
|
||||
onClick={() =>
|
||||
retrievalModels.has(model) &&
|
||||
setValue(Capabilities.retrieval, !getValues(Capabilities.retrieval), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
{version == 1
|
||||
? localize('com_assistants_retrieval')
|
||||
: localize('com_assistants_file_search')}
|
||||
</label>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
side={ESide.Top}
|
||||
disabled={!isDisabled}
|
||||
description="com_assistants_non_retrieval_model"
|
||||
langCode={true}
|
||||
sideOffset={20}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<label
|
||||
className={cn(
|
||||
'form-check-label text-token-text-primary w-full',
|
||||
!retrievalModels.has(model) ? 'cursor-no-drop opacity-50' : 'cursor-pointer',
|
||||
)}
|
||||
htmlFor={Capabilities.retrieval}
|
||||
onClick={() =>
|
||||
retrievalModels.has(model) &&
|
||||
setValue(Capabilities.retrieval, !getValues(Capabilities.retrieval), {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
{localize('com_assistants_retrieval')}
|
||||
</label>
|
||||
</div>
|
||||
</HoverCard>
|
||||
</div>
|
||||
{version == 2 && (
|
||||
<div className="text-token-text-tertiary rounded-lg text-xs">
|
||||
{localize('com_assistants_file_search_info')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { useCallback } from 'react';
|
||||
import {
|
||||
fileConfig as defaultFileConfig,
|
||||
checkOpenAIStorage,
|
||||
mergeFileConfig,
|
||||
megabyte,
|
||||
isAssistantsEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import type { Row } from '@tanstack/react-table';
|
||||
import type { TFile } from 'librechat-data-provider';
|
||||
|
|
@ -36,6 +38,18 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
|
|||
return showToast({ message: localize('com_ui_attach_error'), status: 'error' });
|
||||
}
|
||||
|
||||
if (checkOpenAIStorage(fileData?.source ?? '') && !isAssistantsEndpoint(endpoint)) {
|
||||
return showToast({
|
||||
message: localize('com_ui_attach_error_openai'),
|
||||
status: 'error',
|
||||
});
|
||||
} else if (!checkOpenAIStorage(fileData?.source ?? '') && isAssistantsEndpoint(endpoint)) {
|
||||
showToast({
|
||||
message: localize('com_ui_attach_warn_endpoint'),
|
||||
status: 'warning',
|
||||
});
|
||||
}
|
||||
|
||||
const { fileSizeLimit, supportedMimeTypes } =
|
||||
fileConfig.endpoints[endpoint] ?? fileConfig.endpoints.default;
|
||||
|
||||
|
|
@ -81,7 +95,8 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
|
|||
>
|
||||
<ImagePreview
|
||||
url={file.filepath}
|
||||
className="h-10 w-10 shrink-0 overflow-hidden rounded-md"
|
||||
className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md"
|
||||
source={file.source}
|
||||
/>
|
||||
<span className="self-center truncate text-xs">{file.filename}</span>
|
||||
</div>
|
||||
|
|
@ -94,7 +109,7 @@ export default function PanelFileCell({ row }: { row: Row<TFile> }) {
|
|||
onClick={handleFileClick}
|
||||
className="flex cursor-pointer gap-2 rounded-md dark:hover:bg-gray-700"
|
||||
>
|
||||
{fileType && <FilePreview fileType={fileType} />}
|
||||
{fileType && <FilePreview fileType={fileType} className="relative" file={file} />}
|
||||
<span className="self-center truncate">{file.filename}</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,11 +7,21 @@ type TOptionHoverProps = {
|
|||
description: string;
|
||||
langCode?: boolean;
|
||||
sideOffset?: number;
|
||||
disabled?: boolean;
|
||||
side: ESide;
|
||||
};
|
||||
|
||||
function OptionHover({ side, description, langCode, sideOffset = 30 }: TOptionHoverProps) {
|
||||
function OptionHover({
|
||||
side,
|
||||
description,
|
||||
disabled,
|
||||
langCode,
|
||||
sideOffset = 30,
|
||||
}: TOptionHoverProps) {
|
||||
const localize = useLocalize();
|
||||
if (disabled) {
|
||||
return null;
|
||||
}
|
||||
const text = langCode ? localize(description) : description;
|
||||
return (
|
||||
<HoverCardPortal>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { Capabilities, LocalStorageKeys, defaultAssistantsVersion } from 'librechat-data-provider';
|
||||
import {
|
||||
EToolResources,
|
||||
LocalStorageKeys,
|
||||
defaultAssistantsVersion,
|
||||
} from 'librechat-data-provider';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { UseMutationResult } from '@tanstack/react-query';
|
||||
import type t from 'librechat-data-provider';
|
||||
|
|
@ -420,7 +424,7 @@ export const useUploadFileMutation = (
|
|||
if (!tool_resource) {
|
||||
update['file_ids'] = [...assistant.file_ids, data.file_id];
|
||||
}
|
||||
if (tool_resource === Capabilities.code_interpreter) {
|
||||
if (tool_resource === EToolResources.code_interpreter) {
|
||||
const prevResources = assistant.tool_resources ?? {};
|
||||
const prevResource = assistant.tool_resources?.[tool_resource as string] ?? {
|
||||
file_ids: [],
|
||||
|
|
@ -455,7 +459,8 @@ export const useDeleteFilesMutation = (
|
|||
const queryClient = useQueryClient();
|
||||
const { onSuccess, ...options } = _options || {};
|
||||
return useMutation([MutationKeys.fileDelete], {
|
||||
mutationFn: (body: t.DeleteFilesBody) => dataService.deleteFiles(body.files, body.assistant_id),
|
||||
mutationFn: (body: t.DeleteFilesBody) =>
|
||||
dataService.deleteFiles(body.files, body.assistant_id, body.tool_resource),
|
||||
...(options || {}),
|
||||
onSuccess: (data, ...args) => {
|
||||
queryClient.setQueryData<t.TFile[] | undefined>([QueryKeys.files], (cachefiles) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil';
|
||||
import { useGetEndpointsQuery, useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||
import type {
|
||||
|
|
@ -10,11 +11,10 @@ import type {
|
|||
TEndpointsConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
|
||||
import useOriginNavigate from '../useOriginNavigate';
|
||||
import store from '~/store';
|
||||
|
||||
const useConversation = () => {
|
||||
const navigate = useOriginNavigate();
|
||||
const navigate = useNavigate();
|
||||
const setConversation = useSetRecoilState(store.conversation);
|
||||
const resetLatestMessage = useResetRecoilState(store.latestMessage);
|
||||
const setMessages = useSetRecoilState<TMessagesAtom>(store.messages);
|
||||
|
|
@ -59,7 +59,7 @@ const useConversation = () => {
|
|||
resetLatestMessage();
|
||||
|
||||
if (conversation.conversationId === 'new' && !modelsData) {
|
||||
navigate('new');
|
||||
navigate('/c/new');
|
||||
}
|
||||
},
|
||||
[endpointsConfig, modelsQuery.data],
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useSetRecoilState, useResetRecoilState } from 'recoil';
|
||||
import { QueryKeys, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
|
||||
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
|
||||
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
|
||||
import useOriginNavigate from '../useOriginNavigate';
|
||||
import store from '~/store';
|
||||
|
||||
const useNavigateToConvo = (index = 0) => {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useOriginNavigate();
|
||||
const { setConversation } = store.useCreateConversationAtom(index);
|
||||
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
||||
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
|
||||
|
|
@ -48,7 +48,7 @@ const useNavigateToConvo = (index = 0) => {
|
|||
});
|
||||
}
|
||||
setConversation(convo);
|
||||
navigate(convo?.conversationId);
|
||||
navigate(`/c/${convo.conversationId ?? 'new'}`);
|
||||
};
|
||||
|
||||
const navigateWithLastTools = (conversation: TConversation) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import { FileSources } from 'librechat-data-provider';
|
||||
import { FileSources, EToolResources } from 'librechat-data-provider';
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
import type {
|
||||
BatchFile,
|
||||
|
|
@ -16,18 +16,20 @@ type FileMapSetter = GenericSetter<Map<string, ExtendedFile>>;
|
|||
const useFileDeletion = ({
|
||||
mutateAsync,
|
||||
assistant_id,
|
||||
tool_resource,
|
||||
}: {
|
||||
mutateAsync: UseMutateAsyncFunction<DeleteFilesResponse, unknown, DeleteFilesBody, unknown>;
|
||||
assistant_id?: string;
|
||||
tool_resource?: EToolResources;
|
||||
}) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_batch, setFileDeleteBatch] = useState<BatchFile[]>([]);
|
||||
const setFilesToDelete = useSetFilesToDelete();
|
||||
|
||||
const executeBatchDelete = useCallback(
|
||||
(filesToDelete: BatchFile[], assistant_id?: string) => {
|
||||
console.log('Deleting files:', filesToDelete, assistant_id);
|
||||
mutateAsync({ files: filesToDelete, assistant_id });
|
||||
(filesToDelete: BatchFile[], assistant_id?: string, tool_resource?: EToolResources) => {
|
||||
console.log('Deleting files:', filesToDelete, assistant_id, tool_resource);
|
||||
mutateAsync({ files: filesToDelete, assistant_id, tool_resource });
|
||||
setFileDeleteBatch([]);
|
||||
},
|
||||
[mutateAsync],
|
||||
|
|
@ -81,11 +83,11 @@ const useFileDeletion = ({
|
|||
|
||||
setFileDeleteBatch((prevBatch) => {
|
||||
const newBatch = [...prevBatch, file];
|
||||
debouncedDelete(newBatch, assistant_id);
|
||||
debouncedDelete(newBatch, assistant_id, tool_resource);
|
||||
return newBatch;
|
||||
});
|
||||
},
|
||||
[debouncedDelete, setFilesToDelete, assistant_id],
|
||||
[debouncedDelete, setFilesToDelete, assistant_id, tool_resource],
|
||||
);
|
||||
|
||||
const deleteFiles = useCallback(
|
||||
|
|
|
|||
|
|
@ -58,23 +58,23 @@ export default function useMentions({ assistantMap }: { assistantMap: TAssistant
|
|||
const assistantListMap = useMemo(
|
||||
() => ({
|
||||
[EModelEndpoint.assistants]: listMap[EModelEndpoint.assistants]
|
||||
.map(
|
||||
?.map(
|
||||
assistantMapFn({
|
||||
endpoint: EModelEndpoint.assistants,
|
||||
assistantMap,
|
||||
endpointsConfig,
|
||||
}),
|
||||
)
|
||||
.filter(Boolean),
|
||||
?.filter(Boolean),
|
||||
[EModelEndpoint.azureAssistants]: listMap[EModelEndpoint.azureAssistants]
|
||||
.map(
|
||||
?.map(
|
||||
assistantMapFn({
|
||||
endpoint: EModelEndpoint.azureAssistants,
|
||||
assistantMap,
|
||||
endpointsConfig,
|
||||
}),
|
||||
)
|
||||
.filter(Boolean),
|
||||
?.filter(Boolean),
|
||||
}),
|
||||
[listMap, assistantMap, endpointsConfig],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,5 +22,4 @@ export { default as useScrollToRef } from './useScrollToRef';
|
|||
export { default as useLocalStorage } from './useLocalStorage';
|
||||
export { default as useDelayedRender } from './useDelayedRender';
|
||||
export { default as useOnClickOutside } from './useOnClickOutside';
|
||||
export { default as useOriginNavigate } from './useOriginNavigate';
|
||||
export { default as useGenerationsByLatest } from './useGenerationsByLatest';
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
useGetStartupConfig,
|
||||
useGetEndpointsQuery,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FileSources, LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import {
|
||||
useRecoilState,
|
||||
|
|
@ -30,12 +31,12 @@ import {
|
|||
} from '~/utils';
|
||||
import useAssistantListMap from './Assistants/useAssistantListMap';
|
||||
import { useDeleteFilesMutation } from '~/data-provider';
|
||||
import useOriginNavigate from './useOriginNavigate';
|
||||
|
||||
import { mainTextareaId } from '~/common';
|
||||
import store from '~/store';
|
||||
|
||||
const useNewConvo = (index = 0) => {
|
||||
const navigate = useOriginNavigate();
|
||||
const navigate = useNavigate();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const defaultPreset = useRecoilValue(store.defaultPreset);
|
||||
const { setConversation } = store.useCreateConversationAtom(index);
|
||||
|
|
@ -147,7 +148,7 @@ const useNewConvo = (index = 0) => {
|
|||
if (appTitle) {
|
||||
document.title = appTitle;
|
||||
}
|
||||
navigate('new');
|
||||
navigate('/c/new');
|
||||
}
|
||||
|
||||
clearTimeout(timeoutIdRef.current);
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
const useOriginNavigate = () => {
|
||||
const _navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const navigate = (url?: string | null, opts = {}) => {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
const path = location.pathname.match(/^\/[^/]+\//);
|
||||
_navigate(`${path ? path[0] : '/c/'}${url}`, opts);
|
||||
};
|
||||
|
||||
return navigate;
|
||||
};
|
||||
|
||||
export default useOriginNavigate;
|
||||
|
|
@ -20,6 +20,9 @@ export default {
|
|||
com_sidepanel_attach_files: 'Attach Files',
|
||||
com_sidepanel_manage_files: 'Manage Files',
|
||||
com_assistants_capabilities: 'Capabilities',
|
||||
com_assistants_file_search: 'File Search',
|
||||
com_assistants_file_search_info:
|
||||
'Attaching vector stores for File Search is not yet supported. You can attach them from the Provider Playground or attach files to messages for file search on a thread basis.',
|
||||
com_assistants_knowledge: 'Knowledge',
|
||||
com_assistants_knowledge_info:
|
||||
'If you upload files under Knowledge, conversations with your Assistant may include file contents.',
|
||||
|
|
@ -35,6 +38,8 @@ export default {
|
|||
com_assistants_actions: 'Actions',
|
||||
com_assistants_add_tools: 'Add Tools',
|
||||
com_assistants_add_actions: 'Add Actions',
|
||||
com_assistants_non_retrieval_model:
|
||||
'File search is not enabled on this model. Please select another model.',
|
||||
com_assistants_available_actions: 'Available Actions',
|
||||
com_assistants_running_action: 'Running action',
|
||||
com_assistants_completed_action: 'Talked to {0}',
|
||||
|
|
@ -73,6 +78,8 @@ export default {
|
|||
com_ui_field_required: 'This field is required',
|
||||
com_ui_download_error: 'Error downloading file. The file may have been deleted.',
|
||||
com_ui_attach_error_type: 'Unsupported file type for endpoint:',
|
||||
com_ui_attach_error_openai: 'Cannot attach Assistant files to other endpoints',
|
||||
com_ui_attach_warn_endpoint: 'Non-Assistant files may be ignored without a compatible tool',
|
||||
com_ui_attach_error_size: 'File size limit exceeded for endpoint:',
|
||||
com_ui_attach_error:
|
||||
'Cannot attach file. Create or select a conversation, or try refreshing the page.',
|
||||
|
|
|
|||
|
|
@ -270,4 +270,8 @@
|
|||
.radix-side-top\:animate-slideDownAndFade[data-side=top] {
|
||||
-webkit-animation:slideDownAndFade .4s cubic-bezier(.16,1,.3,1);
|
||||
animation:slideDownAndFade .4s cubic-bezier(.16,1,.3,1)
|
||||
}
|
||||
|
||||
.azure-bg-color {
|
||||
background: linear-gradient(0.375turn, #61bde2, #4389d0);
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ registration:
|
|||
endpoints:
|
||||
# assistants:
|
||||
# disableBuilder: false # Disable Assistants Builder Interface by setting to `true`
|
||||
# pollIntervalMs: 750 # Polling interval for checking assistant updates
|
||||
# pollIntervalMs: 3000 # Polling interval for checking assistant updates
|
||||
# timeoutMs: 180000 # Timeout for assistant operations
|
||||
# # Should only be one or the other, either `supportedIds` or `excludedIds`
|
||||
# supportedIds: ["asst_supportedAssistantId1", "asst_supportedAssistantId2"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "librechat-data-provider",
|
||||
"version": "0.6.4",
|
||||
"version": "0.6.5",
|
||||
"description": "data services for librechat apps",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
|
|
|
|||
|
|
@ -683,7 +683,7 @@ export enum Constants {
|
|||
/** Key for the app's version. */
|
||||
VERSION = 'v0.7.2',
|
||||
/** Key for the Custom Config's version (librechat.yaml). */
|
||||
CONFIG_VERSION = '1.1.0',
|
||||
CONFIG_VERSION = '1.1.1',
|
||||
/** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */
|
||||
NO_PARENT = '00000000-0000-0000-0000-000000000000',
|
||||
/** Fixed, encoded domain length for Azure OpenAI Assistants Function name parsing. */
|
||||
|
|
|
|||
|
|
@ -334,9 +334,10 @@ export const getFileDownload = async (userId: string, file_id: string): Promise<
|
|||
export const deleteFiles = async (
|
||||
files: f.BatchFile[],
|
||||
assistant_id?: string,
|
||||
tool_resource?: a.EToolResources,
|
||||
): Promise<f.DeleteFilesResponse> =>
|
||||
request.deleteWithOptions(endpoints.files(), {
|
||||
data: { files, assistant_id },
|
||||
data: { files, assistant_id, tool_resource },
|
||||
});
|
||||
|
||||
/* actions */
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ export enum Tools {
|
|||
function = 'function',
|
||||
}
|
||||
|
||||
export enum EToolResources {
|
||||
code_interpreter = 'code_interpreter',
|
||||
file_search = 'file_search',
|
||||
}
|
||||
|
||||
export type Tool = {
|
||||
[type: string]: Tools;
|
||||
};
|
||||
|
|
@ -94,6 +99,7 @@ export type AssistantUpdateParams = {
|
|||
metadata?: Metadata | null;
|
||||
name?: string | null;
|
||||
tools?: Array<FunctionTool | string>;
|
||||
tool_resources?: ToolResources;
|
||||
endpoint: AssistantsEndpoint;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { EToolResources } from './assistants';
|
||||
|
||||
export enum FileSources {
|
||||
local = 'local',
|
||||
firebase = 'firebase',
|
||||
|
|
@ -7,6 +9,9 @@ export enum FileSources {
|
|||
vectordb = 'vectordb',
|
||||
}
|
||||
|
||||
export const checkOpenAIStorage = (source: string) =>
|
||||
source === FileSources.openai || source === FileSources.azure;
|
||||
|
||||
export enum FileContext {
|
||||
avatar = 'avatar',
|
||||
unknown = 'unknown',
|
||||
|
|
@ -55,6 +60,7 @@ export type TFile = {
|
|||
usage: number;
|
||||
context?: FileContext;
|
||||
source?: FileSources;
|
||||
filterSource?: FileSources;
|
||||
width?: number;
|
||||
height?: number;
|
||||
expiresAt?: string | Date;
|
||||
|
|
@ -98,6 +104,7 @@ export type BatchFile = {
|
|||
export type DeleteFilesBody = {
|
||||
files: BatchFile[];
|
||||
assistant_id?: string;
|
||||
tool_resource?: EToolResources;
|
||||
};
|
||||
|
||||
export type DeleteMutationOptions = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue