feat: add expert search, legal search and UI improvements

This commit is contained in:
Aktraiser 2024-12-30 13:34:26 +01:00
parent 2c5ca94b3c
commit 271199c527
53 changed files with 4595 additions and 708 deletions

View file

@ -6,49 +6,68 @@ services:
ports: ports:
- 4000:8080 - 4000:8080
networks: networks:
- perplexica-network - xme-network
restart: unless-stopped restart: unless-stopped
perplexica-backend: chroma:
image: chromadb/chroma:latest
environment:
- ALLOW_RESET=true
- CHROMA_SERVER_CORS_ALLOW_ORIGINS=["*"]
ports:
- "8000:8000"
volumes:
- chroma_data:/chroma/chroma
networks:
- xme-network
restart: unless-stopped
xme-backend:
build: build:
context: . context: .
dockerfile: backend.dockerfile dockerfile: backend.dockerfile
image: itzcrazykns1337/perplexica-backend:main image: itzcrazykns1337/xme-backend:main
environment: environment:
- SUPABASE_URL=https://qytbxgzxsywnfhlwcyqa.supabase.co
- SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InF5dGJ4Z3p4c3l3bmZobHdjeXFhIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzEwNTc3MTAsImV4cCI6MjA0NjYzMzcxMH0.XLRq-4CFL2MWxvCLzCv5ZdaF5VSi58cocx9FOyv37jU
- SEARXNG_API_URL=http://searxng:8080 - SEARXNG_API_URL=http://searxng:8080
- DATABASE_URL=postgresql://postgres.ineclpmaolnshsnekjad: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@aws-0-us-east-1.pooler.supabase.com:6543/postgres
depends_on: depends_on:
- searxng - searxng
ports: ports:
- 3001:3001 - 3001:3001
volumes: volumes:
- backend-dbstore:/home/perplexica/data - backend-dbstore:/home/xme/data
- uploads:/home/perplexica/uploads - uploads:/home/xme/uploads
- ./config.toml:/home/perplexica/config.toml - ./config.toml:/home/perplexica/config.toml
extra_hosts: extra_hosts:
- 'host.docker.internal:host-gateway' - host.docker.internal:host-gateway
networks: networks:
- perplexica-network - xme-network
restart: unless-stopped restart: unless-stopped
perplexica-frontend: xme-frontend:
build: build:
context: . context: .
dockerfile: app.dockerfile dockerfile: app.dockerfile
args: args:
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3001/api NEXT_PUBLIC_WS_URL: ws://localhost:3001
- NEXT_PUBLIC_WS_URL=ws://127.0.0.1:3001 NEXT_PUBLIC_API_URL: http://localhost:3001/api
image: itzcrazykns1337/perplexica-frontend:main NEXT_PUBLIC_SUPABASE_URL: https://qytbxgzxsywnfhlwcyqa.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InF5dGJ4Z3p4c3l3bmZobHdjeXFhIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzEwNTc3MTAsImV4cCI6MjA0NjYzMzcxMH0.XLRq-4CFL2MWxvCLzCv5ZdaF5VSi58cocx9FOyv37jU
image: itzcrazykns1337/xme-frontend:main
depends_on: depends_on:
- perplexica-backend - xme-backend
ports: ports:
- 3000:3000 - 3000:3000
networks: networks:
- perplexica-network - xme-network
restart: unless-stopped restart: unless-stopped
networks: networks:
perplexica-network: xme-network:
volumes: volumes:
backend-dbstore: backend-dbstore:
uploads: uploads:
chroma_data:

View file

@ -30,11 +30,13 @@
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@langchain/anthropic": "^0.2.3", "@langchain/anthropic": "^0.2.3",
"@langchain/community": "^0.2.16", "@langchain/community": "^0.2.16",
"@langchain/openai": "^0.0.25",
"@langchain/google-genai": "^0.0.23", "@langchain/google-genai": "^0.0.23",
"@langchain/openai": "^0.0.25",
"@supabase/supabase-js": "latest",
"@xenova/transformers": "^2.17.1", "@xenova/transformers": "^2.17.1",
"axios": "^1.6.8", "axios": "^1.6.8",
"better-sqlite3": "^11.0.0", "better-sqlite3": "^11.0.0",
"chromadb": "^1.9.4",
"compute-cosine-similarity": "^1.1.0", "compute-cosine-similarity": "^1.1.0",
"compute-dot": "^1.1.0", "compute-dot": "^1.1.0",
"cors": "^2.8.5", "cors": "^2.8.5",

157
project_structure.md Normal file
View file

