feat: Add option to download chat as an image

This commit is contained in:
n4ze3m 2025-01-18 17:13:20 +05:30
parent c1de76b660
commit aacade6c84
2 changed files with 177 additions and 7 deletions

View File

@ -21,7 +21,8 @@
"group": "Download", "group": "Download",
"text": "Text File (.txt)", "text": "Text File (.txt)",
"markdown": "Markdown (.md)", "markdown": "Markdown (.md)",
"json": "JSON File (.json)" "json": "JSON File (.json)",
"image": "Image (.png)"
}, },
"share": "Share" "share": "Share"
} }

View File

@ -3,7 +3,8 @@ import {
FileText, FileText,
Share2, Share2,
FileJson, FileJson,
FileCode FileCode,
ImageIcon
} from "lucide-react" } from "lucide-react"
import { Dropdown, MenuProps, message } from "antd" import { Dropdown, MenuProps, message } from "antd"
import { Message } from "@/types/message" import { Message } from "@/types/message"
@ -55,6 +56,162 @@ const downloadFile = (content: string, filename: string) => {
URL.revokeObjectURL(url) 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 = ({ export const MoreOptions = ({
shareModeEnabled = false, shareModeEnabled = false,
historyId, historyId,
@ -65,7 +222,7 @@ export const MoreOptions = ({
const baseItems: MenuProps["items"] = [ const baseItems: MenuProps["items"] = [
{ {
type: "group", type: "group",
label: t("more.copy.group"), label: t("more.copy.group"),
children: [ children: [
{ {
key: "copy-text", key: "copy-text",
@ -73,12 +230,12 @@ export const MoreOptions = ({
icon: <FileText className="w-4 h-4" />, icon: <FileText className="w-4 h-4" />,
onClick: () => { onClick: () => {
navigator.clipboard.writeText(formatAsText(messages)) navigator.clipboard.writeText(formatAsText(messages))
message.success(t("more.copy.success")) message.success(t("more.copy.success"))
} }
}, },
{ {
key: "copy-markdown", key: "copy-markdown",
label: t("more.copy.asMarkdown"), label: t("more.copy.asMarkdown"),
icon: <FileCode className="w-4 h-4" />, icon: <FileCode className="w-4 h-4" />,
onClick: () => { onClick: () => {
navigator.clipboard.writeText(formatAsMarkdown(messages)) navigator.clipboard.writeText(formatAsMarkdown(messages))
@ -92,7 +249,7 @@ export const MoreOptions = ({
}, },
{ {
type: "group", type: "group",
label: t("more.download.group"), label: t("more.download.group"),
children: [ children: [
{ {
key: "download-txt", key: "download-txt",
@ -118,6 +275,18 @@ export const MoreOptions = ({
const jsonContent = JSON.stringify(messages, null, 2) const jsonContent = JSON.stringify(messages, null, 2)
downloadFile(jsonContent, "chat.json") 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()
}
} }
] ]
} }