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,
|
N: 1,
|
||||||
})
|
})
|
||||||
if err != nil || res == nil {
|
if err != nil || res == nil {
|
||||||
return "", fmt.Errorf("chatgpt error: %s", err.Error())
|
return "", fmt.Errorf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
data := utils.MapToStruct[ImageResponse](res)
|
data := utils.MapToStruct[ImageResponse](res)
|
||||||
if data == nil {
|
if data == nil {
|
||||||
return "", fmt.Errorf("chatgpt error: cannot parse response")
|
return "", fmt.Errorf("chatgpt error: cannot parse response")
|
||||||
} else if data.Error.Message != "" {
|
} 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
|
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;
|
children: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
acceptHtml?: boolean;
|
acceptHtml?: boolean;
|
||||||
|
codeStyle?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function doAction(dispatch: AppDispatch, url: string): boolean {
|
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 dispatch = useDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -144,11 +150,11 @@ function MarkdownContent({ children, className, acceptHtml }: MarkdownProps) {
|
|||||||
PreTag="div"
|
PreTag="div"
|
||||||
wrapLongLines={true}
|
wrapLongLines={true}
|
||||||
wrapLines={true}
|
wrapLines={true}
|
||||||
className={`code-block`}
|
className={cn("code-block", codeStyle)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<code className={`code-inline ${className}`} {...props}>
|
<code className={cn("code-inline", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</code>
|
</code>
|
||||||
);
|
);
|
||||||
@ -158,17 +164,27 @@ function MarkdownContent({ children, className, acceptHtml }: MarkdownProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Markdown(props: MarkdownProps) {
|
function Markdown({ children, ...props }: MarkdownProps) {
|
||||||
// memoize the component
|
// memoize the component
|
||||||
const { children, className, acceptHtml } = props;
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => (
|
() => <MarkdownContent {...props}>{children}</MarkdownContent>,
|
||||||
<MarkdownContent className={className} acceptHtml={acceptHtml}>
|
[props, children],
|
||||||
{children}
|
|
||||||
</MarkdownContent>
|
|
||||||
),
|
|
||||||
[props.children, props.className, props.acceptHtml],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
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 { saveAsFile, saveBlobAsFile } from "@/utils/dom.ts";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
|
import FileViewer from "@/components/FileViewer.tsx";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* file format:
|
* file format:
|
||||||
@ -37,7 +38,7 @@ export function parseFile(data: string, acceptDownload?: boolean) {
|
|||||||
<File className={`mr-1`} />
|
<File className={`mr-1`} />
|
||||||
<span className={`name`}>{filename}</span>
|
<span className={`name`}>{filename}</span>
|
||||||
<div className={`grow`} />
|
<div className={`grow`} />
|
||||||
{image && (
|
{image ? (
|
||||||
<Button
|
<Button
|
||||||
variant={`ghost`}
|
variant={`ghost`}
|
||||||
size={`icon`}
|
size={`icon`}
|
||||||
@ -49,6 +50,10 @@ export function parseFile(data: string, acceptDownload?: boolean) {
|
|||||||
>
|
>
|
||||||
<Download className={`cursor-pointer`} />
|
<Download className={`cursor-pointer`} />
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<FileViewer filename={filename} content={content}>
|
||||||
|
<Eye className={`cursor-pointer`} />
|
||||||
|
</FileViewer>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{image && <img src={image} className={`file-image`} alt={""} />}
|
{image && <img src={image} className={`file-image`} alt={""} />}
|
||||||
|
@ -241,6 +241,7 @@
|
|||||||
"confirm": "确认",
|
"confirm": "确认",
|
||||||
"percent": "{{cent}}折",
|
"percent": "{{cent}}折",
|
||||||
"file": {
|
"file": {
|
||||||
|
"file": "文件",
|
||||||
"upload": "上传文件",
|
"upload": "上传文件",
|
||||||
"type": "支持 pdf, docx, pptx, xlsx, 图像, 文本等格式",
|
"type": "支持 pdf, docx, pptx, xlsx, 图像, 文本等格式",
|
||||||
"drop": "拖拽文件到此处或点击上传",
|
"drop": "拖拽文件到此处或点击上传",
|
||||||
|
@ -207,7 +207,8 @@
|
|||||||
"empty-file": "Empty File",
|
"empty-file": "Empty File",
|
||||||
"empty-file-prompt": "File content is empty, has been automatically ignored",
|
"empty-file-prompt": "File content is empty, has been automatically ignored",
|
||||||
"large-file-success": "Parsing Successful",
|
"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": {
|
"generate": {
|
||||||
"title": "AI Project Generator",
|
"title": "AI Project Generator",
|
||||||
|
@ -207,7 +207,8 @@
|
|||||||
"empty-file": "コンテンツファイルがありません",
|
"empty-file": "コンテンツファイルがありません",
|
||||||
"empty-file-prompt": "ファイルの内容が空で、自動的に無視されます",
|
"empty-file-prompt": "ファイルの内容が空で、自動的に無視されます",
|
||||||
"large-file-success": "解析に成功しました",
|
"large-file-success": "解析に成功しました",
|
||||||
"large-file-success-prompt": "{{time}}秒で大きなファイルが正常に解析されました"
|
"large-file-success-prompt": "{{time}}秒で大きなファイルが正常に解析されました",
|
||||||
|
"file": "ファイル"
|
||||||
},
|
},
|
||||||
"generate": {
|
"generate": {
|
||||||
"title": "AIプロジェクトビルダー",
|
"title": "AIプロジェクトビルダー",
|
||||||
|
@ -207,7 +207,8 @@
|
|||||||
"empty-file": "Пустой файл",
|
"empty-file": "Пустой файл",
|
||||||
"empty-file-prompt": "Содержимое файла пустое, автоматически проигнорировано",
|
"empty-file-prompt": "Содержимое файла пустое, автоматически проигнорировано",
|
||||||
"large-file-success": "Синтаксический анализ успешно завершен",
|
"large-file-success": "Синтаксический анализ успешно завершен",
|
||||||
"large-file-success-prompt": "Большой файл успешно проанализирован за {{time}} секунды"
|
"large-file-success-prompt": "Большой файл успешно проанализирован за {{time}} секунды",
|
||||||
|
"file": "Документ"
|
||||||
},
|
},
|
||||||
"generate": {
|
"generate": {
|
||||||
"title": "Генератор AI проектов",
|
"title": "Генератор AI проектов",
|
||||||
|
@ -39,6 +39,10 @@ services:
|
|||||||
links:
|
links:
|
||||||
- mysql
|
- mysql
|
||||||
- redis
|
- redis
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 65535
|
||||||
|
hard: 65535
|
||||||
environment:
|
environment:
|
||||||
MYSQL_HOST: mysql
|
MYSQL_HOST: mysql
|
||||||
MYSQL_USER: chatnio
|
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) {
|
func (c *Connection) Handle(handler func(*conversation.FormMessage) error) {
|
||||||
defer c.conn.DeferClose()
|
|
||||||
|
|
||||||
go c.Process(handler)
|
go c.Process(handler)
|
||||||
c.ReadWorker()
|
c.ReadWorker()
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,7 @@ func ChatAPI(c *gin.Context) {
|
|||||||
if conn = utils.NewWebsocket(c, false); conn == nil {
|
if conn = utils.NewWebsocket(c, false); conn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer conn.DeferClose()
|
||||||
|
|
||||||
db := utils.GetDBFromContext(c)
|
db := utils.GetDBFromContext(c)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user