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
|
@ -1,2 +1,4 @@
|
|||
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';
|
||||
|
||||
import { Search } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Search, Filter, X } from 'lucide-react';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import Link from 'next/link';
|
||||
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 {
|
||||
title: string;
|
||||
content: string;
|
||||
url: string;
|
||||
thumbnail: string;
|
||||
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;
|
||||
}
|
||||
|
||||
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 [discover, setDiscover] = useState<Discover[] | null>(null);
|
||||
const [experts, setExperts] = useState<Expert[] | null>(null);
|
||||
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(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/discover`, {
|
||||
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();
|
||||
}, []);
|
||||
fetchExperts();
|
||||
fetchLocations();
|
||||
}, [fetchExperts]);
|
||||
|
||||
return loading ? (
|
||||
<div className="flex flex-row items-center justify-center min-h-screen">
|
||||
|
@ -66,47 +247,64 @@ const Page = () => {
|
|||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex flex-col pt-4">
|
||||
<div className="flex items-center">
|
||||
<Search />
|
||||
<h1 className="text-3xl font-medium p-2">Discover</h1>
|
||||
<div className="pb-24 lg:pb-0">
|
||||
<div className="flex flex-col pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center">
|
||||
<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>
|
||||
<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">
|
||||
{discover &&
|
||||
discover?.map((item, i) => (
|
||||
<Link
|
||||
href={`/?q=Summary: ${item.url}`}
|
||||
key={i}
|
||||
className="max-w-sm rounded-lg overflow-hidden bg-light-secondary dark:bg-dark-secondary hover:-translate-y-[1px] transition duration-200"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
className="object-cover w-full aspect-video"
|
||||
src={
|
||||
new URL(item.thumbnail).origin +
|
||||
new URL(item.thumbnail).pathname +
|
||||
`?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>
|
||||
))}
|
||||
|
||||
{/* CTA Filtres unifié */}
|
||||
<Button
|
||||
onClick={() => setOpen(true)}
|
||||
variant="outline"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Filter size={18} />
|
||||
<span>Filtrer</span>
|
||||
{activeFiltersCount > 0 && (
|
||||
<span className="bg-blue-500 text-white text-xs px-2 py-0.5 rounded-full">
|
||||
{activeFiltersCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</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 = {
|
||||
title: 'Perplexica - Chat with the internet',
|
||||
title: 'X&me - Chat with the internet',
|
||||
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({
|
||||
|
|
|
@ -3,8 +3,8 @@ import { Metadata } from 'next';
|
|||
import { Suspense } from 'react';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Chat - Perplexica',
|
||||
description: 'Chat with the internet, chat with Perplexica.',
|
||||
title: 'Chat - X-me',
|
||||
description: 'Chat with the internet, chat with X-me.',
|
||||
};
|
||||
|
||||
const Home = () => {
|
||||
|
|
|
@ -38,8 +38,11 @@ const EmptyChat = ({
|
|||
</div>
|
||||
<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">
|
||||
Research begins here.
|
||||
Ici c'est vous le patron.
|
||||
</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
|
||||
sendMessage={sendMessage}
|
||||
focusMode={focusMode}
|
||||
|
|
|
@ -80,7 +80,7 @@ const EmptyChatMessageInput = ({
|
|||
onChange={(e) => setMessage(e.target.value)}
|
||||
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"
|
||||
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 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 Rewrite from './MessageActions/Rewrite';
|
||||
import MessageSources from './MessageSources';
|
||||
import SearchImages from './SearchImages';
|
||||
import LegalSearch from './LegalSearch';
|
||||
import SearchVideos from './SearchVideos';
|
||||
import { useSpeech } from 'react-text-to-speech';
|
||||
|
||||
|
@ -53,8 +53,12 @@ const MessageBox = ({
|
|||
return setParsedMessage(
|
||||
message.content.replace(
|
||||
regex,
|
||||
(_, 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>`,
|
||||
(_, number) => {
|
||||
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);
|
||||
}, [message.content, message.sources, message.role]);
|
||||
|
||||
useEffect(() => {
|
||||
}, [message.sources]);
|
||||
|
||||
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
|
||||
|
||||
return (
|
||||
<div>
|
||||
{message.role === 'user' && (
|
||||
<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}
|
||||
</h2>
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -81,6 +88,24 @@ const MessageBox = ({
|
|||
ref={dividerRef}
|
||||
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 && (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
|
@ -102,7 +127,7 @@ const MessageBox = ({
|
|||
size={20}
|
||||
/>
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||
Answer
|
||||
Question
|
||||
</h3>
|
||||
</div>
|
||||
<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]',
|
||||
'max-w-none break-words text-black dark:text-white',
|
||||
)}
|
||||
options={{
|
||||
overrides: {
|
||||
p: ({ children }) => {
|
||||
return <p>{children}</p>;
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{parsedMessage}
|
||||
</Markdown>
|
||||
|
@ -152,7 +184,7 @@ const MessageBox = ({
|
|||
<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">
|
||||
<Layers3 />
|
||||
<h3 className="text-xl font-medium">Related</h3>
|
||||
<h3 className="text-xl font-medium">Suggestions</h3>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-3">
|
||||
{message.suggestions.map((suggestion, i) => (
|
||||
|
@ -184,7 +216,7 @@ const MessageBox = ({
|
|||
</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">
|
||||
<SearchImages
|
||||
<LegalSearch
|
||||
query={history[messageIndex - 1].content}
|
||||
chatHistory={history.slice(0, messageIndex - 1)}
|
||||
/>
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
Globe,
|
||||
Pencil,
|
||||
ScanEye,
|
||||
SwatchBook,
|
||||
Eye,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
|
@ -19,52 +19,28 @@ import { Fragment } from 'react';
|
|||
const focusModes = [
|
||||
{
|
||||
key: 'webSearch',
|
||||
title: 'All',
|
||||
description: 'Searches across all of the internet',
|
||||
title: 'Recherche internet',
|
||||
description: 'Recherche sur internet directement',
|
||||
icon: <Globe size={20} />,
|
||||
},
|
||||
{
|
||||
key: 'academicSearch',
|
||||
title: 'Academic',
|
||||
description: 'Search in published academic papers',
|
||||
icon: <SwatchBook size={20} />,
|
||||
title: 'Experts',
|
||||
description: 'Recherche un expert pour vous acccompagner',
|
||||
icon: <Eye size={20} />,
|
||||
},
|
||||
{
|
||||
key: 'writingAssistant',
|
||||
title: 'Writing',
|
||||
title: 'Document',
|
||||
description: 'Chat without searching the web',
|
||||
icon: <Pencil size={16} />,
|
||||
},
|
||||
{
|
||||
key: 'wolframAlphaSearch',
|
||||
title: 'Wolfram Alpha',
|
||||
description: 'Computational knowledge engine',
|
||||
title: 'Business Plan',
|
||||
description: 'Réaliser votre Business Plan',
|
||||
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 = ({
|
||||
|
|
|
@ -23,54 +23,60 @@ const SearchImages = ({
|
|||
const [open, setOpen] = useState(false);
|
||||
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 (
|
||||
<>
|
||||
{!loading && images === 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}/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);
|
||||
}}
|
||||
onClick={handleSearch}
|
||||
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">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
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 { useSelectedLayoutSegments } from 'next/navigation';
|
||||
import React, { useState, type ReactNode } from 'react';
|
||||
|
@ -38,6 +38,12 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
|||
active: segments.includes('library'),
|
||||
label: 'Library',
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
href: '/chatroom',
|
||||
active: segments.includes('chatroom'),
|
||||
label: 'Messages',
|
||||
},
|
||||
];
|
||||
|
||||
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 { twMerge } from 'tailwind-merge';
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
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 = (
|
||||
date1: Date | string,
|
||||
|
|
|
@ -5,8 +5,11 @@ const nextConfig = {
|
|||
{
|
||||
hostname: 's2.googleusercontent.com',
|
||||
},
|
||||
{
|
||||
hostname: 'dam.malt.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
export default nextConfig;
|
|
@ -14,8 +14,12 @@
|
|||
"@headlessui/react": "^2.2.0",
|
||||
"@icons-pack/react-simple-icons": "^9.4.0",
|
||||
"@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",
|
||||
"clsx": "^2.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"langchain": "^0.1.30",
|
||||
"lucide-react": "^0.363.0",
|
||||
"markdown-to-jsx": "^7.6.2",
|
||||
|
@ -25,8 +29,8 @@
|
|||
"react-dom": "^18",
|
||||
"react-text-to-speech": "^0.14.5",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"sonner": "^1.4.41",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"sonner": "^1.7.1",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"yet-another-react-lightbox": "^3.17.2",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
|
|
|
@ -18,9 +18,10 @@
|
|||
}
|
||||
],
|
||||
"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"]
|
||||
}
|
||||
|
|
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd"
|
||||
integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==
|
||||
|
@ -316,6 +316,127 @@
|
|||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
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":
|
||||
version "3.18.4"
|
||||
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"
|
||||
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":
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d"
|
||||
|
@ -435,6 +613,11 @@
|
|||
dependencies:
|
||||
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@*":
|
||||
version "15.7.12"
|
||||
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"
|
||||
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":
|
||||
version "6.21.0"
|
||||
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"
|
||||
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:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
|
||||
|
@ -875,21 +1072,23 @@ chokidar@^3.5.3:
|
|||
optionalDependencies:
|
||||
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:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
clsx@^2.0.0:
|
||||
clsx@^2.0.0, clsx@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||
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:
|
||||
version "2.0.1"
|
||||
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"
|
||||
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:
|
||||
version "1.2.2"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
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"
|
||||
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:
|
||||
version "0.14.5"
|
||||
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"
|
||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||
|
||||
sonner@^1.4.41:
|
||||
version "1.4.41"
|
||||
resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.4.41.tgz#ff085ae4f4244713daf294959beaa3e90f842d2c"
|
||||
integrity sha512-uG511ggnnsw6gcn/X+YKkWPo5ep9il9wYi3QJxHsYe7yTZ4+cOd1wuodOUmOpFuXL+/RE3R04LczdNCDygTDgQ==
|
||||
sonner@^1.7.1:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/sonner/-/sonner-1.7.1.tgz#737110a3e6211d8d766442076f852ddde1725205"
|
||||
integrity sha512-b6LHBfH32SoVasRFECrdY8p8s7hXPDn3OHUFbZZbiB1ctLS9Gdh6rpX2dVrpQA0kiL5jcRzDDldwwLkSKk3+QQ==
|
||||
|
||||
source-map-js@^1.0.2, source-map-js@^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"
|
||||
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
|
||||
|
||||
tailwind-merge@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.2.2.tgz#87341e7604f0e20499939e152cd2841f41f7a3df"
|
||||
integrity sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.24.0"
|
||||
tailwind-merge@^2.5.5:
|
||||
version "2.5.5"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.5.tgz#98167859b856a2a6b8d2baf038ee171b9d814e39"
|
||||
integrity sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==
|
||||
|
||||
tailwindcss@^3.3.0:
|
||||
version "3.4.3"
|
||||
|
@ -3192,7 +3426,7 @@ tsconfig-paths@^3.15.0:
|
|||
minimist "^1.2.6"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
@ -3288,6 +3522,13 @@ uri-js@^4.2.2:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.3.0"
|
||||
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:
|
||||
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:
|
||||
version "1.0.2"
|
||||
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"
|
||||
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:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue