From 8aaee2c40ca8d4f983c2cc15e668b0a64d95b0d6 Mon Sep 17 00:00:00 2001 From: wellCh4n Date: Sat, 15 Feb 2025 16:48:21 +0800 Subject: [PATCH 01/13] feat(app): support complex title --- src/utils/documents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/documents.ts b/src/utils/documents.ts index 5cd0366..3656689 100644 --- a/src/utils/documents.ts +++ b/src/utils/documents.ts @@ -65,7 +65,7 @@ export const getDocumentsFromLinks = async ({ links }: { links: string[] }) => { const splittedText = await splitter.splitText(parsedText); const title = res.data .toString('utf8') - .match(/(.*?)<\/title>/)?.[1]; + .match(/<title.*>(.*?)<\/title>/)?.[1]; const linkDocs = splittedText.map((text) => { return new Document({ From aa240009ab615311e62c31314ef8b9258179c01b Mon Sep 17 00:00:00 2001 From: haddadrm <121486289+haddadrm@users.noreply.github.com> Date: Tue, 1 Apr 2025 17:23:28 +0400 Subject: [PATCH 02/13] Feature: Add LM Studio provider integration - Added LM Studio provider to support OpenAI compatible API - Implemented chat and embeddings model loading - Updated config to include LM Studio API endpoint --- sample.config.toml | 5 +- src/lib/config.ts | 5 ++ src/lib/providers/index.ts | 3 + src/lib/providers/lmstudio.ts | 101 ++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 src/lib/providers/lmstudio.ts diff --git a/sample.config.toml b/sample.config.toml index 980e99d..1db2125 100644 --- a/sample.config.toml +++ b/sample.config.toml @@ -25,5 +25,8 @@ API_URL = "" # Ollama API URL - http://host.docker.internal:11434 [MODELS.DEEPSEEK] API_KEY = "" +[MODELS.LM_STUDIO] +API_URL = "" # LM Studio API URL - http://host.docker.internal:1234 + [API_ENDPOINTS] -SEARXNG = "" # SearxNG API URL - http://localhost:32768 \ No newline at end of file +SEARXNG = "" # SearxNG API URL - http://localhost:32768 diff --git a/src/lib/config.ts b/src/lib/config.ts index 2831214..7c6d495 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -28,6 +28,9 @@ interface Config { DEEPSEEK: { API_KEY: string; }; + LM_STUDIO: { + API_URL: string; + }; CUSTOM_OPENAI: { API_URL: string; API_KEY: string; @@ -77,6 +80,8 @@ export const getCustomOpenaiApiUrl = () => export const getCustomOpenaiModelName = () => loadConfig().MODELS.CUSTOM_OPENAI.MODEL_NAME; +export const getLMStudioApiEndpoint = () => loadConfig().MODELS.LM_STUDIO.API_URL; + const mergeConfigs = (current: any, update: any): any => { if (update === null || update === undefined) { return current; diff --git a/src/lib/providers/index.ts b/src/lib/providers/index.ts index eef212f..0a4a6db 100644 --- a/src/lib/providers/index.ts +++ b/src/lib/providers/index.ts @@ -13,6 +13,7 @@ import { loadAnthropicChatModels } from './anthropic'; import { loadGeminiChatModels, loadGeminiEmbeddingModels } from './gemini'; import { loadTransformersEmbeddingsModels } from './transformers'; import { loadDeepseekChatModels } from './deepseek'; +import { loadLMStudioChatModels, loadLMStudioEmbeddingsModels } from './lmstudio'; export interface ChatModel { displayName: string; @@ -34,6 +35,7 @@ export const chatModelProviders: Record< anthropic: loadAnthropicChatModels, gemini: loadGeminiChatModels, deepseek: loadDeepseekChatModels, + lmstudio: loadLMStudioChatModels, }; export const embeddingModelProviders: Record< @@ -44,6 +46,7 @@ export const embeddingModelProviders: Record< ollama: loadOllamaEmbeddingModels, gemini: loadGeminiEmbeddingModels, transformers: loadTransformersEmbeddingsModels, + lmstudio: loadLMStudioEmbeddingsModels, }; export const getAvailableChatModelProviders = async () => { diff --git a/src/lib/providers/lmstudio.ts b/src/lib/providers/lmstudio.ts new file mode 100644 index 0000000..fd8eb75 --- /dev/null +++ b/src/lib/providers/lmstudio.ts @@ -0,0 +1,101 @@ +import { getKeepAlive, getLMStudioApiEndpoint } from '../config'; +import axios from 'axios'; +import { ChatModel, EmbeddingModel } from '.'; +import { ChatOpenAI } from '@langchain/openai'; +import { OpenAIEmbeddings } from '@langchain/openai'; +import { BaseChatModel } from '@langchain/core/language_models/chat_models'; +import { Embeddings } from '@langchain/core/embeddings'; + +interface LMStudioModel { + id: string; + name?: string; +} + +const ensureV1Endpoint = (endpoint: string): string => + endpoint.endsWith('/v1') ? endpoint : `${endpoint}/v1`; + +const checkServerAvailability = async (endpoint: string): Promise<boolean> => { + try { + const keepAlive = getKeepAlive(); + await axios.get(`${ensureV1Endpoint(endpoint)}/models`, { + timeout: parseInt(keepAlive) * 1000 || 5000, + headers: { 'Content-Type': 'application/json' }, + }); + return true; + } catch { + return false; + } +}; + +export const loadLMStudioChatModels = async () => { + const endpoint = getLMStudioApiEndpoint(); + const keepAlive = getKeepAlive(); + + if (!endpoint) return {}; + if (!await checkServerAvailability(endpoint)) return {}; + + try { + const response = await axios.get(`${ensureV1Endpoint(endpoint)}/models`, { + timeout: parseInt(keepAlive) * 1000 || 5000, + headers: { 'Content-Type': 'application/json' }, + }); + + const chatModels: Record<string, ChatModel> = {}; + + response.data.data.forEach((model: LMStudioModel) => { + chatModels[model.id] = { + displayName: model.name || model.id, + model: new ChatOpenAI({ + openAIApiKey: 'lm-studio', + configuration: { + baseURL: ensureV1Endpoint(endpoint), + }, + modelName: model.id, + temperature: 0.7, + streaming: true, + maxRetries: 3 + }) as unknown as BaseChatModel, + }; + }); + + return chatModels; + } catch (err) { + console.error(`Error loading LM Studio models: ${err}`); + return {}; + } +}; + +export const loadLMStudioEmbeddingsModels = async () => { + const endpoint = getLMStudioApiEndpoint(); + const keepAlive = getKeepAlive(); + + if (!endpoint) return {}; + if (!await checkServerAvailability(endpoint)) return {}; + + try { + const response = await axios.get(`${ensureV1Endpoint(endpoint)}/models`, { + timeout: parseInt(keepAlive) * 1000 || 5000, + headers: { 'Content-Type': 'application/json' }, + }); + + const embeddingsModels: Record<string, EmbeddingModel> = {}; + + response.data.data.forEach((model: LMStudioModel) => { + embeddingsModels[model.id] = { + displayName: model.name || model.id, + model: new OpenAIEmbeddings({ + openAIApiKey: 'lm-studio', + configuration: { + baseURL: ensureV1Endpoint(endpoint), + }, + modelName: model.id, + }) as unknown as Embeddings, + }; + }); + + return embeddingsModels; + } catch (err) { + console.error(`Error loading LM Studio embeddings model: ${err}`); + return {}; + } +}; From 7e1dc33a081c563991d2fc084b7b7e3dd143e04e Mon Sep 17 00:00:00 2001 From: haddadrm <121486289+haddadrm@users.noreply.github.com> Date: Tue, 1 Apr 2025 19:26:15 +0400 Subject: [PATCH 03/13] Implement provider formatting improvements and fix client-side compatibility - Add PROVIDER_INFO metadata to each provider file with proper display names - Create centralized PROVIDER_METADATA in index.ts for consistent reference - Update settings UI to use provider metadata for display names - Fix client/server compatibility for Node.js modules in config.ts --- src/app/settings/page.tsx | 11 ++++----- src/lib/config.ts | 41 ++++++++++++++++++++++--------- src/lib/providers/anthropic.ts | 5 ++++ src/lib/providers/deepseek.ts | 5 ++++ src/lib/providers/gemini.ts | 5 ++++ src/lib/providers/groq.ts | 5 ++++ src/lib/providers/index.ts | 31 +++++++++++++++++------ src/lib/providers/lmstudio.ts | 5 ++++ src/lib/providers/ollama.ts | 5 ++++ src/lib/providers/openai.ts | 5 ++++ src/lib/providers/transformers.ts | 5 ++++ 11 files changed, 97 insertions(+), 26 deletions(-) diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 8eee9a4..919304b 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -7,6 +7,7 @@ import { Switch } from '@headlessui/react'; import ThemeSwitcher from '@/components/theme/Switcher'; import { ImagesIcon, VideoIcon } from 'lucide-react'; import Link from 'next/link'; +import { PROVIDER_METADATA } from '@/lib/providers'; interface SettingsType { chatModelProviders: { @@ -547,9 +548,8 @@ const Page = () => { options={Object.keys(config.chatModelProviders).map( (provider) => ({ value: provider, - label: - provider.charAt(0).toUpperCase() + - provider.slice(1), + label: (PROVIDER_METADATA as any)[provider]?.displayName || + provider.charAt(0).toUpperCase() + provider.slice(1), }), )} /> @@ -689,9 +689,8 @@ const Page = () => { options={Object.keys(config.embeddingModelProviders).map( (provider) => ({ value: provider, - label: - provider.charAt(0).toUpperCase() + - provider.slice(1), + label: (PROVIDER_METADATA as any)[provider]?.displayName || + provider.charAt(0).toUpperCase() + provider.slice(1), }), )} /> diff --git a/src/lib/config.ts b/src/lib/config.ts index 7c6d495..e3f2680 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,7 +1,14 @@ -import fs from 'fs'; -import path from 'path'; import toml from '@iarna/toml'; +// Use dynamic imports for Node.js modules to prevent client-side errors +let fs: any; +let path: any; +if (typeof window === 'undefined') { + // We're on the server + fs = require('fs'); + path = require('path'); +} + const configFileName = 'config.toml'; interface Config { @@ -46,10 +53,17 @@ type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]>; }; -const loadConfig = () => - toml.parse( - fs.readFileSync(path.join(process.cwd(), `${configFileName}`), 'utf-8'), - ) as any as Config; +const loadConfig = () => { + // Server-side only + if (typeof window === 'undefined') { + return toml.parse( + fs.readFileSync(path.join(process.cwd(), `${configFileName}`), 'utf-8'), + ) as any as Config; + } + + // Client-side fallback - settings will be loaded via API + return {} as Config; +}; export const getSimilarityMeasure = () => loadConfig().GENERAL.SIMILARITY_MEASURE; @@ -114,10 +128,13 @@ const mergeConfigs = (current: any, update: any): any => { }; export const updateConfig = (config: RecursivePartial<Config>) => { - const currentConfig = loadConfig(); - const mergedConfig = mergeConfigs(currentConfig, config); - fs.writeFileSync( - path.join(path.join(process.cwd(), `${configFileName}`)), - toml.stringify(mergedConfig), - ); + // Server-side only + if (typeof window === 'undefined') { + const currentConfig = loadConfig(); + const mergedConfig = mergeConfigs(currentConfig, config); + fs.writeFileSync( + path.join(path.join(process.cwd(), `${configFileName}`)), + toml.stringify(mergedConfig), + ); + } }; diff --git a/src/lib/providers/anthropic.ts b/src/lib/providers/anthropic.ts index 7ecde4b..e434b32 100644 --- a/src/lib/providers/anthropic.ts +++ b/src/lib/providers/anthropic.ts @@ -1,6 +1,11 @@ import { ChatAnthropic } from '@langchain/anthropic'; import { ChatModel } from '.'; import { getAnthropicApiKey } from '../config'; + +export const PROVIDER_INFO = { + key: 'anthropic', + displayName: 'Anthropic' +}; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; const anthropicChatModels: Record<string, string>[] = [ diff --git a/src/lib/providers/deepseek.ts b/src/lib/providers/deepseek.ts index 88f02ec..b272801 100644 --- a/src/lib/providers/deepseek.ts +++ b/src/lib/providers/deepseek.ts @@ -3,6 +3,11 @@ import { getDeepseekApiKey } from '../config'; import { ChatModel } from '.'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; +export const PROVIDER_INFO = { + key: 'deepseek', + displayName: 'Deepseek AI' +}; + const deepseekChatModels: Record<string, string>[] = [ { displayName: 'Deepseek Chat (Deepseek V3)', diff --git a/src/lib/providers/gemini.ts b/src/lib/providers/gemini.ts index 2a88015..6af9fb2 100644 --- a/src/lib/providers/gemini.ts +++ b/src/lib/providers/gemini.ts @@ -4,6 +4,11 @@ import { } from '@langchain/google-genai'; import { getGeminiApiKey } from '../config'; import { ChatModel, EmbeddingModel } from '.'; + +export const PROVIDER_INFO = { + key: 'gemini', + displayName: 'Google Gemini' +}; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { Embeddings } from '@langchain/core/embeddings'; diff --git a/src/lib/providers/groq.ts b/src/lib/providers/groq.ts index 85c75f4..62481d4 100644 --- a/src/lib/providers/groq.ts +++ b/src/lib/providers/groq.ts @@ -1,6 +1,11 @@ import { ChatOpenAI } from '@langchain/openai'; import { getGroqApiKey } from '../config'; import { ChatModel } from '.'; + +export const PROVIDER_INFO = { + key: 'groq', + displayName: 'Groq' +}; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; const groqChatModels: Record<string, string>[] = [ diff --git a/src/lib/providers/index.ts b/src/lib/providers/index.ts index 0a4a6db..073bd61 100644 --- a/src/lib/providers/index.ts +++ b/src/lib/providers/index.ts @@ -1,19 +1,34 @@ import { Embeddings } from '@langchain/core/embeddings'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { loadOpenAIChatModels, loadOpenAIEmbeddingModels } from './openai'; +import { loadOpenAIChatModels, loadOpenAIEmbeddingModels, PROVIDER_INFO as OpenAIInfo, PROVIDER_INFO } from './openai'; import { getCustomOpenaiApiKey, getCustomOpenaiApiUrl, getCustomOpenaiModelName, } from '../config'; import { ChatOpenAI } from '@langchain/openai'; -import { loadOllamaChatModels, loadOllamaEmbeddingModels } from './ollama'; -import { loadGroqChatModels } from './groq'; -import { loadAnthropicChatModels } from './anthropic'; -import { loadGeminiChatModels, loadGeminiEmbeddingModels } from './gemini'; -import { loadTransformersEmbeddingsModels } from './transformers'; -import { loadDeepseekChatModels } from './deepseek'; -import { loadLMStudioChatModels, loadLMStudioEmbeddingsModels } from './lmstudio'; +import { loadOllamaChatModels, loadOllamaEmbeddingModels, PROVIDER_INFO as OllamaInfo } from './ollama'; +import { loadGroqChatModels, PROVIDER_INFO as GroqInfo } from './groq'; +import { loadAnthropicChatModels, PROVIDER_INFO as AnthropicInfo } from './anthropic'; +import { loadGeminiChatModels, loadGeminiEmbeddingModels, PROVIDER_INFO as GeminiInfo } from './gemini'; +import { loadTransformersEmbeddingsModels, PROVIDER_INFO as TransformersInfo } from './transformers'; +import { loadDeepseekChatModels, PROVIDER_INFO as DeepseekInfo } from './deepseek'; +import { loadLMStudioChatModels, loadLMStudioEmbeddingsModels, PROVIDER_INFO as LMStudioInfo } from './lmstudio'; + +export const PROVIDER_METADATA = { + openai: OpenAIInfo, + ollama: OllamaInfo, + groq: GroqInfo, + anthropic: AnthropicInfo, + gemini: GeminiInfo, + transformers: TransformersInfo, + deepseek: DeepseekInfo, + lmstudio: LMStudioInfo, + custom_openai: { + key: 'custom_openai', + displayName: 'Custom OpenAI' + } +}; export interface ChatModel { displayName: string; diff --git a/src/lib/providers/lmstudio.ts b/src/lib/providers/lmstudio.ts index fd8eb75..f7be638 100644 --- a/src/lib/providers/lmstudio.ts +++ b/src/lib/providers/lmstudio.ts @@ -1,6 +1,11 @@ import { getKeepAlive, getLMStudioApiEndpoint } from '../config'; import axios from 'axios'; import { ChatModel, EmbeddingModel } from '.'; + +export const PROVIDER_INFO = { + key: 'lmstudio', + displayName: 'LM Studio' +}; import { ChatOpenAI } from '@langchain/openai'; import { OpenAIEmbeddings } from '@langchain/openai'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; diff --git a/src/lib/providers/ollama.ts b/src/lib/providers/ollama.ts index 92e98e4..beab58f 100644 --- a/src/lib/providers/ollama.ts +++ b/src/lib/providers/ollama.ts @@ -1,6 +1,11 @@ import axios from 'axios'; import { getKeepAlive, getOllamaApiEndpoint } from '../config'; import { ChatModel, EmbeddingModel } from '.'; + +export const PROVIDER_INFO = { + key: 'ollama', + displayName: 'Ollama' +}; import { ChatOllama } from '@langchain/community/chat_models/ollama'; import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama'; diff --git a/src/lib/providers/openai.ts b/src/lib/providers/openai.ts index 01bacc6..36f7e29 100644 --- a/src/lib/providers/openai.ts +++ b/src/lib/providers/openai.ts @@ -1,6 +1,11 @@ import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai'; import { getOpenaiApiKey } from '../config'; import { ChatModel, EmbeddingModel } from '.'; + +export const PROVIDER_INFO = { + key: 'openai', + displayName: 'OpenAI' +}; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { Embeddings } from '@langchain/core/embeddings'; diff --git a/src/lib/providers/transformers.ts b/src/lib/providers/transformers.ts index a06dd12..fd7cb9e 100644 --- a/src/lib/providers/transformers.ts +++ b/src/lib/providers/transformers.ts @@ -1,5 +1,10 @@ import { HuggingFaceTransformersEmbeddings } from '../huggingfaceTransformer'; +export const PROVIDER_INFO = { + key: 'transformers', + displayName: 'Hugging Face' +}; + export const loadTransformersEmbeddingsModels = async () => { try { const embeddingModels = { From 073b5e897cc5c2081b6963fa4d3b8b4ce1cc6cfc Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 12 Apr 2025 11:58:52 +0530 Subject: [PATCH 04/13] feat(app): lint & beautify --- src/app/api/config/route.ts | 5 ++++ src/app/settings/page.tsx | 13 ++++++--- src/lib/config.ts | 5 ++-- src/lib/providers/anthropic.ts | 2 +- src/lib/providers/deepseek.ts | 2 +- src/lib/providers/gemini.ts | 2 +- src/lib/providers/groq.ts | 2 +- src/lib/providers/index.ts | 44 ++++++++++++++++++++++++------- src/lib/providers/lmstudio.ts | 20 +++++--------- src/lib/providers/ollama.ts | 2 +- src/lib/providers/openai.ts | 2 +- src/lib/providers/transformers.ts | 2 +- 12 files changed, 66 insertions(+), 35 deletions(-) diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts index 39c1f84..c1e5bbd 100644 --- a/src/app/api/config/route.ts +++ b/src/app/api/config/route.ts @@ -8,6 +8,7 @@ import { getOllamaApiEndpoint, getOpenaiApiKey, getDeepseekApiKey, + getLMStudioApiEndpoint, updateConfig, } from '@/lib/config'; import { @@ -51,6 +52,7 @@ export const GET = async (req: Request) => { config['openaiApiKey'] = getOpenaiApiKey(); config['ollamaApiUrl'] = getOllamaApiEndpoint(); + config['lmStudioApiUrl'] = getLMStudioApiEndpoint(); config['anthropicApiKey'] = getAnthropicApiKey(); config['groqApiKey'] = getGroqApiKey(); config['geminiApiKey'] = getGeminiApiKey(); @@ -93,6 +95,9 @@ export const POST = async (req: Request) => { DEEPSEEK: { API_KEY: config.deepseekApiKey, }, + LM_STUDIO: { + API_URL: config.lmStudioApiUrl, + }, CUSTOM_OPENAI: { API_URL: config.customOpenaiApiUrl, API_KEY: config.customOpenaiApiKey, diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 919304b..0385944 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -21,6 +21,7 @@ interface SettingsType { anthropicApiKey: string; geminiApiKey: string; ollamaApiUrl: string; + lmStudioApiUrl: string; deepseekApiKey: string; customOpenaiApiKey: string; customOpenaiApiUrl: string; @@ -548,8 +549,10 @@ const Page = () => { options={Object.keys(config.chatModelProviders).map( (provider) => ({ value: provider, - label: (PROVIDER_METADATA as any)[provider]?.displayName || - provider.charAt(0).toUpperCase() + provider.slice(1), + label: + (PROVIDER_METADATA as any)[provider]?.displayName || + provider.charAt(0).toUpperCase() + + provider.slice(1), }), )} /> @@ -689,8 +692,10 @@ const Page = () => { options={Object.keys(config.embeddingModelProviders).map( (provider) => ({ value: provider, - label: (PROVIDER_METADATA as any)[provider]?.displayName || - provider.charAt(0).toUpperCase() + provider.slice(1), + label: + (PROVIDER_METADATA as any)[provider]?.displayName || + provider.charAt(0).toUpperCase() + + provider.slice(1), }), )} /> diff --git a/src/lib/config.ts b/src/lib/config.ts index e3f2680..78ad09c 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -60,7 +60,7 @@ const loadConfig = () => { fs.readFileSync(path.join(process.cwd(), `${configFileName}`), 'utf-8'), ) as any as Config; } - + // Client-side fallback - settings will be loaded via API return {} as Config; }; @@ -94,7 +94,8 @@ export const getCustomOpenaiApiUrl = () => export const getCustomOpenaiModelName = () => loadConfig().MODELS.CUSTOM_OPENAI.MODEL_NAME; -export const getLMStudioApiEndpoint = () => loadConfig().MODELS.LM_STUDIO.API_URL; +export const getLMStudioApiEndpoint = () => + loadConfig().MODELS.LM_STUDIO.API_URL; const mergeConfigs = (current: any, update: any): any => { if (update === null || update === undefined) { diff --git a/src/lib/providers/anthropic.ts b/src/lib/providers/anthropic.ts index e434b32..2b0f2cc 100644 --- a/src/lib/providers/anthropic.ts +++ b/src/lib/providers/anthropic.ts @@ -4,7 +4,7 @@ import { getAnthropicApiKey } from '../config'; export const PROVIDER_INFO = { key: 'anthropic', - displayName: 'Anthropic' + displayName: 'Anthropic', }; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; diff --git a/src/lib/providers/deepseek.ts b/src/lib/providers/deepseek.ts index b272801..46f2398 100644 --- a/src/lib/providers/deepseek.ts +++ b/src/lib/providers/deepseek.ts @@ -5,7 +5,7 @@ import { BaseChatModel } from '@langchain/core/language_models/chat_models'; export const PROVIDER_INFO = { key: 'deepseek', - displayName: 'Deepseek AI' + displayName: 'Deepseek AI', }; const deepseekChatModels: Record<string, string>[] = [ diff --git a/src/lib/providers/gemini.ts b/src/lib/providers/gemini.ts index 6af9fb2..6cf2243 100644 --- a/src/lib/providers/gemini.ts +++ b/src/lib/providers/gemini.ts @@ -7,7 +7,7 @@ import { ChatModel, EmbeddingModel } from '.'; export const PROVIDER_INFO = { key: 'gemini', - displayName: 'Google Gemini' + displayName: 'Google Gemini', }; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { Embeddings } from '@langchain/core/embeddings'; diff --git a/src/lib/providers/groq.ts b/src/lib/providers/groq.ts index 62481d4..4b0ca92 100644 --- a/src/lib/providers/groq.ts +++ b/src/lib/providers/groq.ts @@ -4,7 +4,7 @@ import { ChatModel } from '.'; export const PROVIDER_INFO = { key: 'groq', - displayName: 'Groq' + displayName: 'Groq', }; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; diff --git a/src/lib/providers/index.ts b/src/lib/providers/index.ts index 073bd61..e536431 100644 --- a/src/lib/providers/index.ts +++ b/src/lib/providers/index.ts @@ -1,19 +1,45 @@ import { Embeddings } from '@langchain/core/embeddings'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { loadOpenAIChatModels, loadOpenAIEmbeddingModels, PROVIDER_INFO as OpenAIInfo, PROVIDER_INFO } from './openai'; +import { + loadOpenAIChatModels, + loadOpenAIEmbeddingModels, + PROVIDER_INFO as OpenAIInfo, + PROVIDER_INFO, +} from './openai'; import { getCustomOpenaiApiKey, getCustomOpenaiApiUrl, getCustomOpenaiModelName, } from '../config'; import { ChatOpenAI } from '@langchain/openai'; -import { loadOllamaChatModels, loadOllamaEmbeddingModels, PROVIDER_INFO as OllamaInfo } from './ollama'; +import { + loadOllamaChatModels, + loadOllamaEmbeddingModels, + PROVIDER_INFO as OllamaInfo, +} from './ollama'; import { loadGroqChatModels, PROVIDER_INFO as GroqInfo } from './groq'; -import { loadAnthropicChatModels, PROVIDER_INFO as AnthropicInfo } from './anthropic'; -import { loadGeminiChatModels, loadGeminiEmbeddingModels, PROVIDER_INFO as GeminiInfo } from './gemini'; -import { loadTransformersEmbeddingsModels, PROVIDER_INFO as TransformersInfo } from './transformers'; -import { loadDeepseekChatModels, PROVIDER_INFO as DeepseekInfo } from './deepseek'; -import { loadLMStudioChatModels, loadLMStudioEmbeddingsModels, PROVIDER_INFO as LMStudioInfo } from './lmstudio'; +import { + loadAnthropicChatModels, + PROVIDER_INFO as AnthropicInfo, +} from './anthropic'; +import { + loadGeminiChatModels, + loadGeminiEmbeddingModels, + PROVIDER_INFO as GeminiInfo, +} from './gemini'; +import { + loadTransformersEmbeddingsModels, + PROVIDER_INFO as TransformersInfo, +} from './transformers'; +import { + loadDeepseekChatModels, + PROVIDER_INFO as DeepseekInfo, +} from './deepseek'; +import { + loadLMStudioChatModels, + loadLMStudioEmbeddingsModels, + PROVIDER_INFO as LMStudioInfo, +} from './lmstudio'; export const PROVIDER_METADATA = { openai: OpenAIInfo, @@ -26,8 +52,8 @@ export const PROVIDER_METADATA = { lmstudio: LMStudioInfo, custom_openai: { key: 'custom_openai', - displayName: 'Custom OpenAI' - } + displayName: 'Custom OpenAI', + }, }; export interface ChatModel { diff --git a/src/lib/providers/lmstudio.ts b/src/lib/providers/lmstudio.ts index f7be638..811208f 100644 --- a/src/lib/providers/lmstudio.ts +++ b/src/lib/providers/lmstudio.ts @@ -4,7 +4,7 @@ import { ChatModel, EmbeddingModel } from '.'; export const PROVIDER_INFO = { key: 'lmstudio', - displayName: 'LM Studio' + displayName: 'LM Studio', }; import { ChatOpenAI } from '@langchain/openai'; import { OpenAIEmbeddings } from '@langchain/openai'; @@ -16,14 +16,12 @@ interface LMStudioModel { name?: string; } -const ensureV1Endpoint = (endpoint: string): string => +const ensureV1Endpoint = (endpoint: string): string => endpoint.endsWith('/v1') ? endpoint : `${endpoint}/v1`; const checkServerAvailability = async (endpoint: string): Promise<boolean> => { try { - const keepAlive = getKeepAlive(); await axios.get(`${ensureV1Endpoint(endpoint)}/models`, { - timeout: parseInt(keepAlive) * 1000 || 5000, headers: { 'Content-Type': 'application/json' }, }); return true; @@ -34,14 +32,12 @@ const checkServerAvailability = async (endpoint: string): Promise<boolean> => { export const loadLMStudioChatModels = async () => { const endpoint = getLMStudioApiEndpoint(); - const keepAlive = getKeepAlive(); - + if (!endpoint) return {}; - if (!await checkServerAvailability(endpoint)) return {}; + if (!(await checkServerAvailability(endpoint))) return {}; try { const response = await axios.get(`${ensureV1Endpoint(endpoint)}/models`, { - timeout: parseInt(keepAlive) * 1000 || 5000, headers: { 'Content-Type': 'application/json' }, }); @@ -58,7 +54,7 @@ export const loadLMStudioChatModels = async () => { modelName: model.id, temperature: 0.7, streaming: true, - maxRetries: 3 + maxRetries: 3, }) as unknown as BaseChatModel, }; }); @@ -72,14 +68,12 @@ export const loadLMStudioChatModels = async () => { export const loadLMStudioEmbeddingsModels = async () => { const endpoint = getLMStudioApiEndpoint(); - const keepAlive = getKeepAlive(); - + if (!endpoint) return {}; - if (!await checkServerAvailability(endpoint)) return {}; + if (!(await checkServerAvailability(endpoint))) return {}; try { const response = await axios.get(`${ensureV1Endpoint(endpoint)}/models`, { - timeout: parseInt(keepAlive) * 1000 || 5000, headers: { 'Content-Type': 'application/json' }, }); diff --git a/src/lib/providers/ollama.ts b/src/lib/providers/ollama.ts index beab58f..cca2142 100644 --- a/src/lib/providers/ollama.ts +++ b/src/lib/providers/ollama.ts @@ -4,7 +4,7 @@ import { ChatModel, EmbeddingModel } from '.'; export const PROVIDER_INFO = { key: 'ollama', - displayName: 'Ollama' + displayName: 'Ollama', }; import { ChatOllama } from '@langchain/community/chat_models/ollama'; import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama'; diff --git a/src/lib/providers/openai.ts b/src/lib/providers/openai.ts index 36f7e29..61621c3 100644 --- a/src/lib/providers/openai.ts +++ b/src/lib/providers/openai.ts @@ -4,7 +4,7 @@ import { ChatModel, EmbeddingModel } from '.'; export const PROVIDER_INFO = { key: 'openai', - displayName: 'OpenAI' + displayName: 'OpenAI', }; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { Embeddings } from '@langchain/core/embeddings'; diff --git a/src/lib/providers/transformers.ts b/src/lib/providers/transformers.ts index fd7cb9e..3098d9f 100644 --- a/src/lib/providers/transformers.ts +++ b/src/lib/providers/transformers.ts @@ -2,7 +2,7 @@ import { HuggingFaceTransformersEmbeddings } from '../huggingfaceTransformer'; export const PROVIDER_INFO = { key: 'transformers', - displayName: 'Hugging Face' + displayName: 'Hugging Face', }; export const loadTransformersEmbeddingsModels = async () => { From 186249149674df5938faecabb3a3b7c48d9bce71 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 12 Apr 2025 11:59:05 +0530 Subject: [PATCH 05/13] feat(settings): add LM Studio API URL --- src/app/settings/page.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 0385944..05338c3 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -862,6 +862,25 @@ const Page = () => { onSave={(value) => saveConfig('deepseekApiKey', value)} /> </div> + + <div className="flex flex-col space-y-1"> + <p className="text-black/70 dark:text-white/70 text-sm"> + LM Studio API URL + </p> + <Input + type="text" + placeholder="LM Studio API URL" + value={config.lmStudioApiUrl} + isSaving={savingStates['lmStudioApiUrl']} + onChange={(e) => { + setConfig((prev) => ({ + ...prev!, + lmStudioApiUrl: e.target.value, + })); + }} + onSave={(value) => saveConfig('lmStudioApiUrl', value)} + /> + </div> </div> </SettingsSection> </div> From 06ff2725417435f7d2f0faaa7cc4f67905d9f4a0 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:10:14 +0530 Subject: [PATCH 06/13] feat(openai): add GPT 4.1 models --- src/lib/providers/openai.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/lib/providers/openai.ts b/src/lib/providers/openai.ts index 61621c3..e68e574 100644 --- a/src/lib/providers/openai.ts +++ b/src/lib/providers/openai.ts @@ -30,6 +30,18 @@ const openaiChatModels: Record<string, string>[] = [ displayName: 'GPT-4 omni mini', key: 'gpt-4o-mini', }, + { + displayName: 'GPT 4.1 nano', + key: 'gpt-4.1-nano', + }, + { + displayName: 'GPT 4.1 mini', + key: 'gpt-4.1-mini', + }, + { + displayName: 'GPT 4.1', + key: 'gpt-4.1', + }, ]; const openaiEmbeddingModels: Record<string, string>[] = [ From 68e151b2bda7490ad9c3f17ee27bf539326143b6 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:13:30 +0530 Subject: [PATCH 07/13] Update README.md --- README.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/README.md b/README.md index 18c9f84..9e94028 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,5 @@ # 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc --> -<div align="center" markdown="1"> - <sup>Special thanks to:</sup> - <br> - <br> - <a href="https://www.warp.dev/perplexica"> - <img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/775dd593-9b5f-40f1-bf48-479faff4c27b"> - </a> - -### [Warp, the AI Devtool that lives in your terminal](https://www.warp.dev/perplexica) - -[Available for MacOS, Linux, & Windows](https://www.warp.dev/perplexica) - -</div> - -<hr/> - [![Discord](https://dcbadge.vercel.app/api/server/26aArMy8tT?style=flat&compact=true)](https://discord.gg/26aArMy8tT) ![preview](.assets/perplexica-screenshot.png?) From 701819d01834e8aefba42ec37ac0a85188851641 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 13 May 2025 20:14:08 +0530 Subject: [PATCH 08/13] Revert "Update README.md" This reverts commit 68e151b2bda7490ad9c3f17ee27bf539326143b6. --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 9e94028..18c9f84 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,21 @@ # 🚀 Perplexica - An AI-powered search engine 🔎 <!-- omit in toc --> +<div align="center" markdown="1"> + <sup>Special thanks to:</sup> + <br> + <br> + <a href="https://www.warp.dev/perplexica"> + <img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/775dd593-9b5f-40f1-bf48-479faff4c27b"> + </a> + +### [Warp, the AI Devtool that lives in your terminal](https://www.warp.dev/perplexica) + +[Available for MacOS, Linux, & Windows](https://www.warp.dev/perplexica) + +</div> + +<hr/> + [![Discord](https://dcbadge.vercel.app/api/server/26aArMy8tT?style=flat&compact=true)](https://discord.gg/26aArMy8tT) ![preview](.assets/perplexica-screenshot.png?) From 0c3740fdf2421d3ac9a1601ae133f35bafdb5b2f Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 27 May 2025 18:23:40 +0530 Subject: [PATCH 09/13] feat(groq-provider): dynamically fetch models --- src/lib/providers/groq.ts | 98 ++++++--------------------------------- 1 file changed, 14 insertions(+), 84 deletions(-) diff --git a/src/lib/providers/groq.ts b/src/lib/providers/groq.ts index 4b0ca92..5435de4 100644 --- a/src/lib/providers/groq.ts +++ b/src/lib/providers/groq.ts @@ -6,101 +6,31 @@ export const PROVIDER_INFO = { key: 'groq', displayName: 'Groq', }; -import { BaseChatModel } from '@langchain/core/language_models/chat_models'; -const groqChatModels: Record<string, string>[] = [ - { - displayName: 'Gemma2 9B IT', - key: 'gemma2-9b-it', - }, - { - displayName: 'Llama 3.3 70B Versatile', - key: 'llama-3.3-70b-versatile', - }, - { - displayName: 'Llama 3.1 8B Instant', - key: 'llama-3.1-8b-instant', - }, - { - displayName: 'Llama3 70B 8192', - key: 'llama3-70b-8192', - }, - { - displayName: 'Llama3 8B 8192', - key: 'llama3-8b-8192', - }, - { - displayName: 'Mixtral 8x7B 32768', - key: 'mixtral-8x7b-32768', - }, - { - displayName: 'Qwen QWQ 32B (Preview)', - key: 'qwen-qwq-32b', - }, - { - displayName: 'Mistral Saba 24B (Preview)', - key: 'mistral-saba-24b', - }, - { - displayName: 'Qwen 2.5 Coder 32B (Preview)', - key: 'qwen-2.5-coder-32b', - }, - { - displayName: 'Qwen 2.5 32B (Preview)', - key: 'qwen-2.5-32b', - }, - { - displayName: 'DeepSeek R1 Distill Qwen 32B (Preview)', - key: 'deepseek-r1-distill-qwen-32b', - }, - { - displayName: 'DeepSeek R1 Distill Llama 70B (Preview)', - key: 'deepseek-r1-distill-llama-70b', - }, - { - displayName: 'Llama 3.3 70B SpecDec (Preview)', - key: 'llama-3.3-70b-specdec', - }, - { - displayName: 'Llama 3.2 1B Preview (Preview)', - key: 'llama-3.2-1b-preview', - }, - { - displayName: 'Llama 3.2 3B Preview (Preview)', - key: 'llama-3.2-3b-preview', - }, - { - displayName: 'Llama 3.2 11B Vision Preview (Preview)', - key: 'llama-3.2-11b-vision-preview', - }, - { - displayName: 'Llama 3.2 90B Vision Preview (Preview)', - key: 'llama-3.2-90b-vision-preview', - }, - /* { - displayName: 'Llama 4 Maverick 17B 128E Instruct (Preview)', - key: 'meta-llama/llama-4-maverick-17b-128e-instruct', - }, */ - { - displayName: 'Llama 4 Scout 17B 16E Instruct (Preview)', - key: 'meta-llama/llama-4-scout-17b-16e-instruct', - }, -]; +import { BaseChatModel } from '@langchain/core/language_models/chat_models'; export const loadGroqChatModels = async () => { const groqApiKey = getGroqApiKey(); - if (!groqApiKey) return {}; try { + const res = await fetch('https://api.groq.com/openai/v1/models', { + method: 'GET', + headers: { + Authorization: `bearer ${groqApiKey}`, + 'Content-Type': 'application/json', + }, + }); + + const groqChatModels = (await res.json()).data; const chatModels: Record<string, ChatModel> = {}; - groqChatModels.forEach((model) => { - chatModels[model.key] = { - displayName: model.displayName, + groqChatModels.forEach((model: any) => { + chatModels[model.id] = { + displayName: model.id, model: new ChatOpenAI({ openAIApiKey: groqApiKey, - modelName: model.key, + modelName: model.id, temperature: 0.7, configuration: { baseURL: 'https://api.groq.com/openai/v1', From bb21184ea273dea127996040fb23f67d107c6a4d Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Tue, 27 May 2025 18:32:09 +0530 Subject: [PATCH 10/13] feat(settings): show loading spinner immediately --- src/app/settings/page.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 05338c3..6f20f01 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -143,7 +143,7 @@ const Page = () => { const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState< string | null >(null); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); const [automaticImageSearch, setAutomaticImageSearch] = useState(false); const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false); const [systemInstructions, setSystemInstructions] = useState<string>(''); @@ -151,7 +151,6 @@ const Page = () => { useEffect(() => { const fetchConfig = async () => { - setIsLoading(true); const res = await fetch(`/api/config`, { headers: { 'Content-Type': 'application/json', From 18da75ad9790e790c0672a8648fa2ec68c613d3a Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 28 May 2025 10:35:19 +0530 Subject: [PATCH 11/13] feat(db): create migration files --- drizzle/0000_fuzzy_randall.sql | 16 +++++ drizzle/meta/0000_snapshot.json | 116 ++++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 13 ++++ 3 files changed, 145 insertions(+) create mode 100644 drizzle/0000_fuzzy_randall.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json diff --git a/drizzle/0000_fuzzy_randall.sql b/drizzle/0000_fuzzy_randall.sql new file mode 100644 index 0000000..0a2ff07 --- /dev/null +++ b/drizzle/0000_fuzzy_randall.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS `chats` ( + `id` text PRIMARY KEY NOT NULL, + `title` text NOT NULL, + `createdAt` text NOT NULL, + `focusMode` text NOT NULL, + `files` text DEFAULT '[]' +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS `messages` ( + `id` integer PRIMARY KEY NOT NULL, + `content` text NOT NULL, + `chatId` text NOT NULL, + `messageId` text NOT NULL, + `type` text, + `metadata` text +); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..850bcd3 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,116 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "ef3a044b-0f34-40b5-babb-2bb3a909ba27", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "focusMode": { + "name": "focusMode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "files": { + "name": "files", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messageId": { + "name": "messageId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..5db59d1 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1748405503809, + "tag": "0000_fuzzy_randall", + "breakpoints": true + } + ] +} From 876487ad1185f3aed2e18e73a54a33aea7609f94 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 28 May 2025 10:41:12 +0530 Subject: [PATCH 12/13] feat(db): add migration script, migrate on each run --- app.dockerfile | 10 +++++++++- docker-compose.yaml | 1 + drizzle.config.ts | 3 ++- entrypoint.sh | 6 ++++++ src/lib/db/index.ts | 3 ++- src/lib/db/migrate.ts | 5 +++++ 6 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 entrypoint.sh create mode 100644 src/lib/db/migrate.ts diff --git a/app.dockerfile b/app.dockerfile index 3433288..c3c0fd0 100644 --- a/app.dockerfile +++ b/app.dockerfile @@ -12,6 +12,9 @@ COPY public ./public RUN mkdir -p /home/perplexica/data RUN yarn build +RUN yarn add --dev @vercel/ncc +RUN yarn ncc build ./src/lib/db/migrate.ts -o migrator + FROM node:20.18.0-slim WORKDIR /home/perplexica @@ -21,7 +24,12 @@ COPY --from=builder /home/perplexica/.next/static ./public/_next/static COPY --from=builder /home/perplexica/.next/standalone ./ COPY --from=builder /home/perplexica/data ./data +COPY drizzle ./drizzle +COPY --from=builder /home/perplexica/migrator/build ./build +COPY --from=builder /home/perplexica/migrator/index.js ./migrate.js RUN mkdir /home/perplexica/uploads -CMD ["node", "server.js"] \ No newline at end of file +COPY entrypoint.sh ./entrypoint.sh +RUN chmod +x ./entrypoint.sh +CMD ["./entrypoint.sh"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index b702b4e..b32e0a9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -16,6 +16,7 @@ services: dockerfile: app.dockerfile environment: - SEARXNG_API_URL=http://searxng:8080 + - DATA_DIR=/home/perplexica ports: - 3000:3000 networks: diff --git a/drizzle.config.ts b/drizzle.config.ts index 58de9e0..a029112 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,10 +1,11 @@ import { defineConfig } from 'drizzle-kit'; +import path from 'path'; export default defineConfig({ dialect: 'sqlite', schema: './src/lib/db/schema.ts', out: './drizzle', dbCredentials: { - url: './data/db.sqlite', + url: path.join(process.cwd(), 'data', 'db.sqlite'), }, }); diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..9f9448a --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -e + +node migrate.js + +exec node server.js \ No newline at end of file diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 9b761d4..515cdb3 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -3,7 +3,8 @@ import Database from 'better-sqlite3'; import * as schema from './schema'; import path from 'path'; -const sqlite = new Database(path.join(process.cwd(), 'data/db.sqlite')); +const DATA_DIR = process.env.DATA_DIR || process.cwd(); +const sqlite = new Database(path.join(DATA_DIR, './data/db.sqlite')); const db = drizzle(sqlite, { schema: schema, }); diff --git a/src/lib/db/migrate.ts b/src/lib/db/migrate.ts new file mode 100644 index 0000000..c3ebff6 --- /dev/null +++ b/src/lib/db/migrate.ts @@ -0,0 +1,5 @@ +import db from './'; +import { migrate } from 'drizzle-orm/better-sqlite3/migrator'; +import path from 'path'; + +migrate(db, { migrationsFolder: path.join(process.cwd(), 'drizzle') }); From bcebdb5fd98bc13413596659823b82be00685315 Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Wed, 28 May 2025 13:01:05 +0530 Subject: [PATCH 13/13] feat(chat): allow export as pdf or markdown --- package.json | 2 + src/components/Navbar.tsx | 155 ++++++++++++++++++++++++++++++++++++-- yarn.lock | 144 +++++++++++++++++++++++++++++++++++ 3 files changed, 295 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e68410f..5746fd3 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "compute-dot": "^1.1.0", "drizzle-orm": "^0.40.1", "html-to-text": "^9.0.5", + "jspdf": "^3.0.1", "langchain": "^0.1.30", "lucide-react": "^0.363.0", "markdown-to-jsx": "^7.7.2", @@ -49,6 +50,7 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.12", "@types/html-to-text": "^9.0.4", + "@types/jspdf": "^2.0.0", "@types/node": "^20", "@types/pdf-parse": "^1.1.4", "@types/react": "^18", diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 13f2da3..e406ade 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,8 +1,122 @@ -import { Clock, Edit, Share, Trash } from 'lucide-react'; +import { Clock, Edit, Share, Trash, FileText, FileDown } from 'lucide-react'; import { Message } from './ChatWindow'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, Fragment } from 'react'; import { formatTimeDifference } from '@/lib/utils'; import DeleteChat from './DeleteChat'; +import { + Popover, + PopoverButton, + PopoverPanel, + Transition, +} from '@headlessui/react'; +import jsPDF from 'jspdf'; + +const downloadFile = (filename: string, content: string, type: string) => { + const blob = new Blob([content], { type }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, 0); +}; + +const exportAsMarkdown = (messages: Message[], title: string) => { + const date = new Date(messages[0]?.createdAt || Date.now()).toLocaleString(); + let md = `# 💬 Chat Export: ${title}\n\n`; + md += `*Exported on: ${date}*\n\n---\n`; + messages.forEach((msg, idx) => { + md += `\n---\n`; + md += `**${msg.role === 'user' ? '🧑 User' : '🤖 Assistant'}** +`; + md += `*${new Date(msg.createdAt).toLocaleString()}*\n\n`; + md += `> ${msg.content.replace(/\n/g, '\n> ')}\n`; + if (msg.sources && msg.sources.length > 0) { + md += `\n**Citations:**\n`; + msg.sources.forEach((src: any, i: number) => { + const url = src.metadata?.url || ''; + md += `- [${i + 1}] [${url}](${url})\n`; + }); + } + }); + md += '\n---\n'; + downloadFile(`${title || 'chat'}.md`, md, 'text/markdown'); +}; + +const exportAsPDF = (messages: Message[], title: string) => { + const doc = new jsPDF(); + const date = new Date(messages[0]?.createdAt || Date.now()).toLocaleString(); + let y = 15; + const pageHeight = doc.internal.pageSize.height; + doc.setFontSize(18); + doc.text(`Chat Export: ${title}`, 10, y); + y += 8; + doc.setFontSize(11); + doc.setTextColor(100); + doc.text(`Exported on: ${date}`, 10, y); + y += 8; + doc.setDrawColor(200); + doc.line(10, y, 200, y); + y += 6; + doc.setTextColor(30); + messages.forEach((msg, idx) => { + if (y > pageHeight - 30) { + doc.addPage(); + y = 15; + } + doc.setFont('helvetica', 'bold'); + doc.text(`${msg.role === 'user' ? 'User' : 'Assistant'}`, 10, y); + doc.setFont('helvetica', 'normal'); + doc.setFontSize(10); + doc.setTextColor(120); + doc.text(`${new Date(msg.createdAt).toLocaleString()}`, 40, y); + y += 6; + doc.setTextColor(30); + doc.setFontSize(12); + const lines = doc.splitTextToSize(msg.content, 180); + for (let i = 0; i < lines.length; i++) { + if (y > pageHeight - 20) { + doc.addPage(); + y = 15; + } + doc.text(lines[i], 12, y); + y += 6; + } + if (msg.sources && msg.sources.length > 0) { + doc.setFontSize(11); + doc.setTextColor(80); + if (y > pageHeight - 20) { + doc.addPage(); + y = 15; + } + doc.text('Citations:', 12, y); + y += 5; + msg.sources.forEach((src: any, i: number) => { + const url = src.metadata?.url || ''; + if (y > pageHeight - 15) { + doc.addPage(); + y = 15; + } + doc.text(`- [${i + 1}] ${url}`, 15, y); + y += 5; + }); + doc.setTextColor(30); + } + y += 6; + doc.setDrawColor(230); + if (y > pageHeight - 10) { + doc.addPage(); + y = 15; + } + doc.line(10, y, 200, y); + y += 4; + }); + doc.save(`${title || 'chat'}.pdf`); +}; const Navbar = ({ chatId, @@ -59,10 +173,39 @@ const Navbar = ({ <p className="hidden lg:flex">{title}</p> <div className="flex flex-row items-center space-x-4"> - <Share - size={17} - className="active:scale-95 transition duration-100 cursor-pointer" - /> + <Popover className="relative"> + <PopoverButton className="active:scale-95 transition duration-100 cursor-pointer p-2 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary"> + <Share size={17} /> + </PopoverButton> + <Transition + as={Fragment} + enter="transition ease-out duration-100" + enterFrom="opacity-0 translate-y-1" + enterTo="opacity-100 translate-y-0" + leave="transition ease-in duration-75" + leaveFrom="opacity-100 translate-y-0" + leaveTo="opacity-0 translate-y-1" + > + <PopoverPanel className="absolute right-0 mt-2 w-64 rounded-xl shadow-xl bg-light-primary dark:bg-dark-primary border border-light-200 dark:border-dark-200 z-50"> + <div className="flex flex-col py-3 px-3 gap-2"> + <button + className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium" + onClick={() => exportAsMarkdown(messages, title || '')} + > + <FileText size={17} className="text-[#24A0ED]" /> + Export as Markdown + </button> + <button + className="flex items-center gap-2 px-4 py-2 text-left hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors text-black dark:text-white rounded-lg font-medium" + onClick={() => exportAsPDF(messages, title || '')} + > + <FileDown size={17} className="text-[#24A0ED]" /> + Export as PDF + </button> + </div> + </PopoverPanel> + </Transition> + </Popover> <DeleteChat redirect chatId={chatId} chats={[]} setChats={() => {}} /> </div> </div> diff --git a/yarn.lock b/yarn.lock index 921186b..58b53ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,6 +40,11 @@ node-fetch "^2.6.7" web-streams-polyfill "^3.2.1" +"@babel/runtime@^7.12.5", "@babel/runtime@^7.26.7": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.3.tgz#10491113799fb8d77e1d9273384d5d68deeea8f6" + integrity sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw== + "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.0": version "7.24.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" @@ -966,6 +971,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jspdf@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/jspdf/-/jspdf-2.0.0.tgz#c64d63e9248a62849902085c1cd4753b33f8ee0c" + integrity sha512-oonYDXI4GegGaG7FFVtriJ+Yqlh4YR3L3NVDiwCEBVG7sbya19SoGx4MW4kg1MCMRPgkbbFTck8YKJL8PrkDfA== + dependencies: + jspdf "*" + "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -1010,6 +1022,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== +"@types/raf@^3.4.0": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.3.tgz#85f1d1d17569b28b8db45e16e996407a56b0ab04" + integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw== + "@types/react-dom@^18": version "18.2.24" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.24.tgz#8dda8f449ae436a7a6e91efed8035d4ab03ff759" @@ -1035,6 +1052,11 @@ resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== +"@types/trusted-types@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + "@types/uuid@^10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" @@ -1317,6 +1339,11 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + autoprefixer@^10.0.1: version "10.4.19" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" @@ -1405,6 +1432,11 @@ base-64@^0.1.0: resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -1476,6 +1508,11 @@ browserslist@^4.23.0: node-releases "^2.0.14" update-browserslist-db "^1.0.13" +btoa@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" + integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1532,6 +1569,20 @@ caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz#b4d5f67ab0746a3b8b5b6d1f06e39c51beb39a9e" integrity sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg== +canvg@^3.0.11: + version "3.0.11" + resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.11.tgz#4b4290a6c7fa36871fac2b14e432eff33b33cf2b" + integrity sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/raf" "^3.4.0" + core-js "^3.8.3" + raf "^3.4.1" + regenerator-runtime "^0.13.7" + rgbcolor "^1.0.1" + stackblur-canvas "^2.0.0" + svg-pathdata "^6.0.3" + chalk@^4.0.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -1691,6 +1742,11 @@ console-table-printer@^2.12.1: dependencies: simple-wcswidth "^1.0.1" +core-js@^3.6.0, core-js@^3.8.3: + version "3.42.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.42.0.tgz#edbe91f78ac8cfb6df8d997e74d368a68082fe37" + integrity sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g== + cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -1705,6 +1761,13 @@ crypt@0.0.2: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== +css-line-break@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" + integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== + dependencies: + utrie "^1.0.2" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -1881,6 +1944,13 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" +dompurify@^3.2.4: + version "3.2.6" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad" + integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ== + optionalDependencies: + "@types/trusted-types" "^2.0.7" + domutils@^3.0.1: version "3.2.2" resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" @@ -2424,6 +2494,11 @@ fecha@^4.2.0: resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== +fflate@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2761,6 +2836,14 @@ html-to-text@^9.0.5: htmlparser2 "^8.0.2" selderee "^0.11.0" +html2canvas@^1.0.0-rc.5: + version "1.4.1" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" + integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== + dependencies: + css-line-break "^2.1.0" + text-segmentation "^1.0.3" + htmlparser2@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" @@ -3123,6 +3206,21 @@ jsonpointer@^5.0.1: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== +jspdf@*, jspdf@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-3.0.1.tgz#d81e1964f354f60412516eb2449ea2cccd4d2a3b" + integrity sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg== + dependencies: + "@babel/runtime" "^7.26.7" + atob "^2.1.2" + btoa "^1.2.1" + fflate "^0.8.1" + optionalDependencies: + canvg "^3.0.11" + core-js "^3.6.0" + dompurify "^3.2.4" + html2canvas "^1.0.0-rc.5" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: version "3.3.5" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" @@ -3823,6 +3921,11 @@ peberminta@^0.9.0: resolved "https://registry.yarnpkg.com/peberminta/-/peberminta-0.9.0.tgz#8ec9bc0eb84b7d368126e71ce9033501dca2a352" integrity sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ== +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + picocolors@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -4002,6 +4105,13 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -4082,6 +4192,11 @@ reflect.getprototypeof@^1.0.4: globalthis "^1.0.3" which-builtin-type "^1.1.3" +regenerator-runtime@^0.13.7: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" @@ -4135,6 +4250,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rgbcolor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d" + integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -4370,6 +4490,11 @@ stack-trace@0.0.x: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== +stackblur-canvas@^2.0.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz#af931277d0b5096df55e1f91c530043e066989b6" + integrity sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ== + streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -4538,6 +4663,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svg-pathdata@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac" + integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw== + tabbable@^6.0.0: version "6.2.0" resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" @@ -4636,6 +4766,13 @@ text-hex@1.0.x: resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== +text-segmentation@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" + integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== + dependencies: + utrie "^1.0.2" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -4822,6 +4959,13 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +utrie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" + integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== + dependencies: + base64-arraybuffer "^1.0.2" + uuid@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"