🗃️ feat: Code Interpreter File Persistence between Sessions (#6790)

* refactor: Enhance FileContainer with customizable button and container styles, onClick button handling, and type override

* refactor: Update file type handling to support partial file objects

* refactor: Extract download handling into a custom hook for improved reusability

* refactor: Replace LogContent with Stdout component and enhance Attachment rendering for added visibility

* feat: Update @librechat/agents to version 2.4.1 for referencing generated files in subsequent code interpreter uses

* feat: Add support for tab-separated values (TSV) in mime type handling and improve error logging for regex patterns

* chore: Update @librechat/agents to version 2.4.11 for better `session_id` instructions when wanting to persist files between executions

* chore: Update @librechat/agents to version 2.4.12 for improved functionality

* fix: Enhance argument parsing in useParseArgs to support JSON input and improve code extraction

* refactor: Update input handling in useAutoSave to require more than one character before saving to local storage
This commit is contained in:
Danny Avila 2025-04-08 23:18:50 -04:00 committed by GitHub
parent 910c73359b
commit 5d668748f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 113 additions and 30 deletions

View file

@ -1,6 +1,27 @@
import { memo } from 'react';
import { imageExtRegex } from 'librechat-data-provider';
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
import FileContainer from '~/components/Chat/Input/Files/FileContainer';
import Image from '~/components/Chat/Messages/Content/Image';
import { useAttachmentLink } from './LogLink';
const FileAttachment = memo(({ attachment }: { attachment: TAttachment }) => {
const { handleDownload } = useAttachmentLink({
href: attachment.filepath,
filename: attachment.filename,
});
const extension = attachment.filename.split('.').pop();
return (
<FileContainer
file={attachment}
onClick={handleDownload}
overrideType={extension}
containerClassName="max-w-fit"
buttonClassName="hover:cursor-pointer hover:bg-surface-secondary active:bg-surface-secondary focus:bg-surface-secondary hover:border-border-heavy active:border-border-heavy"
/>
);
});
export default function Attachment({ attachment }: { attachment?: TAttachment }) {
if (!attachment) {
@ -21,5 +42,5 @@ export default function Attachment({ attachment }: { attachment?: TAttachment })
/>
);
}
return null;
return <FileAttachment attachment={attachment} />;
}

View file

@ -7,7 +7,7 @@ import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import { useProgress, useLocalize } from '~/hooks';
import { CodeInProgress } from './CodeProgress';
import Attachment from './Attachment';
import LogContent from './LogContent';
import Stdout from './Stdout';
import store from '~/store';
interface ParsedArgs {
@ -17,8 +17,17 @@ interface ParsedArgs {
export function useParseArgs(args: string): ParsedArgs {
return useMemo(() => {
let parsedArgs: ParsedArgs | string = args;
try {
parsedArgs = JSON.parse(args);
} catch {
// console.error('Failed to parse args:', e);
}
if (typeof parsedArgs === 'object') {
return parsedArgs;
}
const langMatch = args.match(/"lang"\s*:\s*"(\w+)"/);
const codeMatch = args.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"args"|$)/s);
const codeMatch = args.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"(session_id|args)"|"\s*})/s);
let code = '';
if (codeMatch) {
@ -26,7 +35,7 @@ export function useParseArgs(args: string): ParsedArgs {
if (code.endsWith('"}')) {
code = code.slice(0, -2);
}
code = code.replace(/\\n/g, '\n').replace(/\\/g, '');
code = code.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
}
return {
@ -99,15 +108,17 @@ export default function ExecuteCode({
color: 'white',
}}
>
<pre className="shrink-0">
<LogContent output={output} attachments={attachments} />
</pre>
<Stdout output={output} />
</div>
</div>
)}
</div>
)}
{attachments?.map((attachment, index) => <Attachment attachment={attachment} key={index} />)}
<div className="mb-2 flex flex-wrap items-center gap-2.5">
{attachments?.map((attachment, index) => (
<Attachment attachment={attachment} key={index} />
))}
</div>
</>
);
}

View file

@ -8,11 +8,11 @@ interface LogLinkProps {
children: React.ReactNode;
}
const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => {
export const useAttachmentLink = ({ href, filename }: Pick<LogLinkProps, 'href' | 'filename'>) => {
const { showToast } = useToastContext();
const { refetch: downloadFile } = useCodeOutputDownload(href);
const handleDownload = async (event: React.MouseEvent<HTMLAnchorElement>) => {
const handleDownload = async (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {
event.preventDefault();
try {
const stream = await downloadFile();
@ -36,6 +36,11 @@ const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => {
}
};
return { handleDownload };
};
const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => {
const { handleDownload } = useAttachmentLink({ href, filename });
return (
<a
href={href}

View file

@ -0,0 +1,26 @@
import React, { useMemo } from 'react';
interface StdoutProps {
output?: string;
}
const Stdout: React.FC<StdoutProps> = ({ output = '' }) => {
const processedContent = useMemo(() => {
if (!output) {
return '';
}
const parts = output.split('Generated files:');
return parts[0].trim();
}, [output]);
return (
processedContent && (
<pre className="shrink-0">
<div>{processedContent}</div>
</pre>
)
);
};
export default Stdout;