@ -0,0 +1,157 @@
# Structure du Projet X-me
```
X-me/
├── .assets/
├── .dockerignore
├── .git/
├── .github/
├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app.dockerfile
├── backend.dockerfile
├── config.toml
├── data/
├── docker-compose.yaml
├── docs/
├── drizzle.config.ts
├── package.json
├── project_structure.md
├── searxng/
│ ├── limiter.toml
│ ├── settings.yml
│ └── uwsgi.ini
├── src/
│ ├── app.ts
│ ├── config.ts
│ ├── chains/
│ │ ├── expertSearchAgent.ts
│ │ ├── imageSearchAgent.ts
│ │ ├── legalSearchAgent.ts
│ │ ├── suggestionGeneratorAgent.ts
│ │ └── videoSearchAgent.ts
│ ├── db/
│ │ ├── index.ts
│ │ ├── schema.ts
│ │ └── supabase.ts
│ ├── lib/
│ │ ├── huggingfaceTransformer.ts
│ │ ├── outputParsers/
│ │ │ ├── lineOutputParser.ts
│ │ │ └── listLineOutputParser.ts
│ │ ├── providers/
│ │ │ ├── anthropic.ts
│ │ │ ├── gemini.ts
│ │ │ ├── groq.ts
│ │ │ ├── index.ts
│ │ │ ├── ollama.ts
│ │ │ ├── openai.ts
│ │ │ └── transformers.ts
│ │ └── searxng.ts
│ ├── prompts/
│ │ ├── academicSearch.ts
│ │ ├── index.ts
│ │ ├── redditSearch.ts
│ │ ├── webSearch.ts
│ │ ├── wolframAlpha.ts
│ │ ├── writingAssistant.ts
│ │ └── youtubeSearch.ts
│ ├── routes/
│ │ ├── chats.ts
│ │ ├── config.ts
│ │ ├── discover.ts
│ │ ├── images.ts
│ │ ├── index.ts
│ │ ├── legal.ts
│ │ ├── models.ts
│ │ ├── search.ts
│ │ ├── suggestions.ts
│ │ ├── uploads.ts
│ │ └── videos.ts
│ ├── search/
│ │ └── metaSearchAgent.ts
│ ├── utils/
│ │ ├── computeSimilarity.ts
│ │ ├── documents.ts
│ │ ├── files.ts
│ │ ├── formatHistory.ts
│ │ └── logger.ts
│ └── websocket/
│ ├── connectionManager.ts
│ ├── index.ts
│ ├── messageHandler.ts
│ └── websocketServer.ts
├── tsconfig.json
├── ui/
│ ├── .env.example
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── .prettierrc.js
│ ├── app/
│ │ ├── c/
│ │ │ └── [chatId]/
│ │ │ └── page.tsx
│ │ ├── chatroom/
│ │ │ └── page.tsx
│ │ ├── discover/
│ │ │ └── page.tsx
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── library/
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ └── page.tsx
│ ├── components/
│ │ ├── ui/
│ │ │ ├── button.tsx
│ │ │ └── input.tsx
│ │ ├── Chat.tsx
│ │ ├── ChatWindow.tsx
│ │ ├── DeleteChat.tsx
│ │ ├── EmptyChat.tsx
│ │ ├── EmptyChatMessageInput.tsx
│ │ ├── Layout.tsx
│ │ ├── LegalSearch.tsx
│ │ ├── MessageBox.tsx
│ │ ├── MessageBoxLoading.tsx
│ │ ├── MessageInput.tsx
│ │ ├── MessageSources.tsx
│ │ ├── MessageActions/
│ │ │ ├── Copy.tsx
│ │ │ └── Rewrite.tsx
│ │ ├── MessageInputActions/
│ │ │ ├── Attach.tsx
│ │ │ ├── AttachSmall.tsx
│ │ │ ├── Copilot.tsx
│ │ │ ├── Focus.tsx
│ │ │ └── Optimization.tsx
│ │ ├── Navbar.tsx
│ │ ├── SearchImages.tsx
│ │ ├── SearchVideos.tsx
│ │ ├── SettingsDialog.tsx
│ │ ├── Sidebar.tsx
│ │ └── theme/
│ │ ├── Provider.tsx
│ │ └── Switcher.tsx
│ ├── lib/
│ │ ├── actions.ts
│ │ ├── supabase.ts
│ │ └── utils.ts
│ ├── next.config.mjs
│ ├── package.json
│ ├── postcss.config.js
│ ├── public/
│ │ ├── next.svg
│ │ └── vercel.svg
│ ├── tailwind.config.ts
│ ├── tsconfig.json
│ └── yarn.lock
├── uploads/
└── yarn.lock
Cette arborescence représente la structure complète du projet X-me, incluant tous les fichiers et dossiers.

View file

@ -1,14 +0,0 @@
[GENERAL]
PORT = 3001 # Port to run the server on
SIMILARITY_MEASURE = "cosine" # "cosine" or "dot"
KEEP_ALIVE = "5m" # How long to keep Ollama models loaded into memory. (Instead of using -1 use "-1m")
[API_KEYS]
OPENAI = "" # OpenAI API key - sk-1234567890abcdef1234567890abcdef
GROQ = "" # Groq API key - gsk_1234567890abcdef1234567890abcdef
ANTHROPIC = "" # Anthropic API key - sk-ant-1234567890abcdef1234567890abcdef
GEMINI = "" # Gemini API key - sk-1234567890abcdef1234567890abcdef
[API_ENDPOINTS]
SEARXNG = "http://localhost:32768" # SearxNG API URL
OLLAMA = "" # Ollama API URL - http://host.docker.internal:11434

View file

@ -4,14 +4,79 @@ general:
instance_name: 'searxng' instance_name: 'searxng'
search: search:
# Sources de recherche spécialisées
engines:
- name: legifrance
enabled: true
weight: 3
- name: service_public
enabled: true
weight: 3
- name: journal_officiel
enabled: true
weight: 2
- name: urssaf
enabled: true
weight: 2
- name: cci
enabled: true
weight: 1
- name: conseil_etat
enabled: true
weight: 1
- name: google_images
enabled: true
weight: 2
- name: bing_images
enabled: true
weight: 2
- name: wolframalpha
enabled: true
weight: 1
# Paramètres de recherche
autocomplete: 'google' autocomplete: 'google'
language: 'fr' # ou 'en' selon votre marché cible
formats: formats:
- html - html
- json - json
- csv
- pdf
# Filtres spécialisés
filters:
- type: 'time_range'
default: 'year' # Garder car pertinent pour la législation récente
- type: 'legal_type'
options:
- 'loi'
- 'decret'
- 'arrete'
- 'circulaire'
- type: 'jurisdiction'
options:
- 'national'
- 'regional'
- 'european'
- type: 'source'
options:
- 'legifrance'
- 'service_public'
- 'urssaf'
- 'cci'
# Paramètres de résultats
results:
max_pages: 10
safe_search: 0
categories:
- jurisprudence
- professional
- business
- legal
- entreprise
- sociéte
- images
server: server:
secret_key: 'a2fb23f1b02e6ee83875b09826990de0f6bd908b6638e8c10277d415f6ab852b' # Is overwritten by ${SEARXNG_SECRET} secret_key: 'a2fb23f1b02e6ee83875b09826990de0f6bd908b6638e8c10277d415f6ab852b' # Is overwritten by ${SEARXNG_SECRET}
engines:
- name: wolframalpha
disabled: false

View file

@ -5,6 +5,7 @@ import http from 'http';
import routes from './routes'; import routes from './routes';
import { getPort } from './config'; import { getPort } from './config';
import logger from './utils/logger'; import logger from './utils/logger';
import imagesRouter from './routes/images';
const port = getPort(); const port = getPort();
@ -23,6 +24,8 @@ app.get('/api', (_, res) => {
res.status(200).json({ status: 'ok' }); res.status(200).json({ status: 'ok' });
}); });
app.use('/api/images', imagesRouter);
server.listen(port, () => { server.listen(port, () => {
logger.info(`Server is running on port ${port}`); logger.info(`Server is running on port ${port}`);
}); });

View file

@ -0,0 +1,235 @@
import { ChatPromptTemplate, PromptTemplate } from '@langchain/core/prompts';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import {
RunnableLambda,
RunnableMap,
RunnableSequence,
} from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { BaseMessage } from '@langchain/core/messages';
import { supabase } from '../db/supabase';
import formatChatHistoryAsString from '../utils/formatHistory';
import { Expert, ExpertSearchRequest, ExpertSearchResponse } from '../types/types';
type ExpertSearchChainInput = {
chat_history: BaseMessage[];
query: string;
};
const ExpertSearchChainPrompt = `
Vous êtes un agent spécialisé dans l'analyse et la recherche d'experts professionnels. Votre rôle est d'interpréter les demandes des utilisateurs et d'extraire les informations essentielles pour trouver l'expert le plus pertinent.
OBJECTIF :
Analyser la requête pour identifier précisément :
1. Le domaine d'expertise recherché
2. La localisation souhaitée (si mentionnée)
RÈGLES D'EXTRACTION :
- Pour l'EXPERTISE :
* Identifier le domaine principal (comptabilité, droit, marketing, etc.)
* Reconnaître les spécialisations (droit des affaires, marketing digital, etc.)
* Nettoyer les mots parasites (expert, spécialiste, professionnel, etc.)
- Pour la VILLE :
* Si mentionnée
* Extraire la ville mentionnée
* Ignorer si non spécifiée
* Standardiser le format (tout en minuscules)
FORMAT DE RÉPONSE STRICT :
Répondre en deux lignes exactement :
expertise: [domaine d'expertise]
ville: [ville si mentionnée]
EXEMPLES D'ANALYSE :
1. "Je cherche un expert comptable sur Paris"
expertise: comptabilité
ville: paris
2. "Il me faudrait un avocat spécialisé en droit des affaires à Lyon"
expertise: droit des affaires
ville: lyon
Conversation précédente :
{chat_history}
Requête actuelle : {query}
Principe de recherche d'expert :
- Pour toute recherche d'expert, extraire UNIQUEMENT :
* L'expertise demandée
* La ville (si mentionnée)
- Mots déclencheurs à reconnaître :
* "cherche un expert/spécialiste/consultant"
* "besoin d'un professionnel"
* "recherche quelqu'un pour"
* "qui peut m'aider avec"
<example>
\`<query>
Je cherche un expert comptable
</query>
expertise: comptabilité
ville:
\`
\`<query>
J'ai besoin d'un spécialiste en droit des sociétés à Lyon
</query>
expertise: droit des sociétés
ville: lyon
\`
\`<query>
Qui peut m'aider avec ma comptabilité sur Paris ?
</query>
expertise: comptabilité
ville: paris
\`
</example>
`;
const ExpertAnalysisPrompt = `
Vous devez générer une synthèse des experts trouvés en vous basant UNIQUEMENT sur les données fournies.
Contexte de la recherche : {query}
Experts trouvés (à utiliser EXCLUSIVEMENT) :
{experts}
Format de la synthèse :
🎯 Synthèse de la recherche
[Résumé bref de la demande]
💫 Experts disponibles :
[Pour chaque expert trouvé dans les données :]
- [Prénom Nom] à [Ville]
Expertise : [expertises]
Tarif : [tarif]
[Point clé de la biographie]
IMPORTANT : N'inventez PAS d'experts. Utilisez UNIQUEMENT les données fournies.
`;
const strParser = new StringOutputParser();
// Fonction pour convertir les données de l'expert
const convertToExpert = (data: any): Expert => {
return {
id: data.id,
id_expert: data.id_expert || '',
nom: data.nom,
prenom: data.prenom,
adresse: data.adresse || '',
pays: data.pays,
ville: data.ville,
expertises: data.expertises,
specialite: data.specialite || data.expertises?.[0] || '',
biographie: data.biographie,
tarif: data.tarif || 0,
services: data.services,
created_at: data.created_at,
image_url: data.image_url
};
};
const createExpertSearchChain = (llm: BaseChatModel) => {
return RunnableSequence.from([
RunnableMap.from({
chat_history: (input: ExpertSearchChainInput) => {
return formatChatHistoryAsString(input.chat_history || []);
},
query: (input: ExpertSearchChainInput) => {
return input.query || '';
},
}),
PromptTemplate.fromTemplate(ExpertSearchChainPrompt),
llm,
strParser,
RunnableLambda.from(async (response: string) => {
try {
// Extraire expertise et ville avec gestion des erreurs
const lines = response.split('\n').filter(line => line.trim() !== '');
const expertise = lines[0]?.replace('expertise:', '')?.trim() || '';
const ville = lines[1]?.replace('ville:', '')?.trim() || '';
if (!expertise) {
return {
experts: [],
synthese: "Je n'ai pas pu identifier l'expertise recherchée."
} as ExpertSearchResponse;
}
// Rechercher les experts
let query = supabase
.from('experts')
.select('*')
.ilike('expertises', `%${expertise}%`)
.limit(3);
if (ville) {
query = query.ilike('ville', `%${ville}%`);
}
const { data: experts, error } = await query;
if (error) throw error;
if (!experts || experts.length === 0) {
return {
experts: [],
synthese: "Désolé, je n'ai pas trouvé d'experts correspondant à vos critères."
} as ExpertSearchResponse;
}
const synthesePrompt = PromptTemplate.fromTemplate(ExpertAnalysisPrompt);
const formattedPrompt = await synthesePrompt.format({
query: response,
experts: JSON.stringify(experts, null, 2)
});
const syntheseResponse = await llm.invoke(formattedPrompt);
const syntheseString = typeof syntheseResponse.content === 'string'
? syntheseResponse.content
: JSON.stringify(syntheseResponse.content);
return {
experts: experts.map(convertToExpert),
synthese: syntheseString
} as ExpertSearchResponse;
} catch (error) {
console.error('❌ Erreur:', error);
return {
experts: [],
synthese: "Une erreur est survenue lors de la recherche d'experts."
} as ExpertSearchResponse;
}
}),
]);
};
const handleExpertSearch = async (input: ExpertSearchRequest, llm: BaseChatModel) => {
try {
// 1. Analyse de la requête via LLM pour extraire l'expertise et la ville
const expertSearchChain = createExpertSearchChain(llm);
const result = await expertSearchChain.invoke({
query: input.query,
chat_history: input.chat_history || []
}) as ExpertSearchResponse; // Le résultat est déjà une ExpertSearchResponse
// Pas besoin de retraiter la réponse car createExpertSearchChain fait déjà tout le travail
return result;
} catch (error) {
console.error('❌ Erreur dans handleExpertSearch:', error);
return {
experts: [],
synthese: "Une erreur est survenue."
};
}
};
export default handleExpertSearch;

View file

@ -11,25 +11,35 @@ import { searchSearxng } from '../lib/searxng';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
const imageSearchChainPrompt = ` const imageSearchChainPrompt = `
You will be given a conversation below and a follow up question. You need to rephrase the follow-up question so it is a standalone question that can be used by the LLM to search the web for images. Vous êtes un expert en recherche d'images pour illustrer des contenus business. Votre objectif est de trouver des images élégantes et modernes qui illustrent le sujet de manière indirecte et esthétique.
You need to make sure the rephrased question agrees with the conversation and is relevant to the conversation.
Example: Principes à suivre :
1. Follow up question: What is a cat? - Privilégier des images lifestyle et esthétiques
Rephrased: A cat - Éviter les schémas, graphiques et images trop techniques
- Favoriser des images avec des personnes dans des situations naturelles
- Choisir des images lumineuses et positives
- Préférer des compositions simples et épurées
2. Follow up question: What is a car? How does it works? Format de la requête :
Rephrased: Car working - 2-3 mots-clés maximum
- Ajouter "lifestyle" ou "modern" pour améliorer la qualité
- Toujours ajouter "professional" pour le contexte business
3. Follow up question: How does an AC work? Exemples :
Rephrased: AC working 1. Question : "Comment créer une entreprise ?"
Requête : "entrepreneur lifestyle modern"
Conversation: 2. Question : "Qu'est-ce qu'un business plan ?"
Requête : "business meeting professional"
3. Question : "Comment faire sa comptabilité ?"
Requête : "office work lifestyle"
Conversation :
{chat_history} {chat_history}
Follow up question: {query} Question : {query}
Rephrased question: Requête de recherche d'image :`;
`;
type ImageSearchChainInput = { type ImageSearchChainInput = {
chat_history: BaseMessage[]; chat_history: BaseMessage[];
@ -53,11 +63,12 @@ const createImageSearchChain = (llm: BaseChatModel) => {
strParser, strParser,
RunnableLambda.from(async (input: string) => { RunnableLambda.from(async (input: string) => {
const res = await searchSearxng(input, { const res = await searchSearxng(input, {
engines: ['bing images', 'google images'], engines: ['google_images', 'bing_images'],
language: 'fr',
categories: ['images'],
}); });
const images = []; const images = [];
res.results.forEach((result) => { res.results.forEach((result) => {
if (result.img_src && result.url && result.title) { if (result.img_src && result.url && result.title) {
images.push({ images.push({

View file

@ -0,0 +1,113 @@
import {
RunnableSequence,
RunnableMap,
RunnableLambda,
} from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { searchSearxng } from '../lib/searxng';
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
const legalSearchChainPrompt = `
Vous êtes un assistant juridique expert spécialisé dans la recherche documentaire légale française. Votre rôle est d'analyser la question de l'utilisateur et de générer une requête de recherche optimisée.
Contexte de la conversation :
{chat_history}
Question actuelle : {query}
Instructions détaillées :
1. Analysez précisément :
- Le domaine juridique spécifique (droit du travail, droit des sociétés, etc.)
- Le type de document recherché (loi, décret, jurisprudence, etc.)
- Les points clés de la problématique
2. Construisez une requête qui inclut :
- Les termes juridiques exacts (articles de code, références légales)
- Les mots-clés techniques appropriés
- Les synonymes pertinents
- La période temporelle si pertinente (loi récente, modifications)
3. Priorisez les sources selon la hiérarchie :
- Codes et lois : Légifrance
- Information officielle : Service-public.fr
- Publications : Journal-officiel
- Informations pratiques : URSSAF, CCI
Exemples de reformulation :
Question : "Comment créer une SARL ?"
"Code commerce SARL constitution statuts gérance responsabilité associés capital social formalités légifrance service-public"
Question : "Licenciement économique procédure"
"Code travail licenciement économique procédure CSE PSE motif notification délais recours légifrance"
Question : "Bail commercial résiliation"
"Code commerce bail commercial résiliation article L145-4 congé indemnité éviction légifrance jurisprudence"
Reformulez la question de manière précise et technique :`;
type LegalSearchChainInput = {
chat_history: BaseMessage[];
query: string;
};
const strParser = new StringOutputParser();
const createLegalSearchChain = (llm: BaseChatModel) => {
return RunnableSequence.from([
RunnableMap.from({
chat_history: (input: LegalSearchChainInput) => {
return formatChatHistoryAsString(input.chat_history);
},
query: (input: LegalSearchChainInput) => {
return input.query;
},
}),
PromptTemplate.fromTemplate(legalSearchChainPrompt),
llm,
strParser,
RunnableLambda.from(async (input: string) => {
const pdfQuery = `${input} filetype:pdf`;
const res = await searchSearxng(pdfQuery, {
engines: [
'legifrance',
'journal_officiel',
'service_public',
'URSSAF',
'CCI'
],
language: 'fr',
categories: ['general', 'files']
});
const documents = [];
res.results.forEach((result) => {
if (result.url && result.title) {
documents.push({
url: result.url,
title: result.title,
snippet: result.content || '',
source: result.url.split('/')[2] || 'unknown',
type: 'pdf'
});
}
});
return documents.slice(0, 10);
}),
]);
};
const handleLegalSearch = (
input: LegalSearchChainInput,
llm: BaseChatModel,
) => {
const legalSearchChain = createLegalSearchChain(llm);
return legalSearchChain.invoke(input);
};
export default handleLegalSearch;

View file

@ -0,0 +1,292 @@
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import { Document } from '@langchain/core/documents';
import { Embeddings } from '@langchain/core/embeddings';
import { Chroma } from '@langchain/community/vectorstores/chroma';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { RunnableSequence, RunnableMap } from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import formatChatHistoryAsString from '../utils/formatHistory';
import { BaseMessage } from '@langchain/core/messages';
// Type local pour la chaîne de recherche
type SearchInput = {
query: string;
chat_history: BaseMessage[];
type?: string;
};
export class RAGDocumentChain {
private vectorStore: Chroma | null = null;
private textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
separators: ["\n\n", "\n", ".", "!", "?", ";", ":", " ", ""],
keepSeparator: true,
lengthFunction: (text) => text.length
});
// Add chunk preprocessing
private preprocessChunk(text: string): string {
return text
.replace(/\s+/g, ' ')
.replace(/\n+/g, ' ')
.trim();
}
// Add metadata enrichment
private enrichChunkMetadata(doc: Document): Document {
const metadata = {
...doc.metadata,
chunk_type: 'text',
word_count: doc.pageContent.split(/\s+/).length,
processed_date: new Date().toISOString()
};
return new Document({
pageContent: this.preprocessChunk(doc.pageContent),
metadata
});
}
// Add chunk scoring
private scoreChunk(chunk: string): number {
const wordCount = chunk.split(/\s+/).length;
const sentenceCount = chunk.split(/[.!?]+/).length;
return wordCount > 10 && sentenceCount > 0 ? 1 : 0;
}
public async initializeVectorStoreFromDocuments(
documents: Document[],
embeddings: Embeddings
) {
try {
console.log("🔄 Préparation des documents...");
// Validate and preprocess documents
const validDocuments = documents
.filter(doc => doc.pageContent && doc.pageContent.trim().length > 50)
.map(doc => this.enrichChunkMetadata(doc));
// Split documents into chunks
const texts = await this.textSplitter.splitDocuments(validDocuments);
console.log(`📄 ${texts.length} chunks créés`);
// Score and filter chunks
const scoredTexts = texts.filter(doc => this.scoreChunk(doc.pageContent) > 0);
console.log(`📄 ${scoredTexts.length} chunks valides après scoring`);
// Deduplicate chunks
const uniqueTexts = this.deduplicateChunks(scoredTexts);
console.log(`📄 ${uniqueTexts.length} chunks uniques après déduplication`);
// Initialize vector store with optimized settings
this.vectorStore = await Chroma.fromDocuments(
uniqueTexts,
embeddings,
{
collectionName: "uploaded_docs",
url: "http://chroma:8000",
collectionMetadata: {
"hnsw:space": "cosine",
"hnsw:construction_ef": 100, // Increased for better index quality
"hnsw:search_ef": 50, // Balanced for search performance
"hnsw:m": 16 // Number of connections per element
}
}
);
console.log("✅ VectorStore initialisé avec succès");
return {
totalDocuments: documents.length,
validChunks: uniqueTexts.length,
averageChunkSize: this.calculateAverageChunkSize(uniqueTexts)
};
} catch (error) {
console.error("❌ Erreur lors de l'initialisation:", error);
throw new Error(`Erreur d'initialisation du VectorStore: ${error.message}`);
}
}
private calculateAverageChunkSize(chunks: Document[]): number {
if (chunks.length === 0) return 0;
const totalLength = chunks.reduce((sum, doc) => sum + doc.pageContent.length, 0);
return Math.round(totalLength / chunks.length);
}
private deduplicateChunks(chunks: Document[]): Document[] {
const seen = new Set<string>();
return chunks.filter(chunk => {
const normalized = chunk.pageContent
.toLowerCase()
.replace(/\s+/g, ' ')
.trim();
if (seen.has(normalized)) {
return false;
}
seen.add(normalized);
return true;
});
}
public async searchSimilarDocuments(query: string, limit: number = 5) {
if (!this.vectorStore) {
console.warn("⚠️ VectorStore non initialisé");
return [];
}
try {
console.log("🔍 Recherche pour:", query);
const initialResults = await this.vectorStore.similaritySearch(
query,
limit * 2,
{
filter: { source: { $exists: true } },
minScore: 0.7
}
);
const scoredResults = initialResults
.filter(doc => doc.pageContent.trim().length > 50)
.map(doc => ({
document: doc,
score: this.calculateRelevanceScore(query, doc.pageContent)
}))
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map(item => {
const doc = item.document;
const pageNumber = doc.metadata.page_number || doc.metadata.pageNumber || 1;
const title = doc.metadata.title || 'Document';
const source = doc.metadata.source;
// Préparer le texte à surligner
const searchText = doc.pageContent
.substring(0, 200)
.replace(/[\n\r]+/g, ' ')
.trim();
return new Document({
pageContent: doc.pageContent,
metadata: {
title: title,
pageNumber: pageNumber,
source: source,
type: doc.metadata.type || 'uploaded',
searchText: searchText,
url: source ?
`/api/uploads/${source}/view?page=${pageNumber}&search=${encodeURIComponent(searchText)}` :
undefined
}
});
});
const mergedResults = this.mergeRelatedChunks(scoredResults);
console.log(`📄 ${mergedResults.length} documents pertinents trouvés après reranking`);
return mergedResults;
} catch (error) {
console.error("❌ Erreur de recherche:", error);
return [];
}
}
private calculateRelevanceScore(query: string, content: string): number {
const normalizedQuery = query.toLowerCase();
const normalizedContent = content.toLowerCase();
// Basic relevance scoring based on multiple factors
let score = 0;
// Term frequency
const queryTerms = normalizedQuery.split(/\s+/);
queryTerms.forEach(term => {
const termCount = (normalizedContent.match(new RegExp(term, 'g')) || []).length;
score += termCount * 0.1;
});
// Exact phrase matching
if (normalizedContent.includes(normalizedQuery)) {
score += 1;
}
// Content length penalty (prefer shorter, more focused chunks)
const lengthPenalty = Math.max(0, 1 - (content.length / 5000));
score *= (1 + lengthPenalty);
return score;
}
private mergeRelatedChunks(documents: Document[]): Document[] {
const merged: { [key: string]: Document } = {};
documents.forEach(doc => {
const source = doc.metadata?.source || '';
const page = doc.metadata?.pageNumber || 1;
const key = `${source}-${page}`;
if (!merged[key]) {
merged[key] = doc;
} else {
const existingDoc = merged[key];
merged[key] = new Document({
pageContent: `${existingDoc.pageContent}\n\n${doc.pageContent}`,
metadata: {
...existingDoc.metadata,
searchText: existingDoc.metadata.searchText
}
});
}
});
return Object.values(merged);
}
public createSearchChain(llm: BaseChatModel) {
return RunnableSequence.from([
RunnableMap.from({
query: (input: SearchInput) => input.query,
chat_history: (input: SearchInput) => formatChatHistoryAsString(input.chat_history),
context: async (input: SearchInput) => {
const docs = await this.searchSimilarDocuments(input.query);
return docs.map((doc, i) => {
const source = doc.metadata?.source || 'Document';
const title = doc.metadata?.title || '';
const pageNumber = doc.metadata?.pageNumber;
const url = doc.metadata?.url;
let sourceInfo = `Source: ${title || source}`;
if (pageNumber) sourceInfo += ` (page ${pageNumber})`;
if (url) sourceInfo += `\nURL: ${url}`;
return `[Source ${i + 1}] ${doc.pageContent}\n${sourceInfo}`;
}).join("\n\n");
}
}),
PromptTemplate.fromTemplate(`
Tu es un assistant expert qui répond aux questions en se basant uniquement sur le contexte fourni.
Historique de la conversation:
{chat_history}
Contexte disponible:
{context}
Question: {query}
Instructions:
1. Réponds uniquement en te basant sur le contexte fourni
2. Si la réponse n'est pas dans le contexte, dis-le clairement
3. Cite les sources pertinentes en utilisant [Source X]
4. Sois précis et concis
Réponse:
`),
llm,
new StringOutputParser()
]);
}
public isInitialized(): boolean {
return this.vectorStore !== null;
}
}

View file

@ -15,10 +15,12 @@ interface Config {
GROQ: string; GROQ: string;
ANTHROPIC: string; ANTHROPIC: string;
GEMINI: string; GEMINI: string;
SUPABASE: string;
}; };
API_ENDPOINTS: { API_ENDPOINTS: {
SEARXNG: string; SEARXNG: string;
OLLAMA: string; OLLAMA: string;
SUPABASE_URL: string;
}; };
} }
@ -46,9 +48,15 @@ export const getAnthropicApiKey = () => loadConfig().API_KEYS.ANTHROPIC;
export const getGeminiApiKey = () => loadConfig().API_KEYS.GEMINI; export const getGeminiApiKey = () => loadConfig().API_KEYS.GEMINI;
export const getSupabaseKey = () =>
process.env.SUPABASE_KEY || loadConfig().API_KEYS.SUPABASE;
export const getSearxngApiEndpoint = () => export const getSearxngApiEndpoint = () =>
process.env.SEARXNG_API_URL || loadConfig().API_ENDPOINTS.SEARXNG; process.env.SEARXNG_API_URL || loadConfig().API_ENDPOINTS.SEARXNG;
export const getSupabaseUrl = () =>
process.env.SUPABASE_URL || loadConfig().API_ENDPOINTS.SUPABASE_URL;
export const getOllamaApiEndpoint = () => loadConfig().API_ENDPOINTS.OLLAMA; export const getOllamaApiEndpoint = () => loadConfig().API_ENDPOINTS.OLLAMA;
export const updateConfig = (config: RecursivePartial<Config>) => { export const updateConfig = (config: RecursivePartial<Config>) => {

29
src/db/supabase.ts Normal file
View file

@ -0,0 +1,29 @@
// Dans supabase.ts
import { createClient } from '@supabase/supabase-js';
import { getSupabaseUrl, getSupabaseKey } from '../config';
const supabaseUrl = getSupabaseUrl();
const supabaseKey = getSupabaseKey();
if (!supabaseUrl || !supabaseKey) {
throw new Error('Missing Supabase credentials');
}
export const supabase = createClient(supabaseUrl, supabaseKey);
// Fonction de test de connexion
export async function checkSupabaseConnection() {
try {
const { data, error } = await supabase
.from('experts')
.select('*')
.limit(1);
if (error) throw error;
console.log('✅ Connexion Supabase établie avec succès');
return true;
} catch (error) {
console.error('❌ Erreur de connexion Supabase:', error);
return false;
}
}

View file

@ -0,0 +1,26 @@
import { BaseOutputParser } from "@langchain/core/output_parsers";
export interface ImageSearchResult {
query: string;
context?: string;
}
class ImageOutputParser extends BaseOutputParser<ImageSearchResult> {
lc_namespace = ['langchain', 'output_parsers', 'image_output_parser'];
async parse(text: string): Promise<ImageSearchResult> {
const parts = text.split('IMAGE:');
return {
query: parts[1]?.trim() || '',
context: parts[0].replace('RÉSUMÉ:', '').trim()
};
}
getFormatInstructions(): string {
return `Le format attendu est:
RÉSUMÉ: <contexte>
IMAGE: <requête d'image>`;
}
}
export default ImageOutputParser;

View file

@ -1,10 +1,11 @@
import axios from 'axios'; import axios from 'axios';
import { getSearxngApiEndpoint } from '../config'; import { getSearxngApiEndpoint } from '../config';
interface SearxngSearchOptions { export interface SearxngSearchOptions {
categories?: string[];
engines?: string[];
language?: string; language?: string;
engines?: string[];
categories?: string[];
limit?: number;
pageno?: number; pageno?: number;
} }
@ -19,10 +20,10 @@ interface SearxngSearchResult {
iframe_src?: string; iframe_src?: string;
} }
export const searchSearxng = async ( export async function searchSearxng(
query: string, query: string,
opts?: SearxngSearchOptions, opts: SearxngSearchOptions = {}
) => { ) {
const searxngURL = getSearxngApiEndpoint(); const searxngURL = getSearxngApiEndpoint();
const url = new URL(`${searxngURL}/search?format=json`); const url = new URL(`${searxngURL}/search?format=json`);
@ -44,4 +45,4 @@ export const searchSearxng = async (
const suggestions: string[] = res.data.suggestions; const suggestions: string[] = res.data.suggestions;
return { results, suggestions }; return { results, suggestions };
}; }

View file

@ -1,46 +1,85 @@
export const webSearchRetrieverPrompt = ` export const webSearchRetrieverPrompt = `
You are an AI question rephraser. You will be given a conversation and a follow-up question, you will have to rephrase the follow up question so it is a standalone question and can be used by another LLM to search the web for information to answer it. Tu es X-me une IA analyste spécialisée dans l'entrepreneuriat et le développement des TPE/PME et artisans, avec une expertise particulière en droit des affaires. Votre rôle est de reformuler les questions pour cibler les textes juridiques et réglementaires pertinents.
If it is a smple writing task or a greeting (unless the greeting contains a question after it) like Hi, Hello, How are you, etc. than a question then you need to return \`not_needed\` as the response (This is because the LLM won't need to search the web for finding information on this topic).
If the user asks some question from some URL or wants you to summarize a PDF or a webpage (via URL) you need to return the links inside the \`links\` XML block and the question inside the \`question\` XML block. If the user wants to you to summarize the webpage or the PDF you need to return \`summarize\` inside the \`question\` XML block in place of a question and the link to summarize in the \`links\` XML block.
You must always return the rephrased question inside the \`question\` XML block, if there are no links in the follow-up question then don't insert a \`links\` XML block in your response.
There are several examples attached for your reference inside the below \`examples\` XML block ### Sources Juridiques Prioritaires
1. **Codes**:
- Code civil
- Code de commerce
- Code du travail
- Code de la consommation
- Code général des impôts
2. **Textes Réglementaires**:
- Lois
- Décrets
- Arrêtés
- Circulaires
3. **Jurisprudence**:
- Décisions de la Cour de cassation
- Arrêts du Conseil d'État
- Décisions des Cours d'appel
4. **Sources Officielles**:
- Journal officiel
- Bulletins officiels
- Documentation administrative
Pour chaque question, vous devez :
1. Identifier les textes juridiques applicables
2. Citer les articles précis des codes concernés
3. Rechercher la jurisprudence pertinente
4. Vérifier les dernières modifications législatives
### Sources d'Information Prioritaires
1. **LegalAI**: Légifrance, CNIL, URSSAF pour les aspects juridiques
2. **FinanceAI**: BPI France, Impots.gouv.fr, INSEE pour la finance
3. **GrowthAI**: CREDOC, CMA France pour le développement commercial
4. **MatchAI**: Annuaires des Experts-Comptables, APEC pour l'expertise
5. **StrategyAI**: France Stratégie, Bpifrance Le Lab pour la stratégie
6. **PeopleAI**: DARES, Pôle emploi pour les RH
7. **ToolBoxAI**: CCI France, LegalPlace pour les outils pratiques
8. **TechAI**: INRIA, French Tech pour l'innovation
9. **StartAI**: Portail Auto-Entrepreneur, CCI pour la création
10. **MasterAI**: Data.gouv.fr, Eurostat pour les données centralisées
Dans l'analyse des questions, privilégiez :
- Les aspects de création et développement d'entreprise
- Les exigences administratives et juridiques
- Les considérations financières et opérationnelles
- L'analyse de marché et la stratégie
- Le développement professionnel et la formation
Si c'est une tâche simple d'écriture ou un salut (sauf si le salut contient une question après) comme Hi, Hello, How are you, etc. alors vous devez retourner \`not_needed\` comme réponse (C'est parce que le LLM ne devrait pas chercher des informations sur ce sujet).
Si l'utilisateur demande une question d'un certain URL ou veut que vous résumiez un PDF ou une page web (via URL) vous devez retourner les liens à l'intérieur du bloc \`links\` XML et la question à l'intérieur du bloc \`question\` XML. Si l'utilisateur veut que vous résumiez la page web ou le PDF vous devez retourner \`summarize\` à l'intérieur du bloc \`question\` XML en remplacement de la question et le lien à résumer dans le bloc \`links\` XML.
Vous devez toujours retourner la question reformulée à l'intérieur du bloc \`question\` XML, si il n'y a pas de liens dans la question de suivi alors ne pas insérer un bloc \`links\` XML dans votre réponse.
Il y a plusieurs exemples attachés pour votre référence à l'intérieur du bloc \`examples\` XML
<examples> <examples>
1. Follow up question: What is the capital of France 1. Question de suivi : Comment créer mon entreprise ?
Rephrased question:\` Question reformulée :\`
<question> <question>
Capital of france Étapes et conditions pour créer une entreprise en France, procédures administratives et aides disponibles selon les sources StartAI (CCI, Auto-entrepreneur) et LegalAI (URSSAF)
</question> </question>
\` \`
2. Hi, how are you? 2. Question de suivi : Quels financements sont disponibles ?
Rephrased question\` Question reformulée :\`
<question>
Options de financement et aides financières disponibles pour les TPE/PME et artisans en France selon FinanceAI (BPI France) et MasterAI (Data.gouv.fr)
</question>
\`
3. Question de suivi : Bonjour, comment allez-vous ?
Question reformulée :\`
<question> <question>
not_needed not_needed
</question> </question>
\` \`
3. Follow up question: What is Docker? 4. Question de suivi : Pouvez-vous analyser ce business plan sur https://example.com ?
Rephrased question: \` Question reformulée :\`
<question>
What is Docker
</question>
\`
4. Follow up question: Can you tell me what is X from https://example.com
Rephrased question: \`
<question>
Can you tell me what is X?
</question>
<links>
https://example.com
</links>
\`
5. Follow up question: Summarize the content from https://example.com
Rephrased question: \`
<question> <question>
summarize summarize
</question> </question>
@ -51,27 +90,39 @@ https://example.com
\` \`
</examples> </examples>
Anything below is the part of the actual conversation and you need to use conversation and the follow-up question to rephrase the follow-up question as a standalone question based on the guidelines shared above.
<conversation> <conversation>
{chat_history} {chat_history}
</conversation> </conversation>
Follow up question: {query} Question de suivi : {query}
Rephrased question: Question reformulée :
`; `;
export const webSearchResponsePrompt = ` export const webSearchResponsePrompt = `
You are Perplexica, an AI model skilled in web search and crafting detailed, engaging, and well-structured answers. You excel at summarizing web pages and extracting relevant information to create professional, blog-style responses. Vous êtes X-me, une IA experte en conseil aux entreprises, spécialisée dans l'accompagnement des TPE, PME et artisans. Votre expertise couvre la création d'entreprise, le développement commercial, la gestion et le conseil stratégique. Vous excellez dans l'analyse des informations du marché et fournissez des conseils pratiques et applicables.
Your task is to provide answers that are: ### Sources d'Information Prioritaires
- **Informative and relevant**: Thoroughly address the user's query using the given context. 1. **LegalAI (Administratif & Juridique)**:
- **Well-structured**: Include clear headings and subheadings, and use a professional tone to present information concisely and logically. - Légifrance, CNIL, URSSAF
- **Engaging and detailed**: Write responses that read like a high-quality blog post, including extra details and relevant insights. - Journal officiel, Cours et tribunaux
- **Cited and credible**: Use inline citations with [number] notation to refer to the context source(s) for each fact or detail included.
- **Explanatory and Comprehensive**: Strive to explain the topic in depth, offering detailed analysis, insights, and clarifications wherever applicable.
### Formatting Instructions Vos réponses doivent être :
- **Orientées Business**: Prioriser les informations pertinentes pour les entrepreneurs, dirigeants de TPE/PME et artisans
- **Pratiques et Actionnables**: Fournir des conseils concrets et des solutions réalisables
- **Contextualisées**: Prendre en compte les défis et contraintes spécifiques des petites entreprises
- **Adaptées aux Ressources**: Proposer des solutions tenant compte des moyens limités des petites structures
- **Conformes à la Réglementation**: Inclure les aspects réglementaires et administratifs pertinents pour les entreprises françaises
### Domaines d'Expertise
- Création et Développement d'Entreprise
- Démarches Administratives et Juridiques
- Gestion Financière et Recherche de Financements
- Analyse de Marché et Stratégie
- Gestion Opérationnelle et des Ressources
- Transformation Numérique
- Formation Professionnelle et Développement des Compétences
### Instructions de Formatage
- **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate. - **Structure**: Use a well-organized format with proper headings (e.g., "## Example heading 1" or "## Example heading 2"). Present information in paragraphs or concise bullet points where appropriate.
- **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience. - **Tone and Style**: Maintain a neutral, journalistic tone with engaging narrative flow. Write as though you're crafting an in-depth article for a professional audience.
- **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability. - **Markdown Usage**: Format your response with Markdown for clarity. Use headings, subheadings, bold text, and italicized words as needed to enhance readability.
@ -79,7 +130,7 @@ export const webSearchResponsePrompt = `
- **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title. - **No main heading/title**: Start your response directly with the introduction unless asked to provide a specific title.
- **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate. - **Conclusion or Summary**: Include a concluding paragraph that synthesizes the provided information or suggests potential next steps, where appropriate.
### Citation Requirements ### Citations Requises
- Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`. - Cite every single fact, statement, or sentence using [number] notation corresponding to the source from the provided \`context\`.
- Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]." - Integrate citations naturally at the end of sentences or clauses as appropriate. For example, "The Eiffel Tower is one of the most visited landmarks in the world[1]."
- Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context. - Ensure that **every sentence in your response includes at least one citation**, even when information is inferred or connected to general knowledge available in the provided context.
@ -87,20 +138,17 @@ export const webSearchResponsePrompt = `
- Always prioritize credibility and accuracy by linking all statements back to their respective context sources. - Always prioritize credibility and accuracy by linking all statements back to their respective context sources.
- Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation. - Avoid citing unsupported assumptions or personal interpretations; if no source supports a statement, clearly indicate the limitation.
### Special Instructions ### Instructions Spéciales
- If the query involves technical, historical, or complex topics, provide detailed background and explanatory sections to ensure clarity. - Pour les sujets techniques ou administratifs, fournir des guides étape par étape adaptés aux non-experts
- If the user provides vague input or if relevant information is missing, explain what additional details might help refine the search. - Pour les solutions ou outils, considérer les contraintes budgétaires des petites entreprises
- If no relevant information is found, say: "Hmm, sorry I could not find any relevant information on this topic. Would you like me to search again or ask something else?" Be transparent about limitations and suggest alternatives or ways to reframe the query. - Inclure les informations sur les aides et dispositifs de soutien disponibles
- Pour la réglementation, préciser si elle s'applique spécifiquement aux artisans, TPE ou PME
### Example Output - Mentionner les organisations professionnelles ou ressources pertinentes
- Begin with a brief introduction summarizing the event or query topic.
- Follow with detailed sections under clear headings, covering all aspects of the query if possible.
- Provide explanations or historical context as needed to enhance understanding.
- End with a conclusion or overall perspective if relevant.
<context> <context>
{context} {context}
</context> </context>
Current date & time in ISO format (UTC timezone) is: {date}. Date et heure actuelles au format ISO (fuseau UTC) : {date}.
`; `;

View file

@ -1,47 +1,21 @@
import express from 'express'; import { Router } from 'express';
import { searchSearxng } from '../lib/searxng'; import { supabase } from '../db/supabase';
import logger from '../utils/logger';
const router = express.Router(); const router = Router();
router.get('/', async (req, res) => { // Route pour récupérer les experts
router.get('/experts', async (req, res) => {
try { try {
const data = ( const { data, error } = await supabase
await Promise.all([ .from('experts')
searchSearxng('site:businessinsider.com AI', { .select('*');
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:www.exchangewire.com AI', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:yahoo.com AI', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:businessinsider.com tech', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:www.exchangewire.com tech', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:yahoo.com tech', {
engines: ['bing news'],
pageno: 1,
}),
])
)
.map((result) => result.results)
.flat()
.sort(() => Math.random() - 0.5);
return res.json({ blogs: data }); if (error) throw error;
} catch (err: any) {
logger.error(`Error in discover route: ${err.message}`); res.json(data);
return res.status(500).json({ message: 'An error has occurred' }); } catch (error) {
console.error('Error fetching experts:', error);
res.status(500).json({ error: error.message });
} }
}); });

114
src/routes/experts.ts Normal file
View file

@ -0,0 +1,114 @@
import express from 'express';
import handleExpertSearch from '../chains/expertSearchAgent';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { getAvailableChatModelProviders } from '../lib/providers';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import logger from '../utils/logger';
import { ChatOpenAI } from '@langchain/openai';
import { ExpertSearchRequest } from '../types/types';
import crypto from 'crypto';
const router = express.Router();
interface ChatModel {
provider: string;
model: string;
customOpenAIBaseURL?: string;
customOpenAIKey?: string;
}
interface ExpertSearchBody {
query: string;
chatHistory: any[];
chatModel?: ChatModel;
}
router.post('/', async (req, res) => {
try {
const body: ExpertSearchBody = req.body;
// Conversion de l'historique du chat
const chatHistory = body.chatHistory.map((msg: any) => {
if (msg.role === 'user') {
return new HumanMessage(msg.content);
} else if (msg.role === 'assistant') {
return new AIMessage(msg.content);
}
});
// Configuration du modèle LLM
const chatModelProviders = await getAvailableChatModelProviders();
const chatModelProvider =
body.chatModel?.provider || Object.keys(chatModelProviders)[0];
const chatModel =
body.chatModel?.model ||
Object.keys(chatModelProviders[chatModelProvider])[0];
let llm: BaseChatModel | undefined;
if (body.chatModel?.provider === 'custom_openai') {
if (
!body.chatModel?.customOpenAIBaseURL ||
!body.chatModel?.customOpenAIKey
) {
return res
.status(400)
.json({ message: 'Missing custom OpenAI base URL or key' });
}
llm = new ChatOpenAI({
modelName: body.chatModel.model,
openAIApiKey: body.chatModel.customOpenAIKey,
temperature: 0.7,
configuration: {
baseURL: body.chatModel.customOpenAIBaseURL,
},
}) as unknown as BaseChatModel;
} else if (
chatModelProviders[chatModelProvider] &&
chatModelProviders[chatModelProvider][chatModel]
) {
llm = chatModelProviders[chatModelProvider][chatModel]
.model as unknown as BaseChatModel | undefined;
}
if (!llm) {
return res.status(400).json({ message: 'Invalid model selected' });
}
// Génération des IDs uniques
const messageId = crypto.randomBytes(7).toString('hex');
const chatId = crypto.randomBytes(7).toString('hex');
// Préparation de la requête
const expertSearchRequest: ExpertSearchRequest = {
query: body.query,
chat_history: chatHistory,
messageId,
chatId
};
// Recherche d'experts
const expertResults = await handleExpertSearch(expertSearchRequest, llm);
console.log("🔍 Experts trouvés:", expertResults.experts.length);
// Format unifié de la réponse
res.status(200).json({
type: 'expert_results',
messageId,
data: {
experts: expertResults.experts,
synthese: expertResults.synthese,
query: body.query
}
});
} catch (err) {
console.error("🔍 Erreur dans la recherche d'experts:", err);
res.status(500).json({ message: 'Une erreur est survenue.' });
logger.error(`Erreur dans la recherche d'experts: ${err.message}`);
}
});
export default router;

View file

@ -24,6 +24,7 @@ interface ImageSearchBody {
router.post('/', async (req, res) => { router.post('/', async (req, res) => {
try { try {
let body: ImageSearchBody = req.body; let body: ImageSearchBody = req.body;
console.log("📸 Requête de recherche d'images reçue:", body.query);
const chatHistory = body.chatHistory.map((msg: any) => { const chatHistory = body.chatHistory.map((msg: any) => {
if (msg.role === 'user') { if (msg.role === 'user') {
@ -73,6 +74,7 @@ router.post('/', async (req, res) => {
return res.status(400).json({ message: 'Invalid model selected' }); return res.status(400).json({ message: 'Invalid model selected' });
} }
const images = await handleImageSearch( const images = await handleImageSearch(
{ query: body.query, chat_history: chatHistory }, { query: body.query, chat_history: chatHistory },
llm, llm,

View file

@ -6,8 +6,10 @@ import modelsRouter from './models';
import suggestionsRouter from './suggestions'; import suggestionsRouter from './suggestions';
import chatsRouter from './chats'; import chatsRouter from './chats';
import searchRouter from './search'; import searchRouter from './search';
import discoverRouter from './discover'; import newsRouter from './news';
import uploadsRouter from './uploads'; import uploadsRouter from './uploads';
import legalRouter from './legal';
import discoverRouter from './discover';
const router = express.Router(); const router = express.Router();
@ -18,7 +20,9 @@ router.use('/models', modelsRouter);
router.use('/suggestions', suggestionsRouter); router.use('/suggestions', suggestionsRouter);
router.use('/chats', chatsRouter); router.use('/chats', chatsRouter);
router.use('/search', searchRouter); router.use('/search', searchRouter);
router.use('/discover', discoverRouter); router.use('/news', newsRouter);
router.use('/uploads', uploadsRouter); router.use('/uploads', uploadsRouter);
router.use('/legal', legalRouter);
router.use('/discover', discoverRouter);
export default router; export default router;

88
src/routes/legal.ts Normal file
View file

@ -0,0 +1,88 @@
import express from 'express';
import handleLegalSearch from '../chains/legalSearchAgent'; // Nouveau nom
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { getAvailableChatModelProviders } from '../lib/providers';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import logger from '../utils/logger';
import { ChatOpenAI } from '@langchain/openai';
const router = express.Router();
interface ChatModel {
provider: string;
model: string;
customOpenAIBaseURL?: string;
customOpenAIKey?: string;
}
interface LegalSearchBody { // Renommé
query: string;
chatHistory: any[];
chatModel?: ChatModel;
}
router.post('/', async (req, res) => {
try {
let body: LegalSearchBody = req.body;
const chatHistory = body.chatHistory.map((msg: any) => {
if (msg.role === 'user') {
return new HumanMessage(msg.content);
} else if (msg.role === 'assistant') {
return new AIMessage(msg.content);
}
});
const chatModelProviders = await getAvailableChatModelProviders();
const chatModelProvider =
body.chatModel?.provider || Object.keys(chatModelProviders)[0];
const chatModel =
body.chatModel?.model ||
Object.keys(chatModelProviders[chatModelProvider])[0];
let llm: BaseChatModel | undefined;
if (body.chatModel?.provider === 'custom_openai') {
if (
!body.chatModel?.customOpenAIBaseURL ||
!body.chatModel?.customOpenAIKey
) {
return res
.status(400)
.json({ message: 'Missing custom OpenAI base URL or key' });
}
llm = new ChatOpenAI({
modelName: body.chatModel.model,
openAIApiKey: body.chatModel.customOpenAIKey,
temperature: 0.7,
configuration: {
baseURL: body.chatModel.customOpenAIBaseURL,
},
}) as unknown as BaseChatModel;
} else if (
chatModelProviders[chatModelProvider] &&
chatModelProviders[chatModelProvider][chatModel]
) {
llm = chatModelProviders[chatModelProvider][chatModel]
.model as unknown as BaseChatModel | undefined;
}
if (!llm) {
return res.status(400).json({ message: 'Invalid model selected' });
}
const legalDocuments = await handleLegalSearch( // Renommé
{ query: body.query, chat_history: chatHistory },
llm,
);
res.status(200).json({ documents: legalDocuments }); // Modifié la réponse
} catch (err) {
res.status(500).json({ message: 'An error has occurred.' });
logger.error(`Error in legal search: ${err.message}`); // Mis à jour le message d'erreur
}
});
export default router;

View file

@ -0,0 +1,152 @@
import express from 'express';
import { RAGDocumentChain, handleLegiFranceSearch } from '../chains/rag_document_upload';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { getAvailableChatModelProviders } from '../lib/providers';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import logger from '../utils/logger';
import { ChatOpenAI } from '@langchain/openai';
import crypto from 'crypto';
import { Document } from '@langchain/core/schema/document';
import { OpenAIEmbeddings } from '@langchain/openai';
const router = express.Router();
const ragChain = new RAGDocumentChain();
interface ChatModel {
provider: string;
model: string;
customOpenAIBaseURL?: string;
customOpenAIKey?: string;
}
interface LegiFranceSearchBody {
query: string;
chatHistory: any[];
chatModel?: ChatModel;
urls?: string[];
}
interface LegiFranceRequest {
query: string;
// autres propriétés si nécessaires
}
router.post('/initialize', async (req, res) => {
try {
const { urls } = req.body;
if (!Array.isArray(urls)) {
return res.status(400).json({ error: "URLs must be an array" });
}
// Créer des documents à partir des URLs
const docs = urls.map(url => new Document({
pageContent: "", // À remplir avec le contenu réel
metadata: { source: url }
}));
// Initialiser les embeddings (à ajuster selon votre configuration)
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});
await ragChain.initializeVectorStore(docs, embeddings);
res.json({ success: true });
} catch (err) {
logger.error("Error initializing LegiFrance search:", err);
res.status(500).json({ error: "Failed to initialize LegiFrance search" });
}
});
router.post('/search', async (req, res) => {
try {
const body: LegiFranceSearchBody = req.body;
console.log("📚 [LegiFrance] Début de la recherche avec query:", body.query);
// Configuration du modèle LLM
const chatModelProviders = await getAvailableChatModelProviders();
const chatModelProvider = body.chatModel?.provider || Object.keys(chatModelProviders)[0];
const chatModel = body.chatModel?.model || Object.keys(chatModelProviders[chatModelProvider])[0];
console.log("🤖 [LegiFrance] Modèle sélectionné:", { provider: chatModelProvider, model: chatModel });
let llm: BaseChatModel | undefined;
if (body.chatModel?.provider === 'custom_openai') {
if (!body.chatModel?.customOpenAIBaseURL || !body.chatModel?.customOpenAIKey) {
return res.status(400).json({ message: 'Missing custom OpenAI base URL or key' });
}
llm = new ChatOpenAI({
modelName: body.chatModel.model,
openAIApiKey: body.chatModel.customOpenAIKey,
temperature: 0.7,
configuration: {
baseURL: body.chatModel.customOpenAIBaseURL,
},
}) as unknown as BaseChatModel;
} else if (chatModelProviders[chatModelProvider] &&
chatModelProviders[chatModelProvider][chatModel]) {
llm = chatModelProviders[chatModelProvider][chatModel].model as unknown as BaseChatModel;
}
if (!llm) {
return res.status(400).json({ message: 'Invalid model selected' });
}
// Génération des IDs uniques
const messageId = crypto.randomBytes(7).toString('hex');
const chatId = crypto.randomBytes(7).toString('hex');
// Conversion de l'historique du chat
const chatHistory = body.chatHistory.map((msg: any) => {
if (msg.role === 'user') {
return new HumanMessage(msg.content);
} else if (msg.role === 'assistant') {
return new AIMessage(msg.content);
}
});
console.log("💬 [LegiFrance] Historique du chat converti:", chatHistory);
console.log("🔍 [LegiFrance] Début de handleLegiFranceSearch avec:", {
query: body.query,
llmType: llm?.constructor.name,
chainStatus: ragChain ? "initialisé" : "non initialisé"
});
// Ajouter la recherche avec handleLegiFranceSearch
const result = await handleLegiFranceSearch(
{
query: body.query,
chat_history: chatHistory
},
llm,
ragChain
);
console.log("✅ [LegiFrance] Résultat obtenu:", {
textLength: result.text?.length,
sourcesCount: result.sources?.length
});
// Format unifié de la réponse
res.status(200).json({
type: 'legifrance_results',
messageId,
data: {
text: result.text,
sources: result.sources,
query: body.query
}
});
} catch (err) {
console.error("❌ [LegiFrance] Erreur détaillée:", {
message: err.message,
stack: err.stack,
name: err.name
});
res.status(500).json({ message: 'Une erreur est survenue.' });
logger.error(`Erreur dans la recherche LegiFrance: ${err.message}`);
}
});
export default router;

48
src/routes/news.ts Normal file
View file

@ -0,0 +1,48 @@
import express from 'express';
import { searchSearxng } from '../lib/searxng';
import logger from '../utils/logger';
const router = express.Router();
router.get('/', async (req, res) => {
try {
const data = (
await Promise.all([
searchSearxng('site:businessinsider.com AI', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:www.exchangewire.com AI', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:yahoo.com AI', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:businessinsider.com tech', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:www.exchangewire.com tech', {
engines: ['bing news'],
pageno: 1,
}),
searchSearxng('site:yahoo.com tech', {
engines: ['bing news'],
pageno: 1,
}),
])
)
.map((result) => result.results)
.flat()
.sort(() => Math.random() - 0.5);
return res.json({ articles: data });
} catch (err: any) {
logger.error(`Error in news route: ${err.message}`);
return res.status(500).json({ message: 'An error has occurred' });
}
});
export default router;

View file

@ -9,13 +9,18 @@ import { getAvailableEmbeddingModelProviders } from '../lib/providers';
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf'; import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx'; import { DocxLoader } from '@langchain/community/document_loaders/fs/docx';
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'; import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
import { Document } from 'langchain/document'; import { Document } from '@langchain/core/documents';
import { RAGDocumentChain } from '../chains/rag_document_upload';
import { Chroma } from "langchain/vectorstores/chroma";
const router = express.Router(); const router = express.Router();
const splitter = new RecursiveCharacterTextSplitter({ const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500, chunkSize: 1000,
chunkOverlap: 100, chunkOverlap: 200,
separators: ["\n\n", "\n", ".", "!", "?", ";", ":", " ", ""],
keepSeparator: true,
lengthFunction: (text) => text.length
}); });
const storage = multer.diskStorage({ const storage = multer.diskStorage({
@ -34,6 +39,29 @@ const storage = multer.diskStorage({
const upload = multer({ storage }); const upload = multer({ storage });
const preprocessDocument = (doc: Document): Document => {
const cleanContent = doc.pageContent
.replace(/\s+/g, ' ')
.replace(/\n+/g, ' ')
.trim();
return new Document({
pageContent: cleanContent,
metadata: {
...doc.metadata,
chunk_type: 'text',
word_count: cleanContent.split(/\s+/).length,
processed_date: new Date().toISOString()
}
});
};
const scoreDocument = (doc: Document): number => {
const wordCount = doc.pageContent.split(/\s+/).length;
const sentenceCount = doc.pageContent.split(/[.!?]+/).length;
return wordCount > 10 && sentenceCount > 0 ? 1 : 0;
};
router.post( router.post(
'/', '/',
upload.fields([ upload.fields([
@ -43,109 +71,220 @@ router.post(
]), ]),
async (req, res) => { async (req, res) => {
try { try {
console.log("📥 [Uploads] Début du traitement avec body:", {
embedding_model: req.body.embedding_model,
embedding_model_provider: req.body.embedding_model_provider
});
const { embedding_model, embedding_model_provider } = req.body; const { embedding_model, embedding_model_provider } = req.body;
if (!embedding_model || !embedding_model_provider) { if (!embedding_model || !embedding_model_provider) {
res console.warn("⚠️ [Uploads] Modèle ou provider manquant");
.status(400) res.status(400).json({ message: 'Missing embedding model or provider' });
.json({ message: 'Missing embedding model or provider' });
return; return;
} }
const embeddingModels = await getAvailableEmbeddingModelProviders(); const embeddingModels = await getAvailableEmbeddingModelProviders();
const provider = console.log("🔍 [Uploads] Modèles disponibles:", Object.keys(embeddingModels));
embedding_model_provider ?? Object.keys(embeddingModels)[0];
const embeddingModel: Embeddings = const provider = embedding_model_provider ?? Object.keys(embeddingModels)[0];
embedding_model ?? Object.keys(embeddingModels[provider])[0]; const embeddingModel: Embeddings = embedding_model ?? Object.keys(embeddingModels[provider])[0];
console.log("🤖 [Uploads] Modèle sélectionné:", { provider, model: embeddingModel });
let embeddingsModel: Embeddings | undefined; let embeddingsModel: Embeddings | undefined;
if (embeddingModels[provider] && embeddingModels[provider][embeddingModel]) {
if ( embeddingsModel = embeddingModels[provider][embeddingModel].model as Embeddings | undefined;
embeddingModels[provider] &&
embeddingModels[provider][embeddingModel]
) {
embeddingsModel = embeddingModels[provider][embeddingModel].model as
| Embeddings
| undefined;
} }
if (!embeddingsModel) { if (!embeddingsModel) {
console.error("❌ [Uploads] Modèle invalide");
res.status(400).json({ message: 'Invalid LLM model selected' }); res.status(400).json({ message: 'Invalid LLM model selected' });
return; return;
} }
const files = req.files['files'] as Express.Multer.File[]; const files = req.files['files'] as Express.Multer.File[];
console.log("📁 [Uploads] Fichiers reçus:", files?.map(f => ({
name: f.originalname,
path: f.path,
type: f.mimetype
})));
if (!files || files.length === 0) { if (!files || files.length === 0) {
console.warn("⚠️ [Uploads] Aucun fichier reçu");
res.status(400).json({ message: 'No files uploaded' }); res.status(400).json({ message: 'No files uploaded' });
return; return;
} }
const processedDocs: Document[] = [];
const ragChain = new RAGDocumentChain();
let totalPages = 0;
await Promise.all( await Promise.all(
files.map(async (file) => { files.map(async (file) => {
console.log(`📄 [Uploads] Traitement du fichier: ${file.originalname}`);
let docs: Document[] = []; let docs: Document[] = [];
if (file.mimetype === 'application/pdf') { if (file.mimetype === 'application/pdf') {
const loader = new PDFLoader(file.path); console.log(`📚 [Uploads] Chargement du PDF: ${file.path}`);
const loader = new PDFLoader(file.path, {
splitPages: true
});
docs = await loader.load(); docs = await loader.load();
} else if ( totalPages += docs.length;
file.mimetype === } else if (file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' console.log(`📝 [Uploads] Chargement du DOCX: ${file.path}`);
) {
const loader = new DocxLoader(file.path); const loader = new DocxLoader(file.path);
docs = await loader.load(); docs = await loader.load();
totalPages += docs.length;
} else if (file.mimetype === 'text/plain') { } else if (file.mimetype === 'text/plain') {
console.log(`📄 [Uploads] Chargement du TXT: ${file.path}`);
const text = fs.readFileSync(file.path, 'utf-8'); const text = fs.readFileSync(file.path, 'utf-8');
docs = [ docs = [new Document({
new Document({ pageContent: text,
pageContent: text, metadata: {
metadata: { title: file.originalname,
title: file.originalname, source: file.path,
}, type: 'text'
}), }
]; })];
totalPages += 1;
} }
const splitted = await splitter.splitDocuments(docs); const preprocessedDocs = docs.map(preprocessDocument);
const scoredDocs = preprocessedDocs.filter(doc => scoreDocument(doc) > 0);
const json = JSON.stringify({ console.log(`✂️ [Uploads] Splitting du document en ${scoredDocs.length} parties valides`);
title: file.originalname, const splitted = await splitter.splitDocuments(scoredDocs);
contents: splitted.map((doc) => doc.pageContent),
const enrichedDocs = splitted.map((doc, index) => {
const pageNumber = Math.floor(index / (splitted.length / docs.length)) + 1;
return new Document({
pageContent: doc.pageContent,
metadata: {
...doc.metadata,
source: file.path,
title: file.originalname,
page_number: pageNumber,
chunk_index: index,
total_chunks: splitted.length,
file_type: file.mimetype,
search_text: doc.pageContent.substring(0, 100).trim()
}
});
}); });
processedDocs.push(...enrichedDocs);
const pathToSave = file.path.replace(/\.\w+$/, '-extracted.json'); const pathToSave = file.path.replace(/\.\w+$/, '-extracted.json');
fs.writeFileSync(pathToSave, json); const contentToSave = {
const embeddings = await embeddingsModel.embedDocuments(
splitted.map((doc) => doc.pageContent),
);
const embeddingsJSON = JSON.stringify({
title: file.originalname, title: file.originalname,
embeddings: embeddings, contents: enrichedDocs.map((doc) => ({
}); content: doc.pageContent,
metadata: doc.metadata
})),
pageCount: docs.length,
processingDate: new Date().toISOString()
};
const pathToSaveEmbeddings = file.path.replace( fs.writeFileSync(pathToSave, JSON.stringify(contentToSave, null, 2));
/\.\w+$/,
'-embeddings.json', console.log(`🧮 [Uploads] Génération des embeddings pour ${enrichedDocs.length} chunks`);
const embeddings = await embeddingsModel.embedDocuments(
enrichedDocs.map((doc) => doc.pageContent)
); );
fs.writeFileSync(pathToSaveEmbeddings, embeddingsJSON);
}), const pathToSaveEmbeddings = file.path.replace(/\.\w+$/, '-embeddings.json');
const embeddingsToSave = {
title: file.originalname,
embeddings: embeddings.map((embedding, index) => ({
vector: embedding,
metadata: enrichedDocs[index].metadata
}))
};
fs.writeFileSync(pathToSaveEmbeddings, JSON.stringify(embeddingsToSave));
})
); );
console.log("🔄 [Uploads] Initialisation du vectorStore avec", processedDocs.length, "documents");
const initResult = await ragChain.initializeVectorStoreFromDocuments(
processedDocs,
embeddingsModel
);
console.log("✅ [Uploads] VectorStore initialisé:", initResult);
res.status(200).json({ res.status(200).json({
files: files.map((file) => { files: files.map((file) => ({
return { fileName: file.originalname,
fileName: file.originalname, fileExtension: file.filename.split('.').pop(),
fileExtension: file.filename.split('.').pop(), fileId: file.filename.replace(/\.\w+$/, ''),
fileId: file.filename.replace(/\.\w+$/, ''), stats: {
}; chunks: processedDocs.filter(d => d.metadata.source === file.path).length,
}), pages: totalPages
}
})),
}); });
} catch (err: any) { } catch (err: any) {
console.error("❌ [Uploads] Erreur:", {
message: err.message,
stack: err.stack,
name: err.name
});
logger.error(`Error in uploading file results: ${err.message}`); logger.error(`Error in uploading file results: ${err.message}`);
res.status(500).json({ message: 'An error has occurred.' }); res.status(500).json({ message: 'An error has occurred.' });
} }
}, },
); );
router.get('/:fileId/view', async (req, res) => {
try {
const { fileId } = req.params;
const search = req.query.search as string;
const page = req.query.page as string;
// Chercher tous les fichiers qui commencent par fileId dans le dossier uploads
const uploadsDir = path.join(process.cwd(), 'uploads');
const files = fs.readdirSync(uploadsDir);
const pdfFile = files.find(file => file.startsWith(fileId) && file.endsWith('.pdf'));
if (!pdfFile) {
console.error(`❌ PDF non trouvé pour l'ID: ${fileId}`);
return res.status(404).json({ error: 'Document PDF non trouvé' });
}
const filePath = path.join(uploadsDir, pdfFile);
console.log("📄 Envoi du fichier:", filePath);
// Définir les headers pour le PDF
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${pdfFile}"`);
// Ajouter les paramètres de navigation et de surlignage
if (search) {
// Nettoyer le texte de recherche
const cleanSearch = search
.replace(/[\n\r]+/g, ' ')
.trim();
if (cleanSearch) {
res.setHeader('X-PDF-Search', cleanSearch);
res.setHeader('X-PDF-Highlight', 'true');
res.setHeader('X-PDF-Highlight-Color', '#FFD700'); // Or
}
}
if (page) {
res.setHeader('X-PDF-Page', page);
}
// Envoyer le fichier
res.sendFile(filePath);
} catch (error) {
console.error('❌ Erreur lors de la visualisation du document:', error);
res.status(500).json({ error: 'Erreur lors de la visualisation du document' });
}
});
export default router; export default router;

File diff suppressed because it is too large Load diff

1
src/types/index.ts Normal file
View file

@ -0,0 +1 @@
export * from './types';

73
src/types/types.ts Normal file
View file

@ -0,0 +1,73 @@
import { BaseMessage } from '@langchain/core/messages';
export interface Expert {
id: number;
id_expert: string;
nom: string;
prenom: string;
adresse: string;
pays: string;
ville: string;
expertises: string;
specialite: string;
biographie: string;
tarif: number;
services: any;
created_at: string;
image_url: string;
}
export interface ExpertSearchRequest {
query: string;
chat_history: BaseMessage[];
messageId: string;
chatId: string;
}
export interface ExpertSearchResponse {
experts: Expert[];
synthese: string;
}
export interface EnrichedResponse {
text: string;
sources: Source[];
suggestions: string[];
images: ImageResult[];
}
export interface Source {
title: string;
url: string;
snippet: string;
}
export interface ImageResult {
url: string;
title: string;
source: string;
}
export interface DocumentMetadata {
title?: string;
source?: string;
type?: string;
url?: string;
pageNumber?: number;
score?: number;
expertData?: any;
searchText?: string;
illustrationImage?: string;
imageTitle?: string;
[key: string]: any;
}
export interface NormalizedSource {
pageContent: string;
metadata: DocumentMetadata;
}
export interface SearchResult {
pageContent: string;
metadata: DocumentMetadata;
}

View file

@ -37,6 +37,7 @@ export const searchHandlers = {
rerankThreshold: 0.3, rerankThreshold: 0.3,
searchWeb: true, searchWeb: true,
summarizer: true, summarizer: true,
searchDatabase: true,
}), }),
academicSearch: new MetaSearchAgent({ academicSearch: new MetaSearchAgent({
activeEngines: ['arxiv', 'google scholar', 'pubmed'], activeEngines: ['arxiv', 'google scholar', 'pubmed'],
@ -46,6 +47,7 @@ export const searchHandlers = {
rerankThreshold: 0, rerankThreshold: 0,
searchWeb: true, searchWeb: true,
summarizer: false, summarizer: false,
searchDatabase: true,
}), }),
writingAssistant: new MetaSearchAgent({ writingAssistant: new MetaSearchAgent({
activeEngines: [], activeEngines: [],
@ -55,6 +57,7 @@ export const searchHandlers = {
rerankThreshold: 0, rerankThreshold: 0,
searchWeb: false, searchWeb: false,
summarizer: false, summarizer: false,
searchDatabase: true,
}), }),
wolframAlphaSearch: new MetaSearchAgent({ wolframAlphaSearch: new MetaSearchAgent({
activeEngines: ['wolframalpha'], activeEngines: ['wolframalpha'],
@ -64,6 +67,7 @@ export const searchHandlers = {
rerankThreshold: 0, rerankThreshold: 0,
searchWeb: true, searchWeb: true,
summarizer: false, summarizer: false,
searchDatabase: true,
}), }),
youtubeSearch: new MetaSearchAgent({ youtubeSearch: new MetaSearchAgent({
activeEngines: ['youtube'], activeEngines: ['youtube'],
@ -73,6 +77,7 @@ export const searchHandlers = {
rerankThreshold: 0.3, rerankThreshold: 0.3,
searchWeb: true, searchWeb: true,
summarizer: false, summarizer: false,
searchDatabase: true,
}), }),
redditSearch: new MetaSearchAgent({ redditSearch: new MetaSearchAgent({
activeEngines: ['reddit'], activeEngines: ['reddit'],
@ -82,6 +87,7 @@ export const searchHandlers = {
rerankThreshold: 0.3, rerankThreshold: 0.3,
searchWeb: true, searchWeb: true,
summarizer: false, summarizer: false,
searchDatabase: true,
}), }),
}; };

View file

@ -1,2 +1,4 @@
NEXT_PUBLIC_WS_URL=ws://localhost:3001 NEXT_PUBLIC_WS_URL=ws://localhost:3001
NEXT_PUBLIC_API_URL=http://localhost:3001/api NEXT_PUBLIC_API_URL=http://localhost:3001/api
NEXT_PUBLIC_SUPABASE_URL=https://qytbxgzxsywnfhlwcyqa.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InF5dGJ4Z3p4c3l3bmZobHdjeXFhIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzEwNTc3MTAsImV4cCI6MjA0NjYzMzcxMH0.XLRq-4CFL2MWxvCLzCv5ZdaF5VSi58cocx9FOyv37jU

View file

@ -0,0 +1,295 @@
'use client';
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useParams, useRouter } from 'next/navigation';
import { toast } from 'sonner';
import { formatTimeDifference } from '@/lib/utils';
import Link from 'next/link';
import { Expert, Message } from '@/types';
interface Conversation {
expert: Expert;
lastMessage?: Message;
unreadCount: number;
}
export default function ChatRoom() {
const router = useRouter();
const { expertId } = useParams();
const [conversations, setConversations] = useState<Conversation[]>([]);
const [messages, setMessages] = useState<Message[]>([]);
const [newMessage, setNewMessage] = useState('');
const [currentExpert, setCurrentExpert] = useState<Expert | null>(null);
// Charger les conversations
useEffect(() => {
const loadConversations = async () => {
const { data: messages, error } = await supabase
.from('messages')
.select('*, expert:experts(*)')
.or('sender_id.eq.user_id,receiver_id.eq.user_id')
.order('created_at', { ascending: false });
if (error) {
toast.error("Erreur lors du chargement des conversations");
return;
}
// Grouper les messages par expert
const conversationsMap = new Map<string, Conversation>();
messages?.forEach(message => {
const expertId = message.sender_id === 'user_id' ? message.receiver_id : message.sender_id;
if (!conversationsMap.has(expertId)) {
conversationsMap.set(expertId, {
expert: message.expert,
lastMessage: message,
unreadCount: message.sender_id !== 'user_id' && !message.read ? 1 : 0
});
}
});
setConversations(Array.from(conversationsMap.values()));
};
loadConversations();
}, []);
// Charger les messages de la conversation courante
useEffect(() => {
if (!expertId) return;
const loadMessages = async () => {
const { data: expert, error: expertError } = await supabase
.from('experts')
.select('*')
.eq('id_expert', expertId)
.single();
if (expertError) {
toast.error("Erreur lors du chargement de l'expert");
return;
}
setCurrentExpert(expert);
const { data: messages, error: messagesError } = await supabase
.from('messages')
.select('*')
.or(`sender_id.eq.${expertId},receiver_id.eq.${expertId}`)
.order('created_at', { ascending: true });
if (messagesError) {
toast.error("Erreur lors du chargement des messages");
return;
}
setMessages(messages || []);
};
loadMessages();
// Souscrire aux nouveaux messages
const channel = supabase.channel('public:messages')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
},
(payload) => {
setMessages(current => [...current, payload.new as Message]);
}
)
.subscribe();
return () => {
channel.unsubscribe();
};
}, [expertId]);
const sendMessage = async (e: React.FormEvent) => {
e.preventDefault();
if (!newMessage.trim() || !expertId) return;
const { error } = await supabase
.from('messages')
.insert({
content: newMessage,
sender_id: 'user_id',
receiver_id: expertId,
});
if (error) {
toast.error("Erreur lors de l'envoi du message");
return;
}
setNewMessage('');
};
const markAsRead = async (messageId: string) => {
const { error } = await supabase
.from('messages')
.update({ read: true })
.eq('id', messageId);
if (error) {
toast.error("Erreur lors de la mise à jour du message");
}
};
// Utilisez markAsRead quand un message est affiché
useEffect(() => {
if (!messages.length) return;
// Marquer les messages non lus comme lus
messages
.filter(msg => !msg.read && msg.sender_id !== 'user_id')
.forEach(msg => markAsRead(msg.id));
}, [messages]);
return (
<div className="flex h-[calc(100vh-4rem)]">
{/* Liste des conversations - cachée sur mobile si conversation active */}
<div className={`
${expertId ? 'hidden md:block' : 'block'}
w-full md:w-80 border-r bg-gray-50 dark:bg-gray-900
`}>
<div className="p-4 border-b">
<h2 className="text-lg font-semibold">Messages</h2>
</div>
<div className="overflow-y-auto h-[calc(100vh-8rem)]">
{conversations.length > 0 ? (
conversations.map((conversation) => (
<Link
key={conversation.expert.id_expert}
href={`/chatroom/${conversation.expert.id_expert}`}
className={`flex items-center p-4 hover:bg-gray-100 dark:hover:bg-gray-800 ${
expertId === conversation.expert.id_expert ? 'bg-gray-100 dark:bg-gray-800' : ''
}`}
>
<div className="w-12 h-12 rounded-full bg-gray-300 mr-4">
{(conversation.expert.avatar_url || conversation.expert.image_url) && (
<img
src={conversation.expert.avatar_url || conversation.expert.image_url}
alt={`${conversation.expert.prenom} ${conversation.expert.nom}`}
className="w-full h-full rounded-full object-cover"
/>
)}
</div>
<div className="flex-1">
<h3 className="font-medium">
{conversation.expert.prenom} {conversation.expert.nom}
</h3>
{conversation.lastMessage && (
<p className="text-sm text-gray-500 truncate">
{conversation.lastMessage.content}
</p>
)}
</div>
{conversation.unreadCount > 0 && (
<div className="ml-2 bg-blue-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{conversation.unreadCount}
</div>
)}
</Link>
))
) : (
<div className="p-4 text-center text-gray-500">
Aucune conversation
</div>
)}
</div>
</div>
{/* Zone de chat - plein écran sur mobile si conversation active */}
<div className={`
flex-1 flex flex-col
${!expertId ? 'hidden md:flex' : 'flex'}
`}>
{expertId && currentExpert ? (
<>
{/* En-tête avec bouton retour sur mobile */}
<div className="p-4 border-b flex items-center">
<button
onClick={() => router.push('/chatroom')}
className="md:hidden mr-2 p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<div className="w-10 h-10 rounded-full bg-gray-300 mr-4">
{currentExpert.avatar_url && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={currentExpert.avatar_url}
alt=""
className="w-full h-full rounded-full object-cover"
/>
)}
</div>
<h2 className="text-xl font-semibold">
{currentExpert.prenom} {currentExpert.nom}
</h2>
</div>
{/* Messages avec padding ajusté */}
<div className="flex-1 overflow-y-auto p-4 space-y-4 pb-20 md:pb-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.sender_id === 'user_id' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[70%] rounded-lg p-3 ${
message.sender_id === 'user_id'
? 'bg-blue-500 text-white'
: 'bg-gray-200 dark:bg-gray-700'
}`}
>
<div>{message.content}</div>
<div className="text-xs opacity-70 mt-1">
{formatTimeDifference(new Date(message.created_at), new Date())}
</div>
</div>
</div>
))}
</div>
{/* Formulaire d'envoi fixé en bas sur mobile */}
<form onSubmit={sendMessage} className="p-4 border-t flex gap-2 bg-white dark:bg-gray-900 fixed md:relative bottom-0 left-0 right-0">
<Input
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Écrivez votre message..."
className="flex-1"
/>
<Button type="submit">Envoyer</Button>
</form>
</>
) : (
<div className="flex-1 flex items-center justify-center p-4">
<div className="text-center">
<h2 className="text-2xl font-semibold mb-4">
Bienvenue dans votre messagerie
</h2>
<p className="text-gray-500 mb-8">
Sélectionnez une conversation ou commencez à discuter avec un expert
</p>
<Button onClick={() => router.push('/discover')}>
Trouver un expert
</Button>
</div>
</div>
)}
</div>
</div>
);
}

37
ui/app/chatroom/page.tsx Normal file
View file

@ -0,0 +1,37 @@
'use client';
import { useRouter } from 'next/navigation';
import { Button } from "@/components/ui/button";
export default function ChatRoomHome() {
const router = useRouter();
return (
<div className="flex h-[calc(100vh-4rem)]">
{/* Liste des conversations (même composant que dans [expertId]/page.tsx) */}
<div className="w-full md:w-80 border-r bg-gray-50 dark:bg-gray-900">
<div className="p-4 border-b">
<h2 className="text-lg font-semibold">Messages</h2>
</div>
<div className="overflow-y-auto h-[calc(100vh-8rem)]">
{/* La liste des conversations sera chargée ici */}
</div>
</div>
{/* Zone de bienvenue (visible uniquement sur desktop) */}
<div className="hidden md:flex flex-1 items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-semibold mb-4">
Bienvenue dans votre messagerie
</h2>
<p className="text-gray-500 mb-8">
Sélectionnez une conversation ou commencez à discuter avec un expert
</p>
<Button onClick={() => router.push('/discover')}>
Trouver un expert
</Button>
</div>
</div>
</div>
);
}

View file

@ -1,50 +1,231 @@
'use client'; 'use client';
import { Search } from 'lucide-react'; import { Search, Filter, X } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState, useCallback } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { supabase } from '@/lib/supabase';
import { Button } from "@/components/ui/button";
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { FilterModal } from "@/components/FilterModal";
interface Discover { interface Expert {
title: string; id: number;
content: string; id_expert: string;
url: string; nom: string;
thumbnail: string; prenom: string;
adresse: string;
pays: string;
ville: string;
expertises: string;
biographie: string;
tarif: number;
services: any;
created_at: string;
image_url: string;
} }
interface Location {
pays: string;
villes: string[];
}
interface Expertise {
id: string;
name: string;
}
const ExpertCard = ({ expert }: { expert: Expert }) => {
const router = useRouter();
const handleContact = async (e: React.MouseEvent) => {
e.preventDefault(); // Empêche la navigation vers la page expert
try {
// Vérifier si une conversation existe déjà
const { data: existingMessages } = await supabase
.from('messages')
.select('*')
.or(`sender_id.eq.user_id,receiver_id.eq.${expert.id_expert}`)
.limit(1);
if (!existingMessages || existingMessages.length === 0) {
// Si pas de conversation existante, créer le premier message
const { error: messageError } = await supabase
.from('messages')
.insert({
content: `Bonjour ${expert.prenom}, je souhaiterais échanger avec vous.`,
sender_id: 'user_id', // À remplacer par l'ID de l'utilisateur connecté
receiver_id: expert.id_expert,
read: false
});
if (messageError) {
throw messageError;
}
}
// Rediriger vers la conversation
router.push(`/chatroom/${expert.id_expert}`);
toast.success(`Conversation ouverte avec ${expert.prenom} ${expert.nom}`);
} catch (error) {
console.error('Error starting conversation:', error);
toast.error("Erreur lors de l'ouverture de la conversation");
}
};
return (
<Link
href={`/expert/${expert.id_expert}`}
key={expert.id}
className="max-w-sm w-full rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200"
>
<div className="relative w-full h-48">
{expert.image_url ? (
<Image
src={expert.image_url}
alt={`${expert.prenom} ${expert.nom}`}
fill
className="object-cover"
onError={(e) => {
// Fallback en cas d'erreur de chargement de l'image
const target = e.target as HTMLImageElement;
target.onerror = null;
target.src = '/placeholder-image.jpg';
}}
/>
) : (
<div className="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-400">Pas d&apos;image</span>
</div>
)}
</div>
<div className="px-6 py-4">
<div className="font-bold text-lg mb-2">
{expert.prenom} {expert.nom}
</div>
<div className="flex flex-col space-y-2">
<p className="text-black/70 dark:text-white/70 text-sm">
{expert.ville}, {expert.pays}
</p>
<p className="text-black/70 dark:text-white/70 text-sm">
{expert.expertises}
</p>
{expert.tarif && (
<p className="text-black/90 dark:text-white/90 font-medium">
{expert.tarif} /heure
</p>
)}
<Button
onClick={handleContact}
className="mt-4"
variant="outline"
>
Contacter
</Button>
</div>
</div>
</Link>
);
};
const Page = () => { const Page = () => {
const [discover, setDiscover] = useState<Discover[] | null>(null); const [experts, setExperts] = useState<Expert[] | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedPays, setSelectedPays] = useState('');
const [selectedVille, setSelectedVille] = useState('');
const [locations, setLocations] = useState<Location[]>([]);
const [selectedExpertises, setSelectedExpertises] = useState<string[]>([]);
const [open, setOpen] = useState(false);
// Calcul du nombre de filtres actifs
const activeFiltersCount = [
...(selectedExpertises.length > 0 ? [1] : []),
selectedPays,
selectedVille
].filter(Boolean).length;
// Récupérer les experts avec filtres
const fetchExperts = useCallback(async () => {
try {
let query = supabase
.from('experts')
.select('*');
if (selectedExpertises.length > 0) {
// Adaptez cette partie selon la structure de votre base de données
query = query.contains('expertises', selectedExpertises);
}
// Filtre par pays
if (selectedPays) {
query = query.eq('pays', selectedPays);
}
// Filtre par ville
if (selectedVille) {
query = query.eq('ville', selectedVille);
}
const { data, error } = await query;
if (error) throw error;
setExperts(data);
} catch (err: any) {
console.error('Error fetching experts:', err.message);
toast.error('Erreur lors du chargement des experts');
} finally {
setLoading(false);
}
}, [selectedPays, selectedVille, selectedExpertises]);
// Récupérer la liste des pays et villes uniques
const fetchLocations = async () => {
try {
const { data, error } = await supabase
.from('experts')
.select('pays, ville');
if (error) throw error;
// Créer un objet avec pays et villes uniques
const locationMap = new Map<string, Set<string>>();
data.forEach(expert => {
if (expert.pays) {
if (!locationMap.has(expert.pays)) {
locationMap.set(expert.pays, new Set());
}
if (expert.ville) {
locationMap.get(expert.pays)?.add(expert.ville);
}
}
});
// Convertir en tableau trié
const sortedLocations = Array.from(locationMap).map(([pays, villes]) => ({
pays,
villes: Array.from(villes).sort()
})).sort((a, b) => a.pays.localeCompare(b.pays));
setLocations(sortedLocations);
} catch (err: any) {
console.error('Error fetching locations:', err.message);
}
};
// Reset ville quand le pays change
useEffect(() => {
setSelectedVille('');
}, [selectedPays]);
useEffect(() => { useEffect(() => {
const fetchData = async () => { fetchExperts();
try { fetchLocations();
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/discover`, { }, [fetchExperts]);
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.message);
}
data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail);
setDiscover(data.blogs);
} catch (err: any) {
console.error('Error fetching data:', err.message);
toast.error('Error fetching data');
} finally {
setLoading(false);
}
};
fetchData();
}, []);
return loading ? ( return loading ? (
<div className="flex flex-row items-center justify-center min-h-screen"> <div className="flex flex-row items-center justify-center min-h-screen">
@ -66,47 +247,64 @@ const Page = () => {
</svg> </svg>
</div> </div>
) : ( ) : (
<> <div className="pb-24 lg:pb-0">
<div> <div className="flex flex-col pt-4">
<div className="flex flex-col pt-4"> <div className="flex items-center justify-between">
<div className="flex items-center"> <div className="flex flex-col">
<Search /> <div className="flex items-center">
<h1 className="text-3xl font-medium p-2">Discover</h1> <Search />
<h1 className="text-3xl font-medium p-2">Nos Experts</h1>
</div>
<div className="text-gray-500 ml-10">
Plus de 300 experts à votre écoute
</div>
</div> </div>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
</div>
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4 pb-28 lg:pb-8 w-full justify-items-center lg:justify-items-start"> {/* CTA Filtres unifié */}
{discover && <Button
discover?.map((item, i) => ( onClick={() => setOpen(true)}
<Link variant="outline"
href={`/?q=Summary: ${item.url}`} className="flex items-center gap-2"
key={i} >
className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200" <Filter size={18} />
target="_blank" <span>Filtrer</span>
> {activeFiltersCount > 0 && (
<img <span className="bg-blue-500 text-white text-xs px-2 py-0.5 rounded-full">
className="object-cover w-full aspect-video" {activeFiltersCount}
src={ </span>
new URL(item.thumbnail).origin + )}
new URL(item.thumbnail).pathname + </Button>
`?id=${new URL(item.thumbnail).searchParams.get('id')}`
}
alt={item.title}
/>
<div className="px-6 py-4">
<div className="font-bold text-lg mb-2">
{item.title.slice(0, 100)}...
</div>
<p className="text-black-70 dark:text-white/70 text-sm">
{item.content.slice(0, 100)}...
</p>
</div>
</Link>
))}
</div> </div>
</div> </div>
</>
{/* Modale de filtres */}
<FilterModal
open={open}
setOpen={setOpen}
selectedPays={selectedPays}
setSelectedPays={setSelectedPays}
selectedVille={selectedVille}
setSelectedVille={setSelectedVille}
selectedExpertises={selectedExpertises}
setSelectedExpertises={setSelectedExpertises}
locations={locations}
experts={experts}
/>
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4 pb-28 lg:pb-8 w-full justify-items-center lg:justify-items-start">
{experts && experts.length > 0 ? (
experts.map((expert) => (
<ExpertCard key={expert.id} expert={expert} />
))
) : (
<p className="col-span-full text-center text-gray-500">
Aucun expert trouvé
</p>
)}
</div>
</div>
); );
}; };

View file

@ -14,9 +14,9 @@ const montserrat = Montserrat({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Perplexica - Chat with the internet', title: 'X&me - Chat with the internet',
description: description:
'Perplexica is an AI powered chatbot that is connected to the internet.', 'X&me is an AI powered chatbot that is connected to the internet.',
}; };
export default function RootLayout({ export default function RootLayout({

View file

@ -3,8 +3,8 @@ import { Metadata } from 'next';
import { Suspense } from 'react'; import { Suspense } from 'react';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Chat - Perplexica', title: 'Chat - X-me',
description: 'Chat with the internet, chat with Perplexica.', description: 'Chat with the internet, chat with X-me.',
}; };
const Home = () => { const Home = () => {

View file

@ -38,8 +38,11 @@ const EmptyChat = ({
</div> </div>
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-8"> <div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-8">
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8"> <h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
Research begins here. Ici c&apos;est vous le patron.
</h2> </h2>
<h3 className="text-black/70 dark:text-white/70 font-medium -mt-8">
Posez des questions, recherchez un expert pour répondre à vos besoins entrepreneuriaux
</h3>
<EmptyChatMessageInput <EmptyChatMessageInput
sendMessage={sendMessage} sendMessage={sendMessage}
focusMode={focusMode} focusMode={focusMode}

View file

@ -80,7 +80,7 @@ const EmptyChatMessageInput = ({
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
minRows={2} minRows={2}
className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48" className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
placeholder="Ask anything..." placeholder="Posez votre question..."
/> />
<div className="flex flex-row items-center justify-between mt-4"> <div className="flex flex-row items-center justify-between mt-4">
<div className="flex flex-row items-center space-x-2 lg:space-x-4"> <div className="flex flex-row items-center space-x-2 lg:space-x-4">

View file

@ -0,0 +1,158 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Expert, Location } from "@/types"; // Ajustez le chemin selon votre structure
import { Dispatch, SetStateAction } from 'react'; // Ajout de l'import
interface Expertise {
id: string;
name: string;
}
interface FilterModalProps {
open: boolean;
setOpen: (open: boolean) => void;
selectedPays: string;
setSelectedPays: (pays: string) => void;
selectedVille: string;
setSelectedVille: (ville: string) => void;
selectedExpertises: string[];
setSelectedExpertises: Dispatch<SetStateAction<string[]>>; // Correction du type
locations: Location[];
experts: Expert[] | null;
}
export const FilterModal = ({
open,
setOpen,
selectedPays,
setSelectedPays,
selectedVille,
setSelectedVille,
selectedExpertises,
setSelectedExpertises,
locations,
experts,
}: FilterModalProps) => {
const activeFiltersCount = [
...(selectedExpertises.length > 0 ? [1] : []),
selectedPays,
selectedVille
].filter(Boolean).length;
const expertises: Expertise[] = [
{ id: 'immobilier', name: 'Immobilier' },
{ id: 'finance', name: 'Finance' },
{ id: 'droit', name: 'Droit' },
{ id: 'fiscalite', name: 'Fiscalité' },
{ id: 'assurance', name: 'Assurance' },
{ id: 'patrimoine', name: 'Patrimoine' },
];
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-h-[90vh] h-[90vh] sm:h-auto sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="flex justify-between">
Filtres
<span className="text-sm text-muted-foreground">
{activeFiltersCount} filtre{activeFiltersCount > 1 ? 's' : ''} actif{activeFiltersCount > 1 ? 's' : ''}
</span>
</DialogTitle>
<DialogDescription>
Filtrez les experts par expertise et localisation
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Section Expertises */}
<div>
<h3 className="font-medium mb-3">Expertises</h3>
<div className="flex flex-wrap gap-2">
{expertises.map((expertise) => (
<button
key={expertise.id}
onClick={() => setSelectedExpertises(prev =>
prev.includes(expertise.id)
? prev.filter(id => id !== expertise.id)
: [...prev, expertise.id]
)}
className={`px-3 py-1.5 rounded-full text-sm transition-colors
${selectedExpertises.includes(expertise.id)
? 'bg-blue-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
{expertise.name}
</button>
))}
</div>
</div>
{/* Section Pays */}
<div>
<h3 className="font-medium mb-3">Pays</h3>
<div className="flex flex-wrap gap-2">
{locations.map(({ pays }) => (
<button
key={pays}
onClick={() => setSelectedPays(selectedPays === pays ? '' : pays)}
className={`px-3 py-1.5 rounded-full text-sm transition-colors
${selectedPays === pays
? 'bg-blue-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
{pays}
</button>
))}
</div>
</div>
{/* Section Villes (conditionnelle) */}
{selectedPays && (
<div>
<h3 className="font-medium mb-3">Villes {selectedPays && `(${selectedPays})`}</h3>
<div className="flex flex-wrap gap-2">
{locations
.find(loc => loc.pays === selectedPays)
?.villes.map(ville => (
<button
key={ville}
onClick={() => setSelectedVille(selectedVille === ville ? '' : ville)}
className={`px-3 py-1.5 rounded-full text-sm transition-colors
${selectedVille === ville
? 'bg-blue-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
{ville}
</button>
))}
</div>
</div>
)}
</div>
<div className="p-6 pt-4 flex gap-4 border-t mt-auto">
<Button
variant="outline"
className="flex-1"
onClick={() => {
setSelectedPays('');
setSelectedVille('');
setSelectedExpertises([]);
}}
>
Réinitialiser
</Button>
<Button
className="flex-1"
onClick={() => setOpen(false)}
>
Appliquer
</Button>
</div>
</DialogContent>
</Dialog>
);
};

View file

@ -0,0 +1,200 @@
/* eslint-disable @next/next/no-img-element */
import { BookCopy, PlusIcon } from 'lucide-react';
import { useState } from 'react';
import { Message } from './ChatWindow';
import Lightbox, { GenericSlide } from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css';
type Document = {
url: string;
title: string;
snippet: string;
source: string;
type: string;
iframe_src: string;
};
declare module 'yet-another-react-lightbox' {
export interface PDFSlide extends GenericSlide {
type: 'pdf';
url: string;
iframe_src: string;
}
interface SlideTypes {
'pdf': PDFSlide;
}
}
const LegalSearch = ({
query,
chatHistory,
}: {
query: string;
chatHistory: Message[];
}) => {
const [documents, setDocuments] = useState<Document[] | null>(null);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [currentDoc, setCurrentDoc] = useState<Document | null>(null);
const openDocument = (doc: Document) => {
setCurrentDoc(doc);
setOpen(true);
};
return (
<>
{!loading && documents === null && (
<button
onClick={async () => {
setLoading(true);
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/legal`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
chatHistory: chatHistory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
...(chatModelProvider === 'custom_openai' && {
customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey,
}),
},
}),
},
);
const data = await res.json();
setDocuments(data.documents ?? []);
setLoading(false);
}}
className="border border-dashed border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg dark:text-white text-sm w-full"
>
<div className="flex flex-row items-center space-x-2">
<BookCopy size={17} />
<p>Rechercher des textes légaux</p>
</div>
<PlusIcon className="text-[#24A0ED]" size={17} />
</button>
)}
{loading && (
<div className="flex flex-col space-y-2">
{[...Array(3)].map((_, i) => (
<div
key={i}
className="bg-light-secondary dark:bg-dark-secondary h-24 w-full rounded-lg animate-pulse"
/>
))}
</div>
)}
{documents !== null && documents.length > 0 && (
<>
<div className="flex flex-col space-y-2">
{documents.length > 4
? documents.slice(0, 3).map((doc, i) => (
<div
key={i}
onClick={() => openDocument(doc)}
className="bg-light-100 dark:bg-dark-100 p-3 rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200 cursor-pointer"
>
<h4 className="text-sm font-medium text-black dark:text-white line-clamp-2">
{doc.title}
</h4>
<p className="text-xs text-black/50 dark:text-white/50 mt-1 line-clamp-2">
{doc.snippet}
</p>
<div className="flex items-center space-x-2 mt-2">
<span className="text-xs text-black/30 dark:text-white/30">
{doc.source}
</span>
<span className="text-xs bg-light-secondary dark:bg-dark-secondary px-1.5 py-0.5 rounded text-black/50 dark:text-white/50">
{doc.type}
</span>
</div>
</div>
))
: documents.map((doc, i) => (
<div
key={i}
onClick={() => openDocument(doc)}
className="bg-light-100 dark:bg-dark-100 p-3 rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200 cursor-pointer"
>
<h4 className="text-sm font-medium text-black dark:text-white line-clamp-2">
{doc.title}
</h4>
<p className="text-xs text-black/50 dark:text-white/50 mt-1 line-clamp-2">
{doc.snippet}
</p>
<div className="flex items-center space-x-2 mt-2">
<span className="text-xs text-black/30 dark:text-white/30">
{doc.source}
</span>
<span className="text-xs bg-light-secondary dark:bg-dark-secondary px-1.5 py-0.5 rounded text-black/50 dark:text-white/50">
{doc.type}
</span>
</div>
</div>
))}
{documents.length > 4 && (
<button
onClick={() => openDocument(documents[3])}
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 p-3 rounded-lg text-black/70 dark:text-white/70 text-sm"
>
Voir {documents.length - 3} documents supplémentaires
</button>
)}
</div>
<Lightbox
open={open}
close={() => setOpen(false)}
render={{
slide: ({ slide }) =>
slide.type === 'pdf' ? (
<div className="h-full w-full flex flex-col items-center justify-center">
<div className="text-center mb-4 text-white">
<p>Le document ne peut pas être affiché directement.</p>
<a
href={slide.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 underline mt-2 inline-block"
>
Ouvrir le document dans un nouvel onglet
</a>
</div>
<div className="bg-white/10 p-4 rounded-lg max-w-2xl">
<h3 className="text-white/90 font-medium mb-2">{currentDoc?.title}</h3>
<p className="text-white/70 text-sm">{currentDoc?.snippet}</p>
</div>
</div>
) : null,
}}
slides={[
{
type: 'pdf',
url: currentDoc?.url || '',
iframe_src: currentDoc?.url || '',
}
]}
/>
</>
)}
</>
);
};
export default LegalSearch;

View file

@ -16,7 +16,7 @@ import Markdown from 'markdown-to-jsx';
import Copy from './MessageActions/Copy'; import Copy from './MessageActions/Copy';
import Rewrite from './MessageActions/Rewrite'; import Rewrite from './MessageActions/Rewrite';
import MessageSources from './MessageSources'; import MessageSources from './MessageSources';
import SearchImages from './SearchImages'; import LegalSearch from './LegalSearch';
import SearchVideos from './SearchVideos'; import SearchVideos from './SearchVideos';
import { useSpeech } from 'react-text-to-speech'; import { useSpeech } from 'react-text-to-speech';
@ -53,8 +53,12 @@ const MessageBox = ({
return setParsedMessage( return setParsedMessage(
message.content.replace( message.content.replace(
regex, regex,
(_, number) => (_, number) => {
`<a href="${message.sources?.[number - 1]?.metadata?.url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${number}</a>`, const url = message.sources?.[number - 1]?.metadata?.url || '';
// Extraire le nom de domaine sans l'extension
const sourceName = url.replace(/^(?:https?:\/\/)?(?:www\.)?([^./]+).*$/, '$1');
return `<a href="${url}" target="_blank" class="ml-2 px-3 py-1 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded-md transition-colors duration-200 no-underline inline-flex items-center">${sourceName}</a>`;
}
), ),
); );
} }
@ -63,15 +67,18 @@ const MessageBox = ({
setParsedMessage(message.content); setParsedMessage(message.content);
}, [message.content, message.sources, message.role]); }, [message.content, message.sources, message.role]);
useEffect(() => {
}, [message.sources]);
const { speechStatus, start, stop } = useSpeech({ text: speechMessage }); const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
return ( return (
<div> <div>
{message.role === 'user' && ( {message.role === 'user' && (
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8')}> <div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8')}>
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12"> <h3 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
{message.content} {message.content}
</h2> </h3>
</div> </div>
)} )}
@ -81,6 +88,24 @@ const MessageBox = ({
ref={dividerRef} ref={dividerRef}
className="flex flex-col space-y-6 w-full lg:w-9/12" className="flex flex-col space-y-6 w-full lg:w-9/12"
> >
{message.sources && message.sources[0]?.metadata?.illustrationImage && (
<div className="flex flex-col space-y-2 mb-6">
<div className="w-full aspect-[21/9] relative overflow-hidden rounded-xl shadow-lg">
<img
src={message.sources[0].metadata.illustrationImage}
alt="Illustration"
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
onError={(e) => {
console.error("Erreur de chargement de l'image:", e);
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 italic mt-2">
{message.sources[0].metadata.title || 'Illustration du sujet'}
</p>
</div>
)}
{message.sources && message.sources.length > 0 && ( {message.sources && message.sources.length > 0 && (
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<div className="flex flex-row items-center space-x-2"> <div className="flex flex-row items-center space-x-2">
@ -102,7 +127,7 @@ const MessageBox = ({
size={20} size={20}
/> />
<h3 className="text-black dark:text-white font-medium text-xl"> <h3 className="text-black dark:text-white font-medium text-xl">
Answer Question
</h3> </h3>
</div> </div>
<Markdown <Markdown
@ -110,6 +135,13 @@ const MessageBox = ({
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]', 'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]',
'max-w-none break-words text-black dark:text-white', 'max-w-none break-words text-black dark:text-white',
)} )}
options={{
overrides: {
p: ({ children }) => {
return <p>{children}</p>;
},
},
}}
> >
{parsedMessage} {parsedMessage}
</Markdown> </Markdown>
@ -152,7 +184,7 @@ const MessageBox = ({
<div className="flex flex-col space-y-3 text-black dark:text-white"> <div className="flex flex-col space-y-3 text-black dark:text-white">
<div className="flex flex-row items-center space-x-2 mt-4"> <div className="flex flex-row items-center space-x-2 mt-4">
<Layers3 /> <Layers3 />
<h3 className="text-xl font-medium">Related</h3> <h3 className="text-xl font-medium">Suggestions</h3>
</div> </div>
<div className="flex flex-col space-y-3"> <div className="flex flex-col space-y-3">
{message.suggestions.map((suggestion, i) => ( {message.suggestions.map((suggestion, i) => (
@ -184,7 +216,7 @@ const MessageBox = ({
</div> </div>
</div> </div>
<div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4"> <div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4">
<SearchImages <LegalSearch
query={history[messageIndex - 1].content} query={history[messageIndex - 1].content}
chatHistory={history.slice(0, messageIndex - 1)} chatHistory={history.slice(0, messageIndex - 1)}
/> />

View file

@ -4,7 +4,7 @@ import {
Globe, Globe,
Pencil, Pencil,
ScanEye, ScanEye,
SwatchBook, Eye,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
@ -19,52 +19,28 @@ import { Fragment } from 'react';
const focusModes = [ const focusModes = [
{ {
key: 'webSearch', key: 'webSearch',
title: 'All', title: 'Recherche internet',
description: 'Searches across all of the internet', description: 'Recherche sur internet directement',
icon: <Globe size={20} />, icon: <Globe size={20} />,
}, },
{ {
key: 'academicSearch', key: 'academicSearch',
title: 'Academic', title: 'Experts',
description: 'Search in published academic papers', description: 'Recherche un expert pour vous acccompagner',
icon: <SwatchBook size={20} />, icon: <Eye size={20} />,
}, },
{ {
key: 'writingAssistant', key: 'writingAssistant',
title: 'Writing', title: 'Document',
description: 'Chat without searching the web', description: 'Chat without searching the web',
icon: <Pencil size={16} />, icon: <Pencil size={16} />,
}, },
{ {
key: 'wolframAlphaSearch', key: 'wolframAlphaSearch',
title: 'Wolfram Alpha', title: 'Business Plan',
description: 'Computational knowledge engine', description: 'Réaliser votre Business Plan',
icon: <BadgePercent size={20} />, icon: <BadgePercent size={20} />,
}, },
{
key: 'youtubeSearch',
title: 'Youtube',
description: 'Search and watch videos',
icon: (
<SiYoutube
className="h-5 w-auto mr-0.5"
onPointerEnterCapture={undefined}
onPointerLeaveCapture={undefined}
/>
),
},
{
key: 'redditSearch',
title: 'Reddit',
description: 'Search for discussions and opinions',
icon: (
<SiReddit
className="h-5 w-auto mr-0.5"
onPointerEnterCapture={undefined}
onPointerLeaveCapture={undefined}
/>
),
},
]; ];
const Focus = ({ const Focus = ({

View file

@ -23,54 +23,60 @@ const SearchImages = ({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [slides, setSlides] = useState<any[]>([]); const [slides, setSlides] = useState<any[]>([]);
const handleSearch = async () => {
setLoading(true);
try {
console.log("🖼️ Démarrage de la recherche d'images pour:", query);
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
console.log("🖼️ Modèle configuré:", chatModelProvider, chatModel);
const response = await fetch('/api/images', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
chatHistory: chatHistory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
},
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('🖼️ Résultats de la recherche:', data);
if (data.images && data.images.length > 0) {
setImages(data.images);
setSlides(
data.images.map((image: Image) => ({
src: image.img_src,
}))
);
console.log('🖼️ Images et slides mis à jour:', data.images.length);
} else {
console.log('🖼️ Aucune image trouvée');
}
} catch (error) {
console.error('🖼️ Erreur lors de la recherche:', error);
} finally {
setLoading(false);
}
};
return ( return (
<> <>
{!loading && images === null && ( {!loading && images === null && (
<button <button
onClick={async () => { onClick={handleSearch}
setLoading(true);
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/images`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
chatHistory: chatHistory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
...(chatModelProvider === 'custom_openai' && {
customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey,
}),
},
}),
},
);
const data = await res.json();
const images = data.images ?? [];
setImages(images);
setSlides(
images.map((image: Image) => {
return {
src: image.img_src,
};
}),
);
setLoading(false);
}}
className="border border-dashed border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg dark:text-white text-sm w-full" className="border border-dashed border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg dark:text-white text-sm w-full"
> >
<div className="flex flex-row items-center space-x-2"> <div className="flex flex-row items-center space-x-2">

View file

@ -1,7 +1,7 @@
'use client'; 'use client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { BookOpenText, Home, Search, SquarePen, Settings } from 'lucide-react'; import { BookOpenText, Home, Search, SquarePen, Settings, MessageSquare } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { useSelectedLayoutSegments } from 'next/navigation'; import { useSelectedLayoutSegments } from 'next/navigation';
import React, { useState, type ReactNode } from 'react'; import React, { useState, type ReactNode } from 'react';
@ -38,6 +38,12 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
active: segments.includes('library'), active: segments.includes('library'),
label: 'Library', label: 'Library',
}, },
{
icon: MessageSquare,
href: '/chatroom',
active: segments.includes('chatroom'),
label: 'Messages',
},
]; ];
return ( return (

View file

@ -0,0 +1,52 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

121
ui/components/ui/dialog.tsx Normal file
View file

@ -0,0 +1,121 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-light-200 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-light-100 data-[state=open]:text-gray-500 dark:ring-offset-dark-200 dark:focus:ring-dark-200 dark:data-[state=open]:bg-dark-100 dark:data-[state=open]:text-gray-400">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-gray-500 dark:text-gray-400", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View file

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

38
ui/lib/config.ts Normal file
View file

@ -0,0 +1,38 @@
// Inspiré de la structure du backend mais adapté pour le frontend
interface Config {
GENERAL: {
WS_URL: string;
API_URL: string;
};
SUPABASE: {
URL: string;
ANON_KEY: string;
};
}
// Fonctions utilitaires pour la configuration
export const getSupabaseUrl = (): string =>
process.env.NEXT_PUBLIC_SUPABASE_URL || '';
export const getSupabaseKey = (): string =>
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
export const getApiUrl = (): string =>
process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api';
export const getWsUrl = (): string =>
process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:3001';
// Configuration complète
export const config: Config = {
GENERAL: {
WS_URL: getWsUrl(),
API_URL: getApiUrl(),
},
SUPABASE: {
URL: getSupabaseUrl(),
ANON_KEY: getSupabaseKey(),
},
};
export default config;

62
ui/lib/supabase.ts Normal file
View file

@ -0,0 +1,62 @@
import { createClient } from '@supabase/supabase-js';
import { getSupabaseUrl, getSupabaseKey } from '@/lib/config';
const supabaseUrl = getSupabaseUrl();
const supabaseKey = getSupabaseKey();
if (!supabaseUrl || !supabaseKey) {
throw new Error('Missing Supabase credentials');
}
export const supabase = createClient(supabaseUrl, supabaseKey);
// Fonction de test de connexion
export async function checkSupabaseConnection() {
try {
const { data, error } = await supabase
.from('experts')
.select('*')
.limit(1);
if (error) throw error;
console.log('✅ Frontend Supabase connection successful');
return true;
} catch (error) {
console.error('❌ Frontend Supabase connection error:', error);
return false;
}
}
export async function uploadExpertImage(file: File, expertId: string) {
try {
const fileExt = file.name.split('.').pop();
const fileName = `${expertId}-main.${fileExt}`;
const filePath = `experts/${fileName}`;
const { error: uploadError } = await supabase.storage
.from('expert-images') // Créez ce bucket dans Supabase
.upload(filePath, file, {
upsert: true
});
if (uploadError) throw uploadError;
// Obtenir l'URL publique
const { data: { publicUrl } } = supabase.storage
.from('expert-images')
.getPublicUrl(filePath);
// Mettre à jour l'expert avec l'URL de l'image
const { error: updateError } = await supabase
.from('experts')
.update({ image_url: publicUrl })
.eq('id_expert', expertId);
if (updateError) throw updateError;
return publicUrl;
} catch (error) {
console.error('Error uploading image:', error);
throw error;
}
}

View file

@ -1,7 +1,9 @@
import clsx, { ClassValue } from 'clsx'; import { type ClassValue, clsx } from "clsx"
import { twMerge } from 'tailwind-merge'; import { twMerge } from "tailwind-merge"
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes)); export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export const formatTimeDifference = ( export const formatTimeDifference = (
date1: Date | string, date1: Date | string,

View file

@ -5,6 +5,9 @@ const nextConfig = {
{ {
hostname: 's2.googleusercontent.com', hostname: 's2.googleusercontent.com',
}, },
{
hostname: 'dam.malt.com',
},
], ],
}, },
}; };

View file

@ -14,8 +14,12 @@
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",
"@icons-pack/react-simple-icons": "^9.4.0", "@icons-pack/react-simple-icons": "^9.4.0",
"@langchain/openai": "^0.0.25", "@langchain/openai": "^0.0.25",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-slot": "^1.1.1",
"@supabase/supabase-js": "^2.x.x",
"@tailwindcss/typography": "^0.5.12", "@tailwindcss/typography": "^0.5.12",
"clsx": "^2.1.0", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"langchain": "^0.1.30", "langchain": "^0.1.30",
"lucide-react": "^0.363.0", "lucide-react": "^0.363.0",
"markdown-to-jsx": "^7.6.2", "markdown-to-jsx": "^7.6.2",
@ -25,8 +29,8 @@
"react-dom": "^18", "react-dom": "^18",
"react-text-to-speech": "^0.14.5", "react-text-to-speech": "^0.14.5",
"react-textarea-autosize": "^8.5.3", "react-textarea-autosize": "^8.5.3",
"sonner": "^1.4.41", "sonner": "^1.7.1",
"tailwind-merge": "^2.2.2", "tailwind-merge": "^2.5.5",
"yet-another-react-lightbox": "^3.17.2", "yet-another-react-lightbox": "^3.17.2",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },

View file

@ -18,9 +18,10 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./*"] "@/*": ["./*"],
"@src/*": ["../src/*"]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../src/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

35
ui/types/index.ts Normal file
View file

@ -0,0 +1,35 @@
export interface Expert {
id: number;
id_expert: string;
nom: string;
prenom: string;
adresse: string;
pays: string;
ville: string;
expertises: string;
biographie: string;
tarif: number;
services: any;
created_at: string;
image_url: string;
avatar_url?: string;
}
export interface Location {
pays: string;
villes: string[];
}
export interface Expertise {
id: string;
name: string;
}
export interface Message {
id: string;
content: string;
sender_id: string;
receiver_id: string;
created_at: string;
read: boolean;
}

View file

@ -27,7 +27,7 @@
node-fetch "^2.6.7" node-fetch "^2.6.7"
web-streams-polyfill "^3.2.1" web-streams-polyfill "^3.2.1"
"@babel/runtime@^7.20.13", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.0": "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.2":
version "7.24.4" version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd"
integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==
@ -316,6 +316,127 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@radix-ui/primitive@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3"
integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==
"@radix-ui/react-compose-refs@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec"
integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==
"@radix-ui/react-context@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a"
integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==
"@radix-ui/react-dialog@^1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz#d68e977acfcc0d044b9dab47b6dd2c179d2b3191"
integrity sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-dismissable-layer" "1.1.3"
"@radix-ui/react-focus-guards" "1.1.1"
"@radix-ui/react-focus-scope" "1.1.1"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-portal" "1.1.3"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-slot" "1.1.1"
"@radix-ui/react-use-controllable-state" "1.1.0"
aria-hidden "^1.1.1"
react-remove-scroll "^2.6.1"
"@radix-ui/react-dismissable-layer@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz#4ee0f0f82d53bf5bd9db21665799bb0d1bad5ed8"
integrity sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-focus-guards@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe"
integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==
"@radix-ui/react-focus-scope@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz#5c602115d1db1c4fcfa0fae4c3b09bb8919853cb"
integrity sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-id@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed"
integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==
dependencies:
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-portal@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440"
integrity sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==
dependencies:
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-presence@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc"
integrity sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-primitive@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e"
integrity sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==
dependencies:
"@radix-ui/react-slot" "1.1.1"
"@radix-ui/react-slot@1.1.1", "@radix-ui/react-slot@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz#ab9a0ffae4027db7dc2af503c223c978706affc3"
integrity sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-use-callback-ref@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1"
integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==
"@radix-ui/react-use-controllable-state@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0"
integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==
dependencies:
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754"
integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==
dependencies:
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==
"@react-aria/focus@^3.17.1": "@react-aria/focus@^3.17.1":
version "3.18.4" version "3.18.4"
resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.18.4.tgz#a6e95896bc8680d1b5bcd855e983fc2c195a1a55" resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.18.4.tgz#a6e95896bc8680d1b5bcd855e983fc2c195a1a55"
@ -372,6 +493,63 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz#7ca168b6937818e9a74b47ac4e2112b2e1a024cf" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz#7ca168b6937818e9a74b47ac4e2112b2e1a024cf"
integrity sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg== integrity sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg==
"@supabase/auth-js@2.67.3":
version "2.67.3"
resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.67.3.tgz#a1f5eb22440b0cdbf87fe2ecae662a8dd8bb2028"
integrity sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/functions-js@2.4.3":
version "2.4.3"
resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.4.3.tgz#ac1c696d3a1ebe00f60d5cea69b208078678ef8b"
integrity sha512-sOLXy+mWRyu4LLv1onYydq+10mNRQ4rzqQxNhbrKLTLTcdcmS9hbWif0bGz/NavmiQfPs4ZcmQJp4WqOXlR4AQ==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/node-fetch@2.6.15", "@supabase/node-fetch@^2.6.14":
version "2.6.15"
resolved "https://registry.yarnpkg.com/@supabase/node-fetch/-/node-fetch-2.6.15.tgz#731271430e276983191930816303c44159e7226c"
integrity sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==
dependencies:
whatwg-url "^5.0.0"
"@supabase/postgrest-js@1.17.7":
version "1.17.7"
resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.17.7.tgz#2c0cc07c34cfbafcdf15a27e36c42119095ab154"
integrity sha512-aOzOYaTADm/dVTNksyqv9KsbhVa1gHz1Hoxb2ZEF2Ed9H7qlWOfptECQWmkEmrrFjtNaiPrgiSaPECvzI/seDA==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/realtime-js@2.11.2":
version "2.11.2"
resolved "https://registry.yarnpkg.com/@supabase/realtime-js/-/realtime-js-2.11.2.tgz#7f7399c326be717eadc9d5e259f9e2690fbf83dd"
integrity sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@types/phoenix" "^1.5.4"
"@types/ws" "^8.5.10"
ws "^8.18.0"
"@supabase/storage-js@2.7.1":
version "2.7.1"
resolved "https://registry.yarnpkg.com/@supabase/storage-js/-/storage-js-2.7.1.tgz#761482f237deec98a59e5af1ace18c7a5e0a69af"
integrity sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/supabase-js@^2.x.x":
version "2.47.9"
resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.47.9.tgz#8a6724811c2d695933e2b03bed180e3b9d74f25d"
integrity sha512-4hLBkr1pb7G7BbwW5U5C0xGX5VEOPhHMeFoxOvHjKNkl+KpAblR8bygL7hXFbkff7BrxyeRj9XfgYxXOcPLSDA==
dependencies:
"@supabase/auth-js" "2.67.3"
"@supabase/functions-js" "2.4.3"
"@supabase/node-fetch" "2.6.15"
"@supabase/postgrest-js" "1.17.7"
"@supabase/realtime-js" "2.11.2"
"@supabase/storage-js" "2.7.1"
"@swc/helpers@0.5.2": "@swc/helpers@0.5.2":
version "0.5.2" version "0.5.2"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d"
@ -435,6 +613,11 @@
dependencies: dependencies:
undici-types "~5.26.4" undici-types "~5.26.4"
"@types/phoenix@^1.5.4":
version "1.6.6"
resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.6.tgz#3c1ab53fd5a23634b8e37ea72ccacbf07fbc7816"
integrity sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==
"@types/prop-types@*": "@types/prop-types@*":
version "15.7.12" version "15.7.12"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
@ -465,6 +648,13 @@
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba"
integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==
"@types/ws@^8.5.10":
version "8.5.13"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20"
integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==
dependencies:
"@types/node" "*"
"@typescript-eslint/parser@^5.4.2 || ^6.0.0": "@typescript-eslint/parser@^5.4.2 || ^6.0.0":
version "6.21.0" version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b"
@ -600,6 +790,13 @@ argparse@^2.0.1:
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-hidden@^1.1.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522"
integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==
dependencies:
tslib "^2.0.0"
aria-query@^5.3.0: aria-query@^5.3.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
@ -875,21 +1072,23 @@ chokidar@^3.5.3:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
class-variance-authority@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz#4008a798a0e4553a781a57ac5177c9fb5d043787"
integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==
dependencies:
clsx "^2.1.1"
client-only@0.0.1: client-only@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
clsx@^2.0.0: clsx@^2.0.0, clsx@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
clsx@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
color-convert@^2.0.1: color-convert@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@ -1032,6 +1231,11 @@ dequal@^2.0.3:
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
detect-node-es@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
didyoumean@^1.2.2: didyoumean@^1.2.2:
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@ -1605,6 +1809,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@
has-symbols "^1.0.3" has-symbols "^1.0.3"
hasown "^2.0.0" hasown "^2.0.0"
get-nonce@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==
get-symbol-description@^1.0.2: get-symbol-description@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5"
@ -2738,6 +2947,33 @@ react-is@^16.13.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-remove-scroll-bar@^2.3.7:
version "2.3.8"
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223"
integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==
dependencies:
react-style-singleton "^2.2.2"
tslib "^2.0.0"
react-remove-scroll@^2.6.1:
version "2.6.2"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz#2518d2c5112e71ea8928f1082a58459b5c7a2a97"
integrity sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==
dependencies:
react-remove-scroll-bar "^2.3.7"
react-style-singleton "^2.2.1"
tslib "^2.1.0"
use-callback-ref "^1.3.3"
use-sidecar "^1.1.2"
react-style-singleton@^2.2.1, react-style-singleton@^2.2.2:
version "2.2.3"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==
dependencies:
get-nonce "^1.0.0"
tslib "^2.0.0"
react-text-to-speech@^0.14.5: react-text-to-speech@^0.14.5:
version "0.14.5" version "0.14.5"
resolved "https://registry.yarnpkg.com/react-text-to-speech/-/react-text-to-speech-0.14.5.tgz#f918786ab283311535682011045bd49777193300" resolved "https://registry.yarnpkg.com/react-text-to-speech/-/react-text-to-speech-0.14.5.tgz#f918786ab283311535682011045bd49777193300"
@ -2945,10 +3181,10 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
sonner@^1.4.41: sonner@^1.7.1:
version "1.4.41" version "1.7.1"
resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.4.41.tgz#ff085ae4f4244713daf294959beaa3e90f842d2c" resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.7.1.tgz#737110a3e6211d8d766442076f852ddde1725205"
integrity sha512-uG511ggnnsw6gcn/X+YKkWPo5ep9il9wYi3QJxHsYe7yTZ4+cOd1wuodOUmOpFuXL+/RE3R04LczdNCDygTDgQ== integrity sha512-b6LHBfH32SoVasRFECrdY8p8s7hXPDn3OHUFbZZbiB1ctLS9Gdh6rpX2dVrpQA0kiL5jcRzDDldwwLkSKk3+QQ==
source-map-js@^1.0.2, source-map-js@^1.2.0: source-map-js@^1.0.2, source-map-js@^1.2.0:
version "1.2.0" version "1.2.0"
@ -3101,12 +3337,10 @@ tabbable@^6.0.0:
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
tailwind-merge@^2.2.2: tailwind-merge@^2.5.5:
version "2.2.2" version "2.5.5"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.2.2.tgz#87341e7604f0e20499939e152cd2841f41f7a3df" resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.5.tgz#98167859b856a2a6b8d2baf038ee171b9d814e39"
integrity sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw== integrity sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==
dependencies:
"@babel/runtime" "^7.24.0"
tailwindcss@^3.3.0: tailwindcss@^3.3.0:
version "3.4.3" version "3.4.3"
@ -3192,7 +3426,7 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6" minimist "^1.2.6"
strip-bom "^3.0.0" strip-bom "^3.0.0"
tslib@^2.4.0, tslib@^2.8.0: tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.0:
version "2.8.1" version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@ -3288,6 +3522,13 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" punycode "^2.1.0"
use-callback-ref@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf"
integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==
dependencies:
tslib "^2.0.0"
use-composed-ref@^1.3.0: use-composed-ref@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
@ -3305,6 +3546,14 @@ use-latest@^1.2.1:
dependencies: dependencies:
use-isomorphic-layout-effect "^1.1.1" use-isomorphic-layout-effect "^1.1.1"
use-sidecar@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb"
integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==
dependencies:
detect-node-es "^1.1.0"
tslib "^2.0.0"
util-deprecate@^1.0.2: util-deprecate@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@ -3418,6 +3667,11 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@^8.18.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
yallist@^4.0.0: yallist@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"

153
yarn.lock
View file

@ -548,6 +548,63 @@
domhandler "^5.0.3" domhandler "^5.0.3"
selderee "^0.11.0" selderee "^0.11.0"
"@supabase/auth-js@2.67.1":
version "2.67.1"
resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.67.1.tgz#b72217136df61d645dcfb7b12c7db8cbb7875a4c"
integrity sha512-1SRZG9VkLFz4rtiyEc1l49tMq9jTYu4wJt3pMQEWi7yshZFIBdVH1o5sshk1plQd5LY6GcrPIpCydM2gGDxchA==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/functions-js@2.4.3":
version "2.4.3"
resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.4.3.tgz#ac1c696d3a1ebe00f60d5cea69b208078678ef8b"
integrity sha512-sOLXy+mWRyu4LLv1onYydq+10mNRQ4rzqQxNhbrKLTLTcdcmS9hbWif0bGz/NavmiQfPs4ZcmQJp4WqOXlR4AQ==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/node-fetch@2.6.15", "@supabase/node-fetch@^2.6.14":
version "2.6.15"
resolved "https://registry.yarnpkg.com/@supabase/node-fetch/-/node-fetch-2.6.15.tgz#731271430e276983191930816303c44159e7226c"
integrity sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==
dependencies:
whatwg-url "^5.0.0"
"@supabase/postgrest-js@1.17.7":
version "1.17.7"
resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.17.7.tgz#2c0cc07c34cfbafcdf15a27e36c42119095ab154"
integrity sha512-aOzOYaTADm/dVTNksyqv9KsbhVa1gHz1Hoxb2ZEF2Ed9H7qlWOfptECQWmkEmrrFjtNaiPrgiSaPECvzI/seDA==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/realtime-js@2.11.2":
version "2.11.2"
resolved "https://registry.yarnpkg.com/@supabase/realtime-js/-/realtime-js-2.11.2.tgz#7f7399c326be717eadc9d5e259f9e2690fbf83dd"
integrity sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@types/phoenix" "^1.5.4"
"@types/ws" "^8.5.10"
ws "^8.18.0"
"@supabase/storage-js@2.7.1":
version "2.7.1"
resolved "https://registry.yarnpkg.com/@supabase/storage-js/-/storage-js-2.7.1.tgz#761482f237deec98a59e5af1ace18c7a5e0a69af"
integrity sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/supabase-js@latest":
version "2.47.8"
resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.47.8.tgz#6471a356b694e14170a00e6582bdbd0126944ec6"
integrity sha512-2GjK8/PrGJYDVBcjqGyM2irBLMQXvvkJLbS8VFPlym2uuNz+pPMnwLbNf5njkknUTy3PamjgIRoADpuPPPA6oA==
dependencies:
"@supabase/auth-js" "2.67.1"
"@supabase/functions-js" "2.4.3"
"@supabase/node-fetch" "2.6.15"
"@supabase/postgrest-js" "1.17.7"
"@supabase/realtime-js" "2.11.2"
"@supabase/storage-js" "2.7.1"
"@tsconfig/node10@^1.0.7": "@tsconfig/node10@^1.0.7":
version "1.0.11" version "1.0.11"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"
@ -698,6 +755,11 @@
resolved "https://registry.yarnpkg.com/@types/pdf-parse/-/pdf-parse-1.1.4.tgz#21a539efd2f16009d08aeed3350133948b5d7ed1" resolved "https://registry.yarnpkg.com/@types/pdf-parse/-/pdf-parse-1.1.4.tgz#21a539efd2f16009d08aeed3350133948b5d7ed1"
integrity sha512-+gbBHbNCVGGYw1S9lAIIvrHW47UYOhMIFUsJcMkMrzy1Jf0vulBN3XQIjPgnoOXveMuHnF3b57fXROnY/Or7eg== integrity sha512-+gbBHbNCVGGYw1S9lAIIvrHW47UYOhMIFUsJcMkMrzy1Jf0vulBN3XQIjPgnoOXveMuHnF3b57fXROnY/Or7eg==
"@types/phoenix@^1.5.4":
version "1.6.6"
resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.6.tgz#3c1ab53fd5a23634b8e37ea72ccacbf07fbc7816"
integrity sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==
"@types/qs@*": "@types/qs@*":
version "6.9.14" version "6.9.14"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.14.tgz#169e142bfe493895287bee382af6039795e9b75b" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.14.tgz#169e142bfe493895287bee382af6039795e9b75b"
@ -753,6 +815,13 @@
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba"
integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==
"@types/ws@^8.5.10":
version "8.5.13"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20"
integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==
dependencies:
"@types/node" "*"
"@types/ws@^8.5.12": "@types/ws@^8.5.12":
version "8.5.12" version "8.5.12"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e"
@ -813,6 +882,18 @@ agentkeepalive@^4.2.1:
dependencies: dependencies:
humanize-ms "^1.2.1" humanize-ms "^1.2.1"
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-styles@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
ansi-styles@^5.0.0: ansi-styles@^5.0.0:
version "5.2.0" version "5.2.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
@ -1063,6 +1144,23 @@ chownr@^1.1.1:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chromadb@^1.9.4:
version "1.9.4"
resolved "https://registry.yarnpkg.com/chromadb/-/chromadb-1.9.4.tgz#ffb9f549cfe0909f9053097e0708c985221dd57e"
integrity sha512-KtBy3uvZWV5+B6tlSYxwi8YW+Dv+qBs2spffSaamLeAHLn+N4ss6xoscLWDp5J1ImfscT8NFW1lGZlTZbs8Huw==
dependencies:
cliui "^8.0.1"
isomorphic-fetch "^3.0.0"
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
color-convert@^1.9.3: color-convert@^1.9.3:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -1375,6 +1473,11 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
enabled@2.0.x: enabled@2.0.x:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
@ -1823,6 +1926,11 @@ is-extglob@^2.1.1:
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-glob@^4.0.1, is-glob@~4.0.1: is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
@ -1845,6 +1953,14 @@ isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
isomorphic-fetch@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4"
integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==
dependencies:
node-fetch "^2.6.1"
whatwg-fetch "^3.4.1"
js-tiktoken@^1.0.12: js-tiktoken@^1.0.12:
version "1.0.12" version "1.0.12"
resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.12.tgz#af0f5cf58e5e7318240d050c8413234019424211" resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.12.tgz#af0f5cf58e5e7318240d050c8413234019424211"
@ -2213,7 +2329,7 @@ node-ensure@^0.0.0:
resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7"
integrity sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw== integrity sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==
node-fetch@^2.6.7: node-fetch@^2.6.1, node-fetch@^2.6.7:
version "2.7.0" version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
@ -2767,6 +2883,15 @@ streamx@^2.15.0, streamx@^2.16.1:
optionalDependencies: optionalDependencies:
bare-events "^2.2.0" bare-events "^2.2.0"
string-width@^4.1.0, string-width@^4.2.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string_decoder@^1.1.1: string_decoder@^1.1.1:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@ -2781,6 +2906,13 @@ string_decoder@~1.1.1:
dependencies: dependencies:
safe-buffer "~5.1.0" safe-buffer "~5.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-json-comments@~2.0.1: strip-json-comments@~2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@ -2992,6 +3124,11 @@ webidl-conversions@^3.0.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
whatwg-fetch@^3.4.1:
version "3.6.20"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70"
integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==
whatwg-url@^5.0.0: whatwg-url@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
@ -3026,6 +3163,15 @@ winston@^3.13.0:
triple-beam "^1.3.0" triple-beam "^1.3.0"
winston-transport "^4.7.0" winston-transport "^4.7.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrappy@1: wrappy@1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@ -3036,6 +3182,11 @@ ws@^8.17.1:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
ws@^8.18.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
xmlbuilder@^10.0.0: xmlbuilder@^10.0.0:
version "10.1.1" version "10.1.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz#8cae6688cc9b38d850b7c8d3c0a4161dcaf475b0" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz#8cae6688cc9b38d850b7c8d3c0a4161dcaf475b0"