mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 17:30:16 +01:00
⬇️ feat: Assistant File Downloads (#2234)
* WIP: basic route for file downloads and file strategy for generating readablestream to pipe as res * chore(DALLE3): add typing for OpenAI client * chore: add `CONSOLE_JSON` notes to dotenv.md * WIP: first pass OpenAI Assistants File Output handling * feat: first pass assistants output file download from openai * chore: yml vs. yaml variation to .gitignore for `librechat.yml` * refactor(retrieveAndProcessFile): remove redundancies * fix(syncMessages): explicit sort of apiMessages to fix message order on abort * chore: add logs for warnings and errors, show toast on frontend * chore: add logger where console was still being used
This commit is contained in:
parent
7945fea0f9
commit
a00756c469
27 changed files with 555 additions and 248 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { memo } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import remarkMath from 'remark-math';
|
||||
|
|
@ -9,9 +9,11 @@ import ReactMarkdown from 'react-markdown';
|
|||
import rehypeHighlight from 'rehype-highlight';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import type { PluggableList } from 'unified';
|
||||
import CodeBlock from '~/components/Messages/Content/CodeBlock';
|
||||
import { cn, langSubset, validateIframe, processLaTeX } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import CodeBlock from '~/components/Messages/Content/CodeBlock';
|
||||
import { useChatContext, useToastContext } from '~/Providers';
|
||||
import { useFileDownload } from '~/data-provider';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import store from '~/store';
|
||||
|
||||
type TCodeProps = {
|
||||
|
|
@ -37,6 +39,70 @@ export const code = memo(({ inline, className, children }: TCodeProps) => {
|
|||
}
|
||||
});
|
||||
|
||||
export const a = memo(({ href, children }: { href: string; children: React.ReactNode }) => {
|
||||
const user = useRecoilValue(store.user);
|
||||
const { showToast } = useToastContext();
|
||||
const localize = useLocalize();
|
||||
|
||||
const { filepath, filename } = useMemo(() => {
|
||||
const pattern = new RegExp(`(?:files|outputs)/${user?.id}/([^\\s]+)`);
|
||||
const match = href.match(pattern);
|
||||
if (match && match[0]) {
|
||||
const path = match[0];
|
||||
const name = path.split('/').pop();
|
||||
return { filepath: path, filename: name };
|
||||
}
|
||||
return { filepath: '', filename: '' };
|
||||
}, [user?.id, href]);
|
||||
|
||||
const { refetch: downloadFile } = useFileDownload(user?.id ?? '', filepath);
|
||||
const props: { target?: string; onClick?: React.MouseEventHandler } = { target: '_new' };
|
||||
|
||||
if (!filepath || !filename) {
|
||||
return (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const handleDownload = async (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const stream = await downloadFile();
|
||||
if (!stream.data) {
|
||||
console.error('Error downloading file: No data found');
|
||||
showToast({
|
||||
status: 'error',
|
||||
message: localize('com_ui_download_error'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const link = document.createElement('a');
|
||||
link.href = stream.data;
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(stream.data);
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
props.onClick = handleDownload;
|
||||
props.target = '_blank';
|
||||
|
||||
return (
|
||||
<a
|
||||
href={filepath.startsWith('files/') ? `/api/${filepath}` : `/api/files/${filepath}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
export const p = memo(({ children }: { children: React.ReactNode }) => {
|
||||
return <p className="mb-2 whitespace-pre-wrap">{children}</p>;
|
||||
});
|
||||
|
|
@ -98,6 +164,7 @@ const Markdown = memo(({ content, message, showCursor }: TContentProps) => {
|
|||
components={
|
||||
{
|
||||
code,
|
||||
a,
|
||||
p,
|
||||
} as {
|
||||
[nodeType: string]: React.ElementType;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import ReactMarkdown from 'react-markdown';
|
|||
import rehypeHighlight from 'rehype-highlight';
|
||||
import type { PluggableList } from 'unified';
|
||||
import { langSubset } from '~/utils';
|
||||
import { code, p } from './Markdown';
|
||||
import { code, a, p } from './Markdown';
|
||||
|
||||
const MarkdownLite = memo(({ content = '' }: { content?: string }) => {
|
||||
const rehypePlugins: PluggableList = [
|
||||
|
|
@ -30,6 +30,7 @@ const MarkdownLite = memo(({ content = '' }: { content?: string }) => {
|
|||
components={
|
||||
{
|
||||
code,
|
||||
a,
|
||||
p,
|
||||
} as {
|
||||
[nodeType: string]: React.ElementType;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue