Merge pull request #298 from n4ze3m/next

v1.4.3
This commit is contained in:
Muhammed Nazeem 2025-01-19 13:25:54 +05:30 committed by GitHub
commit 17dc6a68c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 292 additions and 13 deletions

View File

@ -21,6 +21,9 @@
"copilotResumeLastChat": {
"label": "استئناف آخر محادثة عند فتح اللوحة الجانبية (كوبيلوت)"
},
"turnOnChatWithWebsite": {
"label": "تمكين الدردشة مع الموقع بشكل افتراضي (كوبيلوت)"
},
"webUIResumeLastChat": {
"label": "استئناف آخر محادثة عند فتح واجهة المستخدم"
},

View File

@ -21,6 +21,9 @@
"copilotResumeLastChat": {
"label": "Letzten Chat beim Öffnen des Seitenpanels fortsetzen (Copilot)"
},
"turnOnChatWithWebsite": {
"label": "Chat mit Website standardmäßig aktivieren (Copilot)"
},
"webUIResumeLastChat": {
"label": "Letzten Chat beim Öffnen der Web-UI fortsetzen"
},

View File

@ -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"
}

View File

@ -21,6 +21,9 @@
"copilotResumeLastChat": {
"label": "Resume the last chat when opening the SidePanel (Copilot)"
},
"turnOnChatWithWebsite": {
"label": "Enable Chat with Website by default (Copilot)"
},
"webUIResumeLastChat": {
"label": "Resume the last chat when opening the Web UI"
},

View File

@ -21,6 +21,9 @@
"copilotResumeLastChat": {
"label": "Retomar el último chat al abrir el Panel Lateral (Copilot)"
},
"turnOnChatWithWebsite": {
"label": "Habilitar Chat con Sitio Web por defecto (Copilot)"
},
"webUIResumeLastChat": {
"label": "Retomar el último chat al abrir la Interfaz Web"
},

View File

@ -21,6 +21,9 @@
"copilotResumeLastChat": {
"label": "آخرین گفتگو را هنگام باز کردن SidePanel (Copilot) از سر بگیر"
},
"turnOnChatWithWebsite": {
"label": "فعال کردن گپ با وب سایت به صورت پیش‌فرض (کوپایلوت)"
},
"hideCurrentChatModelSettings": {
"label": "مخفی کردن تنظیمات مدل گپ فعلی را"
},

View File

@ -21,6 +21,9 @@
"copilotResumeLastChat": {
"label": "Reprendre la dernière conversation lors de l'ouverture du sidepanel (Copilot)"
},
"turnOnChatWithWebsite": {
"label": "Activer le chat avec le site Web par défaut (Copilot)"
},
"webUIResumeLastChat": {
"label": "Reprendre la dernière conversation lors de l'ouverture de l'interface Web"
},

View File

@ -21,6 +21,9 @@
"copilotResumeLastChat": {
"label": "Riprendi l'ultima chat quando apri il Pannello Laterale (Copilot)"
},
"turnOnChatWithWebsite": {
"label": "Abilita Chat con il Sito Web per impostazione predefinita (Copilot)"
},
"webUIResumeLastChat": {
"label": "Riprendi l'ultima chat quando apri l'interfaccia Web"
},

View File

@ -24,6 +24,9 @@
"copilotResumeLastChat": {
"label": "サイドパネルを開いたときに最後のチャットを再開 (Copilot)"
},
"turnOnChatWithWebsite": {
"label": "デフォルトでウェブサイトとのチャットを有効にする (Copilot)"
},
"webUIResumeLastChat": {
"label": "Web UIを開いたときに最後のチャットを再開"
},

View File

@ -24,6 +24,9 @@
"copilotResumeLastChat": {
"label": "사이드 패널을 열 때 마지막 채팅 재개 (Copilot)"
},
"turnOnChatWithWebsite": {
"label": "웹사이트와의 채팅 기본 활성화 (Copilot)"
},
"webUIResumeLastChat": {
"label": "웹 UI를 열 때 마지막 채팅 재개"
},

View File

@ -24,6 +24,9 @@
"copilotResumeLastChat": {
"label": "സൈഡ്പാനൽ തുറക്കുമ്പോൾ അവസാനത്തെ ചാറ്റ് പുനരാരംഭിക്കുക (Copilot)"
},
"turnOnChatWithWebsite": {
"label": "സ്ഥിരസ്ഥിതിയായി വെബ്സൈറ്റുമായുള്ള ചാറ്റ് പ്രവർത്തനക്ഷമമാക്കുക (കോപൈലറ്റ്)"
},
"webUIResumeLastChat": {
"label": "വെബ് UI തുറക്കുമ്പോൾ അവസാനത്തെ ചാറ്റ് പുനരാരംഭിക്കുക"
},

View File

@ -21,6 +21,9 @@
"copilotResumeLastChat": {
"label": "Gjenoppta siste chat ved åpning av SidePanel (copilot)"
},
"turnOnChatWithWebsite": {
"label": "Aktiver Chat med Nettsted som standard (Copilot)"
},
"webUIResumeLastChat": {
"label": "Gjenoppta siste chat når Web UI åpnes"
},

View File

@ -21,6 +21,9 @@
"copilotResumeLastChat": {
"label": "Retomar o último chat ao abrir o Painel Lateral (Copilot)"
},
"turnOnChatWithWebsite": {
"label": "Ativar Chat com o Site por padrão (Copilot)"
},
"webUIResumeLastChat": {
"label": "Retomar o último chat ao abrir a Interface Web"
},

View File

@ -21,6 +21,9 @@
"copilotResumeLastChat": {
"label": "Возобновить последний чат при открытии боковой панели (Copilot)"
},
"turnOnChatWithWebsite": {
"label": "Включить чат с веб-сайтом по умолчанию (Copilot)"
},
"webUIResumeLastChat": {
"label": "Возобновить последний чат при открытии веб-интерфейса"
},

View File

@ -21,6 +21,9 @@
"copilotResumeLastChat": {
"label": "Återuppta den senaste chatten när du öppnar sidopanelen (Copilot)"
},
"turnOnChatWithWebsite": {
"label": "Aktivera Chatta med Webbplats som standard (Copilot)"
},
"webUIResumeLastChat": {
"label": "Återuppta den senaste chatten när du öppnar webbgränssnittet"
},

View File

@ -21,6 +21,9 @@
"copilotResumeLastChat": {
"label": "Поновити останню розмову при відкритті бічної панелі (Copilot)"
},
"turnOnChatWithWebsite": {
"label": "Увімкнути чат з веб-сайтом за замовчуванням (Copilot)"
},
"webUIResumeLastChat": {
"label": "Поновити останню розмову при відкритті веб-інтерфейсу"
},

View File

@ -24,6 +24,9 @@
"copilotResumeLastChat": {
"label": "打开侧边栏时恢复上次聊天Copilot"
},
"turnOnChatWithWebsite": {
"label": "默认启用与网站对话Copilot"
},
"webUIResumeLastChat": {
"label": "打开Web UI时恢复上次聊天"
},

View File

@ -9,6 +9,7 @@ import { OpenRouterIcon } from "../Icons/OpenRouter"
import { LLamaFile } from "../Icons/Llamafile"
import { GeminiIcon } from "../Icons/GeminiIcon"
import { MistarlIcon } from "../Icons/Mistral"
import { DeepSeekIcon } from "../Icons/DeepSeek"
export const ProviderIcons = ({
provider,
@ -40,6 +41,8 @@ export const ProviderIcons = ({
return <GeminiIcon className={className} />
case "mistral":
return <MistarlIcon className={className} />
case "deepseek":
return <DeepSeekIcon className={className} />
default:
return <OllamaIcon className={className} />
}

View File

@ -0,0 +1,19 @@
import React from "react"
export const DeepSeekIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
fill="currentColor"
fillRule="evenodd"
ref={ref}
style={{ flex: "none", lineHeight: 1 }}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" />
</svg>
)
})

View File

@ -3,13 +3,15 @@ import {
FileText,
Share2,
FileJson,
FileCode
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[]
@ -24,11 +26,10 @@ const formatAsText = (messages: Message[]) => {
})
.join("\n\n")
}
const formatAsMarkdown = (messages: Message[]) => {
return messages
.map((msg) => {
let content = `**${msg.isBot ? msg.name : "You"}**:\n${msg.message}`
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
@ -55,6 +56,161 @@ 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 ? 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,
@ -118,6 +274,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()
}
}
]
}

View File

@ -31,6 +31,10 @@ export const GeneralSettings = () => {
"webUIResumeLastChat",
false
)
const [defaultChatWithWebsite, setDefaultChatWithWebsite] = useStorage(
"defaultChatWithWebsite",
false
)
const [restoreLastChatModel, setRestoreLastChatModel] = useStorage(
"restoreLastChatModel",
@ -118,6 +122,17 @@ export const GeneralSettings = () => {
onChange={(checked) => setCopilotResumeLastChat(checked)}
/>
</div>
<div className="flex flex-row justify-between">
<div className="inline-flex items-center gap-2">
<span className="text-gray-700 dark:text-neutral-50">
{t("generalSettings.settings.turnOnChatWithWebsite.label")}
</span>
</div>
<Switch
checked={defaultChatWithWebsite}
onChange={(checked) => setDefaultChatWithWebsite(checked)}
/>
</div>
<div className="flex flex-row justify-between">
<div className="inline-flex items-center gap-2">
<span className="text-gray-700 dark:text-neutral-50">

View File

@ -134,7 +134,8 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
speechToTextLanguage,
useOCR,
setUseOCR,
defaultInternetSearchOn
defaultInternetSearchOn,
defaultChatWithWebsite
} = useMessage()
React.useEffect(() => {
@ -200,6 +201,10 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
setWebSearch(true)
}
if (defaultChatWithWebsite) {
setChatMode("rag")
}
return () => {
textareaRef.current?.removeEventListener("drop", handleDrop)
textareaRef.current?.removeEventListener("dragover", handleDragOver)
@ -212,6 +217,12 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
}
}, [defaultInternetSearchOn])
React.useEffect(() => {
if (defaultChatWithWebsite) {
setChatMode("rag")
}
}, [defaultChatWithWebsite])
return (
<div className="px-3 pt-3 md:px-6 md:pt-6 bg-white dark:bg-[#262626] border rounded-t-xl border-gray-300 dark:border-gray-600">
<div

View File

@ -59,6 +59,11 @@ export const useMessage = () => {
"defaultInternetSearchOn",
false
)
const [
defaultChatWithWebsite,
] = useStorage("defaultChatWithWebsite", false)
const [chatWithWebsiteEmbedding] = useStorage(
"chatWithWebsiteEmbedding",
true
@ -113,6 +118,9 @@ export const useMessage = () => {
if(defaultInternetSearchOn) {
setWebSearch(true)
}
if(defaultChatWithWebsite) {
setChatMode("rag")
}
}
const chatWithWebsiteMode = async (
@ -1721,6 +1729,7 @@ export const useMessage = () => {
setSpeechToTextLanguage,
useOCR,
setUseOCR,
defaultInternetSearchOn
defaultInternetSearchOn,
defaultChatWithWebsite
}
}

View File

@ -53,5 +53,10 @@ export const OAI_API_PROVIDERS = [
label: "Mistral",
value: "mistral",
baseUrl: "https://api.mistral.ai/v1"
},
{
label: "DeepSeek",
value: "deepseek",
baseUrl: "https://api.deepseek.com"
}
]

View File

@ -51,7 +51,7 @@ export default defineConfig({
outDir: "build",
manifest: {
version: "1.4.2",
version: "1.4.3",
name:
process.env.TARGET === "firefox"
? "Page Assist - A Web UI for Local AI Models"