feat: add expert search, legal search and UI improvements
This commit is contained in:
parent
2c5ca94b3c
commit
271199c527
53 changed files with 4595 additions and 708 deletions
|
@ -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:
|
||||||
|
|
|
@ -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
157
project_structure.md
Normal 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.
|
|
@ -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
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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}`);
|
||||||
});
|
});
|
||||||
|
|
235
src/chains/expertSearchAgent.ts
Normal file
235
src/chains/expertSearchAgent.ts
Normal 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;
|
|
@ -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({
|
||||||
|
|
113
src/chains/legalSearchAgent.ts
Normal file
113
src/chains/legalSearchAgent.ts
Normal 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;
|
292
src/chains/rag_document_upload.ts
Normal file
292
src/chains/rag_document_upload.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
29
src/db/supabase.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
26
src/lib/outputParsers/imageOutputParser.ts
Normal file
26
src/lib/outputParsers/imageOutputParser.ts
Normal 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;
|
|
@ -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 };
|
||||||
};
|
}
|
||||||
|
|
|
@ -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}.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -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
114
src/routes/experts.ts
Normal 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;
|
|
@ -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,
|
||||||
|
|
|
@ -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
88
src/routes/legal.ts
Normal 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;
|
152
src/routes/legifrance.ts.bak
Normal file
152
src/routes/legifrance.ts.bak
Normal 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
48
src/routes/news.ts
Normal 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;
|
|
@ -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
1
src/types/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './types';
|
73
src/types/types.ts
Normal file
73
src/types/types.ts
Normal 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;
|
||||||
|
}
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
295
ui/app/chatroom/[expertId]/page.tsx
Normal file
295
ui/app/chatroom/[expertId]/page.tsx
Normal 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
37
ui/app/chatroom/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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'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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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 = () => {
|
||||||
|
|
|
@ -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'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}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
158
ui/components/FilterModal.tsx
Normal file
158
ui/components/FilterModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
200
ui/components/LegalSearch.tsx
Normal file
200
ui/components/LegalSearch.tsx
Normal 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;
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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 = ({
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
52
ui/components/ui/button.tsx
Normal file
52
ui/components/ui/button.tsx
Normal 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
121
ui/components/ui/dialog.tsx
Normal 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,
|
||||||
|
}
|
24
ui/components/ui/input.tsx
Normal file
24
ui/components/ui/input.tsx
Normal 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
38
ui/lib/config.ts
Normal 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
62
ui/lib/supabase.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -5,6 +5,9 @@ const nextConfig = {
|
||||||
{
|
{
|
||||||
hostname: 's2.googleusercontent.com',
|
hostname: 's2.googleusercontent.com',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
hostname: 'dam.malt.com',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
35
ui/types/index.ts
Normal 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;
|
||||||
|
}
|
290
ui/yarn.lock
290
ui/yarn.lock
|
@ -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
153
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Add table
Reference in a new issue