mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
* refactor: expand container * chore: bump @codesandbox/sandpack-react to latest * WIP: first pass, show editor * feat: implement ArtifactCodeEditor and ArtifactTabs components for enhanced artifact management * refactor: fileKey * refactor: auto scrolling code editor and add messageId to artifact * feat: first pass, editing artifact * feat: first pass, robust artifact replacement * fix: robust artifact replacement & re-render when expected * feat: Download Artifacts * refactor: improve artifact editing UX * fix: layout shift of new download button * fix: enhance missing output checks and logging in StreamRunManager
122 lines
3.2 KiB
TypeScript
122 lines
3.2 KiB
TypeScript
import React, { useEffect, useCallback, useRef, useState } from 'react';
|
|
import throttle from 'lodash/throttle';
|
|
import { visit } from 'unist-util-visit';
|
|
import { useSetRecoilState } from 'recoil';
|
|
import type { Pluggable } from 'unified';
|
|
import type { Artifact } from '~/common';
|
|
import { useMessageContext, useArtifactContext } from '~/Providers';
|
|
import { artifactsState } from '~/store/artifacts';
|
|
import ArtifactButton from './ArtifactButton';
|
|
import { logger } from '~/utils';
|
|
|
|
export const artifactPlugin: Pluggable = () => {
|
|
return (tree) => {
|
|
visit(tree, ['textDirective', 'leafDirective', 'containerDirective'], (node) => {
|
|
node.data = {
|
|
hName: node.name,
|
|
hProperties: node.attributes,
|
|
...node.data,
|
|
};
|
|
return node;
|
|
});
|
|
};
|
|
};
|
|
|
|
const extractContent = (
|
|
children: React.ReactNode | { props: { children: React.ReactNode } } | string,
|
|
): string => {
|
|
if (typeof children === 'string') {
|
|
return children;
|
|
}
|
|
if (React.isValidElement(children)) {
|
|
return extractContent((children.props as { children?: React.ReactNode }).children);
|
|
}
|
|
if (Array.isArray(children)) {
|
|
return children.map(extractContent).join('');
|
|
}
|
|
return '';
|
|
};
|
|
|
|
export function Artifact({
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
node,
|
|
...props
|
|
}: Artifact & {
|
|
children: React.ReactNode | { props: { children: React.ReactNode } };
|
|
node: unknown;
|
|
}) {
|
|
const { messageId } = useMessageContext();
|
|
const { getNextIndex, resetCounter } = useArtifactContext();
|
|
const artifactIndex = useRef(getNextIndex(false)).current;
|
|
|
|
const setArtifacts = useSetRecoilState(artifactsState);
|
|
const [artifact, setArtifact] = useState<Artifact | null>(null);
|
|
|
|
const throttledUpdateRef = useRef(
|
|
throttle((updateFn: () => void) => {
|
|
updateFn();
|
|
}, 25),
|
|
);
|
|
|
|
const updateArtifact = useCallback(() => {
|
|
const content = extractContent(props.children);
|
|
logger.log('artifacts', 'updateArtifact: content.length', content.length);
|
|
|
|
if (!content || content.trim() === '') {
|
|
return;
|
|
}
|
|
|
|
const title = props.title ?? 'Untitled Artifact';
|
|
const type = props.type ?? 'unknown';
|
|
const identifier = props.identifier ?? 'no-identifier';
|
|
const artifactKey = `${identifier}_${type}_${title}_${messageId}`
|
|
.replace(/\s+/g, '_')
|
|
.toLowerCase();
|
|
|
|
throttledUpdateRef.current(() => {
|
|
const now = Date.now();
|
|
|
|
const currentArtifact: Artifact = {
|
|
id: artifactKey,
|
|
identifier,
|
|
title,
|
|
type,
|
|
content,
|
|
messageId,
|
|
index: artifactIndex,
|
|
lastUpdateTime: now,
|
|
};
|
|
|
|
setArtifacts((prevArtifacts) => {
|
|
if (
|
|
prevArtifacts?.[artifactKey] != null &&
|
|
prevArtifacts[artifactKey].content === content
|
|
) {
|
|
return prevArtifacts;
|
|
}
|
|
|
|
return {
|
|
...prevArtifacts,
|
|
[artifactKey]: currentArtifact,
|
|
};
|
|
});
|
|
|
|
setArtifact(currentArtifact);
|
|
});
|
|
}, [
|
|
props.type,
|
|
props.title,
|
|
setArtifacts,
|
|
props.children,
|
|
props.identifier,
|
|
messageId,
|
|
artifactIndex,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
resetCounter();
|
|
updateArtifact();
|
|
}, [updateArtifact, resetCounter]);
|
|
|
|
return <ArtifactButton artifact={artifact} />;
|
|
}
|