feat: Add option to download chat as an image
This commit is contained in:
parent
c1de76b660
commit
aacade6c84
@ -21,7 +21,8 @@
|
||||
"group": "Download",
|
||||
"text": "Text File (.txt)",
|
||||
"markdown": "Markdown (.md)",
|
||||
"json": "JSON File (.json)"
|
||||
"json": "JSON File (.json)",
|
||||
"image": "Image (.png)"
|
||||
},
|
||||
"share": "Share"
|
||||
}
|
||||
|
@ -3,7 +3,8 @@ import {
|
||||
FileText,
|
||||
Share2,
|
||||
FileJson,
|
||||
FileCode
|
||||
FileCode,
|
||||
ImageIcon
|
||||
} from "lucide-react"
|
||||
import { Dropdown, MenuProps, message } from "antd"
|
||||
import { Message } from "@/types/message"
|
||||
@ -55,6 +56,162 @@ const downloadFile = (content: string, filename: string) => {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const generateChatImage = async (messages: Message[]) => {
|
||||
const canvas = document.createElement("canvas")
|
||||
const ctx = canvas.getContext("2d")!
|
||||
|
||||
canvas.width = 1200
|
||||
const padding = 40
|
||||
let yPosition = padding
|
||||
|
||||
const wrapText = (text: string, maxWidth: number) => {
|
||||
const paragraphs = text.split("\n")
|
||||
const lines = []
|
||||
|
||||
paragraphs.forEach((paragraph) => {
|
||||
if (paragraph.length === 0) {
|
||||
lines.push("")
|
||||
return
|
||||
}
|
||||
|
||||
const words = paragraph.split(" ")
|
||||
let currentLine = words[0]
|
||||
|
||||
for (let i = 1; i < words.length; i++) {
|
||||
const word = words[i]
|
||||
const width = ctx.measureText(currentLine + " " + word).width
|
||||
if (width < maxWidth) {
|
||||
currentLine += " " + word
|
||||
} else {
|
||||
lines.push(currentLine)
|
||||
currentLine = word
|
||||
}
|
||||
}
|
||||
lines.push(currentLine)
|
||||
})
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
let totalHeight = padding
|
||||
messages.forEach((msg) => {
|
||||
totalHeight += 20
|
||||
const maxWidth = canvas.width - padding * 2
|
||||
|
||||
if (msg.message.includes("```")) {
|
||||
const blocks = msg.message.split("```")
|
||||
blocks.forEach((block, index) => {
|
||||
if (index % 2 === 1) {
|
||||
const codeLines = block.split("\n")
|
||||
totalHeight += codeLines.length * 25 + 20
|
||||
} else {
|
||||
const wrappedText = wrapText(block, maxWidth)
|
||||
totalHeight += wrappedText.length * 25
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const wrappedText = wrapText(msg.message, maxWidth)
|
||||
totalHeight += wrappedText.length * 25
|
||||
}
|
||||
|
||||
if (msg.images?.length) {
|
||||
totalHeight += msg.images.length * 250
|
||||
}
|
||||
|
||||
totalHeight += 30
|
||||
|
||||
})
|
||||
|
||||
canvas.height = totalHeight
|
||||
console.log(totalHeight)
|
||||
|
||||
ctx.fillStyle = "#ffffff"
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
const drawText = async () => {
|
||||
for (const msg of messages) {
|
||||
ctx.font = "bold 18px Inter, Arial"
|
||||
ctx.fillStyle = msg.isBot ? "#1A202C" : "#1E4E8C"
|
||||
ctx.fillText(`${msg.isBot ? msg.name : "You"}:`, padding, yPosition)
|
||||
yPosition += 35
|
||||
|
||||
if (msg.message.includes("```")) {
|
||||
const blocks = msg.message.split("```")
|
||||
blocks.forEach((block, index) => {
|
||||
if (index % 2 === 1) {
|
||||
const codeLines = block.split("\n")
|
||||
const codeHeight = codeLines.length * 25 + 20
|
||||
ctx.fillStyle = "#1a1a1a"
|
||||
ctx.fillRect(
|
||||
padding,
|
||||
yPosition,
|
||||
canvas.width - padding * 2,
|
||||
codeHeight
|
||||
)
|
||||
ctx.font = "15px Consolas, monospace"
|
||||
ctx.fillStyle = "#e6e6e6"
|
||||
codeLines.forEach((line, lineIndex) => {
|
||||
ctx.fillText(line, padding + 15, yPosition + 25 + lineIndex * 25)
|
||||
})
|
||||
yPosition += codeHeight + 20
|
||||
} else {
|
||||
ctx.font = "16px Inter, Arial"
|
||||
ctx.fillStyle = "#1A202C"
|
||||
const wrappedText = wrapText(block, canvas.width - padding * 2)
|
||||
wrappedText.forEach((line) => {
|
||||
ctx.fillText(line, padding, yPosition)
|
||||
yPosition += 30
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
ctx.font = "16px Inter, Arial"
|
||||
ctx.fillStyle = "#1A202C"
|
||||
const wrappedText = wrapText(msg.message, canvas.width - padding * 2)
|
||||
wrappedText.forEach((line) => {
|
||||
ctx.fillText(line, padding, yPosition)
|
||||
yPosition += 30
|
||||
})
|
||||
}
|
||||
|
||||
if (msg.images?.length) {
|
||||
for (const imgUrl of msg.images) {
|
||||
if (imgUrl) {
|
||||
try {
|
||||
const img = new Image()
|
||||
img.crossOrigin = "anonymous"
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve
|
||||
img.onerror = reject
|
||||
img.src = imgUrl
|
||||
})
|
||||
|
||||
const maxWidth = canvas.width - padding * 2
|
||||
const maxHeight = 100
|
||||
const scale = Math.min(
|
||||
maxWidth / img.width,
|
||||
maxHeight / img.height,
|
||||
0.5
|
||||
)
|
||||
const drawWidth = img.width * scale
|
||||
const drawHeight = img.height * scale
|
||||
|
||||
ctx.drawImage(img, padding, yPosition + 10, drawWidth, drawHeight)
|
||||
yPosition += drawHeight + 30
|
||||
} catch (e) {
|
||||
console.warn("Failed to load image:", imgUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
yPosition += 30
|
||||
}
|
||||
}
|
||||
|
||||
await drawText()
|
||||
return canvas.toDataURL("image/png")
|
||||
}
|
||||
|
||||
export const MoreOptions = ({
|
||||
shareModeEnabled = false,
|
||||
historyId,
|
||||
@ -118,6 +275,18 @@ export const MoreOptions = ({
|
||||
const jsonContent = JSON.stringify(messages, null, 2)
|
||||
downloadFile(jsonContent, "chat.json")
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "download-image",
|
||||
label: t("more.download.image"),
|
||||
icon: <ImageIcon className="w-4 h-4" />,
|
||||
onClick: async () => {
|
||||
const dataUrl = await generateChatImage(messages)
|
||||
const link = document.createElement("a")
|
||||
link.download = "chat.png"
|
||||
link.href = dataUrl
|
||||
link.click()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user