mirror of
https://github.com/coaidev/coai.git
synced 2025-05-20 05:20:15 +09:00
feat: support file viewer
This commit is contained in:
parent
898fdd3b66
commit
c627e5edec
@ -32,14 +32,14 @@ func (c *ChatInstance) CreateImageRequest(props ImageProps) (string, error) {
|
||||
N: 1,
|
||||
})
|
||||
if err != nil || res == nil {
|
||||
return "", fmt.Errorf("chatgpt error: %s", err.Error())
|
||||
return "", fmt.Errorf(err.Error())
|
||||
}
|
||||
|
||||
data := utils.MapToStruct[ImageResponse](res)
|
||||
if data == nil {
|
||||
return "", fmt.Errorf("chatgpt error: cannot parse response")
|
||||
} else if data.Error.Message != "" {
|
||||
return "", fmt.Errorf("chatgpt error: %s", data.Error.Message)
|
||||
return "", fmt.Errorf(data.Error.Message)
|
||||
}
|
||||
|
||||
return data.Data[0].Url, nil
|
||||
|
87
app/src/components/FileViewer.tsx
Normal file
87
app/src/components/FileViewer.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "./ui/dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React from "react";
|
||||
import { Heading2, Paperclip, Text } from "lucide-react";
|
||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||
import { CodeMarkdown } from "@/components/Markdown.tsx";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group.tsx";
|
||||
|
||||
type FileViewerProps = {
|
||||
filename: string;
|
||||
content: string;
|
||||
children: React.ReactNode;
|
||||
asChild?: boolean;
|
||||
};
|
||||
|
||||
enum viewerType {
|
||||
Text = "text",
|
||||
Image = "image",
|
||||
}
|
||||
|
||||
function FileViewer({ filename, content, children, asChild }: FileViewerProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [renderedType, setRenderedType] = React.useState(viewerType.Text);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild={asChild}>{children}</DialogTrigger>
|
||||
<DialogContent className={`flex-dialog`}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className={`flex flex-row items-center select-none`}>
|
||||
<Paperclip className={`h-4 w-4 mr-2`} />
|
||||
{filename ?? t("file.file")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className={`file-viewer-action`}>
|
||||
<ToggleGroup
|
||||
variant={`outline`}
|
||||
type={`single`}
|
||||
value={renderedType}
|
||||
onValueChange={console.log}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value={viewerType.Text}
|
||||
onClick={() => setRenderedType(viewerType.Text)}
|
||||
>
|
||||
<Text className={`h-4 w-4`} />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value={viewerType.Image}
|
||||
onClick={() => setRenderedType(viewerType.Image)}
|
||||
>
|
||||
<Heading2 className={`h-4 w-4`} />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<div className={`file-viewer-content`}>
|
||||
{renderedType === viewerType.Text ? (
|
||||
<Textarea
|
||||
className={`file-viewer-textarea thin-scrollbar`}
|
||||
value={content}
|
||||
rows={15}
|
||||
readOnly
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<CodeMarkdown
|
||||
filename={filename}
|
||||
codeStyle={`overflow-auto max-h-[60vh] thin-scrollbar`}
|
||||
>
|
||||
{content}
|
||||
</CodeMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileViewer;
|
@ -31,6 +31,7 @@ type MarkdownProps = {
|
||||
children: string;
|
||||
className?: string;
|
||||
acceptHtml?: boolean;
|
||||
codeStyle?: string;
|
||||
};
|
||||
|
||||
function doAction(dispatch: AppDispatch, url: string): boolean {
|
||||
@ -72,7 +73,12 @@ function getSocialIcon(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function MarkdownContent({ children, className, acceptHtml }: MarkdownProps) {
|
||||
function MarkdownContent({
|
||||
children,
|
||||
className,
|
||||
acceptHtml,
|
||||
codeStyle,
|
||||
}: MarkdownProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
@ -144,11 +150,11 @@ function MarkdownContent({ children, className, acceptHtml }: MarkdownProps) {
|
||||
PreTag="div"
|
||||
wrapLongLines={true}
|
||||
wrapLines={true}
|
||||
className={`code-block`}
|
||||
className={cn("code-block", codeStyle)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<code className={`code-inline ${className}`} {...props}>
|
||||
<code className={cn("code-inline", className)} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
@ -158,17 +164,27 @@ function MarkdownContent({ children, className, acceptHtml }: MarkdownProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function Markdown(props: MarkdownProps) {
|
||||
function Markdown({ children, ...props }: MarkdownProps) {
|
||||
// memoize the component
|
||||
const { children, className, acceptHtml } = props;
|
||||
return useMemo(
|
||||
() => (
|
||||
<MarkdownContent className={className} acceptHtml={acceptHtml}>
|
||||
{children}
|
||||
</MarkdownContent>
|
||||
),
|
||||
[props.children, props.className, props.acceptHtml],
|
||||
() => <MarkdownContent {...props}>{children}</MarkdownContent>,
|
||||
[props, children],
|
||||
);
|
||||
}
|
||||
|
||||
type CodeMarkdownProps = MarkdownProps & {
|
||||
filename: string;
|
||||
};
|
||||
|
||||
export function CodeMarkdown({ filename, ...props }: CodeMarkdownProps) {
|
||||
const suffix = filename.includes(".") ? filename.split(".").pop() : "";
|
||||
const children = useMemo(() => {
|
||||
const content = props.children.toString();
|
||||
|
||||
return `\`\`\`${suffix}\n${content}\n\`\`\``;
|
||||
}, [props.children]);
|
||||
|
||||
return <Markdown {...props}>{children}</Markdown>;
|
||||
}
|
||||
|
||||
export default Markdown;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Download, File } from "lucide-react";
|
||||
import { Download, Eye, File } from "lucide-react";
|
||||
import { saveAsFile, saveBlobAsFile } from "@/utils/dom.ts";
|
||||
import { useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import FileViewer from "@/components/FileViewer.tsx";
|
||||
|
||||
/**
|
||||
* file format:
|
||||
@ -37,7 +38,7 @@ export function parseFile(data: string, acceptDownload?: boolean) {
|
||||
<File className={`mr-1`} />
|
||||
<span className={`name`}>{filename}</span>
|
||||
<div className={`grow`} />
|
||||
{image && (
|
||||
{image ? (
|
||||
<Button
|
||||
variant={`ghost`}
|
||||
size={`icon`}
|
||||
@ -49,6 +50,10 @@ export function parseFile(data: string, acceptDownload?: boolean) {
|
||||
>
|
||||
<Download className={`cursor-pointer`} />
|
||||
</Button>
|
||||
) : (
|
||||
<FileViewer filename={filename} content={content}>
|
||||
<Eye className={`cursor-pointer`} />
|
||||
</FileViewer>
|
||||
)}
|
||||
</div>
|
||||
{image && <img src={image} className={`file-image`} alt={""} />}
|
||||
|
@ -241,6 +241,7 @@
|
||||
"confirm": "确认",
|
||||
"percent": "{{cent}}折",
|
||||
"file": {
|
||||
"file": "文件",
|
||||
"upload": "上传文件",
|
||||
"type": "支持 pdf, docx, pptx, xlsx, 图像, 文本等格式",
|
||||
"drop": "拖拽文件到此处或点击上传",
|
||||
|
@ -207,7 +207,8 @@
|
||||
"empty-file": "Empty File",
|
||||
"empty-file-prompt": "File content is empty, has been automatically ignored",
|
||||
"large-file-success": "Parsing Successful",
|
||||
"large-file-success-prompt": "Large file parsed successfully in {{time}} seconds"
|
||||
"large-file-success-prompt": "Large file parsed successfully in {{time}} seconds",
|
||||
"file": "Files"
|
||||
},
|
||||
"generate": {
|
||||
"title": "AI Project Generator",
|
||||
|
@ -207,7 +207,8 @@
|
||||
"empty-file": "コンテンツファイルがありません",
|
||||
"empty-file-prompt": "ファイルの内容が空で、自動的に無視されます",
|
||||
"large-file-success": "解析に成功しました",
|
||||
"large-file-success-prompt": "{{time}}秒で大きなファイルが正常に解析されました"
|
||||
"large-file-success-prompt": "{{time}}秒で大きなファイルが正常に解析されました",
|
||||
"file": "ファイル"
|
||||
},
|
||||
"generate": {
|
||||
"title": "AIプロジェクトビルダー",
|
||||
|
@ -207,7 +207,8 @@
|
||||
"empty-file": "Пустой файл",
|
||||
"empty-file-prompt": "Содержимое файла пустое, автоматически проигнорировано",
|
||||
"large-file-success": "Синтаксический анализ успешно завершен",
|
||||
"large-file-success-prompt": "Большой файл успешно проанализирован за {{time}} секунды"
|
||||
"large-file-success-prompt": "Большой файл успешно проанализирован за {{time}} секунды",
|
||||
"file": "Документ"
|
||||
},
|
||||
"generate": {
|
||||
"title": "Генератор AI проектов",
|
||||
|
@ -39,6 +39,10 @@ services:
|
||||
links:
|
||||
- mysql
|
||||
- redis
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 65535
|
||||
hard: 65535
|
||||
environment:
|
||||
MYSQL_HOST: mysql
|
||||
MYSQL_USER: chatnio
|
||||
|
@ -147,8 +147,6 @@ func (c *Connection) Process(handler func(*conversation.FormMessage) error) {
|
||||
}
|
||||
|
||||
func (c *Connection) Handle(handler func(*conversation.FormMessage) error) {
|
||||
defer c.conn.DeferClose()
|
||||
|
||||
go c.Process(handler)
|
||||
c.ReadWorker()
|
||||
}
|
||||
|
@ -58,6 +58,7 @@ func ChatAPI(c *gin.Context) {
|
||||
if conn = utils.NewWebsocket(c, false); conn == nil {
|
||||
return
|
||||
}
|
||||
defer conn.DeferClose()
|
||||
|
||||
db := utils.GetDBFromContext(c)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user