add ask-ia page

This commit is contained in:
mickael 2026-01-21 19:00:01 +03:00
parent 0b8c217a91
commit 1e2f833509
8 changed files with 495 additions and 3 deletions

View file

@ -27,7 +27,8 @@ import {
Calculator,
ListTodo,
ShieldCheck,
PanelLeft
PanelLeft,
MessageSquare,
} from 'lucide-react';
export const MODULE_GESTION = 'gestion';
@ -113,6 +114,12 @@ export const getMenuForModule = (currentModule) => {
{ 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",
// items: [

395
src/pages/AskIaPage.tsx Normal file
View 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;

View file

@ -75,6 +75,7 @@ import InvoiceCreatePage from '@/pages/sales/InvoiceCreatePage';
import SageBuilderPage from '@/pages/SageBuilderPage';
import PaymentsPage from '@/pages/sales/PaymentsPage';
import PaymentDetailPage from '@/pages/sales/PaymentDetailPage';
import AskIaPage from '@/pages/AskIaPage';
const DatavenRoute = () => {
return (
@ -110,7 +111,10 @@ const DatavenRoute = () => {
{/* iframe */}
<Route path="/sage-builder" element={<SageBuilderPage />} />
{/* ask ia */}
<Route path="/ask-ia" element={<AskIaPage />} />
{/* Sales */}
<Route path="/opportunites" element={<OpportunitiesPipelinePage />} />
<Route path="/opportunites/:id" element={<OpportunityDetailPage />} />

View 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;

View 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;

View 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;
}
}
);

View file

@ -0,0 +1,5 @@
export interface AskiaState {
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
htmlContent: string | null;
}

View file

@ -20,6 +20,7 @@ import userReducer from "./features/user/slice";
import entrepriseReducer from "./features/entreprise/slice"
import sageBuilderReducer from "./features/sage-builder/slice";
import reglementReducer from "./features/reglement/slice";
import askIaReducer from "./features/ask-ia/slice";
import {
FLUSH,
@ -48,7 +49,8 @@ const appReducer = combineReducers({
user: userReducer,
entreprise: entrepriseReducer,
sageBuilder: sageBuilderReducer,
reglement: reglementReducer
reglement: reglementReducer,
askIa: askIaReducer,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any