add ask-ia page
This commit is contained in:
parent
0b8c217a91
commit
1e2f833509
8 changed files with 495 additions and 3 deletions
|
|
@ -27,7 +27,8 @@ import {
|
||||||
Calculator,
|
Calculator,
|
||||||
ListTodo,
|
ListTodo,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
PanelLeft
|
PanelLeft,
|
||||||
|
MessageSquare,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export const MODULE_GESTION = 'gestion';
|
export const MODULE_GESTION = 'gestion';
|
||||||
|
|
@ -113,6 +114,12 @@ export const getMenuForModule = (currentModule) => {
|
||||||
{ to: "/home/sage-builder", icon: PanelLeft, label: "Tableau des ventes" },
|
{ to: "/home/sage-builder", icon: PanelLeft, label: "Tableau des ventes" },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Modules",
|
||||||
|
items: [
|
||||||
|
{ to: "/home/ask-ia", icon: MessageSquare, label: "Sage Ask.AI" },
|
||||||
|
]
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// title: "Signature",
|
// title: "Signature",
|
||||||
// items: [
|
// items: [
|
||||||
|
|
|
||||||
395
src/pages/AskIaPage.tsx
Normal file
395
src/pages/AskIaPage.tsx
Normal file
|
|
@ -0,0 +1,395 @@
|
||||||
|
import { ModalLoading } from '@/components/modal/ModalLoading';
|
||||||
|
import { askIaStatus, getAskIaHtmlContent } from '@/store/features/ask-ia/selectors';
|
||||||
|
import { getAskIa } from '@/store/features/ask-ia/thunk';
|
||||||
|
import { sageBuilderError } from '@/store/features/sage-builder/selectors';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
const AskIaPage = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
|
||||||
|
const htmlContent = useAppSelector(getAskIaHtmlContent);
|
||||||
|
const status = useAppSelector(askIaStatus);
|
||||||
|
const error = useAppSelector(sageBuilderError);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Charger le dashboard au montage du composant
|
||||||
|
if (status === 'idle') {
|
||||||
|
dispatch(getAskIa());
|
||||||
|
}
|
||||||
|
}, [dispatch, status]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Injecter le HTML dans l'iframe quand il est disponible
|
||||||
|
if (htmlContent && iframeRef.current && status === 'succeeded') {
|
||||||
|
const iframeDoc = iframeRef.current.contentDocument || iframeRef.current.contentWindow?.document;
|
||||||
|
if (iframeDoc) {
|
||||||
|
iframeDoc.open();
|
||||||
|
iframeDoc.write(htmlContent);
|
||||||
|
iframeDoc.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [htmlContent, status]);
|
||||||
|
|
||||||
|
const handleRetry = () => {
|
||||||
|
dispatch(getAskIa());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen w-full overflow-hidden bg-gray-50 relative">
|
||||||
|
{/* Loading Overlay */}
|
||||||
|
{status === 'loading' && <ModalLoading />}
|
||||||
|
|
||||||
|
{/* Error State */}
|
||||||
|
{status === 'failed' && error && (
|
||||||
|
<div className="absolute inset-0 z-50 flex items-center justify-center bg-gray-50">
|
||||||
|
<div className="text-center max-w-md px-4">
|
||||||
|
<div className="inline-flex items-center justify-center w-16 h-16 mb-4 bg-red-100 rounded-full">
|
||||||
|
<svg
|
||||||
|
className="w-8 h-8 text-red-600"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Erreur de chargement
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleRetry}
|
||||||
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
||||||
|
>
|
||||||
|
Réessayer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Iframe */}
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
className={`w-full h-full border-0 transition-opacity duration-300 ${
|
||||||
|
status === 'succeeded' ? 'opacity-100' : 'opacity-0'
|
||||||
|
}`}
|
||||||
|
title="Sage Builder Dashboard"
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AskIaPage;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
|
// import { Send, Loader2, ChevronDown, Plug, Zap, Cpu } from 'lucide-react';
|
||||||
|
|
||||||
|
// // Configuration (à adapter selon ton contexte)
|
||||||
|
// const config = {
|
||||||
|
// apiUrl: "https://api.sage-ai-studio.webexpr.dev",
|
||||||
|
// authToken: "Bearer sk_live_bef70d4dd6bc671fd815d21104d5dce8",
|
||||||
|
// fixedProduct: { slug: "sage100dataven", name: "Sage 100 (Dataven)" },
|
||||||
|
// fixedModel: { key: "mistral-small", label: "Mistral Small" },
|
||||||
|
// t: {
|
||||||
|
// howCanIHelp: "Comment puis-je vous aider aujourd'hui ?",
|
||||||
|
// querySageData: "Interrogez vos données Sage en langage naturel",
|
||||||
|
// writeMessage: "Écrivez votre message...",
|
||||||
|
// enterToSend: "Appuyez sur Entrée pour envoyer, Shift+Entrée pour une nouvelle ligne",
|
||||||
|
// consulting: "Consultation en cours",
|
||||||
|
// error: "Erreur"
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Simple Markdown renderer
|
||||||
|
// const renderMarkdown = (text) => {
|
||||||
|
// if (!text) return '';
|
||||||
|
// return text
|
||||||
|
// .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
|
// .replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||||
|
// .replace(/```([\s\S]*?)```/g, '<pre class="bg-gray-100 p-3 rounded-md overflow-x-auto my-2"><code>$1</code></pre>')
|
||||||
|
// .replace(/`(.+?)`/g, '<code class="bg-gray-100 px-1.5 py-0.5 rounded text-sm">$1</code>')
|
||||||
|
// .replace(/^### (.+)$/gm, '<h3 class="font-semibold text-base mt-3 mb-1">$1</h3>')
|
||||||
|
// .replace(/^## (.+)$/gm, '<h2 class="font-semibold text-lg mt-3 mb-1">$1</h2>')
|
||||||
|
// .replace(/^# (.+)$/gm, '<h1 class="font-bold text-xl mt-3 mb-2">$1</h1>')
|
||||||
|
// .replace(/^- (.+)$/gm, '<li class="ml-4">• $1</li>')
|
||||||
|
// .replace(/\n/g, '<br/>');
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Message Component
|
||||||
|
// const Message = ({ message, isUser }) => {
|
||||||
|
// const modelLabel = message.model ? (
|
||||||
|
// message.model.toLowerCase().includes('haiku') ? 'Haiku' :
|
||||||
|
// message.model.toLowerCase().includes('sonnet') ? 'Sonnet' :
|
||||||
|
// message.model.toLowerCase().includes('mistral-small') ? 'Mistral Small' :
|
||||||
|
// message.model.toLowerCase().includes('mistral-large') ? 'Mistral Large' :
|
||||||
|
// message.model
|
||||||
|
// ) : null;
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className={`flex gap-4 ${isUser ? 'justify-end' : 'justify-start'}`}>
|
||||||
|
// {!isUser && (
|
||||||
|
// <div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-r from-green-600 to-blue-600 text-white shadow-md">
|
||||||
|
// <span className="font-bold text-sm">S</span>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// <div className={`max-w-[80%] rounded-2xl px-4 py-3 ${isUser ? 'bg-zinc-900 text-white' : 'bg-gray-100'}`}>
|
||||||
|
// {isUser ? (
|
||||||
|
// <p className="text-sm whitespace-pre-wrap">{message.content}</p>
|
||||||
|
// ) : (
|
||||||
|
// <>
|
||||||
|
// <div
|
||||||
|
// className="text-sm prose prose-sm max-w-none"
|
||||||
|
// dangerouslySetInnerHTML={{ __html: renderMarkdown(message.content) }}
|
||||||
|
// />
|
||||||
|
// {modelLabel && (
|
||||||
|
// <div className="flex items-center gap-1 mt-2 pt-2 border-t border-gray-200">
|
||||||
|
// {message.model?.toLowerCase().includes('haiku') || message.model?.toLowerCase().includes('mistral-small') ? (
|
||||||
|
// <Zap size={12} className="text-gray-500" />
|
||||||
|
// ) : (
|
||||||
|
// <Cpu size={12} className="text-gray-500" />
|
||||||
|
// )}
|
||||||
|
// <span className="text-xs text-gray-500">{modelLabel}</span>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// {isUser && (
|
||||||
|
// <div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-zinc-400 text-white">
|
||||||
|
// <span className="font-medium text-sm">U</span>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Loading Indicator
|
||||||
|
// const LoadingIndicator = ({ tool }) => (
|
||||||
|
// <div className="flex gap-4 justify-start">
|
||||||
|
// <div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-r from-green-600 to-blue-600 text-white shadow-md">
|
||||||
|
// <span className="font-bold text-sm">S</span>
|
||||||
|
// </div>
|
||||||
|
// {tool ? (
|
||||||
|
// <div className="bg-amber-50 border border-amber-200 rounded-xl px-4 py-3">
|
||||||
|
// <div className="flex items-center gap-2">
|
||||||
|
// <Loader2 size={16} className="animate-spin text-amber-600" />
|
||||||
|
// <span className="text-sm text-amber-600">
|
||||||
|
// {config.t.consulting}: {tool}
|
||||||
|
// </span>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ) : (
|
||||||
|
// <div className="max-w-[80%] rounded-2xl bg-gray-100 px-4 py-3">
|
||||||
|
// <div className="flex gap-1">
|
||||||
|
// <div className="size-2 animate-bounce rounded-full bg-gray-400" style={{ animationDelay: '-0.3s' }} />
|
||||||
|
// <div className="size-2 animate-bounce rounded-full bg-gray-400" style={{ animationDelay: '-0.15s' }} />
|
||||||
|
// <div className="size-2 animate-bounce rounded-full bg-gray-400" />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Empty State
|
||||||
|
// const EmptyState = () => (
|
||||||
|
// <div className="h-full flex flex-col items-center justify-center gap-6 px-4 py-8">
|
||||||
|
// <div className="flex size-16 items-center justify-center rounded-full bg-gradient-to-r from-green-600 to-blue-600 text-white shadow-lg">
|
||||||
|
// <span className="font-bold text-3xl">S</span>
|
||||||
|
// </div>
|
||||||
|
// <div className="text-center">
|
||||||
|
// <h2 className="mb-2 font-bold text-2xl text-gray-900">{config.t.howCanIHelp}</h2>
|
||||||
|
// <p className="text-gray-500">{config.t.querySageData}</p>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Main Component
|
||||||
|
// const AskIaPage = () => {
|
||||||
|
// const [messages, setMessages] = useState([]);
|
||||||
|
// const [input, setInput] = useState('');
|
||||||
|
// const [isLoading, setIsLoading] = useState(false);
|
||||||
|
// const [currentTool, setCurrentTool] = useState(null);
|
||||||
|
// const [error, setError] = useState(null);
|
||||||
|
// const scrollRef = useRef(null);
|
||||||
|
// const inputRef = useRef(null);
|
||||||
|
|
||||||
|
// // Auto-scroll
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (scrollRef.current) {
|
||||||
|
// scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
// }
|
||||||
|
// }, [messages, isLoading]);
|
||||||
|
|
||||||
|
// // Send message with streaming
|
||||||
|
// const sendMessage = useCallback(async (text) => {
|
||||||
|
// if (!text.trim() || isLoading) return;
|
||||||
|
|
||||||
|
// const userMessage = {
|
||||||
|
// id: Date.now().toString(),
|
||||||
|
// role: 'user',
|
||||||
|
// content: text.trim(),
|
||||||
|
// };
|
||||||
|
|
||||||
|
// setMessages(prev => [...prev, userMessage]);
|
||||||
|
// setInput('');
|
||||||
|
// setIsLoading(true);
|
||||||
|
// setError(null);
|
||||||
|
// setCurrentTool(null);
|
||||||
|
|
||||||
|
// const assistantId = (Date.now() + 1).toString();
|
||||||
|
// setMessages(prev => [...prev, { id: assistantId, role: 'assistant', content: '', model: null }]);
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const history = messages.map(m => ({ role: m.role, content: m.content }));
|
||||||
|
|
||||||
|
// const response = await fetch(config.apiUrl + '/api/ask/chat/stream', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: {
|
||||||
|
// 'Content-Type': 'application/json',
|
||||||
|
// 'Authorization': config.authToken,
|
||||||
|
// },
|
||||||
|
// body: JSON.stringify({
|
||||||
|
// question: text.trim(),
|
||||||
|
// history,
|
||||||
|
// productSlug: config.fixedProduct.slug,
|
||||||
|
// model: config.fixedModel.key,
|
||||||
|
// }),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (!response.ok) {
|
||||||
|
// const errorData = await response.json().catch(() => ({}));
|
||||||
|
// throw new Error(errorData.error || 'Erreur ' + response.status);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const reader = response.body.getReader();
|
||||||
|
// const decoder = new TextDecoder();
|
||||||
|
// let buffer = '';
|
||||||
|
|
||||||
|
// while (true) {
|
||||||
|
// const { done, value } = await reader.read();
|
||||||
|
// if (done) break;
|
||||||
|
|
||||||
|
// buffer += decoder.decode(value, { stream: true });
|
||||||
|
// const lines = buffer.split('\n');
|
||||||
|
// buffer = lines.pop() || '';
|
||||||
|
|
||||||
|
// for (const line of lines) {
|
||||||
|
// if (line.startsWith('data: ')) {
|
||||||
|
// try {
|
||||||
|
// const data = JSON.parse(line.slice(6));
|
||||||
|
// if (data.type === 'text') {
|
||||||
|
// setMessages(prev => prev.map(m =>
|
||||||
|
// m.id === assistantId ? { ...m, content: m.content + data.content } : m
|
||||||
|
// ));
|
||||||
|
// } else if (data.type === 'tool_start') {
|
||||||
|
// setCurrentTool(data.tool);
|
||||||
|
// } else if (data.type === 'tool_end') {
|
||||||
|
// setCurrentTool(null);
|
||||||
|
// } else if (data.type === 'complete') {
|
||||||
|
// setMessages(prev => prev.map(m =>
|
||||||
|
// m.id === assistantId ? { ...m, model: data.usage?.model || null } : m
|
||||||
|
// ));
|
||||||
|
// } else if (data.type === 'error') {
|
||||||
|
// setError(data.message);
|
||||||
|
// }
|
||||||
|
// } catch (e) {}
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } catch (err) {
|
||||||
|
// setError(err.message || "Erreur lors de l'envoi du message");
|
||||||
|
// setMessages(prev => prev.filter(m => m.id !== assistantId));
|
||||||
|
// } finally {
|
||||||
|
// setIsLoading(false);
|
||||||
|
// setCurrentTool(null);
|
||||||
|
// }
|
||||||
|
// }, [messages, isLoading]);
|
||||||
|
|
||||||
|
// const handleKeyDown = (e) => {
|
||||||
|
// if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
// e.preventDefault();
|
||||||
|
// sendMessage(input);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="flex flex-col h-screen bg-gray-50">
|
||||||
|
// {/* Error alert */}
|
||||||
|
// {error && (
|
||||||
|
// <div className="shrink-0 px-6 pt-4">
|
||||||
|
// <div className="p-4 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">
|
||||||
|
// <strong>{config.t.error}:</strong> {error}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// {/* Messages area */}
|
||||||
|
// <div ref={scrollRef} className="flex-1 overflow-y-auto">
|
||||||
|
// {messages.length === 0 && !isLoading ? (
|
||||||
|
// <EmptyState />
|
||||||
|
// ) : (
|
||||||
|
// <div className="mx-auto max-w-3xl space-y-6 px-4 py-6">
|
||||||
|
// {messages.map(message => (
|
||||||
|
// <Message key={message.id} message={message} isUser={message.role === 'user'} />
|
||||||
|
// ))}
|
||||||
|
// {isLoading && <LoadingIndicator tool={currentTool} />}
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Input area */}
|
||||||
|
// <div className="shrink-0 border-t border-gray-200 bg-white p-4 px-6">
|
||||||
|
// <div className="mx-auto max-w-3xl">
|
||||||
|
// {/* Product badge */}
|
||||||
|
// <div className="flex items-center gap-2 mb-3">
|
||||||
|
// <div className="flex items-center gap-1.5 h-9 px-3 rounded-md border border-zinc-300 bg-zinc-50 text-sm">
|
||||||
|
// <Plug size={14} className="text-zinc-600" />
|
||||||
|
// <span className="font-medium text-zinc-700">{config.fixedProduct.name}</span>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Input + Send button */}
|
||||||
|
// <div className="flex items-end gap-2">
|
||||||
|
// <div className="flex-1">
|
||||||
|
// <textarea
|
||||||
|
// ref={inputRef}
|
||||||
|
// value={input}
|
||||||
|
// onChange={(e) => setInput(e.target.value)}
|
||||||
|
// onKeyDown={handleKeyDown}
|
||||||
|
// placeholder={config.t.writeMessage}
|
||||||
|
// disabled={isLoading}
|
||||||
|
// rows={1}
|
||||||
|
// className="w-full resize-none rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-zinc-900 focus:border-transparent disabled:opacity-50"
|
||||||
|
// style={{ minHeight: '48px', maxHeight: '200px' }}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// <button
|
||||||
|
// onClick={() => sendMessage(input)}
|
||||||
|
// disabled={isLoading || !input.trim()}
|
||||||
|
// className="flex h-12 w-12 items-center justify-center rounded-lg bg-zinc-900 text-white transition-colors hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
// >
|
||||||
|
// {isLoading ? <Loader2 size={20} className="animate-spin" /> : <Send size={20} />}
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <p className="mt-2 text-center text-gray-400 text-xs">{config.t.enterToSend}</p>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default AskIaPage;
|
||||||
|
|
@ -75,6 +75,7 @@ import InvoiceCreatePage from '@/pages/sales/InvoiceCreatePage';
|
||||||
import SageBuilderPage from '@/pages/SageBuilderPage';
|
import SageBuilderPage from '@/pages/SageBuilderPage';
|
||||||
import PaymentsPage from '@/pages/sales/PaymentsPage';
|
import PaymentsPage from '@/pages/sales/PaymentsPage';
|
||||||
import PaymentDetailPage from '@/pages/sales/PaymentDetailPage';
|
import PaymentDetailPage from '@/pages/sales/PaymentDetailPage';
|
||||||
|
import AskIaPage from '@/pages/AskIaPage';
|
||||||
|
|
||||||
const DatavenRoute = () => {
|
const DatavenRoute = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -110,7 +111,10 @@ const DatavenRoute = () => {
|
||||||
|
|
||||||
{/* iframe */}
|
{/* iframe */}
|
||||||
<Route path="/sage-builder" element={<SageBuilderPage />} />
|
<Route path="/sage-builder" element={<SageBuilderPage />} />
|
||||||
|
|
||||||
|
{/* ask ia */}
|
||||||
|
<Route path="/ask-ia" element={<AskIaPage />} />
|
||||||
|
|
||||||
{/* Sales */}
|
{/* Sales */}
|
||||||
<Route path="/opportunites" element={<OpportunitiesPipelinePage />} />
|
<Route path="/opportunites" element={<OpportunitiesPipelinePage />} />
|
||||||
<Route path="/opportunites/:id" element={<OpportunityDetailPage />} />
|
<Route path="/opportunites/:id" element={<OpportunityDetailPage />} />
|
||||||
|
|
|
||||||
6
src/store/features/ask-ia/selectors.ts
Normal file
6
src/store/features/ask-ia/selectors.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
import type { RootState } from "@/store/store";
|
||||||
|
|
||||||
|
export const getAskIaHtmlContent = (state: RootState) => state.askIa.htmlContent;
|
||||||
|
export const askIaStatus = (state: RootState) => state.askIa.status;
|
||||||
|
export const askIaError = (state: RootState) => state.askIa.error;
|
||||||
45
src/store/features/ask-ia/slice.ts
Normal file
45
src/store/features/ask-ia/slice.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// store/ask-ia/slice.ts
|
||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
import { AskiaState } from "./type";
|
||||||
|
import { getAskIa } from "./thunk";
|
||||||
|
|
||||||
|
const initialState: AskiaState = {
|
||||||
|
status: "idle",
|
||||||
|
error: null,
|
||||||
|
htmlContent: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const askIaSlice = createSlice({
|
||||||
|
name: "askIa",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
resetAskIa: (state) => {
|
||||||
|
state.status = "idle";
|
||||||
|
state.error = null;
|
||||||
|
state.htmlContent = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
/**
|
||||||
|
* Get Sage Builder Dashboard
|
||||||
|
*/
|
||||||
|
builder.addCase(getAskIa.fulfilled, (state, action) => {
|
||||||
|
state.htmlContent = action.payload;
|
||||||
|
state.error = null;
|
||||||
|
state.status = "succeeded";
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(getAskIa.pending, (state) => {
|
||||||
|
state.status = "loading";
|
||||||
|
state.error = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addCase(getAskIa.rejected, (state, action) => {
|
||||||
|
state.status = "failed";
|
||||||
|
state.error = action.error.message || "Une erreur est survenue";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { resetAskIa } = askIaSlice.actions;
|
||||||
|
export default askIaSlice.reducer;
|
||||||
28
src/store/features/ask-ia/thunk.ts
Normal file
28
src/store/features/ask-ia/thunk.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Ask IA HTML
|
||||||
|
*/
|
||||||
|
export const getAskIa = createAsyncThunk(
|
||||||
|
'askIa/getAskIa',
|
||||||
|
async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.sage-ai-studio.webexpr.dev/api/embed/ask?mode=light&lang=fr&productSlug=sage100dataven&model=mistral-small',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer sk_live_bef70d4dd6bc671fd815d21104d5dce8'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Erreur de chargement du dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.text();
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
5
src/store/features/ask-ia/type.ts
Normal file
5
src/store/features/ask-ia/type.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface AskiaState {
|
||||||
|
status: 'idle' | 'loading' | 'succeeded' | 'failed';
|
||||||
|
error: string | null;
|
||||||
|
htmlContent: string | null;
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import userReducer from "./features/user/slice";
|
||||||
import entrepriseReducer from "./features/entreprise/slice"
|
import entrepriseReducer from "./features/entreprise/slice"
|
||||||
import sageBuilderReducer from "./features/sage-builder/slice";
|
import sageBuilderReducer from "./features/sage-builder/slice";
|
||||||
import reglementReducer from "./features/reglement/slice";
|
import reglementReducer from "./features/reglement/slice";
|
||||||
|
import askIaReducer from "./features/ask-ia/slice";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FLUSH,
|
FLUSH,
|
||||||
|
|
@ -48,7 +49,8 @@ const appReducer = combineReducers({
|
||||||
user: userReducer,
|
user: userReducer,
|
||||||
entreprise: entrepriseReducer,
|
entreprise: entrepriseReducer,
|
||||||
sageBuilder: sageBuilderReducer,
|
sageBuilder: sageBuilderReducer,
|
||||||
reglement: reglementReducer
|
reglement: reglementReducer,
|
||||||
|
askIa: askIaReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue