mirror of
https://github.com/coaidev/coai.git
synced 2025-05-30 18:30:32 +09:00
feat: support generate image and save image
This commit is contained in:
parent
304e071718
commit
b444748113
@ -10,10 +10,6 @@
|
||||
<meta name="author" content="Deeptrain Team">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta itemprop="image" content="/favicon.ico">
|
||||
<meta name="baidu-site-verification" content="codeva-TJkbi40ZBi" />
|
||||
<link href="https://open.lightxi.com/fonts/Andika" rel="stylesheet">
|
||||
<link href="https://open.lightxi.com/fonts/Jetbrains-Mono" rel="stylesheet">
|
||||
<link href="https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/katex.min.css" rel="stylesheet">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<script src="/workbox.js" defer></script>
|
||||
</head>
|
||||
|
@ -39,6 +39,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"html-to-image": "^1.11.11",
|
||||
"i18next": "^23.4.6",
|
||||
"localforage": "^1.10.0",
|
||||
"lucide-react": "^0.309.0",
|
||||
|
7
app/pnpm-lock.yaml
generated
7
app/pnpm-lock.yaml
generated
@ -86,6 +86,9 @@ dependencies:
|
||||
cmdk:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||
html-to-image:
|
||||
specifier: ^1.11.11
|
||||
version: 1.11.11
|
||||
i18next:
|
||||
specifier: ^23.4.6
|
||||
version: 23.6.0
|
||||
@ -3683,6 +3686,10 @@ packages:
|
||||
void-elements: 3.1.0
|
||||
dev: false
|
||||
|
||||
/html-to-image@1.11.11:
|
||||
resolution: {integrity: sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==}
|
||||
dev: false
|
||||
|
||||
/html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
dev: false
|
||||
|
2
app/src/assets/fonts/all.less
Normal file
2
app/src/assets/fonts/all.less
Normal file
@ -0,0 +1,2 @@
|
||||
@import "common";
|
||||
@import "katex";
|
14
app/src/assets/fonts/common.less
Normal file
14
app/src/assets/fonts/common.less
Normal file
@ -0,0 +1,14 @@
|
||||
/* Copyright Jetbrains Mono https://www.jetbrains.com/lp/mono/ */
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url(https://open.lightxi.com/gstatic/JetBrainsMono-Regular.woff2) format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Andika';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://open.lightxi.com/gstatic/s/andika/v25/mem_Ya6iyW-LwqgwarYV.ttf) format('truetype');
|
||||
}
|
1078
app/src/assets/fonts/katex.less
Normal file
1078
app/src/assets/fonts/katex.less
Normal file
File diff suppressed because it is too large
Load Diff
@ -42,6 +42,102 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sharing-screenshot {
|
||||
position: absolute;
|
||||
z-index: -999;
|
||||
height: max-content;
|
||||
width: 100vw;
|
||||
background: hsl(var(--background));
|
||||
|
||||
.shot-body {
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
padding: 1.5rem;
|
||||
|
||||
.shot-wrapper {
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
|
||||
.shot-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
font-family: var(--font-family) !important;
|
||||
|
||||
.shot-column {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.shot-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: 0.3rem;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.shot-avatar {
|
||||
width: 1.25rem !important;
|
||||
height: 1.25rem !important;
|
||||
border-radius: 4px !important;
|
||||
transform: translateY(1px);
|
||||
margin-right: 0.25rem;
|
||||
|
||||
p {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.shot-label {
|
||||
font-size: 0.95rem;
|
||||
color: hsl(var(--text-secondary));
|
||||
margin-right: 0.25rem;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-family) !important;
|
||||
}
|
||||
|
||||
.shot-value {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin: 0 0.25rem;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-family) !important;
|
||||
}
|
||||
|
||||
.shot-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
padding: 0.35rem 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shot-content {
|
||||
padding: 2rem;
|
||||
|
||||
& > * {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sharing-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@ -53,6 +149,7 @@
|
||||
border-radius: var(--radius);
|
||||
width: 80vw;
|
||||
height: 70vh;
|
||||
background: hsl(var(--background));
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
|
@ -1,3 +1,5 @@
|
||||
@import "fonts/all";
|
||||
|
||||
.gold-text {
|
||||
color: hsl(var(--gold)) !important;
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ function Avatar({ username, ...props }: AvatarProps) {
|
||||
return useDeeptrain ? (
|
||||
<img {...props} src={`${deeptrainApiEndpoint}/avatar/${username}`} alt="" />
|
||||
) : (
|
||||
<div {...props} className={cn("avatar", background)}>
|
||||
<div {...props} className={cn("avatar", background, props.className)}>
|
||||
<p className={`text-white`}>{code}</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -144,12 +144,24 @@
|
||||
"message": {
|
||||
"copy": "复制消息",
|
||||
"save": "保存为文件",
|
||||
"save-image": "保存图片",
|
||||
"use": "使用消息",
|
||||
"edit": "编辑消息",
|
||||
"stop": "暂停回答",
|
||||
"remove": "删除消息",
|
||||
"restart": "重新回答",
|
||||
"copy-area": "复制选中区域"
|
||||
"copy-area": "复制选中区域",
|
||||
"saving-image-prompt": "图片生成中",
|
||||
"saving-image-prompt-desc": "正在生成图片中,请稍等...",
|
||||
"saving-image-failed": "图片生成失败",
|
||||
"saving-image-failed-prompt": "图片生成失败,原因:{{reason}}",
|
||||
"saving-image-success": "图片生成成功",
|
||||
"saving-image-success-prompt": "图片已成功保存。",
|
||||
"sharing": {
|
||||
"title": "标题",
|
||||
"time": "时间",
|
||||
"message": "消息"
|
||||
}
|
||||
},
|
||||
"quota-description": "消息的点数支出",
|
||||
"buy": {
|
||||
|
@ -97,7 +97,19 @@
|
||||
"restart": "Restart Answer",
|
||||
"copy-area": "Copy Selected Area",
|
||||
"edit": "Edit messages",
|
||||
"remove": "Delete a Message"
|
||||
"remove": "Delete a Message",
|
||||
"save-image": "Save image",
|
||||
"saving-image-prompt": "Image Generation in Progress",
|
||||
"saving-image-prompt-desc": "Generating image, please wait...",
|
||||
"saving-image-failed": "Image generation failed",
|
||||
"saving-image-failed-prompt": "Image generation failed for {{reason}}",
|
||||
"saving-image-success": "Image generated successfully",
|
||||
"saving-image-success-prompt": "Picture saved successfully.",
|
||||
"sharing": {
|
||||
"title": "Title",
|
||||
"time": "Time",
|
||||
"message": "NO NAME SPACE NO KEY VALUE!!"
|
||||
}
|
||||
},
|
||||
"quota-description": "spending quota for the message",
|
||||
"buy": {
|
||||
|
@ -97,7 +97,19 @@
|
||||
"restart": "再回答",
|
||||
"copy-area": "選択を複製",
|
||||
"edit": "メッセージを編集",
|
||||
"remove": "メッセージを削除"
|
||||
"remove": "メッセージを削除",
|
||||
"save-image": "写真を保存",
|
||||
"saving-image-prompt": "画像生成中",
|
||||
"saving-image-prompt-desc": "画像を生成しています。しばらくお待ちください...",
|
||||
"saving-image-failed": "画像の生成に失敗しました",
|
||||
"saving-image-failed-prompt": "{{reason}}のために画像生成に失敗しました",
|
||||
"saving-image-success": "画像が正常に生成されました",
|
||||
"saving-image-success-prompt": "画像が正常に保存されました。",
|
||||
"sharing": {
|
||||
"title": "タイトル",
|
||||
"time": "期日",
|
||||
"message": "メッセージ"
|
||||
}
|
||||
},
|
||||
"quota-description": "メッセージのクレジット使用額",
|
||||
"buy": {
|
||||
|
@ -97,7 +97,19 @@
|
||||
"restart": "Перезапустить ответ",
|
||||
"copy-area": "Копировать выбранную область",
|
||||
"edit": "Редактировать сообщение",
|
||||
"remove": "Удалить сообщение"
|
||||
"remove": "Удалить сообщение",
|
||||
"save-image": "Сохранить",
|
||||
"saving-image-prompt": "Выполняется создание изображения",
|
||||
"saving-image-prompt-desc": "Создание изображения, подождите...",
|
||||
"saving-image-failed": "Не удалось создать изображение",
|
||||
"saving-image-failed-prompt": "Не удалось создать изображение по {{reason}}",
|
||||
"saving-image-success": "Изображение успешно сгенерировано",
|
||||
"saving-image-success-prompt": "Изображение успешно сохранено.",
|
||||
"sharing": {
|
||||
"title": "заглавие",
|
||||
"time": "Время",
|
||||
"message": "Сообщения"
|
||||
}
|
||||
},
|
||||
"quota-description": "квота расходов на сообщение",
|
||||
"buy": {
|
||||
|
@ -1,10 +1,18 @@
|
||||
import "@/assets/pages/sharing.less";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { viewConversation, ViewData, ViewForm } from "@/api/sharing.ts";
|
||||
import { copyClipboard, saveAsFile } from "@/utils/dom.ts";
|
||||
import { copyClipboard, saveImageAsFile } from "@/utils/dom.ts";
|
||||
import { useEffectAsync } from "@/utils/hook.ts";
|
||||
import { useState } from "react";
|
||||
import { Copy, File, HelpCircle, Loader2, MessagesSquare } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
Clock,
|
||||
Copy,
|
||||
HelpCircle,
|
||||
Image,
|
||||
Loader2,
|
||||
MessagesSquare,
|
||||
Newspaper,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import MessageSegment from "@/components/Message.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
@ -13,6 +21,9 @@ import { useToast } from "@/components/ui/use-toast.ts";
|
||||
import { sharingEvent } from "@/events/sharing.ts";
|
||||
import { Message } from "@/api/types.ts";
|
||||
import Avatar from "@/components/Avatar.tsx";
|
||||
import { toJpeg } from "html-to-image";
|
||||
import { appLogo } from "@/conf/env.ts";
|
||||
import { extractMessage } from "@/utils/processor.ts";
|
||||
|
||||
type SharingFormProps = {
|
||||
refer?: string;
|
||||
@ -21,16 +32,84 @@ type SharingFormProps = {
|
||||
|
||||
function SharingForm({ refer, data }: SharingFormProps) {
|
||||
if (data === null) return null;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const date = new Date(data.time);
|
||||
const time = `${
|
||||
date.getMonth() + 1
|
||||
}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
|
||||
const value = JSON.stringify(data, null, 2);
|
||||
const { toast } = useToast();
|
||||
|
||||
const saveImage = async () => {
|
||||
toast({
|
||||
title: t("message.saving-image-prompt"),
|
||||
description: t("message.saving-image-prompt-desc"),
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!container.current) return;
|
||||
toJpeg(container.current)
|
||||
.then((blob) => {
|
||||
saveImageAsFile(`${extractMessage(data.name, 12)}.png`, blob);
|
||||
toast({
|
||||
title: t("message.saving-image-success"),
|
||||
description: t("message.saving-image-success-prompt"),
|
||||
});
|
||||
})
|
||||
.catch((reason) => {
|
||||
toast({
|
||||
title: t("message.saving-image-failed"),
|
||||
description: t("message.saving-image-failed-prompt", { reason }),
|
||||
});
|
||||
});
|
||||
}, 10);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`sharing-container`}>
|
||||
<div className={`sharing-screenshot`}>
|
||||
<div className={`shot-body`} ref={container}>
|
||||
<div className={`shot-wrapper`}>
|
||||
<div className={`shot-header`}>
|
||||
<div className={`shot-column`}>
|
||||
<div className={`shot-row`}>
|
||||
<Newspaper className={`shot-icon`} />
|
||||
<p className={`shot-label`}>{t("message.sharing.title")}</p>
|
||||
<div className={`grow`} />
|
||||
<p className={`shot-value`}>{data.name}</p>
|
||||
</div>
|
||||
<div className={`shot-row`}>
|
||||
<Clock className={`shot-icon`} />
|
||||
<p className={`shot-label`}>{t("message.sharing.time")}</p>
|
||||
<div className={`grow`} />
|
||||
<p className={`shot-value`}>{time}</p>
|
||||
</div>
|
||||
<div className={`shot-row`}>
|
||||
<MessagesSquare className={`shot-icon`} />
|
||||
<p className={`shot-label`}>{t("message.sharing.message")}</p>
|
||||
<div className={`grow`} />
|
||||
<p className={`shot-value`}>{data.messages.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`grow`} />
|
||||
<div className={`shot-column`}>
|
||||
<img className={`w-12 h-12 m-4`} src={appLogo} alt={""} />
|
||||
<div className={`shot-row`}>
|
||||
<Avatar username={data.username} className={`shot-avatar`} />
|
||||
<p className={`shot-value`}>{data.username}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`shot-content`}>
|
||||
{data.messages.map((message, i) => (
|
||||
<MessageSegment message={message} key={i} index={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`header`}>
|
||||
<div className={`user`}>
|
||||
<Avatar username={data.username} />
|
||||
@ -57,12 +136,9 @@ function SharingForm({ refer, data }: SharingFormProps) {
|
||||
<Copy className={`h-4 w-4 mr-2`} />
|
||||
{t("message.copy")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
onClick={() => saveAsFile("conversation.json", value)}
|
||||
>
|
||||
<File className={`h-4 w-4 mr-2`} />
|
||||
{t("message.save")}
|
||||
<Button variant={`outline`} onClick={saveImage}>
|
||||
<Image className={`h-4 w-4 mr-2`} />
|
||||
{t("message.save-image")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
|
@ -60,6 +60,21 @@ export function saveBlobAsFile(filename: string, blob: Blob) {
|
||||
a.click();
|
||||
}
|
||||
|
||||
export function saveImageAsFile(filename: string, data_url: string) {
|
||||
/**
|
||||
* Save data url as image file
|
||||
* @param filename Filename
|
||||
* @param data_url Data url
|
||||
* @example
|
||||
* saveImageAsFile("hello.png", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABwElEQVRIS+2VwQ3CMBBF/4f7B0Qf4B9
|
||||
*/
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = data_url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
}
|
||||
|
||||
export function getSelectionText(): string {
|
||||
/**
|
||||
* Get selected text
|
||||
|
Loading…
Reference in New Issue
Block a user