mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
feat: export to screenshot
This commit is contained in:
parent
6f0b559927
commit
96914387a6
5 changed files with 73 additions and 25 deletions
|
@ -40,8 +40,10 @@
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
|
"downloadjs": "^1.4.7",
|
||||||
"esbuild": "0.17.15",
|
"esbuild": "0.17.15",
|
||||||
"export-from-json": "^1.7.2",
|
"export-from-json": "^1.7.2",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.113.0",
|
"lucide-react": "^0.113.0",
|
||||||
"rc-input-number": "^7.4.2",
|
"rc-input-number": "^7.4.2",
|
||||||
|
@ -65,6 +67,7 @@
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"tailwindcss-radix": "^2.8.0",
|
"tailwindcss-radix": "^2.8.0",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
|
"use-react-screenshot": "^3.0.0",
|
||||||
"uuidv4": "^6.2.13"
|
"uuidv4": "^6.2.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -7,6 +7,8 @@ import store from './store';
|
||||||
import userAuth from './utils/userAuth';
|
import userAuth from './utils/userAuth';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { ScreenshotProvider } from './utils/screenshotContext.jsx';
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
|
@ -97,4 +99,8 @@ const App = () => {
|
||||||
else return <div className="flex h-screen"></div>;
|
else return <div className="flex h-screen"></div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default () => (
|
||||||
|
<ScreenshotProvider>
|
||||||
|
<App />
|
||||||
|
</ScreenshotProvider>
|
||||||
|
);
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { CSSTransition } from 'react-transition-group';
|
||||||
import ScrollToBottom from './ScrollToBottom';
|
import ScrollToBottom from './ScrollToBottom';
|
||||||
import MultiMessage from './MultiMessage';
|
import MultiMessage from './MultiMessage';
|
||||||
import MessageHeader from './MessageHeader';
|
import MessageHeader from './MessageHeader';
|
||||||
|
import { useScreenshot } from '~/utils/screenshotContext.jsx';
|
||||||
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
@ -23,6 +24,8 @@ export default function Messages({ isSearchView = false }) {
|
||||||
const conversation = useRecoilValue(store.conversation) || {};
|
const conversation = useRecoilValue(store.conversation) || {};
|
||||||
const { conversationId } = conversation;
|
const { conversationId } = conversation;
|
||||||
|
|
||||||
|
const { screenshotTargetRef } = useScreenshot();
|
||||||
|
|
||||||
// const models = useRecoilValue(store.models) || [];
|
// const models = useRecoilValue(store.models) || [];
|
||||||
// const modelName = models.find(element => element.model == model)?.name;
|
// const modelName = models.find(element => element.model == model)?.name;
|
||||||
|
|
||||||
|
@ -84,8 +87,11 @@ export default function Messages({ isSearchView = false }) {
|
||||||
ref={scrollableRef}
|
ref={scrollableRef}
|
||||||
onScroll={debouncedHandleScroll}
|
onScroll={debouncedHandleScroll}
|
||||||
>
|
>
|
||||||
<div className="dark:gpt-dark-gray h-full">
|
<div
|
||||||
<div className="dark:gpt-dark-gray flex h-full flex-col items-center text-sm">
|
className="dark:gpt-dark-gray h-auto"
|
||||||
|
ref={screenshotTargetRef}
|
||||||
|
>
|
||||||
|
<div className="dark:gpt-dark-gray flex h-auto flex-col items-center text-sm">
|
||||||
<MessageHeader isSearchView={isSearchView} />
|
<MessageHeader isSearchView={isSearchView} />
|
||||||
{_messagesTree === null ? (
|
{_messagesTree === null ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useRecoilValue, useRecoilCallback } from 'recoil';
|
import { useRecoilValue, useRecoilCallback } from 'recoil';
|
||||||
import exportFromJSON from 'export-from-json';
|
import exportFromJSON from 'export-from-json';
|
||||||
|
import download from 'downloadjs';
|
||||||
import DialogTemplate from '~/components/ui/DialogTemplate.jsx';
|
import DialogTemplate from '~/components/ui/DialogTemplate.jsx';
|
||||||
import { Dialog, DialogClose, DialogButton } from '~/components/ui/Dialog.tsx';
|
import { Dialog, DialogClose, DialogButton } from '~/components/ui/Dialog.tsx';
|
||||||
import { Input } from '~/components/ui/Input.tsx';
|
import { Input } from '~/components/ui/Input.tsx';
|
||||||
|
@ -8,11 +9,14 @@ import { Label } from '~/components/ui/Label.tsx';
|
||||||
import { Checkbox } from '~/components/ui/Checkbox.tsx';
|
import { Checkbox } from '~/components/ui/Checkbox.tsx';
|
||||||
import Dropdown from '~/components/ui/Dropdown';
|
import Dropdown from '~/components/ui/Dropdown';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
|
import { useScreenshot } from '~/utils/screenshotContext';
|
||||||
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
import cleanupPreset from '~/utils/cleanupPreset.js';
|
import cleanupPreset from '~/utils/cleanupPreset.js';
|
||||||
|
|
||||||
export default function ExportModel({ open, onOpenChange }) {
|
export default function ExportModel({ open, onOpenChange }) {
|
||||||
|
const { captureScreenshot } = useScreenshot();
|
||||||
|
|
||||||
const [filename, setFileName] = useState('');
|
const [filename, setFileName] = useState('');
|
||||||
const [type, setType] = useState('');
|
const [type, setType] = useState('');
|
||||||
|
|
||||||
|
@ -32,7 +36,7 @@ export default function ExportModel({ open, onOpenChange }) {
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const typeOptions = ['text', 'markdown', 'csv', 'json']; //, 'screenshot', 'webpage'];
|
const typeOptions = ['text', 'markdown', 'csv', 'json', 'screenshot']; //,, 'webpage'];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFileName(
|
setFileName(
|
||||||
|
@ -105,6 +109,11 @@ export default function ExportModel({ open, onOpenChange }) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportScreenshot = async () => {
|
||||||
|
const data = await captureScreenshot();
|
||||||
|
download(data, `${filename}.png`, 'image/png');
|
||||||
|
};
|
||||||
|
|
||||||
const exportCSV = async () => {
|
const exportCSV = async () => {
|
||||||
let data = [];
|
let data = [];
|
||||||
|
|
||||||
|
@ -256,6 +265,7 @@ export default function ExportModel({ open, onOpenChange }) {
|
||||||
else if (type == 'text') exportText();
|
else if (type == 'text') exportText();
|
||||||
else if (type == 'markdown') exportMarkdown();
|
else if (type == 'markdown') exportMarkdown();
|
||||||
else if (type == 'csv') exportCSV();
|
else if (type == 'csv') exportCSV();
|
||||||
|
else if (type == 'screenshot') exportScreenshot();
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultTextProps =
|
const defaultTextProps =
|
||||||
|
@ -311,7 +321,7 @@ export default function ExportModel({ open, onOpenChange }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid w-full gap-6 sm:grid-cols-2">
|
<div className="grid w-full gap-6 sm:grid-cols-2">
|
||||||
{type !== 'csv' ? (
|
{type !== 'csv' && type !== 'screenshot' ? (
|
||||||
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||||
<div className="grid w-full items-center gap-2">
|
<div className="grid w-full items-center gap-2">
|
||||||
<Label
|
<Label
|
||||||
|
@ -337,29 +347,31 @@ export default function ExportModel({ open, onOpenChange }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="grid w-full items-center gap-2">
|
{type !== 'screenshot' ? (
|
||||||
<Label
|
<div className="grid w-full items-center gap-2">
|
||||||
htmlFor="exportBranches"
|
<Label
|
||||||
className="text-left text-sm font-medium"
|
|
||||||
>
|
|
||||||
Export all message branches
|
|
||||||
</Label>
|
|
||||||
<div className="flex h-[40px] w-full items-center space-x-3">
|
|
||||||
<Checkbox
|
|
||||||
id="exportBranches"
|
|
||||||
disabled={!exportBranchesSupport}
|
|
||||||
checked={exportBranches}
|
|
||||||
className="focus:ring-opacity-20 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:focus:ring-gray-600 dark:focus:ring-opacity-50 dark:focus:ring-offset-0"
|
|
||||||
onCheckedChange={setExportBranches}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="exportBranches"
|
htmlFor="exportBranches"
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
className="text-left text-sm font-medium"
|
||||||
>
|
>
|
||||||
{exportBranchesSupport ? 'Enabled' : 'Not Supported'}
|
Export all message branches
|
||||||
</label>
|
</Label>
|
||||||
|
<div className="flex h-[40px] w-full items-center space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="exportBranches"
|
||||||
|
disabled={!exportBranchesSupport}
|
||||||
|
checked={exportBranches}
|
||||||
|
className="focus:ring-opacity-20 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:focus:ring-gray-600 dark:focus:ring-opacity-50 dark:focus:ring-offset-0"
|
||||||
|
onCheckedChange={setExportBranches}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="exportBranches"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-50"
|
||||||
|
>
|
||||||
|
{exportBranchesSupport ? 'Enabled' : 'Not Supported'}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
{type === 'json' ? (
|
{type === 'json' ? (
|
||||||
<div className="grid w-full items-center gap-2">
|
<div className="grid w-full items-center gap-2">
|
||||||
<Label
|
<Label
|
||||||
|
|
21
client/src/utils/screenshotContext.jsx
Normal file
21
client/src/utils/screenshotContext.jsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React, { createContext, useRef, useContext, useCallback } from 'react';
|
||||||
|
import { useScreenshot as useScreenshot_ } from 'use-react-screenshot';
|
||||||
|
|
||||||
|
const ScreenshotContext = createContext({});
|
||||||
|
|
||||||
|
export const useScreenshot = () => {
|
||||||
|
const { ref } = useContext(ScreenshotContext);
|
||||||
|
const [image, takeScreenshot] = useScreenshot_();
|
||||||
|
|
||||||
|
const captureScreenshot = () => {
|
||||||
|
return takeScreenshot(ref.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { screenshotTargetRef: ref, captureScreenshot };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ScreenshotProvider = ({ children }) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
return <ScreenshotContext.Provider value={{ ref }}>{children}</ScreenshotContext.Provider>;
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue