Merge pull request #129 from n4ze3m/next

v1.1.13
This commit is contained in:
Muhammed Nazeem 2024-06-26 12:58:14 +05:30 committed by GitHub
commit ba654f9574
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 846 additions and 124 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -21,6 +21,7 @@
"@langchain/community": "^0.0.41", "@langchain/community": "^0.0.41",
"@mantine/form": "^7.5.0", "@mantine/form": "^7.5.0",
"@mantine/hooks": "^7.5.3", "@mantine/hooks": "^7.5.3",
"@mozilla/readability": "^0.5.0",
"@plasmohq/storage": "^1.9.0", "@plasmohq/storage": "^1.9.0",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",

View File

@ -37,6 +37,9 @@
"totalSearchResults": { "totalSearchResults": {
"label": "Total Search Results", "label": "Total Search Results",
"placeholder": "Enter Total Search Results" "placeholder": "Enter Total Search Results"
},
"visitSpecificWebsite": {
"label": "Visit the website mentioned in the message"
} }
}, },
"system": { "system": {

View File

@ -0,0 +1,88 @@
{
"pageAssist": "Page Assist",
"selectAModel": "Selecione un Modelo",
"save": "Guardar",
"saved": "Guardado",
"cancel": "Cancelar",
"retry": "Reintentar",
"share": {
"tooltip": {
"share": "Compartir"
},
"modal": {
"title": "Compartir enlace para chat"
},
"form": {
"defaultValue": {
"name": "Anónimo",
"title": "Chat sin título"
},
"title": {
"label": "Título del Chat",
"placeholder": "Ingresar el título del Chat",
"required": "El título del Chat es obligatorio"
},
"name": {
"label": "Tu nombre",
"placeholder": "Ingresar tu nombre",
"required": "Tu nombre es obligatorio"
},
"btn": {
"save": "Generar enlace",
"saving": "Generando enlace..."
}
},
"notification": {
"successGenerate": "Enlace copiado al Clipboard",
"failGenerate": "Fallo al generar el enlace"
}
},
"copyToClipboard": "Copiar al clipboard",
"webSearch": "Buscando en la web",
"regenerate": "Regenerar",
"edit": "Editar",
"saveAndSubmit": "Guardar y Enviar",
"editMessage": {
"placeholder": "Ingresar un mensaje..."
},
"submit": "Enviar",
"noData": "Sin datos",
"noHistory": "Chat sin histórico",
"chatWithCurrentPage": "Conversar con la página actual",
"beta": "Beta",
"tts": "Leer en voz alta",
"currentChatModelSettings": "Configuraciones del Modelo de Chat Actual",
"modelSettings": {
"label": "Configuraciones del Modelo",
"description": "Definir las opciones del modelo globalmente para todos los chats",
"form": {
"keepAlive": {
"label": "Mantener vivo",
"help": "controlar cuanto tiempo el modelo permanecera cargado en la memoria luego de su utilización (por defecto: 5m)",
"placeholder": "Ingresar duración para mantenerlo vivo (ej: 5m, 10m, 1h)"
},
"temperature": {
"label": "Temperatura",
"placeholder": "Ingresar valor de la Temperatura (ej: 0.7, 1.0)"
},
"numCtx": {
"label": "Cantidad de contextos",
"placeholder": "Ingresar el valor de tamaño de la ventana de contexto (por defecto: 2048)"
},
"seed": {
"label": "Semilla",
"placeholder": "Ingresar el valor de la semilla (ej: 1234)",
"help": "Reproductibilidad de la salida del modelo"
},
"topK": {
"label": "Top K",
"placeholder": "Ingresar el valor de Top K (ej: 40, 100)"
},
"topP": {
"label": "Top P",
"placeholder": "Ingresar el valor de Top P (ej: 0.9, 0.95)"
}
},
"advanced": "Más Configuraciones del Modelo"
}
}

View File

@ -0,0 +1,42 @@
{
"addBtn": "Agregar Nuevo Conocimiento",
"columns": {
"title": "Título",
"status": "Estado",
"embeddings": "Modelo de Embedding",
"createdAt": "Creado",
"action": "Acciones"
},
"expandedColumns": {
"name": "Nombre"
},
"tooltip": {
"delete": "Borrar"
},
"confirm": {
"delete": "¿Esta seguro que desea borrar este conocimiento?"
},
"deleteSuccess": "Conocimiento borrado",
"status": {
"pending": "Pendiente",
"finished": "Finalizado",
"processing": "Procesando"
},
"addKnowledge": "Agregar Conocimiento",
"form": {
"title": {
"label": "Título del Conocimiento",
"placeholder": "Ingresar un título de conocimiento",
"required": "El Título de conocimiento es obligatorio"
},
"uploadFile": {
"label": "Subir un Archivo",
"uploadText": "Arraste y suelte un archivo aquí o haga click para subirlo",
"uploadHint": "Tipos de archivo soportados: .pdf, .csv, .txt, .md, .docx",
"required": "El archivo es obligatorio"
},
"submit": "Enviar",
"success": "Conocimiento agregado exitosamente"
},
"noEmbeddingModel": "Por favor, agregue un modelo de embedding de la página de configuraciones de RAG primero"
}

View File

@ -0,0 +1,12 @@
{
"newChat": "Nuevo Chat",
"selectAPrompt": "Selecione un Prompt",
"githubRepository": "Repositorio de GitHub",
"settings": "Configuraciones",
"sidebarTitle": "Histórico del Chat",
"error": "Error",
"somethingWentWrong": "Hubo un error",
"validationSelectModel": "Selecione un modelo para continuar",
"deleteHistoryConfirmation": "¿Esta seguro que quiere borrar éste histórico?",
"editHistoryTitle": "Ingrese un nuevo título"
}

View File

@ -0,0 +1,29 @@
{
"ollamaState": {
"searching": "Buscando tu Ollama 🦙",
"running": "Ollama está funcionando 🦙",
"notRunning": "No fue posible conectar con Ollama 🦙",
"connectionError": "Hubo un error de conexión. Por favor, consulte la <anchor>documentación</anchor> para solucionar el problema."
},
"formError": {
"noModel": "Por favor, selecione un modelo",
"noEmbeddingModel": "Por favor, defina un modelo de embedding para la página de configuraciones > RAG"
},
"form": {
"textarea": {
"placeholder": "Ingrese un mensaje..."
},
"webSearch": {
"on": "On",
"off": "Off"
}
},
"tooltip": {
"searchInternet": "Buscar en Internet",
"speechToText": "Voz a Texto",
"uploadImage": "Subir Imagén",
"stopStreaming": "Parar Transmisión",
"knowledge": "Conocimiento"
},
"sendWhenEnter": "Enviar cuando presione Enter"
}

View File

@ -0,0 +1,292 @@
{
"generalSettings": {
"title": "Configuraciones Generales",
"settings": {
"heading": "Configuraciones de la Interfaz Web",
"speechRecognitionLang": {
"label": "Idioma de Reconocimiento de Voz",
"placeholder": "Selecione un idioma"
},
"language": {
"label": "Idioma",
"placeholder": "Selecione un idioma"
},
"darkMode": {
"label": "Cambiar Tema",
"options": {
"light": "Claro",
"dark": "Oscuro"
}
},
"copilotResumeLastChat": {
"label": "Retomar el último chat al abrir el Panel Lateral (Copilot)"
},
"hideCurrentChatModelSettings": {
"label": "Ocultar Configuraciones del Modelo de Chat Actual"
}
},
"webSearch": {
"heading": "Manejo de la busqueda Web",
"searchMode": {
"label": "Realizar busquedas Simples en Internet"
},
"provider": {
"label": "Motor de Busqueda",
"placeholder": "Selecione un motor de busqueda"
},
"totalSearchResults": {
"label": "Resultados totales de la busqueda",
"placeholder": "Ingresar el total de Resultados de la busqueda"
},
"visitSpecificWebsite": {
"label": "Visita el sitio web mencionado en el mensaje"
}
},
"system": {
"heading": "Configuraciones del Sistema",
"deleteChatHistory": {
"label": "Borrar Histórico del Chat",
"button": "Borrar",
"confirm": "¿Esta seguro que desea borrar su histórico del chat? Esta acción no podra ser desecha."
},
"export": {
"label": "Exportar Histórico del Chat, Base de Conocimiento y Prompts",
"button": "Exportar Datos",
"success": "Exportación exitosa"
},
"import": {
"label": "Importar Histórico del Chat, Base de Conocimiento y Prompts",
"button": "Importar Datos",
"success": "Importación existosa",
"error": "Error de importación"
}
},
"tts": {
"heading": "Configuraciones de Text-to-speech",
"ttsEnabled": {
"label": "Habilitar Texto-a-Voz"
},
"ttsProvider": {
"label": "Proveedor de Text-to-speech",
"placeholder": "Selecione un proveedor"
},
"ttsVoice": {
"label": "Voz de Text-to-speech",
"placeholder": "Selecione una voz"
},
"ssmlEnabled": {
"label": "Habilitar SSML (Speech Synthesis Markup Language)"
}
}
},
"manageModels": {
"title": "Administar de Modelos",
"addBtn": "Agregar Nuevo Modelo",
"columns": {
"name": "Nombre",
"digest": "Resumen",
"modifiedAt": "Modificado",
"size": "Tamaño",
"actions": "Acciones"
},
"expandedColumns": {
"parentModel": "Modelo Padre",
"format": "Formato",
"family": "Familia",
"parameterSize": "Tamaño de Parametros",
"quantizationLevel": "Nível de Quantización"
},
"tooltip": {
"delete": "Borrar Modelo",
"repull": "Traer nuevamente el Modelo"
},
"confirm": {
"delete": "¿Esta seguro que desea borrar este modelos?",
"repull": "¿Esta seguro que desea traer nuevamente este modelo?"
},
"modal": {
"title": "Traer Nuevo Modelo",
"placeholder": "Ingresar el nombre del modelo",
"pull": "Traer Modelo"
},
"notification": {
"pullModel": "Trayendo Modelo",
"pullModelDescription": "Trayendo modelo {{modelName}}. Para más detalles, verifique el ícono de la extensión.",
"success": "Exito",
"error": "Error",
"successDescription": "Modelo traido exitosamente",
"successDeleteDescription": "Modelo borrado exitosamente",
"someError": "Hubo un error. Intente nuevamente más tarde"
}
},
"managePrompts": {
"title": "Administrar de Prompts",
"addBtn": "Agregar Nuevo Prompt",
"option1": "Normal",
"option2": "RAG",
"questionPrompt": "Prompt de Pregunta",
"columns": {
"title": "Título",
"prompt": "Prompt",
"type": "Tipo de Prompt",
"actions": "Acciones"
},
"systemPrompt": "Prompt del Sistema",
"quickPrompt": "Prompt Rápido",
"tooltip": {
"delete": "Borrar Prompt",
"edit": "Editar Prompt"
},
"confirm": {
"delete": "¿Esta seguro que desea borrar este prompt? Esta acción no tiene vuelta a atrás."
},
"modal": {
"addTitle": "Agregar Nuevo Prompt",
"editTitle": "Editar Prompt"
},
"form": {
"title": {
"label": "Título",
"placeholder": "Mi Prompt genial",
"required": "Por favor, ingrese un título"
},
"prompt": {
"label": "Prompt",
"placeholder": "Ingrese un prompt",
"required": "Por favor, ingrese un prompt",
"help": "Puede usar {key} como variable en su prompt."
},
"isSystem": {
"label": "Es un Prompt del Sistema"
},
"btnSave": {
"saving": "Agregando un Prompt...",
"save": "Agregar Prompt"
},
"btnEdit": {
"saving": "Actualizando Prompt...",
"save": "Actualizar Prompt"
}
},
"notification": {
"addSuccess": "Prompt Agregado",
"addSuccessDesc": "Prompt agregado exitosamente",
"error": "Error",
"someError": "Hubo un error. Intente nuevamente más tarde",
"updatedSuccess": "Prompt Actualizado",
"updatedSuccessDesc": "Prompt actualizado exitosamente",
"deletedSuccess": "Prompt Borrado",
"deletedSuccessDesc": "Prompt borrado exitosamente"
}
},
"manageShare": {
"title": "Administrar los recursos compartidos",
"heading": "Configurar URL de Página Compartida",
"form": {
"url": {
"label": "URL de Página compartida",
"placeholder": "Ingresar URL de Página compartida",
"required": "Por favor, ingrese URL de Página compartida",
"help": "Por motivos de privacidad, podes hacer self-host de la página compartida y proveer una URL aqui. <anchor>Aprende más</anchor>."
}
},
"webshare": {
"heading": "Compartir una Web",
"columns": {
"title": "Título",
"url": "URL",
"actions": "Acciones"
},
"tooltip": {
"delete": "Borrar lo compartido"
},
"confirm": {
"delete": "¿Esta seguro de desear borrar esta web compartida? Esta acción no tiene vuelta a atrás."
},
"label": "Administrar páginas compartidas",
"description": "Habilitar o deshabilitar el recurso de páginas compartidas"
},
"notification": {
"pageShareSuccess": "URL compartida actualizada exitosamente",
"someError": "Hubo un error. Intente nuevamente más tarde",
"webShareDeleteSuccess": "Web compartida borrada exitosamente com sucesso"
}
},
"ollamaSettings": {
"title": "Configuraciones de Ollama",
"heading": "Configurar Ollama",
"settings": {
"ollamaUrl": {
"label": "URL de Ollama",
"placeholder": "Ingrese la URL de Ollama"
},
"advanced": {
"label": "Configuración avanzada de URL de Ollama",
"urlRewriteEnabled": {
"label": "Habilitar o Deshabilitar URL Personalizada"
},
"rewriteUrl": {
"label": "URL Personalizada",
"placeholder": "Ingresar URL Personalizada"
},
"help": "Si tenes problemas de conexión con Ollama en Page Assist, podes configurar una URL de personalizada. Para saber más sobre la configuración, <anchor>click aqui</anchor>."
}
}
},
"manageSearch": {
"title": "Administrar Busqueda Web",
"heading": "Configurar Busqueda Web"
},
"about": {
"title": "Sobre",
"heading": "Sobre",
"chromeVersion": "Versión de Page Assist",
"ollamaVersion": "Versión de Ollama",
"support": "Podes apoyar el proyecto Page Assist haciendo donaciones o patrocinarnos a través de las seguientes plataformas:",
"koFi": "Apoyar en Ko-fi",
"githubSponsor": "Patrocinarnos en GitHub",
"githubRepo": "Repositorio de GitHub"
},
"manageKnowledge": {
"title": "Administrar Conocimiento",
"heading": "Configurar Bases de Conocimiento"
},
"rag": {
"title": "Configuraciones de RAG",
"ragSettings": {
"label": "Configuraciones de RAG",
"model": {
"label": "Modelo de embeddings",
"required": "Por favor, selecione un modelo",
"help": "Es recomendable usar modelos de embeddings como `nomic-embed-text`.",
"placeholder": "Selecione un modelo"
},
"chunkSize": {
"label": "Tamaño del Chunk",
"placeholder": "Ingresar el tamaño del chunk",
"required": "Por favor, ingrese el tamaño del chunk"
},
"chunkOverlap": {
"label": "Solapamiento del Chunk",
"placeholder": "Ingrese el solapamiento del chunk",
"required": "Por favor, ingresar el solapamiento del chunk"
}
},
"prompt": {
"label": "Configurar el Prompt del RAG",
"option1": "Normal",
"option2": "Web",
"alert": "Es obsoleto configurar aquí el prompt del sistema. Por favor, use la sección de Administrar Prompts para agregar o editar prompts. Esta sección se quitará en una versión futura",
"systemPrompt": "Prompt del Sistema",
"systemPromptPlaceholder": "Ingresar el prompt del sistema",
"webSearchPrompt": "Prompt de la busqueda Web",
"webSearchPromptHelp": "No borre `{search_results}` del prompt.",
"webSearchPromptError": "Por favor, ingresar un prompt de busqueda web",
"webSearchPromptPlaceholder": "Ingrese un prompt de busqueda web",
"webSearchFollowUpPrompt": "Prompt de Seguimiento de busqueda Web",
"webSearchFollowUpPromptHelp": "No borre `{chat_history}` y `{question}` del prompt.",
"webSearchFollowUpPromptError": "Por favor, ingrese el prompt de seguimiento de la busqueda web",
"webSearchFollowUpPromptPlaceholder": "Su prompt de seguimiento de busqueda web"
}
}
}

View File

@ -0,0 +1,7 @@
{
"tooltip": {
"embed": "Puede demorar algunos minutos para incluir la página. Por favor, aguarde...",
"clear": "Borrar el histórico de conversación",
"history": "Histórico de la conversación"
}
}

View File

@ -37,6 +37,9 @@
"totalSearchResults": { "totalSearchResults": {
"label": "Résultats de la recherche totaux", "label": "Résultats de la recherche totaux",
"placeholder": "Entrez les résultats de la recherche totaux" "placeholder": "Entrez les résultats de la recherche totaux"
},
"visitSpecificWebsite": {
"label": "Visitez le site web mentionné dans le message"
} }
}, },
"system": { "system": {

View File

@ -37,6 +37,9 @@
"totalSearchResults": { "totalSearchResults": {
"label": "Risultati della ricerca", "label": "Risultati della ricerca",
"placeholder": "Inserisci il totale delle ricerche" "placeholder": "Inserisci il totale delle ricerche"
},
"visitSpecificWebsite": {
"label": "Visita il sito web menzionato nel messaggio"
} }
}, },
"system": { "system": {

View File

@ -40,6 +40,9 @@
"totalSearchResults": { "totalSearchResults": {
"label": "合計検索結果", "label": "合計検索結果",
"placeholder": "合計検索結果を入力する" "placeholder": "合計検索結果を入力する"
},
"visitSpecificWebsite": {
"label": "メッセージに記載されたウェブサイトを訪問してください"
} }
}, },
"system": { "system": {

View File

@ -40,6 +40,9 @@
"totalSearchResults": { "totalSearchResults": {
"label": "ആകെ തിരച്ചിൽ ഫലങ്ങൾ", "label": "ആകെ തിരച്ചിൽ ഫലങ്ങൾ",
"placeholder": "ആകെ തിരച്ചിൽ ഫലങ്ങളുടെ എണ്ണം നൽകുക" "placeholder": "ആകെ തിരച്ചിൽ ഫലങ്ങളുടെ എണ്ണം നൽകുക"
},
"visitSpecificWebsite": {
"label": "സന്ദേശത്തിൽ പറയുന്ന വെബ്സൈറ്റ് സന്ദർശിക്കുക."
} }
}, },
"system": { "system": {

View File

@ -37,6 +37,9 @@
"totalSearchResults": { "totalSearchResults": {
"label": "Resultados de Pesquisa Totais", "label": "Resultados de Pesquisa Totais",
"placeholder": "Insira Resultados de Pesquisa Totais" "placeholder": "Insira Resultados de Pesquisa Totais"
},
"visitSpecificWebsite": {
"label": "Visite o site mencionado na mensagem."
} }
}, },
"system": { "system": {

View File

@ -37,6 +37,9 @@
"totalSearchResults": { "totalSearchResults": {
"label": "Общее количество результатов поиска", "label": "Общее количество результатов поиска",
"placeholder": "Введите общее количество результатов поиска" "placeholder": "Введите общее количество результатов поиска"
},
"visitSpecificWebsite": {
"label": "Посетите веб-сайт, указанный в сообщении."
} }
}, },
"system": { "system": {

View File

@ -40,7 +40,10 @@
"totalSearchResults": { "totalSearchResults": {
"label": "总搜索结果", "label": "总搜索结果",
"placeholder": "输入总搜索结果" "placeholder": "输入总搜索结果"
} },
"visitSpecificWebsite": {
"label": "访问消息中提到的网站。"
}
}, },
"system": { "system": {
"heading": "系统设置", "heading": "系统设置",

View File

@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next"
import { KnowledgeSelect } from "../Knowledge/KnowledgeSelect" import { KnowledgeSelect } from "../Knowledge/KnowledgeSelect"
import { useSpeechRecognition } from "@/hooks/useSpeechRecognition" import { useSpeechRecognition } from "@/hooks/useSpeechRecognition"
import { PiGlobe } from "react-icons/pi" import { PiGlobe } from "react-icons/pi"
import { extractReadabilityContent } from "@/parser/reader"
type Props = { type Props = {
dropedFile: File | undefined dropedFile: File | undefined

View File

@ -13,7 +13,8 @@ export const SearchModeSettings = () => {
initialValues: { initialValues: {
isSimpleInternetSearch: false, isSimpleInternetSearch: false,
searchProvider: "", searchProvider: "",
totalSearchResults: 0 totalSearchResults: 0,
visitSpecificWebsite: false
} }
}) })
@ -67,7 +68,7 @@ export const SearchModeSettings = () => {
</span> </span>
<div> <div>
<Switch <Switch
className="mt-4 sm:mt-0" className="mt-4 sm:mt-0"
{...form.getInputProps("isSimpleInternetSearch", { {...form.getInputProps("isSimpleInternetSearch", {
type: "checkbox" type: "checkbox"
})} })}
@ -89,6 +90,20 @@ export const SearchModeSettings = () => {
</div> </div>
</div> </div>
<div className="flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between">
<span className="text-gray-700 dark:text-neutral-50 ">
{t("generalSettings.webSearch.visitSpecificWebsite.label")}
</span>
<div>
<Switch
className="mt-4 sm:mt-0"
{...form.getInputProps("visitSpecificWebsite", {
type: "checkbox"
})}
/>
</div>
</div>
<div className="flex justify-end"> <div className="flex justify-end">
<SaveButton btnType="submit" /> <SaveButton btnType="submit" />
</div> </div>

View File

@ -1,87 +1,106 @@
import { useCallback, useEffect, useRef, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import { useMessageOption } from "./useMessageOption"
export const useScrollAnchor = () => { export const useScrollAnchor = () => {
const messagesRef = useRef<HTMLDivElement>(null) const { isProcessing, messages } = useMessageOption()
const scrollRef = useRef<HTMLDivElement>(null)
const visibilityRef = useRef<HTMLDivElement>(null)
const [isAtTop, setIsAtTop] = useState(false)
const [isAtBottom, setIsAtBottom] = useState(true) const [isAtBottom, setIsAtBottom] = useState(true)
const [isVisible, setIsVisible] = useState(false) const [userScrolled, setUserScrolled] = useState(false)
const [isOverflowing, setIsOverflowing] = useState(false)
const messagesStartRef = useRef<HTMLDivElement>(null)
const messagesEndRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const isAutoScrolling = useRef(false)
console.log(`isAtTop: ${isAtTop}, isAtBottom: ${isAtBottom}, userScrolled: ${userScrolled}, isOverflowing: ${isOverflowing}`)
useEffect(() => {
if (!isProcessing && userScrolled) {
console.log("userScrolled")
setUserScrolled(false)
}
}, [isProcessing])
useEffect(() => {
if (isProcessing && !userScrolled) {
scrollToBottom()
}
}, [messages])
useEffect(() => {
const container = containerRef.current
if (!container) return
const topObserver = new IntersectionObserver(
([entry]) => {
setIsAtTop(entry.isIntersecting)
},
{ threshold: 1 }
)
const bottomObserver = new IntersectionObserver(
([entry]) => {
setIsAtBottom(entry.isIntersecting)
if (entry.isIntersecting) {
setUserScrolled(false)
} else if (!isAutoScrolling.current) {
setUserScrolled(true)
}
},
{ threshold: 1 }
)
if (messagesStartRef.current) {
topObserver.observe(messagesStartRef.current)
}
if (messagesEndRef.current) {
bottomObserver.observe(messagesEndRef.current)
}
const resizeObserver = new ResizeObserver(() => {
setIsOverflowing(container.scrollHeight > container.clientHeight)
})
resizeObserver.observe(container)
return () => {
topObserver.disconnect()
bottomObserver.disconnect()
resizeObserver.disconnect()
}
}, [])
const scrollToTop = useCallback(() => {
if (messagesStartRef.current) {
messagesStartRef.current.scrollIntoView({ behavior: "smooth" })
}
}, [])
const scrollToBottom = useCallback(() => { const scrollToBottom = useCallback(() => {
if (messagesRef.current) { isAutoScrolling.current = true
messagesRef.current.scrollIntoView({
block: "end", setTimeout(() => {
behavior: "smooth" if (messagesEndRef.current) {
}) messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
} }
isAutoScrolling.current = false
}, 100)
}, []) }, [])
useEffect(() => {
if (messagesRef.current) {
if (isAtBottom && !isVisible) {
messagesRef.current.scrollIntoView({
block: "end"
})
}
}
}, [isAtBottom, isVisible])
useEffect(() => {
const { current } = scrollRef
if (current) {
const handleScroll = (event: Event) => {
const target = event.target as HTMLDivElement
const offset = 25
const isAtBottom =
target.scrollTop + target.clientHeight >= target.scrollHeight - offset
console.log(target.scrollTop, target.clientHeight, target.scrollHeight)
setIsAtBottom(isAtBottom)
}
current.addEventListener("scroll", handleScroll, {
passive: true
})
return () => {
current.removeEventListener("scroll", handleScroll)
}
}
}, [])
useEffect(() => {
if (visibilityRef.current) {
let observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
console.log(entry.isIntersecting)
if (entry.isIntersecting) {
setIsVisible(true)
} else {
setIsVisible(false)
}
})
},
{
rootMargin: "0px 0px -100px 0px"
}
)
observer.observe(visibilityRef.current)
return () => {
observer.disconnect()
}
}
})
return { return {
messagesRef, messagesStartRef,
scrollRef, messagesEndRef,
visibilityRef, containerRef,
scrollToBottom, isAtTop,
isAtBottom, isAtBottom,
isVisible userScrolled,
isOverflowing,
scrollToTop,
scrollToBottom,
setIsAtBottom
} }
} }

View File

@ -8,6 +8,7 @@ import { ml } from "./lang/ml";
import { zh } from "./lang/zh"; import { zh } from "./lang/zh";
import { ja } from "./lang/ja"; import { ja } from "./lang/ja";
import { it } from "./lang/it"; import { it } from "./lang/it";
import { es } from "./lang/es";
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector';
i18n i18n
@ -16,6 +17,7 @@ i18n
.init({ .init({
resources: { resources: {
en: en, en: en,
es: es,
fr: fr, fr: fr,
"it": it, "it": it,
ml: ml, ml: ml,
@ -31,4 +33,4 @@ i18n
lng: localStorage.getItem("i18nextLng") || "en", lng: localStorage.getItem("i18nextLng") || "en",
}) })
export default i18n; export default i18n;

15
src/i18n/lang/es.ts Normal file
View File

@ -0,0 +1,15 @@
import option from "@/assets/locale/es/option.json";
import playground from "@/assets/locale/es/playground.json";
import common from "@/assets/locale/es/common.json";
import sidepanel from "@/assets/locale/es/sidepanel.json";
import settings from "@/assets/locale/es/settings.json";
import knowledge from "@/assets/locale/es/knowledge.json";
export const es = {
option,
playground,
common,
sidepanel,
settings,
knowledge
}

View File

@ -4,6 +4,10 @@ export const supportLanguage = [
label: "English", label: "English",
value: "en" value: "en"
}, },
{
label: "Español",
value: "es"
},
{ {
label: "Français", label: "Français",
value: "fr" value: "fr"
@ -19,7 +23,7 @@ export const supportLanguage = [
{ {
label: "Português (Brasil)", label: "Português (Brasil)",
value: "pt-BR" value: "pt-BR"
}, },
{ {
label: "മലയാളം", label: "മലയാളം",
value: "ml" value: "ml"
@ -32,4 +36,4 @@ export const supportLanguage = [
label: "日本語", label: "日本語",
value: "ja-JP" value: "ja-JP"
} }
] ]

View File

@ -1,9 +1,9 @@
import { BaseDocumentLoader } from "langchain/document_loaders/base" import { BaseDocumentLoader } from "langchain/document_loaders/base"
import { Document } from "@langchain/core/documents" import { Document } from "@langchain/core/documents"
import { compile } from "html-to-text"
import { urlRewriteRuntime } from "~/libs/runtime" import { urlRewriteRuntime } from "~/libs/runtime"
import { YtTranscript } from "yt-transcript" import { YtTranscript } from "yt-transcript"
import { isWikipedia, parseWikipedia } from "@/parser/wiki" import { isWikipedia, parseWikipedia } from "@/parser/wiki"
import { extractReadabilityContent } from "@/parser/reader"
const YT_REGEX = const YT_REGEX =
/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?([a-zA-Z0-9_-]+)/ /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?([a-zA-Z0-9_-]+)/
@ -24,8 +24,7 @@ export interface WebLoaderParams {
export class PageAssistHtmlLoader export class PageAssistHtmlLoader
extends BaseDocumentLoader extends BaseDocumentLoader
implements WebLoaderParams implements WebLoaderParams {
{
html: string html: string
url: string url: string
@ -52,30 +51,14 @@ export class PageAssistHtmlLoader
{ {
metadata: { metadata: {
source: this.url, source: this.url,
url: this.url,
audio: { chunks: transcript } audio: { chunks: transcript }
}, },
pageContent: text pageContent: text
} }
] ]
} }
const metadata = { source: this.url, url: this.url, }
// let html = this.html
// if (isWikipedia(this.url)) {
// console.log("Wikipedia URL detected")
// html = parseWikipedia(html)
// }
// // else if (isTwitter(this.url)) {
// // console.log("Twitter URL detected")
// // html = parseTweet(html, this.url)
// // }
// const htmlCompiler = compile({
// wordwrap: false
// })
// const text = htmlCompiler(html)
const metadata = { source: this.url }
return [new Document({ pageContent: this.html, metadata })] return [new Document({ pageContent: this.html, metadata })]
} }
@ -95,6 +78,7 @@ export class PageAssistHtmlLoader
return [ return [
{ {
metadata: { metadata: {
url: this.url,
source: this.url, source: this.url,
audio: { chunks: transcript } audio: { chunks: transcript }
}, },
@ -103,22 +87,15 @@ export class PageAssistHtmlLoader
] ]
} }
await urlRewriteRuntime(this.url, "web") await urlRewriteRuntime(this.url, "web")
const fetchHTML = await fetch(this.url) let text = "";
let html = await fetchHTML.text()
if (isWikipedia(this.url)) { if (isWikipedia(this.url)) {
console.log("Wikipedia URL detected") console.log("Wikipedia URL detected")
html = parseWikipedia(await fetchHTML.text()) const fetchHTML = await fetch(this.url)
text = parseWikipedia(await fetchHTML.text())
} else {
text = await extractReadabilityContent(this.url)
} }
const htmlCompiler = compile({
wordwrap: false,
selectors: [
{ selector: "img", format: "skip" },
{ selector: "script", format: "skip" }
]
})
const text = htmlCompiler(html)
const metadata = { url: this.url } const metadata = { url: this.url }
return [new Document({ pageContent: text, metadata })] return [new Document({ pageContent: text, metadata })]
} }

19
src/parser/reader.ts Normal file
View File

@ -0,0 +1,19 @@
import { Readability } from "@mozilla/readability"
import { defaultExtractContent } from "./default"
export const extractReadabilityContent = async (url: string) => {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to fetch ${url}`)
}
const html = await response.text()
// create a fake dom for Readability
const doc = new DOMParser().parseFromString(html, "text/html")
const reader = new Readability(doc)
const article = reader.parse()
// convert the article to markdown
const markdown = defaultExtractContent(article.content)
return markdown
}

View File

@ -1,4 +1,5 @@
import * as cheerio from "cheerio" import * as cheerio from "cheerio"
import { defaultExtractContent } from "./default"
export const isWikipedia = (url: string) => { export const isWikipedia = (url: string) => {
const WIKI_REGEX = /wikipedia\.org\/wiki\//g const WIKI_REGEX = /wikipedia\.org\/wiki\//g
@ -24,5 +25,5 @@ export const parseWikipedia = (html: string) => {
content?.find("div.toc")?.remove() content?.find("div.toc")?.remove()
const newHtml = content?.html() const newHtml = content?.html()
return `<div>TITLE: ${title?.text()}</div><div>${newHtml}</div>` return defaultExtractContent(`<div>TITLE: ${title?.text()}</div><div>${newHtml}</div>`)
} }

View File

@ -38,4 +38,18 @@ export const getAdvancedOllamaSettings = async () => {
export const copilotResumeLastChat = async () => { export const copilotResumeLastChat = async () => {
return await storage.get<boolean>("copilotResumeLastChat") return await storage.get<boolean>("copilotResumeLastChat")
}
export const defaultSidebarOpen = async () => {
const sidebarOpen = await storage.get("sidebarOpen")
if (!sidebarOpen || sidebarOpen === "") {
return "right_clk"
}
return sidebarOpen
}
export const setSidebarOpen = async (sidebarOpen: string) => {
await storage.set("sidebarOpen", sidebarOpen)
} }

View File

@ -15,6 +15,21 @@ export const getIsSimpleInternetSearch = async () => {
return isSimpleInternetSearch === "true" return isSimpleInternetSearch === "true"
} }
export const getIsVisitSpecificWebsite = async () => {
const isVisitSpecificWebsite = await storage.get("isVisitSpecificWebsite")
if (!isVisitSpecificWebsite || isVisitSpecificWebsite.length === 0) {
return true
}
return isVisitSpecificWebsite === "true"
}
export const setIsVisitSpecificWebsite = async (
isVisitSpecificWebsite: boolean
) => {
await storage.set("isVisitSpecificWebsite", isVisitSpecificWebsite.toString())
}
export const setIsSimpleInternetSearch = async ( export const setIsSimpleInternetSearch = async (
isSimpleInternetSearch: boolean isSimpleInternetSearch: boolean
) => { ) => {
@ -48,32 +63,37 @@ export const setTotalSearchResults = async (totalSearchResults: number) => {
} }
export const getSearchSettings = async () => { export const getSearchSettings = async () => {
const [isSimpleInternetSearch, searchProvider, totalSearchResult] = const [isSimpleInternetSearch, searchProvider, totalSearchResult, visitSpecificWebsite] =
await Promise.all([ await Promise.all([
getIsSimpleInternetSearch(), getIsSimpleInternetSearch(),
getSearchProvider(), getSearchProvider(),
totalSearchResults() totalSearchResults(),
getIsVisitSpecificWebsite()
]) ])
return { return {
isSimpleInternetSearch, isSimpleInternetSearch,
searchProvider, searchProvider,
totalSearchResults: totalSearchResult totalSearchResults: totalSearchResult,
visitSpecificWebsite
} }
} }
export const setSearchSettings = async ({ export const setSearchSettings = async ({
isSimpleInternetSearch, isSimpleInternetSearch,
searchProvider, searchProvider,
totalSearchResults totalSearchResults,
visitSpecificWebsite
}: { }: {
isSimpleInternetSearch: boolean isSimpleInternetSearch: boolean
searchProvider: string searchProvider: string
totalSearchResults: number totalSearchResults: number
visitSpecificWebsite: boolean
}) => { }) => {
await Promise.all([ await Promise.all([
setIsSimpleInternetSearch(isSimpleInternetSearch), setIsSimpleInternetSearch(isSimpleInternetSearch),
setSearchProvider(searchProvider), setSearchProvider(searchProvider),
setTotalSearchResults(totalSearchResults) setTotalSearchResults(totalSearchResults),
setIsVisitSpecificWebsite(visitSpecificWebsite)
]) ])
} }

44
src/utils/rerank.ts Normal file
View File

@ -0,0 +1,44 @@
import type { Embeddings } from "@langchain/core/embeddings"
import type { Document } from "@langchain/core/documents"
import * as ml_distance from "ml-distance"
export const rerankDocs = async ({
query,
docs,
embedding
}: {
query: string
docs: Document[]
embedding: Embeddings
}) => {
if (docs.length === 0) {
return docs
}
const docsWithContent = docs.filter(
(doc) => doc.pageContent && doc.pageContent.length > 0
)
const [docEmbeddings, queryEmbedding] = await Promise.all([
embedding.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),
embedding.embedQuery(query)
])
const similarity = docEmbeddings.map((docEmbedding, i) => {
// perform cosine similarity between query and document
const sim = ml_distance.similarity.cosine(queryEmbedding, docEmbedding)
return {
index: i,
similarity: sim
}
})
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.filter((sim) => sim.similarity > 0.5)
.slice(0, 15)
.map((sim) => docsWithContent[sim.index])
return sortedDocs
}

View File

@ -1,9 +1,10 @@
import { getWebSearchPrompt } from "~/services/ollama" import { getWebSearchPrompt } from "~/services/ollama"
import { webGoogleSearch } from "./search-engines/google" import { webGoogleSearch } from "./search-engines/google"
import { webDuckDuckGoSearch } from "./search-engines/duckduckgo" import { webDuckDuckGoSearch } from "./search-engines/duckduckgo"
import { getSearchProvider } from "@/services/search" import { getIsVisitSpecificWebsite, getSearchProvider } from "@/services/search"
import { webSogouSearch } from "./search-engines/sogou" import { webSogouSearch } from "./search-engines/sogou"
import { webBraveSearch } from "./search-engines/brave" import { webBraveSearch } from "./search-engines/brave"
import { getWebsiteFromQuery, processSingleWebsite } from "./website"
const getHostName = (url: string) => { const getHostName = (url: string) => {
try { try {
@ -29,8 +30,27 @@ const searchWeb = (provider: string, query: string) => {
export const getSystemPromptForWeb = async (query: string) => { export const getSystemPromptForWeb = async (query: string) => {
try { try {
const searchProvider = await getSearchProvider()
const search = await searchWeb(searchProvider, query) const websiteVisit = getWebsiteFromQuery(query)
let search: {
url: any;
content: string;
}[] = []
const isVisitSpecificWebsite = await getIsVisitSpecificWebsite()
if (isVisitSpecificWebsite && websiteVisit.hasUrl) {
const url = websiteVisit.url
const queryWithoutUrl = websiteVisit.queryWithouUrls
search = await processSingleWebsite(url, queryWithoutUrl)
} else {
const searchProvider = await getSearchProvider()
search = await searchWeb(searchProvider, query)
}
const search_results = search const search_results = search
.map( .map(

76
src/web/website/index.ts Normal file
View File

@ -0,0 +1,76 @@
import { cleanUrl } from "@/libs/clean-url"
import { PageAssistHtmlLoader } from "@/loader/html"
import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getOllamaURL } from "@/services/ollama"
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
import { MemoryVectorStore } from "langchain/vectorstores/memory"
export const processSingleWebsite = async (url: string, query: string) => {
const loader = new PageAssistHtmlLoader({
html: "",
url
})
const docs = await loader.loadByURL()
const ollamaUrl = await getOllamaURL()
const embeddingModle = await defaultEmbeddingModelForRag()
const ollamaEmbedding = new OllamaEmbeddings({
model: embeddingModle || "",
baseUrl: cleanUrl(ollamaUrl)
})
const chunkSize = await defaultEmbeddingChunkSize()
const chunkOverlap = await defaultEmbeddingChunkOverlap()
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize,
chunkOverlap
})
const chunks = await textSplitter.splitDocuments(docs)
const store = new MemoryVectorStore(ollamaEmbedding)
await store.addDocuments(chunks)
const resultsWithEmbeddings = await store.similaritySearch(query, 4)
const searchResult = resultsWithEmbeddings.map((result) => {
return {
url: result.metadata.url,
content: result.pageContent
}
})
return searchResult
}
export const getWebsiteFromQuery = (query: string): {
queryWithouUrls: string,
url: string,
hasUrl: boolean
} => {
const urlRegex = /https?:\/\/[^\s]+/g
const urls = query.match(urlRegex)
if (!urls) {
return {
queryWithouUrls: query,
url: "",
hasUrl: false
}
}
const url = urls[0]
const queryWithouUrls = query.replace(url, "")
return {
queryWithouUrls,
url,
hasUrl: true
}
}

View File

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