diff --git a/chrome-extension/src/background/handlers/message-handler.ts b/chrome-extension/src/background/handlers/message-handler.ts index 6fe83d2..5795df6 100644 --- a/chrome-extension/src/background/handlers/message-handler.ts +++ b/chrome-extension/src/background/handlers/message-handler.ts @@ -56,8 +56,8 @@ export class MessageHandler { string, (message: BaseRuntimeMessage, sender: chrome.runtime.MessageSender) => Promise > = { - [MessageType.AI_CHAT_REQUEST]: (message: BaseRuntimeMessage) => - this.handleAiChatRequest(message as AIChatRequestMessage), + [MessageType.AI_CHAT_REQUEST]: (message: BaseRuntimeMessage, sender: chrome.runtime.MessageSender) => + this.handleAiChatRequest(message as AIChatRequestMessage, sender), [MessageType.CHAT_WITH_TOOLS]: (message: BaseRuntimeMessage) => this.handleChatWithTools(message as ChatWithToolsMessage), [MessageType.CHECK_AGENT_STATUS]: this.handleCheckAgentStatus.bind(this), @@ -72,8 +72,8 @@ export class MessageHandler { this.handleListKeyPoints(message as ListKeyPointsMessage), [MessageType.CUSTOM_PAGE_PROMPT_REQUEST]: (message: BaseRuntimeMessage) => this.handleCustomPagePrompt(message as CustomPagePromptMessage), - [MessageType.RAG_CHAT_REQUEST]: (message: BaseRuntimeMessage) => - this.handleRagChatRequest(message as RagChatRequestMessage), + [MessageType.RAG_CHAT_REQUEST]: (message: BaseRuntimeMessage, sender: chrome.runtime.MessageSender) => + this.handleRagChatRequest(message as RagChatRequestMessage, sender), [MessageType.SAVE_MESSAGE_AS_NOTE]: (message: BaseRuntimeMessage) => this.handleSaveMessageAsNote(message as SaveMessageAsNoteMessage), [MessageType.GET_INLINE_COMPLETION_REQUEST]: (message: BaseRuntimeMessage) => @@ -422,8 +422,12 @@ export class MessageHandler { /** * Gestionnaire pour les requêtes de chat avec l'agent AI */ - private async handleAiChatRequest(message: AIChatRequestMessage): Promise { + private async handleAiChatRequest( + message: AIChatRequestMessage, + sender?: chrome.runtime.MessageSender, + ): Promise { logger.debug('Reçu AI_CHAT_REQUEST', message.payload); + logger.debug('Request sender:', sender); // Vérifier si on veut du streaming const { @@ -441,9 +445,11 @@ export class MessageHandler { // Convertir l'historique du chat const history = convertChatHistory(chatHistory); - // Récupérer le contenu de la page active si non fourni + // Récupérer le contenu de la page active si non fourni et si l'expéditeur n'est pas interne let pageContent = providedPageContent; - if (!pageContent) { + const senderIsInternal = sender?.url?.startsWith(`chrome-extension://${chrome.runtime.id}/`); + + if (!pageContent && !senderIsInternal) { try { pageContent = await this.fetchCurrentPageContent(); logger.debug( @@ -452,7 +458,12 @@ export class MessageHandler { ); } catch (error) { logger.warn('Erreur lors de la récupération du contenu de la page pour le streaming:', error); + // pageContent reste undefined, ce qui est géré par l'agent } + } else if (senderIsInternal && !pageContent) { + logger.debug( + 'Requête interne (streaming) et pas de providedPageContent. pageContent non récupéré via fetchCurrentPageContent.', + ); } // Lancer le streaming en asynchrone @@ -479,15 +490,22 @@ export class MessageHandler { try { const history = convertChatHistory(chatHistory); - // Récupérer le contenu de la page active si non fourni + // Récupérer le contenu de la page active si non fourni et si l'expéditeur n'est pas interne let pageContent = providedPageContent; - if (!pageContent) { + const senderIsInternal = sender?.url?.startsWith(`chrome-extension://${chrome.runtime.id}/`); + + if (!pageContent && !senderIsInternal) { try { pageContent = await this.fetchCurrentPageContent(); logger.debug('Contenu de la page récupéré:', pageContent ? `${pageContent.substring(0, 100)}...` : 'Aucun'); } catch (error) { logger.warn('Erreur lors de la récupération du contenu de la page:', error); + // pageContent reste undefined, ce qui est géré par l'agent } + } else if (senderIsInternal && !pageContent) { + logger.debug( + 'Requête interne (non-streaming) et pas de providedPageContent. pageContent non récupéré via fetchCurrentPageContent.', + ); } // Utiliser l'agent ou fallback au LLM direct selon l'état @@ -752,7 +770,10 @@ export class MessageHandler { /** * Gestionnaire pour les requêtes de chat RAG avec les notes de l'utilisateur */ - private async handleRagChatRequest(message: RagChatRequestMessage): Promise { + private async handleRagChatRequest( + message: RagChatRequestMessage, + sender?: chrome.runtime.MessageSender, + ): Promise { logger.debug('Reçu RAG_CHAT_REQUEST', message.payload); const { message: userInput, chatHistory = [], streamHandler = false, portId, selectedModel } = message.payload; @@ -1075,6 +1096,7 @@ Instruction de modification: ${prompt}`; }; } const { port } = streamingPortInfo; + let effectiveModelName: string | undefined; try { const isReady = await agentService.isAgentReady(); @@ -1089,12 +1111,12 @@ Instruction de modification: ${prompt}`; const history = chatHistoryPayload ? convertChatHistory(chatHistoryPayload) : []; const settings = await aiAgentStorage.get(); - const modelToUse = modelNameFromPayload || settings.selectedModel; + effectiveModelName = modelNameFromPayload || settings.selectedModel; - port.postMessage({ type: StreamEventType.STREAM_START, model: modelToUse }); + port.postMessage({ type: StreamEventType.STREAM_START, model: effectiveModelName }); agentService - .streamArtifactGeneration(prompt, history, port, modelToUse) + .streamArtifactGeneration(prompt, history, port, effectiveModelName) .then(() => { logger.debug('[DodaiCanvasStream] Streaming terminé avec succès par agentService pour le port', portId); }) @@ -1105,7 +1127,7 @@ Instruction de modification: ${prompt}`; type: StreamEventType.STREAM_ERROR, error: error instanceof Error ? error.message : "Erreur inconnue durant le streaming d'artefact", }); - port.postMessage({ type: StreamEventType.STREAM_END, success: false, model: modelToUse }); + port.postMessage({ type: StreamEventType.STREAM_END, success: false, model: effectiveModelName }); } catch (portError) { logger.warn( "[DodaiCanvasStream] Impossible d'envoyer l'erreur sur le port après échec agentService:", @@ -1122,7 +1144,7 @@ Instruction de modification: ${prompt}`; type: StreamEventType.STREAM_ERROR, error: error instanceof Error ? error.message : "Erreur inconnue avant le lancement du streaming d'artefact", }); - port.postMessage({ type: StreamEventType.STREAM_END, success: false, model: modelToUse }); + port.postMessage({ type: StreamEventType.STREAM_END, success: false, model: effectiveModelName }); } catch (portError) { logger.warn("[DodaiCanvasStream] Impossible d'envoyer l'erreur sur le port après erreur majeure:", portError); } diff --git a/chrome-extension/src/background/services/agent-service.ts b/chrome-extension/src/background/services/agent-service.ts index e8ab2d1..92f2140 100644 --- a/chrome-extension/src/background/services/agent-service.ts +++ b/chrome-extension/src/background/services/agent-service.ts @@ -483,6 +483,12 @@ Complète l'entrée de l'utilisateur avec une suggestion pertinente et concise. try { const llm = await this.createLLMInstance(modelName); + // Envoyer STREAM_START pour notifier l'UI que le streaming commence + port.postMessage({ + type: StreamEventType.STREAM_START, + model: modelName, + }); + const systemPromptArtifact = `Tu es un assistant expert en rédaction. En te basant sur la demande suivante, génère un document Markdown complet et bien structuré.\\nTa réponse DOIT être uniquement le contenu Markdown brut et directement utilisable.\\nN'inclus AUCUNE introduction, phrase de politesse, conclusion, explication, commentaire, ni aucun type d'encapsulation de code (comme \\\`\\\`\\\`markdown ... \\\`\\\`\\\` ou des backticks simples autour de la réponse entière).\\nLa sortie doit commencer directement par le contenu Markdown (par exemple, un titre comme '# Mon Titre', une liste, ou du texte simple).\\n\\nSi la demande est explicitement de générer du CODE SOURCE (par exemple Python, JavaScript, etc.), alors seulement tu généreras uniquement le code demandé. Dans ce cas de figure, tu peux utiliser des backticks pour délimiter des blocs de code si cela fait partie de la syntaxe standard du langage demandé ou si c'est pour imbriquer un bloc de code dans un autre format. Mais pour une demande de document MARKDOWN, la sortie doit être le Markdown pur.\\n\\nDemande utilisateur: ${prompt}`; const streamIterator = await llm.stream([ @@ -509,32 +515,77 @@ Complète l'entrée de l'utilisateur avec une suggestion pertinente et concise. ); // Génération de la réponse conversationnelle après la génération de l'artefact - if (fullArtifactForLog.trim()) { - const systemPromptChat = `L'utilisateur a fait la demande suivante : "${prompt}".\nEn réponse, tu as généré l'artefact suivant :\n---\n${fullArtifactForLog.substring(0, 2000)}${fullArtifactForLog.length > 2000 ? '...' : ''}\n---\nFournis une réponse conversationnelle courte et pertinente à l'utilisateur, en accusant réception de sa demande et en mentionnant brièvement l'artefact généré. Ne répète pas le contenu de l'artefact. Sois concis.`; + try { + let systemPromptChat: string; + if (fullArtifactForLog.trim()) { + systemPromptChat = `L'utilisateur a fait la demande suivante : "${prompt}". + +En réponse, tu as généré un artefact de ${fullArtifactForLog.length} caractères. + +Fournis une réponse conversationnelle courte et engageante à l'utilisateur, en: +- Confirmant que tu as créé l'artefact +- Proposant des modifications ou améliorations possibles +- Invitant l'utilisateur à continuer la conversation + +Sois concis, utile et encourage l'interaction.`; + } else { + systemPromptChat = `L'utilisateur a fait la demande suivante : "${prompt}". + +Cependant, la génération de l'artefact semble avoir échoué ou être vide. + +Fournis une réponse conversationnelle courte pour: +- Expliquer qu'il y a eu un problème avec la génération +- Proposer de reformuler ou préciser la demande +- Rester helpful et positif + +Sois concis et encourage l'utilisateur à réessayer.`; + } + + const chatResponseContent = await llm.invoke([ + ...history, // Inclure l'historique pour un meilleur contexte + { type: 'system', content: systemPromptChat }, + { type: 'human', content: prompt }, + ]); + + if (typeof chatResponseContent.content === 'string' && chatResponseContent.content.trim()) { + port.postMessage({ + type: StreamEventType.ARTIFACT_CHAT_RESPONSE, + chatResponse: chatResponseContent.content.trim(), + model: modelName, + }); + logger.debug('[AgentService/DodaiCanvasStream] Réponse conversationnelle générée et envoyée.'); + } else { + // Fallback si la génération de réponse échoue complètement + const fallbackMessage = fullArtifactForLog.trim() + ? "J'ai créé l'artefact demandé. Souhaitez-vous y apporter des modifications ou avez-vous d'autres questions ?" + : 'Il semble y avoir eu un problème avec la génération. Pouvez-vous reformuler votre demande ?'; + + port.postMessage({ + type: StreamEventType.ARTIFACT_CHAT_RESPONSE, + chatResponse: fallbackMessage, + model: modelName, + }); + logger.warn('[AgentService/DodaiCanvasStream] Utilisation du message de fallback.'); + } + } catch (chatError) { + logger.error( + '[AgentService/DodaiCanvasStream] Erreur lors de la génération de la réponse conversationnelle:', + chatError, + ); + // Fallback message en cas d'erreur complète + const fallbackMessage = fullArtifactForLog.trim() + ? 'Artefact généré avec succès ! Souhaitez-vous y apporter des modifications ?' + : 'Il y a eu un problème avec la génération. Pouvez-vous réessayer ?'; try { - const chatResponseContent = await llm.invoke([ - // On pourrait inclure l'historique ici aussi si pertinent pour la réponse chat - { type: 'system', content: systemPromptChat }, - { type: 'human', content: `J'ai bien reçu l'artefact. Que devrais-je dire à l'utilisateur ?` }, // Prompt simple pour déclencher la réponse - ]); - - if (typeof chatResponseContent.content === 'string' && chatResponseContent.content.trim()) { - port.postMessage({ - type: StreamEventType.ARTIFACT_CHAT_RESPONSE, - chatResponse: chatResponseContent.content.trim(), - model: modelName, // On peut aussi inclure le modèle ici - }); - logger.debug('[AgentService/DodaiCanvasStream] Réponse conversationnelle générée et envoyée.'); - } else { - logger.warn('[AgentService/DodaiCanvasStream] La réponse conversationnelle générée est vide.'); - } - } catch (chatError) { - logger.error( - '[AgentService/DodaiCanvasStream] Erreur lors de la génération de la réponse conversationnelle:', - chatError, - ); - // Ne pas bloquer la fin du stream principal pour ça, mais logger l'erreur. + port.postMessage({ + type: StreamEventType.ARTIFACT_CHAT_RESPONSE, + chatResponse: fallbackMessage, + model: modelName, + }); + logger.debug('[AgentService/DodaiCanvasStream] Message de fallback envoyé après erreur.'); + } catch (portError) { + logger.error("[AgentService/DodaiCanvasStream] Impossible d'envoyer le message de fallback:", portError); } } diff --git a/chrome-extension/src/background/services/streaming-service.ts b/chrome-extension/src/background/services/streaming-service.ts index 0ea16e8..c615152 100644 --- a/chrome-extension/src/background/services/streaming-service.ts +++ b/chrome-extension/src/background/services/streaming-service.ts @@ -40,7 +40,9 @@ export class StreamingService { if ( port.name.startsWith('ai_streaming_') || port.name.startsWith('rag_streaming_') || - port.name.startsWith('dodai_canvas_artifact_stream_') + port.name.startsWith('dodai_canvas_artifact_stream_') || + port.name.startsWith('simple_text_chat_stream_') || + port.name.startsWith('simple_chat_stream_') ) { const portId = port.name; logger.debug(`Nouvelle connexion de streaming établie: ${portId}`); diff --git a/packages/storage/lib/impl/chat-history-storage.ts b/packages/storage/lib/impl/chat-history-storage.ts index d5b8562..3a2a621 100644 --- a/packages/storage/lib/impl/chat-history-storage.ts +++ b/packages/storage/lib/impl/chat-history-storage.ts @@ -11,6 +11,19 @@ export interface ChatMessage { model?: string; } +// Définition de la structure pour l'artefact +export interface ChatArtifact { + type: string; + title: string; + fullMarkdown: string; +} + +// Définition de la structure d'un artefact complet avec historique +export interface ChatArtifactWithHistory { + currentIndex: number; + contents: ChatArtifact[]; +} + // Définition de la structure d'une conversation export interface ChatConversation { id: string; @@ -19,6 +32,7 @@ export interface ChatConversation { createdAt: number; updatedAt: number; model?: string; + artifact?: ChatArtifactWithHistory | null; } // État initial pour l'historique des conversations diff --git a/packages/ui/lib/components/DodaiSidebar.tsx b/packages/ui/lib/components/DodaiSidebar.tsx index c73fedc..f040402 100644 --- a/packages/ui/lib/components/DodaiSidebar.tsx +++ b/packages/ui/lib/components/DodaiSidebar.tsx @@ -51,7 +51,9 @@ const DodaiSidebar: React.FC = ({ const getNavItemClasses = (expanded: boolean, isActive: boolean): string => { const dynamic = expanded ? 'w-full justify-start' : 'justify-center'; - const activeClasses = isActive ? 'bg-blue-600/25 text-blue-200 hover:bg-blue-600/30' : 'hover:bg-background-quaternary/60'; // Enhanced active state + const activeClasses = isActive + ? 'bg-blue-600/25 text-blue-200 hover:bg-blue-600/30' + : 'hover:bg-background-quaternary/60'; // Enhanced active state return `${navItemBaseClasses} ${dynamic} ${activeClasses}`; }; diff --git a/pages/main/src/components/layout/MainLayout.tsx b/pages/main/src/components/layout/MainLayout.tsx index 42caf00..54b2cfb 100644 --- a/pages/main/src/components/layout/MainLayout.tsx +++ b/pages/main/src/components/layout/MainLayout.tsx @@ -6,25 +6,50 @@ import { BookText, LayoutDashboard, PlusCircle, FilePlus2 } from 'lucide-react'; import { useDodai } from '@src/features/canvas/contexts/DodaiContext'; import { useNotes } from '@src/features/notes/hooks/useNotes'; import { useNoteSelection } from '@src/features/notes/hooks/useNoteSelection'; +import { useDodaiCanvasHistory } from '@src/features/canvas/hooks/useDodaiCanvasHistory'; const MainLayout: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); - const { resetChatAndArtifact } = useDodai(); + const { + resetChatAndArtifact, + messages: dodaiContextMessages, + selectedDodaiModel: dodaiContextSelectedModel, + currentArtifact: dodaiContextCurrentArtifact, + } = useDodai(); const { notes, addNote, getNote } = useNotes(); const { handleCreateNewNote } = useNoteSelection(notes, getNote, addNote); + const { resetActiveSession, saveCurrentChatSessionAsNew } = useDodaiCanvasHistory(); // Define individual button configurations const newCanvasButton: NavItemProps = { id: 'new-canvas', label: 'Nouveau Canvas', icon: , - onClick: () => { - resetChatAndArtifact(); - navigate('/canvas'); // Navigate to ensure canvas view is shown + onClick: async () => { + console.log('[MainLayout] New Canvas button clicked.'); + await resetChatAndArtifact(async () => { + if (dodaiContextMessages.length > 0) { + console.log('[MainLayout] Saving current session before reset...'); + await saveCurrentChatSessionAsNew( + dodaiContextMessages, + dodaiContextCurrentArtifact, + dodaiContextSelectedModel || undefined, + ); + console.log('[MainLayout] Current session saved.'); + resetActiveSession(); + } else { + console.log('[MainLayout] No active session to save before reset.'); + resetActiveSession(); + } + }); + + console.log('[MainLayout] New Canvas session started - ready for user input.'); + + navigate('/canvas'); }, - isActive: false, // Action buttons are not typically "active" + isActive: false, title: 'Commencer un nouveau Canvas', }; @@ -43,7 +68,7 @@ const MainLayout: React.FC = () => { icon: , onClick: async () => { await handleCreateNewNote(null); - navigate('/notes'); // Ensure notes view and list are active/updated + navigate('/notes'); }, isActive: false, title: 'Créer une nouvelle note', @@ -62,16 +87,12 @@ const MainLayout: React.FC = () => { const isCanvasPath = location.pathname.startsWith('/canvas'); const isNotesPath = location.pathname.startsWith('/notes'); - // Determine which "New" button to show based on the active path - // If neither, or on a different path, default to newCanvas or make a specific choice. - let primaryNewButton = newCanvasButton; // Default + let primaryNewButton = newCanvasButton; if (isCanvasPath) { primaryNewButton = newCanvasButton; } else if (isNotesPath) { primaryNewButton = newNoteButton; } else { - // Fallback if on a path other than /canvas or /notes - // Decide what the default "+" button should be. For instance, new canvas. primaryNewButton = newCanvasButton; } diff --git a/pages/main/src/features/canvas/CanvasView.tsx b/pages/main/src/features/canvas/CanvasView.tsx index d753c66..955a097 100644 --- a/pages/main/src/features/canvas/CanvasView.tsx +++ b/pages/main/src/features/canvas/CanvasView.tsx @@ -2,19 +2,42 @@ import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@extension import { useDodai } from './contexts/DodaiContext'; import ChatPanel from './components/ChatPanel'; import ArtifactPanel from './components/ArtifactPanel'; -import { useState } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import TagGraphView from '../notes/components/tag/TagGraphView'; import { useNotes } from '../notes/hooks/useNotes'; import { useTagGraph } from '../notes/hooks/useTagGraph'; +import DodaiCanvasHistoryPanel from './components/DodaiCanvasHistoryPanel'; +import { useDodaiCanvasHistory } from './hooks/useDodaiCanvasHistory'; +import { v4 as uuidv4 } from 'uuid'; const CanvasViewContent = () => { - const { currentArtifact } = useDodai(); + const { + currentArtifact, + setMessages, + setCurrentArtifact, + setSelectedDodaiModel, + resetChatAndArtifact, + setOnChatTurnEnd, + isArtifactModeActive, + isStreamingArtifact, + } = useDodai(); + const { + chatHistory, + activeConversationId: activeHistoryConvId, + loadConversation, + deleteConversation, + saveCurrentChatSession, + renameConversationInHistory, + resetActiveSession, + } = useDodaiCanvasHistory(); + const { notes } = useNotes(); const tagData = useTagGraph(notes); const [activeTag, setActiveTag] = useState(null); + const [showHistoryPanel, setShowHistoryPanel] = useState(false); - const showArtifactPanel = !!currentArtifact; - const panelGroupKey = showArtifactPanel ? 'artifactMode' : 'hubMode'; + // Determine what to show in the right panel + const shouldShowArtifactPanel = isArtifactModeActive && (currentArtifact !== null || isStreamingArtifact); const handleTagSelect = (tag: string) => { setActiveTag(tag); @@ -26,21 +49,111 @@ const CanvasViewContent = () => { console.log('Filter cleared in CanvasView Hub'); }; + const toggleHistoryPanel = () => { + setShowHistoryPanel(prev => !prev); + }; + + const handleLoadConversation = useCallback( + async (id: string) => { + console.log('[CanvasView] Attempting to load conversation:', id); + const result = await loadConversation(id); + console.log('[CanvasView] loadConversation result:', result); + if (result.success && result.messages) { + setMessages(result.messages); + setCurrentArtifact(result.artifact || null); + if (result.model) { + setSelectedDodaiModel(result.model); + } + setShowHistoryPanel(false); + console.log('[CanvasView] Conversation loaded successfully, new message count:', result.messages.length); + } else { + console.error('[CanvasView] Failed to load conversation:', result.error); + // Afficher une notification à l'utilisateur ici si nécessaire + } + }, + [loadConversation, setMessages, setCurrentArtifact, setSelectedDodaiModel, setShowHistoryPanel], + ); + + const handleDeleteConversation = useCallback( + async (id: string) => { + console.log('[CanvasView] Attempting to delete conversation:', id); + const result = await deleteConversation(id); + if (result.success) { + console.log('[CanvasView] Conversation deleted. Was active:', result.wasActive); + if (result.wasActive) { + resetChatAndArtifact(); + setMessages([ + { + id: uuidv4(), + role: 'assistant', + content: 'Conversation supprimée. Veuillez en sélectionner une autre ou en créer une nouvelle.', + timestamp: Date.now(), + }, + ]); + } + } else { + console.error('[CanvasView] Failed to delete conversation:', result.error); + // Afficher une notification à l'utilisateur ici si nécessaire + } + }, + [deleteConversation, resetChatAndArtifact, setMessages], + ); + + const handleRenameConversation = useCallback( + async (id: string, newName: string) => { + console.log(`[CanvasView] Attempting to rename conversation ${id} to "${newName}"`); + const result = await renameConversationInHistory(id, newName); + if (result) { + console.log(`[CanvasView] Conversation ${id} renamed successfully to "${newName}"`); + // Optional: Show success notification + } else { + console.error(`[CanvasView] Failed to rename conversation ${id}`); + // Optional: Show error notification + } + // The history panel will close the input itself. + // The chatHistory list from useDodaiCanvasHistory will update automatically due to useStorage. + }, + [renameConversationInHistory], + ); + + const handleNewConversation = useCallback(() => { + console.log('[CanvasView] Creating new conversation...'); + resetActiveSession(); + resetChatAndArtifact(); + setShowHistoryPanel(false); + console.log('[CanvasView] New conversation ready.'); + }, [resetActiveSession, resetChatAndArtifact, setShowHistoryPanel]); + + // Effect to register the onChatTurnEnd handler + useEffect(() => { + console.log('[CanvasView] Registering onChatTurnEnd handler.'); + setOnChatTurnEnd((finalMessages, modelUsed) => { + console.log('[CanvasView] onChatTurnEnd triggered. Saving session with current artifact.'); + // Get the current artifact at the time of the callback + const artifactToSave = currentArtifact; + console.log('[CanvasView] Current artifact at save time:', artifactToSave ? 'present' : 'null'); + saveCurrentChatSession(finalMessages, artifactToSave, modelUsed || undefined); + }); + + // Cleanup: Unregister handler when component unmounts or dependencies change + return () => { + console.log('[CanvasView] Unregistering onChatTurnEnd handler.'); + setOnChatTurnEnd(null); + }; + }, [setOnChatTurnEnd, saveCurrentChatSession, currentArtifact]); // Added currentArtifact as a dependency + return ( -
- - +
+ +
- +
- +
- {showArtifactPanel ? ( + {shouldShowArtifactPanel ? ( ) : ( {
+ + {showHistoryPanel && ( + setShowHistoryPanel(false)} + onNewConversation={handleNewConversation} + /> + )}
); }; diff --git a/pages/main/src/features/canvas/components/ChatPanel.tsx b/pages/main/src/features/canvas/components/ChatPanel.tsx index a330f45..0cd8d1e 100644 --- a/pages/main/src/features/canvas/components/ChatPanel.tsx +++ b/pages/main/src/features/canvas/components/ChatPanel.tsx @@ -2,7 +2,7 @@ import { useRef, useEffect, useState } from 'react'; import { ChatInput } from './ChatInput'; import { ChatMessage } from './ChatMessage'; import { useDodai } from '../contexts/DodaiContext'; -import { History, Send, PlusCircle } from 'lucide-react'; +import { History, Send, PlusCircle, Sparkles, MessageSquare } from 'lucide-react'; import { DodaiModelSelector } from './DodaiModelSelector'; // Re-defined suggestion prompts (simplified for this example) @@ -27,14 +27,13 @@ const suggestionPrompts = [ }, ]; -const ChatPanel = () => { - const { - messages, - chatInput, - setChatInput, - isLoading, - sendPromptAndGenerateArtifact, - } = useDodai(); +type ChatPanelProps = { + onToggleHistory?: () => void; +}; + +const ChatPanel: React.FC = ({ onToggleHistory }) => { + const { messages, chatInput, setChatInput, isLoading, sendMessage, isArtifactModeActive, setIsArtifactModeActive } = + useDodai(); const messagesEndRef = useRef(null); const [initialHubPrompt, setInitialHubPrompt] = useState(''); @@ -48,7 +47,7 @@ const ChatPanel = () => { e.preventDefault(); const currentInput = (promptToSend || chatInput || initialHubPrompt).trim(); if (!currentInput) return; - await sendPromptAndGenerateArtifact(currentInput); + await sendMessage(currentInput); setInitialHubPrompt(''); }; @@ -99,7 +98,9 @@ const ChatPanel = () => { key={index} className="bg-background-tertiary p-4 rounded-xl border border-border-primary hover:border-border-accent cursor-pointer transition-all duration-200 hover:shadow-lg transform hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-border-accent focus:ring-opacity-75 text-left group" onClick={() => handleSuggestionClick(suggestion.fullPrompt)}> -

{suggestion.title}

+

+ {suggestion.title} +

{suggestion.description}

))} @@ -113,13 +114,38 @@ const ChatPanel = () => {
- + +
+ + +
+ + {onToggleHistory && ( + + )} + {!onToggleHistory &&
}
{messages.length === 0 && !isLoading ? ( diff --git a/pages/main/src/features/canvas/components/DodaiCanvasHistoryPanel.tsx b/pages/main/src/features/canvas/components/DodaiCanvasHistoryPanel.tsx new file mode 100644 index 0000000..84c01ed --- /dev/null +++ b/pages/main/src/features/canvas/components/DodaiCanvasHistoryPanel.tsx @@ -0,0 +1,160 @@ +import type React from 'react'; +import { useState } from 'react'; +import type { ChatConversation } from '@extension/storage'; +import { XIcon, Trash2Icon, Edit3Icon, CheckIcon, XCircleIcon, PlusIcon } from 'lucide-react'; + +interface DodaiCanvasHistoryPanelProps { + chatHistory: ChatConversation[]; + activeConversationId: string | null; + onLoadConversation: (id: string) => void; + onDeleteConversation: (id: string) => void; + onRenameConversation: (id: string, newName: string) => Promise; + onNewConversation?: () => void; + onClose: () => void; +} + +/** + * Panneau pour afficher l'historique des conversations dans Dodai Canvas. + */ +export const DodaiCanvasHistoryPanel: React.FC = ({ + chatHistory, + activeConversationId, + onLoadConversation, + onDeleteConversation, + onRenameConversation, + onNewConversation, + onClose, +}) => { + const [editingConversationId, setEditingConversationId] = useState(null); + const [editingName, setEditingName] = useState(''); + const [originalName, setOriginalName] = useState(''); + + const handleStartEdit = (conversation: ChatConversation) => { + setEditingConversationId(conversation.id); + setEditingName(conversation.name); + setOriginalName(conversation.name); + }; + + const handleCancelEdit = () => { + setEditingConversationId(null); + setEditingName(''); + setOriginalName(''); + }; + + const handleRenameSubmit = async () => { + if (editingConversationId && editingName.trim() && editingName.trim() !== originalName) { + await onRenameConversation(editingConversationId, editingName.trim()); + } + handleCancelEdit(); + }; + + return ( +
+ {/* En-tête du panneau */} +
+

Historique des Chats

+
+ {onNewConversation && ( + + )} + +
+
+ + {/* Liste des conversations */} +
+ {chatHistory && chatHistory.length > 0 ? ( + [...chatHistory] + .sort((a, b) => b.updatedAt - a.updatedAt) + .map(conversation => ( +
+ {editingConversationId === conversation.id ? ( +
+ setEditingName(e.target.value)} + onBlur={handleRenameSubmit} + onKeyDown={e => { + if (e.key === 'Enter') { + e.preventDefault(); + handleRenameSubmit(); + } + if (e.key === 'Escape') { + e.preventDefault(); + handleCancelEdit(); + } + }} + className="flex-1 px-2 py-1 text-sm bg-background-primary border border-border-accent rounded-md focus:ring-1 focus:ring-blue-500 outline-none" + maxLength={100} + /> + + +
+ ) : ( + + )} + + {editingConversationId !== conversation.id && ( +
+ + +
+ )} +
+ )) + ) : ( +
Aucune conversation dans l'historique.
+ )} +
+
+ ); +}; + +export default DodaiCanvasHistoryPanel; diff --git a/pages/main/src/features/canvas/components/DodaiModelSelector.tsx b/pages/main/src/features/canvas/components/DodaiModelSelector.tsx index 6c3136f..0057074 100644 --- a/pages/main/src/features/canvas/components/DodaiModelSelector.tsx +++ b/pages/main/src/features/canvas/components/DodaiModelSelector.tsx @@ -85,7 +85,7 @@ export const DodaiModelSelector: React.FC = () => { {showDropdown && ( -
+

Modèles Disponibles

-
+
(
-

+

{folder.title || 'Dossier sans nom'}

diff --git a/pages/main/src/features/notes/components/list/NoteCard.tsx b/pages/main/src/features/notes/components/list/NoteCard.tsx index 046712e..3fe85e9 100644 --- a/pages/main/src/features/notes/components/list/NoteCard.tsx +++ b/pages/main/src/features/notes/components/list/NoteCard.tsx @@ -64,9 +64,10 @@ const NoteCard = forwardRef(({ note, isSelected, {...listeners} {...attributes} className={`relative p-3 rounded-lg cursor-pointer transition-all duration-150 ease-in-out group - ${isSelected - ? 'bg-background-tertiary shadow-lg shadow-slate-900/40 ring-2 ring-border-accent' - : 'bg-background-tertiary/60 hover:bg-background-tertiary/90 hover:shadow-md hover:shadow-slate-900/20' + ${ + isSelected + ? 'bg-background-tertiary shadow-lg shadow-slate-900/40 ring-2 ring-border-accent' + : 'bg-background-tertiary/60 hover:bg-background-tertiary/90 hover:shadow-md hover:shadow-slate-900/20' } ${isDragging ? 'opacity-60 border-dashed border-2 border-blue-400 bg-background-tertiary/80 shadow-xl scale-[1.02] z-10' : ''} ${isOver ? 'ring-2 ring-green-500/70 bg-background-tertiary/90 shadow-lg' : ''}`} @@ -81,7 +82,8 @@ const NoteCard = forwardRef(({ note, isSelected, {isSelected &&
}
-
+
(({ note, isSelected,
-

+

{note.title || 'Sans titre'}

diff --git a/pages/main/src/features/notes/components/tag/TagsPanel.tsx b/pages/main/src/features/notes/components/tag/TagsPanel.tsx index bc2218b..b8aa922 100644 --- a/pages/main/src/features/notes/components/tag/TagsPanel.tsx +++ b/pages/main/src/features/notes/components/tag/TagsPanel.tsx @@ -25,19 +25,21 @@ const TagsPanel: FC = ({ notes, allTags, activeTag, onTagSelect,
diff --git a/pages/main/tailwind.config.ts b/pages/main/tailwind.config.ts index 410e9ea..683c390 100644 --- a/pages/main/tailwind.config.ts +++ b/pages/main/tailwind.config.ts @@ -38,7 +38,7 @@ export default withUI({ 850: 'var(--color-bg-secondary)', }, }, - typography: (theme) => ({ + typography: theme => ({ DEFAULT: { css: { color: 'var(--color-text-secondary)',