fix unicode detector

This commit is contained in:
Zhang Minghan 2023-09-13 22:15:59 +08:00
parent bd959d89db
commit e989003fb4
8 changed files with 112 additions and 82 deletions

View File

@ -1,5 +1,5 @@
import React, {useEffect, useRef, useState} from "react"; import React, { useEffect, useRef, useState } from "react";
import {AlertCircle, File, FileCheck, Plus, X} from "lucide-react"; import { AlertCircle, File, FileCheck, Plus, X } from "lucide-react";
import "../assets/file.less"; import "../assets/file.less";
import { import {
Dialog, Dialog,
@ -12,12 +12,12 @@ import {
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Alert, AlertTitle } from "./ui/alert.tsx"; import { Alert, AlertTitle } from "./ui/alert.tsx";
import { useToast } from "./ui/use-toast.ts"; import { useToast } from "./ui/use-toast.ts";
import {useDraggableInput} from "../utils.ts"; import { useDraggableInput } from "../utils.ts";
export type FileObject = { export type FileObject = {
name: string; name: string;
content: string; content: string;
} };
type FileProviderProps = { type FileProviderProps = {
id: string; id: string;
@ -34,6 +34,22 @@ type FileObjectProps = {
onChange?: (filename?: string, data?: string) => void; onChange?: (filename?: string, data?: string) => void;
}; };
function isValidUnicode(str: string): boolean {
if (!Array.from(str).every(c => {
const code = c.codePointAt(0);
return c.length === 1 ? code <= 0xFFFF : code >= 0x010000 && code <= 0x10FFFF;
})) return false;
if (str.includes('\0')) {
return false;
}
const binaryRegex = /[\0-\x1F\x7F-\xFF]/;
if (binaryRegex.test(str)) {
return false;
}
return true;
}
function FileProvider({ function FileProvider({
id, id,
className, className,
@ -51,7 +67,7 @@ function FileProvider({
return () => { return () => {
setClearEvent && setClearEvent(() => {}); setClearEvent && setClearEvent(() => {});
} };
}, [setClearEvent]); }, [setClearEvent]);
function clear() { function clear() {
@ -86,11 +102,11 @@ function FileProvider({
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<div className={`file-action`}> <div className={`file-action`}>
{ {active ? (
active ? <FileCheck className={`h-3.5 w-3.5`} />
<FileCheck className={`h-3.5 w-3.5`} /> : ) : (
<Plus className={`h-3.5 w-3.5`} /> <Plus className={`h-3.5 w-3.5`} />
} )}
</div> </div>
</DialogTrigger> </DialogTrigger>
<DialogContent className={`file-dialog flex-dialog`}> <DialogContent className={`file-dialog flex-dialog`}>
@ -117,12 +133,7 @@ function FileProvider({
); );
} }
function FileObject({ function FileObject({ id, filename, className, onChange }: FileObjectProps) {
id,
filename,
className,
onChange,
}: FileObjectProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const ref = useRef(null); const ref = useRef(null);
@ -134,7 +145,7 @@ function FileObject({
return () => { return () => {
target.removeEventListener("dragover", () => {}); target.removeEventListener("dragover", () => {});
target.removeEventListener("drop", () => {}); target.removeEventListener("drop", () => {});
} };
}, [ref]); }, [ref]);
const handleChange = (e?: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e?: React.ChangeEvent<HTMLInputElement>) => {
@ -143,7 +154,7 @@ function FileObject({
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
const data = e.target?.result as string; const data = e.target?.result as string;
if (!/^[\x00-\x7F]*$/.test(data)) { if (!isValidUnicode(data)) {
toast({ toast({
title: t("file.parse-error"), title: t("file.parse-error"),
description: t("file.parse-error-prompt"), description: t("file.parse-error-prompt"),

View File

@ -1,13 +1,13 @@
import { LightAsync as SyntaxHighlighter } from "react-syntax-highlighter"; import { LightAsync as SyntaxHighlighter } from "react-syntax-highlighter";
import { atomOneDark as style } from "react-syntax-highlighter/dist/esm/styles/hljs"; import { atomOneDark as style } from "react-syntax-highlighter/dist/esm/styles/hljs";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from 'remark-gfm'; import remarkGfm from "remark-gfm";
import remarkMath from "remark-math"; import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex"; import rehypeKatex from "rehype-katex";
import remarkFile from "./plugins/file.tsx"; import remarkFile from "./plugins/file.tsx";
import "../assets/markdown/all.less"; import "../assets/markdown/all.less";
import {useEffect} from "react"; import { useEffect } from "react";
import {saveAsFile} from "../utils.ts"; import { saveAsFile } from "../utils.ts";
type MarkdownProps = { type MarkdownProps = {
children: string; children: string;
@ -23,7 +23,10 @@ function Markdown({ children, className }: MarkdownProps) {
e.stopPropagation(); e.stopPropagation();
// prevent double click // prevent double click
// @ts-ignore // @ts-ignore
if (window.hasOwnProperty("file") && window.file + 250 > new Date().getTime()) { if (
window.hasOwnProperty("file") &&
window.file + 250 > new Date().getTime()
) {
return; return;
} else { } else {
// @ts-ignore // @ts-ignore

View File

@ -14,7 +14,12 @@ import {
ContextMenuItem, ContextMenuItem,
ContextMenuTrigger, ContextMenuTrigger,
} from "./ui/context-menu.tsx"; } from "./ui/context-menu.tsx";
import {copyClipboard, filterMessage, saveAsFile, useInputValue} from "../utils.ts"; import {
copyClipboard,
filterMessage,
saveAsFile,
useInputValue,
} from "../utils.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
Tooltip, Tooltip,
@ -56,12 +61,17 @@ function MessageSegment({ message }: MessageProps) {
</div> </div>
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem onClick={() => copyClipboard(filterMessage(message.content))}> <ContextMenuItem
onClick={() => copyClipboard(filterMessage(message.content))}
>
<Copy className={`h-4 w-4 mr-2`} /> {t("message.copy")} <Copy className={`h-4 w-4 mr-2`} /> {t("message.copy")}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem <ContextMenuItem
onClick={() => onClick={() =>
saveAsFile(`message-${message.role}.txt`, filterMessage(message.content)) saveAsFile(
`message-${message.role}.txt`,
filterMessage(message.content),
)
} }
> >
<File className={`h-4 w-4 mr-2`} /> {t("message.save")} <File className={`h-4 w-4 mr-2`} /> {t("message.save")}

View File

@ -1,4 +1,4 @@
import {visit} from 'unist-util-visit'; import { visit } from "unist-util-visit";
/** /**
* file format: * file format:
@ -17,10 +17,10 @@ function fileMarkdownPlugin() {
content: "", content: "",
last: false, last: false,
cursor: 0, cursor: 0,
} };
function parse(data: string, index: number, parent: any) { function parse(data: string, index: number, parent: any) {
if (data.startsWith(':::file')) { if (data.startsWith(":::file")) {
cache.name = ""; cache.name = "";
cache.content = ""; cache.content = "";
cache.last = true; cache.last = true;
@ -30,25 +30,25 @@ function fileMarkdownPlugin() {
if (part.length > 0) { if (part.length > 0) {
parse(part.trimStart(), index, parent); parse(part.trimStart(), index, parent);
} }
} else if (data.startsWith(':::')) { } else if (data.startsWith(":::")) {
cache.last = false; cache.last = false;
parent.children.splice(cache.cursor, index - cache.cursor + 1, { parent.children.splice(cache.cursor, index - cache.cursor + 1, {
type: 'div', type: "div",
data: { data: {
hName: 'div', hName: "div",
hProperties: { hProperties: {
className: 'file-instance', className: "file-instance",
file: cache.name, file: cache.name,
content: cache.content, content: cache.content,
}, },
} },
}); });
cache.name = ""; cache.name = "";
cache.content = ""; cache.content = "";
} else if (cache.last) { } else if (cache.last) {
if (cache.name.length === 0 && data.startsWith('[[')) { if (cache.name.length === 0 && data.startsWith("[[")) {
// may contain content // may contain content
const end = data.indexOf(']]'); const end = data.indexOf("]]");
if (end !== -1) { if (end !== -1) {
cache.name = data.slice(2, end); cache.name = data.slice(2, end);
parse(data.slice(end + 2).trimStart(), index, parent); parse(data.slice(end + 2).trimStart(), index, parent);
@ -61,9 +61,9 @@ function fileMarkdownPlugin() {
} }
} }
visit(tree, 'paragraph', (node, index, parent) => { visit(tree, "paragraph", (node, index, parent) => {
for (const child of node.children) { for (const child of node.children) {
parse((child.value || ""), index as number, parent); parse(child.value || "", index as number, parent);
} }
}); });
}; };

View File

@ -1,6 +1,6 @@
import axios from "axios"; import axios from "axios";
export const version: string = "2.4.0"; export const version: string = "2.5.0";
export const deploy: boolean = true; export const deploy: boolean = true;
export let rest_api: string = "http://localhost:8094"; export let rest_api: string = "http://localhost:8094";
export let ws_api: string = "ws://localhost:8094"; export let ws_api: string = "ws://localhost:8094";

View File

@ -136,10 +136,12 @@ const resources = {
type: "Currently only text files are supported for upload", type: "Currently only text files are supported for upload",
drop: "Drag and drop files here or click to upload", drop: "Drag and drop files here or click to upload",
"parse-error": "Parse Error", "parse-error": "Parse Error",
"parse-error-prompt": "Parse error, currently only text files are supported", "parse-error-prompt":
"Parse error, currently only text files are supported",
"max-length": "Content too long", "max-length": "Content too long",
"max-length-prompt": "The content has been truncated due to the context length limit", "max-length-prompt":
} "The content has been truncated due to the context length limit",
},
}, },
}, },
cn: { cn: {

View File

@ -30,7 +30,13 @@ import {
updateConversationList, updateConversationList,
} from "../conversation/history.ts"; } from "../conversation/history.ts";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import {filterMessage, formatMessage, mobile, useAnimation, useEffectAsync} from "../utils.ts"; import {
filterMessage,
formatMessage,
mobile,
useAnimation,
useEffectAsync,
} from "../utils.ts";
import { useToast } from "../components/ui/use-toast.ts"; import { useToast } from "../components/ui/use-toast.ts";
import { ConversationInstance, Message } from "../conversation/types.ts"; import { ConversationInstance, Message } from "../conversation/types.ts";
import { import {
@ -57,7 +63,7 @@ import { manager } from "../conversation/manager.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import MessageSegment from "../components/Message.tsx"; import MessageSegment from "../components/Message.tsx";
import { setMenu } from "../store/menu.ts"; import { setMenu } from "../store/menu.ts";
import FileProvider, {FileObject} from "../components/FileProvider.tsx"; import FileProvider, { FileObject } from "../components/FileProvider.tsx";
function SideBar() { function SideBar() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -122,7 +128,9 @@ function SideBar() {
}} }}
> >
<MessageSquare className={`h-4 w-4 mr-1`} /> <MessageSquare className={`h-4 w-4 mr-1`} />
<div className={`title`}>{filterMessage(conversation.name)}</div> <div className={`title`}>
{filterMessage(conversation.name)}
</div>
<div className={`id`}>{conversation.id}</div> <div className={`id`}>{conversation.id}</div>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
@ -242,11 +250,11 @@ function ChatInterface() {
function ChatWrapper() { function ChatWrapper() {
const { t } = useTranslation(); const { t } = useTranslation();
const [ file, setFile ] = useState<FileObject>({ const [file, setFile] = useState<FileObject>({
name: "", name: "",
content: "", content: "",
}); });
const [ clearEvent, setClearEvent ] = useState<() => void>(() => {}); const [clearEvent, setClearEvent] = useState<() => void>(() => {});
const dispatch = useDispatch(); const dispatch = useDispatch();
const auth = useSelector(selectAuthenticated); const auth = useSelector(selectAuthenticated);
const gpt4 = useSelector(selectGPT4); const gpt4 = useSelector(selectGPT4);
@ -311,8 +319,7 @@ function ChatWrapper() {
if (e.key === "Enter") await handleSend(auth, gpt4, web); if (e.key === "Enter") await handleSend(auth, gpt4, web);
}} }}
/> />
{ {auth && (
auth &&
<FileProvider <FileProvider
id={`file`} id={`file`}
className={`file`} className={`file`}
@ -320,7 +327,7 @@ function ChatWrapper() {
maxLength={4000 * 1.25} maxLength={4000 * 1.25}
setClearEvent={setClearEvent} setClearEvent={setClearEvent}
/> />
} )}
</div> </div>
<Button <Button
size={`icon`} size={`icon`}

View File

@ -1,5 +1,5 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import {FileObject} from "./components/FileProvider.tsx"; import { FileObject } from "./components/FileProvider.tsx";
export let mobile = export let mobile =
window.innerWidth <= 468 || window.innerWidth <= 468 ||
@ -166,17 +166,14 @@ export function filterMessage(message: string): string {
* ::: * :::
*/ */
return message.replace( return message.replace(/:::file\n\[\[.*]]\n[\s\S]*?\n:::\n\n/g, "");
/:::file\n\[\[.*]]\n[\s\S]*?\n:::\n\n/g,
"",
);
} }
export function useDraggableInput( export function useDraggableInput(
t: any, t: any,
toast: any, toast: any,
target: HTMLLabelElement, target: HTMLLabelElement,
handleChange: (filename?: string, content?: string) => void handleChange: (filename?: string, content?: string) => void,
) { ) {
target.addEventListener("dragover", (e) => { target.addEventListener("dragover", (e) => {
e.preventDefault(); e.preventDefault();