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
|
@ -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 }
|
Loading…
Add table
Add a link
Reference in a new issue