From d4e02676a14924aa1badd15f92f66499a0a2d5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Di=20Biase?= Date: Tue, 18 Jun 2024 18:03:09 -0300 Subject: [PATCH 1/7] i18n: initial spanish translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Luis Di Biase --- src/assets/locale/es/common.json | 88 ++++++++ src/assets/locale/es/knowledge.json | 42 ++++ src/assets/locale/es/option.json | 12 ++ src/assets/locale/es/playground.json | 29 +++ src/assets/locale/es/settings.json | 289 +++++++++++++++++++++++++++ src/assets/locale/es/sidepanel.json | 7 + src/i18n/index.ts | 4 +- src/i18n/lang/es.ts | 15 ++ src/i18n/support-language.ts | 8 +- 9 files changed, 491 insertions(+), 3 deletions(-) create mode 100644 src/assets/locale/es/common.json create mode 100644 src/assets/locale/es/knowledge.json create mode 100644 src/assets/locale/es/option.json create mode 100644 src/assets/locale/es/playground.json create mode 100644 src/assets/locale/es/settings.json create mode 100644 src/assets/locale/es/sidepanel.json create mode 100644 src/i18n/lang/es.ts diff --git a/src/assets/locale/es/common.json b/src/assets/locale/es/common.json new file mode 100644 index 0000000..e316a0b --- /dev/null +++ b/src/assets/locale/es/common.json @@ -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" + } +} diff --git a/src/assets/locale/es/knowledge.json b/src/assets/locale/es/knowledge.json new file mode 100644 index 0000000..86ddbf3 --- /dev/null +++ b/src/assets/locale/es/knowledge.json @@ -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" +} diff --git a/src/assets/locale/es/option.json b/src/assets/locale/es/option.json new file mode 100644 index 0000000..3c48761 --- /dev/null +++ b/src/assets/locale/es/option.json @@ -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" +} diff --git a/src/assets/locale/es/playground.json b/src/assets/locale/es/playground.json new file mode 100644 index 0000000..afc3771 --- /dev/null +++ b/src/assets/locale/es/playground.json @@ -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 documentación 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" +} diff --git a/src/assets/locale/es/settings.json b/src/assets/locale/es/settings.json new file mode 100644 index 0000000..e8c46cb --- /dev/null +++ b/src/assets/locale/es/settings.json @@ -0,0 +1,289 @@ +{ + "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" + } + }, + "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. Aprende más." + } + }, + "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, click aqui." + } + } + }, + "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" + } + } +} diff --git a/src/assets/locale/es/sidepanel.json b/src/assets/locale/es/sidepanel.json new file mode 100644 index 0000000..214c8b6 --- /dev/null +++ b/src/assets/locale/es/sidepanel.json @@ -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" + } +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 525ada9..6c5915f 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -8,6 +8,7 @@ import { ml } from "./lang/ml"; import { zh } from "./lang/zh"; import { ja } from "./lang/ja"; import { it } from "./lang/it"; +import { es } from "./lang/es"; import LanguageDetector from 'i18next-browser-languagedetector'; i18n @@ -16,6 +17,7 @@ i18n .init({ resources: { en: en, + es: es, fr: fr, "it": it, ml: ml, @@ -31,4 +33,4 @@ i18n lng: localStorage.getItem("i18nextLng") || "en", }) -export default i18n; \ No newline at end of file +export default i18n; diff --git a/src/i18n/lang/es.ts b/src/i18n/lang/es.ts new file mode 100644 index 0000000..c666286 --- /dev/null +++ b/src/i18n/lang/es.ts @@ -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 +} diff --git a/src/i18n/support-language.ts b/src/i18n/support-language.ts index 89b0d97..7cd56a0 100644 --- a/src/i18n/support-language.ts +++ b/src/i18n/support-language.ts @@ -4,6 +4,10 @@ export const supportLanguage = [ label: "English", value: "en" }, + { + label: "Español", + value: "es" + }, { label: "Français", value: "fr" @@ -19,7 +23,7 @@ export const supportLanguage = [ { label: "Português (Brasil)", value: "pt-BR" - }, + }, { label: "മലയാളം", value: "ml" @@ -32,4 +36,4 @@ export const supportLanguage = [ label: "日本語", value: "ja-JP" } -] \ No newline at end of file +] From d23b70b97965214c2583b22414a4d2cd749841d3 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Sat, 22 Jun 2024 00:25:12 +0530 Subject: [PATCH 2/7] feat: Add @mozilla/readability dependency for extracting content from web pages --- bun.lockb | Bin 416642 -> 417010 bytes package.json | 1 + .../Option/Playground/PlaygroundForm.tsx | 1 + src/parser/reader.ts | 19 ++++ src/web/web.ts | 22 +++- src/web/website/index.ts | 94 ++++++++++++++++++ wxt.config.ts | 2 +- 7 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 src/parser/reader.ts create mode 100644 src/web/website/index.ts diff --git a/bun.lockb b/bun.lockb index 12127135e0bea16dd150033ce3a298bc3db4b7c8..c2f8f9b5dc2090b7c823f60f56915e4a53436647 100644 GIT binary patch delta 54747 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 delta 54304 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 aIsRc|{-qmo`O}HUL<5ChH2lAfcmE$8wde!@ diff --git a/package.json b/package.json index 5d64161..44daa54 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@langchain/community": "^0.0.41", "@mantine/form": "^7.5.0", "@mantine/hooks": "^7.5.3", + "@mozilla/readability": "^0.5.0", "@plasmohq/storage": "^1.9.0", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", diff --git a/src/components/Option/Playground/PlaygroundForm.tsx b/src/components/Option/Playground/PlaygroundForm.tsx index c3e97ec..8d248f8 100644 --- a/src/components/Option/Playground/PlaygroundForm.tsx +++ b/src/components/Option/Playground/PlaygroundForm.tsx @@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next" import { KnowledgeSelect } from "../Knowledge/KnowledgeSelect" import { useSpeechRecognition } from "@/hooks/useSpeechRecognition" import { PiGlobe } from "react-icons/pi" +import { extractReadabilityContent } from "@/parser/reader" type Props = { dropedFile: File | undefined diff --git a/src/parser/reader.ts b/src/parser/reader.ts new file mode 100644 index 0000000..23b22bf --- /dev/null +++ b/src/parser/reader.ts @@ -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 +} \ No newline at end of file diff --git a/src/web/web.ts b/src/web/web.ts index e9c1765..8661489 100644 --- a/src/web/web.ts +++ b/src/web/web.ts @@ -4,6 +4,7 @@ import { webDuckDuckGoSearch } from "./search-engines/duckduckgo" import { getSearchProvider } from "@/services/search" import { webSogouSearch } from "./search-engines/sogou" import { webBraveSearch } from "./search-engines/brave" +import { getWebsiteFromQuery, processSingleWebsite } from "./website" const getHostName = (url: string) => { try { @@ -29,8 +30,25 @@ const searchWeb = (provider: string, query: string) => { export const getSystemPromptForWeb = async (query: string) => { try { - const searchProvider = await getSearchProvider() - const search = await searchWeb(searchProvider, query) + + const websiteVisit = getWebsiteFromQuery(query) + let search: { + url: any; + content: string; + }[] = [] + + if (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 .map( diff --git a/src/web/website/index.ts b/src/web/website/index.ts new file mode 100644 index 0000000..021db16 --- /dev/null +++ b/src/web/website/index.ts @@ -0,0 +1,94 @@ +import { cleanUrl } from "@/libs/clean-url" +import { extractReadabilityContent } from "@/parser/reader" +import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getOllamaURL } from "@/services/ollama" +import { getIsSimpleInternetSearch } from "@/services/search" +import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama" +import type { Document } from "@langchain/core/documents" +import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" +import { MemoryVectorStore } from "langchain/vectorstores/memory" + +export const processSingleWebsite = async (url: string, query: string) => { + let content = await extractReadabilityContent(url) + + const isSimpleMode = await getIsSimpleInternetSearch() + + if (isSimpleMode) { + return [ + { + url, + content: content.length > 5000 ? content.slice(0, 5000) : content + } + ] + } + + const docs: Document>[] = [ + { + metadata: { + url + }, + pageContent: content + } + ] + + 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, 3) + + 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 + } +} \ No newline at end of file diff --git a/wxt.config.ts b/wxt.config.ts index 8d94b9f..be6459d 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -48,7 +48,7 @@ export default defineConfig({ outDir: "build", manifest: { - version: "1.1.12", + version: "1.1.13", name: process.env.TARGET === "firefox" ? "Page Assist - A Web UI for Local AI Models" From 4363a4b0deb9d2ed2380b73cb07f763ac3efc795 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Sat, 22 Jun 2024 16:55:02 +0530 Subject: [PATCH 3/7] feat: Add localization support for visitSpecificWebsite label This commit adds localization support for the "visitSpecificWebsite" label in the settings.json file for multiple languages. Now, the label can be translated into different languages, including Japanese, Chinese, English, Malayalam, Italian, Portuguese, Russian, French, and Spanish. --- src/assets/locale/en/settings.json | 3 ++ src/assets/locale/es/settings.json | 5 +++- src/assets/locale/fr/settings.json | 3 ++ src/assets/locale/it/settings.json | 3 ++ src/assets/locale/ja-JP/settings.json | 3 ++ src/assets/locale/ml/settings.json | 3 ++ src/assets/locale/pt-BR/settings.json | 3 ++ src/assets/locale/ru/settings.json | 3 ++ src/assets/locale/zh/settings.json | 5 +++- .../Option/Settings/search-mode.tsx | 19 ++++++++++-- src/services/search.ts | 30 +++++++++++++++---- src/web/web.ts | 6 ++-- src/web/website/index.ts | 18 +++++------ 13 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/assets/locale/en/settings.json b/src/assets/locale/en/settings.json index 0396fda..7107a00 100644 --- a/src/assets/locale/en/settings.json +++ b/src/assets/locale/en/settings.json @@ -37,6 +37,9 @@ "totalSearchResults": { "label": "Total Search Results", "placeholder": "Enter Total Search Results" + }, + "visitSpecificWebsite": { + "label": "Visit the website mentioned in the message" } }, "system": { diff --git a/src/assets/locale/es/settings.json b/src/assets/locale/es/settings.json index e8c46cb..d1f1785 100644 --- a/src/assets/locale/es/settings.json +++ b/src/assets/locale/es/settings.json @@ -37,6 +37,9 @@ "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": { @@ -286,4 +289,4 @@ "webSearchFollowUpPromptPlaceholder": "Su prompt de seguimiento de busqueda web" } } -} +} \ No newline at end of file diff --git a/src/assets/locale/fr/settings.json b/src/assets/locale/fr/settings.json index e06e7ed..634358f 100644 --- a/src/assets/locale/fr/settings.json +++ b/src/assets/locale/fr/settings.json @@ -37,6 +37,9 @@ "totalSearchResults": { "label": "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": { diff --git a/src/assets/locale/it/settings.json b/src/assets/locale/it/settings.json index c083e5f..41bef3d 100644 --- a/src/assets/locale/it/settings.json +++ b/src/assets/locale/it/settings.json @@ -37,6 +37,9 @@ "totalSearchResults": { "label": "Risultati della ricerca", "placeholder": "Inserisci il totale delle ricerche" + }, + "visitSpecificWebsite": { + "label": "Visita il sito web menzionato nel messaggio" } }, "system": { diff --git a/src/assets/locale/ja-JP/settings.json b/src/assets/locale/ja-JP/settings.json index cef567f..74c9567 100644 --- a/src/assets/locale/ja-JP/settings.json +++ b/src/assets/locale/ja-JP/settings.json @@ -40,6 +40,9 @@ "totalSearchResults": { "label": "合計検索結果", "placeholder": "合計検索結果を入力する" + }, + "visitSpecificWebsite": { + "label": "メッセージに記載されたウェブサイトを訪問してください" } }, "system": { diff --git a/src/assets/locale/ml/settings.json b/src/assets/locale/ml/settings.json index b83a07e..46780df 100644 --- a/src/assets/locale/ml/settings.json +++ b/src/assets/locale/ml/settings.json @@ -40,6 +40,9 @@ "totalSearchResults": { "label": "ആകെ തിരച്ചിൽ ഫലങ്ങൾ", "placeholder": "ആകെ തിരച്ചിൽ ഫലങ്ങളുടെ എണ്ണം നൽകുക" + }, + "visitSpecificWebsite": { + "label": "സന്ദേശത്തിൽ പറയുന്ന വെബ്സൈറ്റ് സന്ദർശിക്കുക." } }, "system": { diff --git a/src/assets/locale/pt-BR/settings.json b/src/assets/locale/pt-BR/settings.json index fb22698..21bcee4 100644 --- a/src/assets/locale/pt-BR/settings.json +++ b/src/assets/locale/pt-BR/settings.json @@ -37,6 +37,9 @@ "totalSearchResults": { "label": "Resultados de Pesquisa Totais", "placeholder": "Insira Resultados de Pesquisa Totais" + }, + "visitSpecificWebsite": { + "label": "Visite o site mencionado na mensagem." } }, "system": { diff --git a/src/assets/locale/ru/settings.json b/src/assets/locale/ru/settings.json index d745db8..6099b2a 100644 --- a/src/assets/locale/ru/settings.json +++ b/src/assets/locale/ru/settings.json @@ -37,6 +37,9 @@ "totalSearchResults": { "label": "Общее количество результатов поиска", "placeholder": "Введите общее количество результатов поиска" + }, + "visitSpecificWebsite": { + "label": "Посетите веб-сайт, указанный в сообщении." } }, "system": { diff --git a/src/assets/locale/zh/settings.json b/src/assets/locale/zh/settings.json index 9b27993..44213eb 100644 --- a/src/assets/locale/zh/settings.json +++ b/src/assets/locale/zh/settings.json @@ -40,7 +40,10 @@ "totalSearchResults": { "label": "总搜索结果", "placeholder": "输入总搜索结果" - } + }, + "visitSpecificWebsite": { + "label": "访问消息中提到的网站。" + } }, "system": { "heading": "系统设置", diff --git a/src/components/Option/Settings/search-mode.tsx b/src/components/Option/Settings/search-mode.tsx index e7cc04d..53f391b 100644 --- a/src/components/Option/Settings/search-mode.tsx +++ b/src/components/Option/Settings/search-mode.tsx @@ -13,7 +13,8 @@ export const SearchModeSettings = () => { initialValues: { isSimpleInternetSearch: false, searchProvider: "", - totalSearchResults: 0 + totalSearchResults: 0, + visitSpecificWebsite: false } }) @@ -67,7 +68,7 @@ export const SearchModeSettings = () => {
{
+
+ + {t("generalSettings.webSearch.visitSpecificWebsite.label")} + +
+ +
+
+
diff --git a/src/services/search.ts b/src/services/search.ts index f3851b9..19e5f28 100644 --- a/src/services/search.ts +++ b/src/services/search.ts @@ -15,6 +15,21 @@ export const getIsSimpleInternetSearch = async () => { return isSimpleInternetSearch === "true" } +export const getIsVisitSpecificWebsite = async () => { + const isVisitSpecificWebsite = await storage.get("isVisitSpecificWebsite") + if (!isVisitSpecificWebsite || isVisitSpecificWebsite.length === 0) { + return false + } + return isVisitSpecificWebsite === "true" +} + + +export const setIsVisitSpecificWebsite = async ( + isVisitSpecificWebsite: boolean +) => { + await storage.set("isVisitSpecificWebsite", isVisitSpecificWebsite.toString()) +} + export const setIsSimpleInternetSearch = async ( isSimpleInternetSearch: boolean ) => { @@ -48,32 +63,37 @@ export const setTotalSearchResults = async (totalSearchResults: number) => { } export const getSearchSettings = async () => { - const [isSimpleInternetSearch, searchProvider, totalSearchResult] = + const [isSimpleInternetSearch, searchProvider, totalSearchResult, visitSpecificWebsite] = await Promise.all([ getIsSimpleInternetSearch(), getSearchProvider(), - totalSearchResults() + totalSearchResults(), + getIsVisitSpecificWebsite() ]) return { isSimpleInternetSearch, searchProvider, - totalSearchResults: totalSearchResult + totalSearchResults: totalSearchResult, + visitSpecificWebsite } } export const setSearchSettings = async ({ isSimpleInternetSearch, searchProvider, - totalSearchResults + totalSearchResults, + visitSpecificWebsite }: { isSimpleInternetSearch: boolean searchProvider: string totalSearchResults: number + visitSpecificWebsite: boolean }) => { await Promise.all([ setIsSimpleInternetSearch(isSimpleInternetSearch), setSearchProvider(searchProvider), - setTotalSearchResults(totalSearchResults) + setTotalSearchResults(totalSearchResults), + setIsVisitSpecificWebsite(visitSpecificWebsite) ]) } diff --git a/src/web/web.ts b/src/web/web.ts index 8661489..88aabc4 100644 --- a/src/web/web.ts +++ b/src/web/web.ts @@ -1,7 +1,7 @@ import { getWebSearchPrompt } from "~/services/ollama" import { webGoogleSearch } from "./search-engines/google" import { webDuckDuckGoSearch } from "./search-engines/duckduckgo" -import { getSearchProvider } from "@/services/search" +import { getIsVisitSpecificWebsite, getSearchProvider } from "@/services/search" import { webSogouSearch } from "./search-engines/sogou" import { webBraveSearch } from "./search-engines/brave" import { getWebsiteFromQuery, processSingleWebsite } from "./website" @@ -37,7 +37,9 @@ export const getSystemPromptForWeb = async (query: string) => { content: string; }[] = [] - if (websiteVisit.hasUrl) { + const isVisitSpecificWebsite = await getIsVisitSpecificWebsite() + + if (isVisitSpecificWebsite && websiteVisit.hasUrl) { const url = websiteVisit.url const queryWithoutUrl = websiteVisit.queryWithouUrls diff --git a/src/web/website/index.ts b/src/web/website/index.ts index 021db16..e0ce399 100644 --- a/src/web/website/index.ts +++ b/src/web/website/index.ts @@ -10,16 +10,16 @@ import { MemoryVectorStore } from "langchain/vectorstores/memory" export const processSingleWebsite = async (url: string, query: string) => { let content = await extractReadabilityContent(url) - const isSimpleMode = await getIsSimpleInternetSearch() + // const isSimpleMode = await getIsSimpleInternetSearch() - if (isSimpleMode) { - return [ - { - url, - content: content.length > 5000 ? content.slice(0, 5000) : content - } - ] - } + // if (isSimpleMode) { + // return [ + // { + // url, + // content: content.length > 5000 ? content.slice(0, 5000) : content + // } + // ] + // } const docs: Document>[] = [ { From 1e9b66d823296674bf5db9eb24051f3f12bc85ba Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Sun, 23 Jun 2024 20:34:43 +0530 Subject: [PATCH 4/7] refactor: Update PageAssistHtmlLoader to use extractReadabilityContent for parsing web page content --- src/loader/html.ts | 43 ++++++++++------------------------------ src/parser/wiki.ts | 3 ++- src/services/search.ts | 2 +- src/web/website/index.ts | 30 ++++++---------------------- 4 files changed, 19 insertions(+), 59 deletions(-) diff --git a/src/loader/html.ts b/src/loader/html.ts index 786c60e..5eeb168 100644 --- a/src/loader/html.ts +++ b/src/loader/html.ts @@ -1,9 +1,9 @@ import { BaseDocumentLoader } from "langchain/document_loaders/base" import { Document } from "@langchain/core/documents" -import { compile } from "html-to-text" import { urlRewriteRuntime } from "~/libs/runtime" import { YtTranscript } from "yt-transcript" import { isWikipedia, parseWikipedia } from "@/parser/wiki" +import { extractReadabilityContent } from "@/parser/reader" const YT_REGEX = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?([a-zA-Z0-9_-]+)/ @@ -24,8 +24,7 @@ export interface WebLoaderParams { export class PageAssistHtmlLoader extends BaseDocumentLoader - implements WebLoaderParams -{ + implements WebLoaderParams { html: string url: string @@ -52,30 +51,14 @@ export class PageAssistHtmlLoader { metadata: { source: this.url, + url: this.url, audio: { chunks: transcript } }, pageContent: text } ] } - - // 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 } + const metadata = { source: this.url, url: this.url, } return [new Document({ pageContent: this.html, metadata })] } @@ -95,6 +78,7 @@ export class PageAssistHtmlLoader return [ { metadata: { + url: this.url, source: this.url, audio: { chunks: transcript } }, @@ -103,22 +87,15 @@ export class PageAssistHtmlLoader ] } await urlRewriteRuntime(this.url, "web") - const fetchHTML = await fetch(this.url) - let html = await fetchHTML.text() - + let text = ""; if (isWikipedia(this.url)) { 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 } return [new Document({ pageContent: text, metadata })] } diff --git a/src/parser/wiki.ts b/src/parser/wiki.ts index 36f567c..2dbe88c 100644 --- a/src/parser/wiki.ts +++ b/src/parser/wiki.ts @@ -1,4 +1,5 @@ import * as cheerio from "cheerio" +import { defaultExtractContent } from "./default" export const isWikipedia = (url: string) => { const WIKI_REGEX = /wikipedia\.org\/wiki\//g @@ -24,5 +25,5 @@ export const parseWikipedia = (html: string) => { content?.find("div.toc")?.remove() const newHtml = content?.html() - return `
TITLE: ${title?.text()}
${newHtml}
` + return defaultExtractContent(`
TITLE: ${title?.text()}
${newHtml}
`) } diff --git a/src/services/search.ts b/src/services/search.ts index 19e5f28..f548cfe 100644 --- a/src/services/search.ts +++ b/src/services/search.ts @@ -18,7 +18,7 @@ export const getIsSimpleInternetSearch = async () => { export const getIsVisitSpecificWebsite = async () => { const isVisitSpecificWebsite = await storage.get("isVisitSpecificWebsite") if (!isVisitSpecificWebsite || isVisitSpecificWebsite.length === 0) { - return false + return true } return isVisitSpecificWebsite === "true" } diff --git a/src/web/website/index.ts b/src/web/website/index.ts index e0ce399..ba0bed1 100644 --- a/src/web/website/index.ts +++ b/src/web/website/index.ts @@ -1,34 +1,16 @@ import { cleanUrl } from "@/libs/clean-url" -import { extractReadabilityContent } from "@/parser/reader" +import { PageAssistHtmlLoader } from "@/loader/html" import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getOllamaURL } from "@/services/ollama" -import { getIsSimpleInternetSearch } from "@/services/search" import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama" -import type { Document } from "@langchain/core/documents" import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" import { MemoryVectorStore } from "langchain/vectorstores/memory" export const processSingleWebsite = async (url: string, query: string) => { - let content = await extractReadabilityContent(url) - - // const isSimpleMode = await getIsSimpleInternetSearch() - - // if (isSimpleMode) { - // return [ - // { - // url, - // content: content.length > 5000 ? content.slice(0, 5000) : content - // } - // ] - // } - - const docs: Document>[] = [ - { - metadata: { - url - }, - pageContent: content - } - ] + const loader = new PageAssistHtmlLoader({ + html: "", + url + }) + const docs = await loader.loadByURL() const ollamaUrl = await getOllamaURL() From 5ac03f51232968a01823c076b2b7a095cf9f2e64 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Tue, 25 Jun 2024 21:13:45 +0530 Subject: [PATCH 5/7] refactor: Update useScrollAnchor to handle user scrolling and overflow --- src/hooks/useScrollAnchor.tsx | 169 +++++++++++++++++++--------------- src/services/app.ts | 14 +++ 2 files changed, 108 insertions(+), 75 deletions(-) diff --git a/src/hooks/useScrollAnchor.tsx b/src/hooks/useScrollAnchor.tsx index a8ec2ad..c2d72a2 100644 --- a/src/hooks/useScrollAnchor.tsx +++ b/src/hooks/useScrollAnchor.tsx @@ -1,87 +1,106 @@ import { useCallback, useEffect, useRef, useState } from "react" +import { useMessageOption } from "./useMessageOption" export const useScrollAnchor = () => { - const messagesRef = useRef(null) - const scrollRef = useRef(null) - const visibilityRef = useRef(null) + const { isProcessing, messages } = useMessageOption() + const [isAtTop, setIsAtTop] = useState(false) const [isAtBottom, setIsAtBottom] = useState(true) - const [isVisible, setIsVisible] = useState(false) + const [userScrolled, setUserScrolled] = useState(false) + const [isOverflowing, setIsOverflowing] = useState(false) + + const messagesStartRef = useRef(null) + const messagesEndRef = useRef(null) + const containerRef = useRef(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(() => { - if (messagesRef.current) { - messagesRef.current.scrollIntoView({ - block: "end", - behavior: "smooth" - }) - } + isAutoScrolling.current = true + + setTimeout(() => { + 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 { - messagesRef, - scrollRef, - visibilityRef, - scrollToBottom, + messagesStartRef, + messagesEndRef, + containerRef, + isAtTop, isAtBottom, - isVisible + userScrolled, + isOverflowing, + scrollToTop, + scrollToBottom, + setIsAtBottom } -} +} \ No newline at end of file diff --git a/src/services/app.ts b/src/services/app.ts index e7d871a..9fb2ccd 100644 --- a/src/services/app.ts +++ b/src/services/app.ts @@ -38,4 +38,18 @@ export const getAdvancedOllamaSettings = async () => { export const copilotResumeLastChat = async () => { return await storage.get("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) } \ No newline at end of file From d6d28b325f05dd66670c30ba9676bc21910d4a97 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Tue, 25 Jun 2024 23:15:17 +0530 Subject: [PATCH 6/7] refactor: Update similaritySearch parameter to use 4 instead of 3 --- src/web/website/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/website/index.ts b/src/web/website/index.ts index ba0bed1..31ed5f9 100644 --- a/src/web/website/index.ts +++ b/src/web/website/index.ts @@ -33,7 +33,7 @@ export const processSingleWebsite = async (url: string, query: string) => { await store.addDocuments(chunks) - const resultsWithEmbeddings = await store.similaritySearch(query, 3) + const resultsWithEmbeddings = await store.similaritySearch(query, 4) const searchResult = resultsWithEmbeddings.map((result) => { return { From 885d938bbf37d9571982cbb3a3a98536536688cd Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Tue, 25 Jun 2024 23:22:45 +0530 Subject: [PATCH 7/7] add rerank --- src/utils/rerank.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/utils/rerank.ts diff --git a/src/utils/rerank.ts b/src/utils/rerank.ts new file mode 100644 index 0000000..fcf30cf --- /dev/null +++ b/src/utils/rerank.ts @@ -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 +}