import { MoreHorizontal, FileText, Share2, FileJson, FileCode, ImageIcon } from "lucide-react" import { Dropdown, MenuProps, message } from "antd" import { Message } from "@/types/message" import { useState } from "react" import { ShareModal } from "../Common/ShareModal" import { useTranslation } from "react-i18next" import { removeModelSuffix } from "@/db/models" interface MoreOptionsProps { messages: Message[] historyId: string shareModeEnabled: boolean } const formatAsText = (messages: Message[]) => { return messages .map((msg) => { const text = `${msg.isBot ? msg.name : "You"}: ${msg.message}` return text }) .join("\n\n") } const formatAsMarkdown = (messages: Message[]) => { return messages .map((msg) => { let content = `**${msg.isBot ? removeModelSuffix(msg.name?.replaceAll(/accounts\/[^\/]+\/models\//g, "")) : "You"}**:\n${msg.message}` if (msg.images && msg.images.length > 0) { const imageMarkdown = msg.images .filter((img) => img.length > 0) .map((img) => `\n\n![Image](${img})`) .join("\n") content += imageMarkdown } return content }) .join("\n\n") } const downloadFile = (content: string, filename: string) => { const blob = new Blob([content], { type: "text/plain;charset=utf-8" }) const url = URL.createObjectURL(blob) const link = document.createElement("a") link.href = url link.download = filename document.body.appendChild(link) link.click() document.body.removeChild(link) 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 ? removeModelSuffix(msg.name?.replaceAll(/accounts\/[^\/]+\/models\//g, "")) : "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, messages }: MoreOptionsProps) => { const { t } = useTranslation("option") const [onShareOpen, setOnShareOpen] = useState(false) const baseItems: MenuProps["items"] = [ { type: "group", label: t("more.copy.group"), children: [ { key: "copy-text", label: t("more.copy.asText"), icon: , onClick: () => { navigator.clipboard.writeText(formatAsText(messages)) message.success(t("more.copy.success")) } }, { key: "copy-markdown", label: t("more.copy.asMarkdown"), icon: , onClick: () => { navigator.clipboard.writeText(formatAsMarkdown(messages)) message.success(t("more.copy.success")) } } ] }, { type: "divider" }, { type: "group", label: t("more.download.group"), children: [ { key: "download-txt", label: t("more.download.text"), icon: , onClick: () => { downloadFile(formatAsText(messages), "chat.txt") } }, { key: "download-md", label: t("more.download.markdown"), icon: , onClick: () => { downloadFile(formatAsMarkdown(messages), "chat.md") } }, { key: "download-json", label: t("more.download.json"), icon: , onClick: () => { const jsonContent = JSON.stringify(messages, null, 2) downloadFile(jsonContent, "chat.json") } }, { key: "download-image", label: t("more.download.image"), icon: , onClick: async () => { const dataUrl = await generateChatImage(messages) const link = document.createElement("a") link.download = "chat.png" link.href = dataUrl link.click() } } ] } ] const shareItem = { type: "divider" } as const const shareOption = { key: "share", label: t("more.share"), icon: , onClick: () => { setOnShareOpen(true) } } const items = shareModeEnabled ? [...baseItems, shareItem, shareOption] : baseItems return ( <> ) }