From a4d69b41839cc65fff5cbaf303a8edd0b4203260 Mon Sep 17 00:00:00 2001 From: ASSEF Andrew Date: Tue, 20 May 2025 18:43:53 +0200 Subject: [PATCH 1/9] feat: enhance CanvasView and ChatPanel for improved user interaction - Introduced a toggle between 'canvas' and 'chat' view modes in CanvasView, allowing users to switch seamlessly. - Updated ChatPanel to accept view mode as a prop, enabling dynamic rendering based on the selected mode. - Enhanced message handling in message-handler.ts to utilize effective model names consistently across streaming events. - Added optional properties to the Message interface for better flexibility in handling chat messages. --- .../background/handlers/message-handler.ts | 11 +- packages/ui/lib/components/DodaiSidebar.tsx | 4 +- pages/main/src/features/canvas/CanvasView.tsx | 57 +++-- .../features/canvas/components/ChatPanel.tsx | 59 +++-- pages/main/src/features/canvas/types/index.ts | 2 + .../notes/components/list/FolderCard.tsx | 6 +- .../notes/components/list/NoteCard.tsx | 13 +- .../notes/components/tag/TagsPanel.tsx | 10 +- .../text-chat/components/TextChatView.tsx | 51 ++++ .../text-chat/hooks/useSimpleTextChat.ts | 229 ++++++++++++++++++ pages/main/tailwind.config.ts | 2 +- 11 files changed, 385 insertions(+), 59 deletions(-) create mode 100644 pages/main/src/features/text-chat/components/TextChatView.tsx create mode 100644 pages/main/src/features/text-chat/hooks/useSimpleTextChat.ts diff --git a/chrome-extension/src/background/handlers/message-handler.ts b/chrome-extension/src/background/handlers/message-handler.ts index 6fe83d2..d190680 100644 --- a/chrome-extension/src/background/handlers/message-handler.ts +++ b/chrome-extension/src/background/handlers/message-handler.ts @@ -1075,6 +1075,7 @@ Instruction de modification: ${prompt}`; }; } const { port } = streamingPortInfo; + let effectiveModelName: string | undefined; try { const isReady = await agentService.isAgentReady(); @@ -1089,12 +1090,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 +1106,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 +1123,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/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/features/canvas/CanvasView.tsx b/pages/main/src/features/canvas/CanvasView.tsx index d753c66..baaaf0d 100644 --- a/pages/main/src/features/canvas/CanvasView.tsx +++ b/pages/main/src/features/canvas/CanvasView.tsx @@ -6,15 +6,16 @@ import { useState } from 'react'; import TagGraphView from '../notes/components/tag/TagGraphView'; import { useNotes } from '../notes/hooks/useNotes'; import { useTagGraph } from '../notes/hooks/useTagGraph'; +import TextChatView from '../text-chat/components/TextChatView'; const CanvasViewContent = () => { const { currentArtifact } = useDodai(); const { notes } = useNotes(); const tagData = useTagGraph(notes); const [activeTag, setActiveTag] = useState(null); + const [activeViewMode, setActiveViewMode] = useState<'canvas' | 'chat'>('canvas'); const showArtifactPanel = !!currentArtifact; - const panelGroupKey = showArtifactPanel ? 'artifactMode' : 'hubMode'; const handleTagSelect = (tag: string) => { setActiveTag(tag); @@ -28,31 +29,39 @@ const CanvasViewContent = () => { return (
- - -
- + {activeViewMode === 'canvas' ? ( + + +
+ +
+
+ + +
+ {showArtifactPanel ? ( + + ) : ( + + )} +
+
+
+ ) : ( +
+
+
- - - -
- {showArtifactPanel ? ( - - ) : ( - - )} +
+
- - +
+ )}
); }; diff --git a/pages/main/src/features/canvas/components/ChatPanel.tsx b/pages/main/src/features/canvas/components/ChatPanel.tsx index a330f45..61c1577 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, LayoutDashboard, MessagesSquare } 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 = { + activeViewMode: 'canvas' | 'chat'; + setActiveViewMode: (mode: 'canvas' | 'chat') => void; +}; + +const ChatPanel: React.FC = ({ activeViewMode, setActiveViewMode }) => { + const { messages, chatInput, setChatInput, isLoading, sendPromptAndGenerateArtifact } = useDodai(); const messagesEndRef = useRef(null); const [initialHubPrompt, setInitialHubPrompt] = useState(''); @@ -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,37 @@ const ChatPanel = () => {
- +
+ + +
+ {activeViewMode === 'chat' && ( + + )} + {/* Placeholder for alignment if history button is hidden */} + {activeViewMode === 'canvas' &&
}
{messages.length === 0 && !isLoading ? ( diff --git a/pages/main/src/features/canvas/types/index.ts b/pages/main/src/features/canvas/types/index.ts index fb30e8c..155ff36 100644 --- a/pages/main/src/features/canvas/types/index.ts +++ b/pages/main/src/features/canvas/types/index.ts @@ -3,6 +3,8 @@ export interface Message { role: 'user' | 'assistant' | 'system'; content: string; timestamp: number; + isStreaming?: boolean; + model?: string; } export type ArtifactType = 'text' | 'code'; diff --git a/pages/main/src/features/notes/components/list/FolderCard.tsx b/pages/main/src/features/notes/components/list/FolderCard.tsx index 46e5b9f..1e3dd7f 100644 --- a/pages/main/src/features/notes/components/list/FolderCard.tsx +++ b/pages/main/src/features/notes/components/list/FolderCard.tsx @@ -124,7 +124,8 @@ const FolderCard = forwardRef( ))}
-
+
(
-

+

{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/src/features/text-chat/components/TextChatView.tsx b/pages/main/src/features/text-chat/components/TextChatView.tsx new file mode 100644 index 0000000..aae012a --- /dev/null +++ b/pages/main/src/features/text-chat/components/TextChatView.tsx @@ -0,0 +1,51 @@ +import type React from 'react'; +// import type { Message } from '../../canvas/types'; // No longer directly needed here +import { ChatMessage } from '../../canvas/components/ChatMessage'; +import { ChatInput } from '../../canvas/components/ChatInput'; +import { useSimpleTextChat } from '../hooks/useSimpleTextChat'; // Import the new hook + +// TextChatViewProps is no longer needed as props come from the hook +// interface TextChatViewProps { +// messages: Message[]; +// chatInput: string; +// setChatInput: (value: string) => void; +// handleSubmit: (e: React.FormEvent, promptToSend?: string) => Promise; +// isLoading: boolean; +// messagesEndRef: React.RefObject; +// } + +const TextChatView: React.FC = () => { + const { messages, chatInput, setChatInput, handleSubmit, isLoading, messagesEndRef } = useSimpleTextChat(); + + // Static example messages are removed as the hook handles messages + // const exampleMessages: Message[] = messages.length === 0 && !isLoading ? [ + // { id: '1', role: 'assistant', content: 'Bonjour! Ceci est une vue de chat textuel.', timestamp: Date.now() }, + // { id: '2', role: 'user', content: 'Super! Comment ça marche?', timestamp: Date.now() + 1000 }, + // { id: '3', role: 'assistant', content: 'Vous pouvez taper votre message ci-dessous.', timestamp: Date.now() + 2000 }, + // ] : messages; + + return ( +
+ {/* Messages area */} +
+ {messages.map(message => ( + + ))} +
+
+ + {/* Input area */} +
+ +
+
+ ); +}; + +export default TextChatView; diff --git a/pages/main/src/features/text-chat/hooks/useSimpleTextChat.ts b/pages/main/src/features/text-chat/hooks/useSimpleTextChat.ts new file mode 100644 index 0000000..b53ea22 --- /dev/null +++ b/pages/main/src/features/text-chat/hooks/useSimpleTextChat.ts @@ -0,0 +1,229 @@ +import { useState, useCallback, useRef, useEffect } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import type { Message } from '../../canvas/types'; // Assuming Message type is in canvas/types +import { + MessageType, + StreamEventType, + type ChatHistoryMessage, + type GenerateDodaiCanvasArtifactStreamResponse, // More specific type for port messages +} from '../../../../../../chrome-extension/src/background/types'; +import { dodaiCanvasConfigStorage } from '@extension/storage'; // Import storage + +export interface UseSimpleTextChatReturn { + messages: Message[]; + chatInput: string; + isLoading: boolean; + messagesEndRef: React.RefObject; // Allow null for initial ref value + setChatInput: React.Dispatch>; + handleSubmit: (e: React.FormEvent, promptToSend?: string) => Promise; +} + +export function useSimpleTextChat(): UseSimpleTextChatReturn { + const [messages, setMessages] = useState([]); + const [chatInput, setChatInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const streamingPort = useRef(null); + const streamingPortId = useRef(null); + const messagesEndRef = useRef(null); + const currentAssistantMessageId = useRef(null); + + const cleanupStreamingConnection = useCallback(() => { + if (streamingPort.current) { + try { + streamingPort.current.disconnect(); + } catch (e) { + console.warn('[SimpleTextChat] Error disconnecting port:', e); + } + streamingPort.current = null; + streamingPortId.current = null; + } + setIsLoading(false); + currentAssistantMessageId.current = null; + }, []); + + useEffect(() => { + return () => { + cleanupStreamingConnection(); + }; + }, [cleanupStreamingConnection]); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages, isLoading]); + + const handleSubmit = useCallback( + async (e: React.FormEvent, promptToSend?: string) => { + e.preventDefault(); + const currentInput = (promptToSend || chatInput).trim(); + if (!currentInput || isLoading) return; + + const userMessage: Message = { + id: uuidv4(), + role: 'user', + content: currentInput, + timestamp: Date.now(), + }; + setMessages(prev => [...prev, userMessage]); + setChatInput(''); + setIsLoading(true); + + const assistantMsgId = uuidv4(); + currentAssistantMessageId.current = assistantMsgId; + const assistantPlaceholderMessage: Message = { + id: assistantMsgId, + role: 'assistant', + content: '', // Start with empty content, will be filled by stream + timestamp: Date.now() + 1, + isStreaming: true, + }; + setMessages(prev => [...prev, assistantPlaceholderMessage]); + + const uniquePortId = `simple_text_chat_stream_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; + streamingPortId.current = uniquePortId; + const port = chrome.runtime.connect({ name: uniquePortId }); + streamingPort.current = port; + + // Read selected model from storage for the request + let modelToUseForRequest: string | undefined; + try { + const canvasSettings = await dodaiCanvasConfigStorage.get(); + modelToUseForRequest = canvasSettings.selectedModel || undefined; + } catch (err) { + console.warn('[SimpleTextChat] Could not read selected model from dodaiCanvasConfigStorage', err); + // Proceed without a specific model, background will use its default + } + + port.onMessage.addListener((msg: GenerateDodaiCanvasArtifactStreamResponse) => { + switch (msg.type) { + case StreamEventType.STREAM_START: + console.log('[SimpleTextChat] Stream started:', msg.model); + setMessages(prev => prev.map(m => (m.id === assistantMsgId ? { ...m, model: msg.model, content: '' } : m))); + break; + case StreamEventType.STREAM_CHUNK: + if (msg.chunk) { + setMessages(prev => + prev.map(m => + m.id === assistantMsgId ? { ...m, content: m.content + msg.chunk, isStreaming: true } : m, + ), + ); + } + break; + case StreamEventType.STREAM_END: + console.log('[SimpleTextChat] Stream ended, success:', msg.success); + setMessages(prev => prev.map(m => (m.id === assistantMsgId ? { ...m, isStreaming: false } : m))); + if (!msg.success) { + setMessages(prev => + prev.map(m => + m.id === assistantMsgId + ? { + ...m, + content: m.content + `\nErreur: ${msg.error || 'Erreur inconnue'}`, + isStreaming: false, + } + : m, + ), + ); + } + cleanupStreamingConnection(); + break; + case StreamEventType.STREAM_ERROR: + console.error('[SimpleTextChat] Stream error:', msg.error); + setMessages(prev => + prev.map(m => + m.id === assistantMsgId + ? { + ...m, + content: `Erreur de streaming: ${msg.error || 'Erreur inconnue'}`, + isStreaming: false, + } + : m, + ), + ); + cleanupStreamingConnection(); + break; + default: + console.warn('[SimpleTextChat] Unknown stream message:', msg); + } + }); + + port.onDisconnect.addListener(() => { + if (isLoading) { + // Disconnected unexpectedly + console.warn('[SimpleTextChat] Port disconnected unexpectedly.'); + setMessages(prev => + prev.map(m => + m.id === currentAssistantMessageId.current && m.isStreaming + ? { ...m, content: m.content + '\n(Connexion perdue)', isStreaming: false } + : m, + ), + ); + } + cleanupStreamingConnection(); + }); + + const chatHistoryForPayload: ChatHistoryMessage[] = messages + .filter(m => m.id !== assistantMsgId) // Exclude current placeholder/streaming message + .map(m => ({ role: m.role, content: m.content })); + + chrome.runtime.sendMessage( + { + type: MessageType.AI_CHAT_REQUEST, + payload: { + message: currentInput, + chatHistory: chatHistoryForPayload, + streamHandler: true, + portId: uniquePortId, + modelName: modelToUseForRequest, // Pass the model name + // pageContent: undefined, // Not needed for simple chat + }, + }, + response => { + if (chrome.runtime.lastError) { + console.error('[SimpleTextChat] SendMessage error:', chrome.runtime.lastError); + setMessages(prev => + prev.map(m => + m.id === assistantMsgId + ? { + ...m, + content: `Erreur (envoi): ${chrome.runtime.lastError?.message || 'Inconnue'}`, + isStreaming: false, + } + : m, + ), + ); + cleanupStreamingConnection(); + return; + } + if (response && !response.success && !response.streaming) { + // If it wasn't a streaming setup success, and not a general success either + console.error('[SimpleTextChat] Background refused request:', response.error); + setMessages(prev => + prev.map(m => + m.id === assistantMsgId + ? { + ...m, + content: `Erreur (refus BG): ${response.error || 'Inconnue'}`, + isStreaming: false, + } + : m, + ), + ); + cleanupStreamingConnection(); + } + // If response.streaming is true, we wait for port messages. + }, + ); + }, + [chatInput, isLoading, messages, cleanupStreamingConnection], // Added messages to dependency array for chatHistoryForPayload + ); + + return { + messages, + chatInput, + isLoading, + messagesEndRef, + setChatInput, + handleSubmit, + }; +} 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)', From 775ff6cf0e3135fc2e9e24a65a57408fc7fe44d7 Mon Sep 17 00:00:00 2001 From: drewano Date: Tue, 20 May 2025 19:11:07 +0200 Subject: [PATCH 2/9] feat: enhance message handling in MessageHandler for improved context awareness - Updated AI and RAG chat request handlers to accept the sender parameter, allowing for better context management. - Added logic to conditionally fetch page content based on whether the request is internal or external, improving efficiency. - Enhanced logging to provide clearer insights into request origins and handling processes. - Introduced a new streaming request type in StreamingService for better handling of simple text chat streams. --- .../background/handlers/message-handler.ts | 41 ++++++++++++++----- .../background/services/streaming-service.ts | 3 +- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/chrome-extension/src/background/handlers/message-handler.ts b/chrome-extension/src/background/handlers/message-handler.ts index d190680..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; diff --git a/chrome-extension/src/background/services/streaming-service.ts b/chrome-extension/src/background/services/streaming-service.ts index 0ea16e8..169df9f 100644 --- a/chrome-extension/src/background/services/streaming-service.ts +++ b/chrome-extension/src/background/services/streaming-service.ts @@ -40,7 +40,8 @@ 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_') ) { const portId = port.name; logger.debug(`Nouvelle connexion de streaming établie: ${portId}`); From 734279ea0b8592a5a03b2866967ba1e404753434 Mon Sep 17 00:00:00 2001 From: drewano Date: Tue, 20 May 2025 19:41:53 +0200 Subject: [PATCH 3/9] fix: refine layout and rendering logic in CanvasView and ChatPanel - Removed unnecessary overflow-hidden class from the header in CanvasView for better visual consistency. - Updated ChatPanel to conditionally render message content based on activeViewMode, improving user experience and interaction flow. - Streamlined the rendering logic for initial hub view and message display, enhancing clarity in the component structure. --- pages/main/src/features/canvas/CanvasView.tsx | 2 +- .../features/canvas/components/ChatPanel.tsx | 43 ++++++++++--------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/pages/main/src/features/canvas/CanvasView.tsx b/pages/main/src/features/canvas/CanvasView.tsx index baaaf0d..03921a2 100644 --- a/pages/main/src/features/canvas/CanvasView.tsx +++ b/pages/main/src/features/canvas/CanvasView.tsx @@ -54,7 +54,7 @@ const CanvasViewContent = () => { ) : (
-
+
diff --git a/pages/main/src/features/canvas/components/ChatPanel.tsx b/pages/main/src/features/canvas/components/ChatPanel.tsx index 61c1577..b540d6a 100644 --- a/pages/main/src/features/canvas/components/ChatPanel.tsx +++ b/pages/main/src/features/canvas/components/ChatPanel.tsx @@ -147,28 +147,29 @@ const ChatPanel: React.FC = ({ activeViewMode, setActiveViewMode {activeViewMode === 'canvas' &&
}
- {messages.length === 0 && !isLoading ? ( - renderInitialHubView() - ) : ( - <> -
- {messages.map(message => ( - - ))} -
-
+ {activeViewMode === 'canvas' && + (messages.length === 0 && !isLoading ? ( + renderInitialHubView() + ) : ( + <> +
+ {messages.map(message => ( + + ))} +
+
-
- -
- - )} +
+ +
+ + ))}
); }; From 81e48ea74fc6726924fca53aae9f5847e7778818 Mon Sep 17 00:00:00 2001 From: drewano Date: Tue, 20 May 2025 20:27:52 +0200 Subject: [PATCH 4/9] style: update background color in DodaiModelSelector for improved UI consistency - Changed background color from 'bg-slate-850' to 'bg-slate-800' in the dropdown menu of DodaiModelSelector to enhance visual coherence with the overall theme. --- .../main/src/features/canvas/components/DodaiModelSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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

)} 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..7223f40 --- /dev/null +++ b/pages/main/src/features/canvas/components/DodaiCanvasHistoryPanel.tsx @@ -0,0 +1,150 @@ +import type React from 'react'; +import { useState } from 'react'; +import type { ChatConversation } from '@extension/storage'; +import { XIcon, Trash2Icon, Edit3Icon, CheckIcon, XCircleIcon } from 'lucide-react'; + +interface DodaiCanvasHistoryPanelProps { + chatHistory: ChatConversation[]; + activeConversationId: string | null; + onLoadConversation: (id: string) => void; + onDeleteConversation: (id: string) => void; + onRenameConversation: (id: string, newName: string) => Promise; + onClose: () => void; +} + +/** + * Panneau pour afficher l'historique des conversations dans Dodai Canvas. + */ +export const DodaiCanvasHistoryPanel: React.FC = ({ + chatHistory, + activeConversationId, + onLoadConversation, + onDeleteConversation, + onRenameConversation, + 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

+
+ +
+
+ + {/* 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/contexts/DodaiContext.tsx b/pages/main/src/features/canvas/contexts/DodaiContext.tsx index a29b49a..4445754 100644 --- a/pages/main/src/features/canvas/contexts/DodaiContext.tsx +++ b/pages/main/src/features/canvas/contexts/DodaiContext.tsx @@ -28,7 +28,8 @@ interface DodaiContextType { updateCurrentArtifactContent: (newMarkdown: string) => void; modifyCurrentArtifact: (promptSuffix: string, currentMarkdown: string) => Promise; // Nouvelle fonction cancelCurrentStreaming: () => void; // New function to cancel streaming - resetChatAndArtifact: () => void; // Added new function signature + resetChatAndArtifact: (onBeforeResetCallback?: () => Promise) => Promise; // Modified signature + setOnChatTurnEnd: (handler: ((finalMessages: Message[], modelUsed?: string) => void) | null) => void; // New setter } const defaultContext: DodaiContextType = { @@ -49,7 +50,8 @@ const defaultContext: DodaiContextType = { updateCurrentArtifactContent: () => {}, modifyCurrentArtifact: async () => {}, // Valeur par défaut cancelCurrentStreaming: () => {}, // Default for cancel - resetChatAndArtifact: () => {}, // Added default for new function + resetChatAndArtifact: async () => {}, // Modified default + setOnChatTurnEnd: () => {}, // New default }; const DodaiContext = createContext(defaultContext); @@ -72,6 +74,11 @@ export const DodaiProvider: React.FC = ({ children }) => { const streamingPort = useRef(null); const streamingPortId = useRef(null); const currentStreamingPrompt = useRef(null); // To store the prompt that initiated streaming + const onChatTurnEndCallbackRef = useRef<((finalMessages: Message[], modelUsed?: string) => void) | null>(null); + + const setOnChatTurnEnd = useCallback((handler: ((finalMessages: Message[], modelUsed?: string) => void) | null) => { + onChatTurnEndCallbackRef.current = handler; + }, []); // Fonction utilitaire pour générer un titre simple const generateTitleFromMarkdown = (markdown: string): string => { @@ -177,6 +184,7 @@ export const DodaiProvider: React.FC = ({ children }) => { ? { ...msg, content: `Génération avec ${message.model || 'le modèle par défaut'} commencée...`, + model: message.model, // Sauvegarder le modèle ici aussi } : msg, ), @@ -206,13 +214,21 @@ export const DodaiProvider: React.FC = ({ children }) => { case StreamEventType.STREAM_END: console.log("[DodaiCanvas] Fin du streaming d'artefact, succès:", message.success); if (message.success) { - // Le message de succès est maintenant géré par ARTIFACT_CHAT_RESPONSE - // ou affiché s'il n'y a pas de réponse chat. - // On garde une trace de l'artefact ici. if (currentArtifact) { setArtifactHistory(prev => [...prev, currentArtifact]); } - // Ne pas appeler cleanupStreamingConnection() ici si on attend ARTIFACT_CHAT_RESPONSE + const finalMessages = messages.map((msg: Message) => + msg.id === assistantPlaceholderId && !msg.content.includes('Réponse chat reçue') + ? { + ...msg, + content: `Artefact généré avec succès ! (Modèle: ${message.model || 'inconnu'})\nVous pouvez le consulter dans le panneau de droite.`, + model: message.model, + isStreaming: false, + } + : msg, + ); + setMessages(finalMessages); + onChatTurnEndCallbackRef.current?.(finalMessages, message.model); } else { setMessages(prev => prev.map(msg => @@ -236,38 +252,26 @@ export const DodaiProvider: React.FC = ({ children }) => { } break; - case StreamEventType.ARTIFACT_CHAT_RESPONSE: + case StreamEventType.ARTIFACT_CHAT_RESPONSE: { console.log('[DodaiCanvas] Réponse chat reçue:', message.chatResponse); - if (message.chatResponse) { - setMessages(prev => - prev.map(msg => - msg.id === assistantPlaceholderId - ? { - ...msg, - content: `${message.chatResponse} (Modèle: ${message.model || 'inconnu'})`, - } - : msg, - ), - ); - } else { - // Fallback si ARTIFACT_CHAT_RESPONSE est vide mais STREAM_END était success - setMessages(prev => - prev.map(msg => - msg.id === assistantPlaceholderId - ? { - ...msg, - content: `Artefact généré avec succès ! (Modèle: ${message.model || 'inconnu'}) -Vous pouvez le consulter dans le panneau de droite.`, - } - : msg, - ), - ); - } - // C'est le vrai signal de fin de la séquence complète. + const updatedMessages = messages.map((msg: Message) => + msg.id === assistantPlaceholderId + ? { + ...msg, + content: message.chatResponse + ? `${message.chatResponse} (Modèle: ${message.model || 'inconnu'})` + : `Artefact généré avec succès ! (Modèle: ${message.model || 'inconnu'})\nVous pouvez le consulter dans le panneau de droite.`, + model: message.model, + isStreaming: false, + } + : msg, + ); + setMessages(updatedMessages); + onChatTurnEndCallbackRef.current?.(updatedMessages, message.model); cleanupStreamingConnection(); break; - - case StreamEventType.STREAM_ERROR: + } + case StreamEventType.STREAM_ERROR: { console.error("[DodaiCanvas] Erreur de streaming d'artefact:", message.error); setMessages(prev => prev.map(msg => @@ -275,15 +279,18 @@ Vous pouvez le consulter dans le panneau de droite.`, ? { ...msg, content: `Erreur de streaming : ${message.error || 'Erreur inconnue'}`, + model: message.model, + isStreaming: false, } : msg, ), ); cleanupStreamingConnection(); break; - - default: + } + default: { console.warn("[DodaiCanvas] Message de streaming d'artefact inconnu:", message); + } } }); @@ -435,8 +442,9 @@ Vous pouvez le consulter dans le panneau de droite.`, const assistantPlaceholderMessage: Message = { id: assistantPlaceholderId, role: 'assistant', - content: "Modification de l'artefact en cours...", // Guillemets doubles, sans échappement inutile + content: "Modification de l'artefact en cours...", timestamp: Date.now() + 1, + isStreaming: true, // Indiquer que c'est en cours }; setMessages(prev => [...prev, userMessage, assistantPlaceholderMessage]); @@ -487,16 +495,18 @@ Vous pouvez le consulter dans le panneau de droite.`, return newArtifactState; }); - setMessages(prev => - prev.map(msg => - msg.id === assistantPlaceholderId - ? { - ...msg, - content: `Artefact modifié avec succès ! (Modèle: ${response.model || 'inconnu'})`, - } - : msg, - ), + const updatedMessages = messages.map((msg: Message) => + msg.id === assistantPlaceholderId + ? { + ...msg, + content: `Artefact modifié avec succès ! (Modèle: ${response.model || 'inconnu'})`, + model: response.model, + isStreaming: false, + } + : msg, ); + setMessages(updatedMessages); + onChatTurnEndCallbackRef.current?.(updatedMessages, response.model); } else { setMessages(prev => prev.map(msg => @@ -504,6 +514,8 @@ Vous pouvez le consulter dans le panneau de droite.`, ? { ...msg, content: `Erreur lors de la modification : ${response.error || 'Erreur inconnue'}`, + model: response.model, + isStreaming: false, } : msg, ), @@ -517,6 +529,7 @@ Vous pouvez le consulter dans le panneau de droite.`, ? { ...msg, content: `Erreur de communication: ${error instanceof Error ? error.message : String(error)}`, + isStreaming: false, } : msg, ), @@ -526,20 +539,27 @@ Vous pouvez le consulter dans le panneau de droite.`, }; // Implementation for resetChatAndArtifact - const resetChatAndArtifact = useCallback(() => { - setMessages([]); - setCurrentArtifact(null); - // setSelectedDodaiModel(null); // Reset model selection if needed, or retain user preference - setChatInput(''); - // Potentially clear artifactHistory as well if a full reset is desired - setIsLoading(false); - setIsStreamingArtifact(false); - // If a stream was active, ensure it is cleaned up - if (streamingPort.current) { - cleanupStreamingConnection(); - } - console.log('[DodaiCanvas] Chat and artifact reset.'); - }, [cleanupStreamingConnection]); // Added cleanupStreamingConnection as dependency + const resetChatAndArtifact = useCallback( + async (onBeforeResetCallback?: () => Promise) => { + if (onBeforeResetCallback) { + console.log('[DodaiContext] Executing onBeforeResetCallback...'); + await onBeforeResetCallback(); + console.log('[DodaiContext] onBeforeResetCallback finished.'); + } + + setMessages([]); + setCurrentArtifact(null); + setChatInput(''); + setIsLoading(false); + setIsStreamingArtifact(false); + + if (streamingPort.current) { + cleanupStreamingConnection(); + } + console.log('[DodaiContext] Chat and artifact state has been reset.'); + }, + [cleanupStreamingConnection], + ); const value = { messages, @@ -553,13 +573,14 @@ Vous pouvez le consulter dans le panneau de droite.`, isLoading, setIsLoading, isStreamingArtifact, - selectedDodaiModel, // Expose selected model - setSelectedDodaiModel, // Expose setter + selectedDodaiModel, + setSelectedDodaiModel, sendPromptAndGenerateArtifact, updateCurrentArtifactContent, modifyCurrentArtifact, cancelCurrentStreaming, - resetChatAndArtifact, // Added new function to context value + resetChatAndArtifact, + setOnChatTurnEnd, }; return {children}; diff --git a/pages/main/src/features/canvas/hooks/useDodaiCanvasHistory.ts b/pages/main/src/features/canvas/hooks/useDodaiCanvasHistory.ts new file mode 100644 index 0000000..9810ee7 --- /dev/null +++ b/pages/main/src/features/canvas/hooks/useDodaiCanvasHistory.ts @@ -0,0 +1,266 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useStorage } from '@extension/shared'; +import { + chatHistoryStorage, + type ChatConversation, // Sera utilisé dans Omit + type ChatMessage, +} from '@extension/storage'; +import type { Message } from '../types'; // Importer le type Message spécifique au Canvas + +/** + * Hook pour gérer l'historique des conversations spécifiquement pour Dodai Canvas. + */ +export function useDodaiCanvasHistory() { + const chatHistory = useStorage(chatHistoryStorage); + const [activeConversationId, setActiveConversationId] = useState(null); + const [currentChatName, setCurrentChatName] = useState('Nouvelle conversation'); + + // Optionnel : Effet pour charger la conversation la plus récente au démarrage, + // similaire à useChatHistory.ts si ce comportement est désiré. + useEffect(() => { + if (!activeConversationId && chatHistory && chatHistory.length > 0) { + // Trier pour trouver la plus récente conversation mise à jour + const mostRecentConversation = [...chatHistory].sort((a, b) => b.updatedAt - a.updatedAt)[0]; + if (mostRecentConversation) { + setActiveConversationId(mostRecentConversation.id); + setCurrentChatName(mostRecentConversation.name); + } + } + // Exécuter seulement si chatHistory change et qu'il n'y a pas d'ID actif + }, [chatHistory, activeConversationId]); + + const canvasMessagesToStorage = (messages: Message[]): ChatMessage[] => { + return messages.map(msg => ({ + role: msg.role, + content: msg.content, + // reasoning: null, // Canvas 'Message' n'a pas de 'reasoning' + isStreaming: msg.isStreaming || false, + timestamp: msg.timestamp || Date.now(), + model: msg.model, + })); + }; + + const storageMessagesToCanvas = (messages: ChatMessage[], convId: string): Message[] => { + return messages.map((msg, index) => ({ + id: `loaded-${convId}-${index}`, + role: msg.role, + content: msg.content, + timestamp: msg.timestamp || Date.now(), + isStreaming: msg.isStreaming || false, + model: msg.model, + })); + }; + + const loadConversation = useCallback( + async (id: string): Promise<{ success: boolean; messages?: Message[]; model?: string; error?: string }> => { + try { + const conversation = await chatHistoryStorage.getConversation(id); + if (conversation) { + setActiveConversationId(conversation.id); + setCurrentChatName(conversation.name); + const canvasMessages = storageMessagesToCanvas(conversation.messages, conversation.id); + return { success: true, messages: canvasMessages, model: conversation.model }; + } + return { success: false, error: 'Conversation non trouvée.' }; + } catch (error) { + console.error('Erreur lors du chargement de la conversation:', error); + return { success: false, error: error instanceof Error ? error.message : 'Erreur inconnue' }; + } + }, + [], + ); + + const deleteConversation = useCallback( + async (id: string): Promise<{ success: boolean; wasActive: boolean; error?: string }> => { + try { + const wasActive = id === activeConversationId; + await chatHistoryStorage.deleteConversation(id); + + if (wasActive) { + setActiveConversationId(null); + setCurrentChatName('Nouvelle conversation'); + } + return { success: true, wasActive }; + } catch (error) { + console.error('Erreur lors de la suppression de la conversation:', error); + return { + success: false, + wasActive: false, + error: error instanceof Error ? error.message : 'Erreur inconnue', + }; + } + }, + [activeConversationId], // Dépend de activeConversationId pour la logique wasActive + ); + + const createNewConversation = useCallback( + async ( + welcomeMessageContent: string, + model?: string, + ): Promise<{ + success: boolean; + id: string | null; + initialMessages?: Message[]; + model?: string; + error?: string; + }> => { + const assistantChatMessage: ChatMessage = { + role: 'assistant', + content: welcomeMessageContent, + timestamp: Date.now(), + model: model, + }; + + const newConversationData: Omit = { + name: 'Nouvelle conversation', // Nom par défaut, pourrait être généré plus tard + messages: [assistantChatMessage], + model: model, // Modèle global pour la conversation + }; + + try { + const newId = await chatHistoryStorage.addConversation(newConversationData); + setActiveConversationId(newId); + setCurrentChatName(newConversationData.name); // Ou 'Nouvelle conversation' + + // Convertir le message initial pour le retour + const initialCanvasMessages = storageMessagesToCanvas([assistantChatMessage], newId); + + return { success: true, id: newId, initialMessages: initialCanvasMessages, model: model }; + } catch (error) { + console.error("Erreur lors de la création d'une nouvelle conversation:", error); + return { + success: false, + id: null, + error: error instanceof Error ? error.message : 'Erreur inconnue', + }; + } + }, + [], // setActiveConversationId, setCurrentChatName sont stables + ); + + const renameCurrentConversation = useCallback( + async (newName: string): Promise => { + if (!activeConversationId) { + console.warn('Tentative de renommer sans conversation active.'); + return false; + } + try { + await chatHistoryStorage.renameConversation(activeConversationId, newName); + setCurrentChatName(newName); + return true; + } catch (error) { + console.error('Erreur lors du renommage de la conversation:', error); + return false; + } + }, + [activeConversationId], // setCurrentChatName est stable + ); + + const renameConversationInHistory = useCallback( + async (id: string, newName: string): Promise => { + if (!newName.trim()) { + console.warn('Tentative de renommer avec un nom vide.'); + return false; + } + try { + await chatHistoryStorage.renameConversation(id, newName); + if (id === activeConversationId) { + setCurrentChatName(newName); + } + return true; + } catch (error) { + console.error("Erreur lors du renommage de la conversation (dans l'historique):", error); + return false; + } + }, + [activeConversationId], + ); + + const addMessageToCurrentConversation = useCallback( + async (message: Message): Promise => { + if (!activeConversationId) return false; + try { + const chatMessage: ChatMessage = { + role: message.role, + content: message.content, + isStreaming: message.isStreaming || false, + timestamp: message.timestamp || Date.now(), + model: message.model, + }; + await chatHistoryStorage.addMessageToConversation(activeConversationId, chatMessage); + return true; + } catch (error) { + console.error("Erreur lors de l'ajout du message:", error); + return false; + } + }, + [activeConversationId], + ); + + const extractNameFromMessages = (messages: Message[]): string => { + const firstUserMessage = messages.find(m => m.role === 'user'); + if (firstUserMessage?.content) { + const cleanedContent = firstUserMessage.content.trim(); + if (cleanedContent) { + const words = cleanedContent.split(' '); + const nameCandidate = words.slice(0, 5).join(' '); + if (nameCandidate) { + return nameCandidate.length > 30 ? `${nameCandidate.substring(0, 27)}...` : nameCandidate; + } + } + } + // Fallback + const today = new Date(); + const day = String(today.getDate()).padStart(2, '0'); + const month = String(today.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed + const year = today.getFullYear(); + return `Conversation du ${day}/${month}/${year}`; + }; + + const saveCurrentChatSession = useCallback( + async (messages: Message[], model?: string): Promise => { + const storageChatMessages = canvasMessagesToStorage(messages); + try { + if (!activeConversationId) { + // Nouvelle conversation + const name = extractNameFromMessages(messages); + const newConversationData: Omit = { + name, + messages: storageChatMessages, + model, + }; + const newId = await chatHistoryStorage.addConversation(newConversationData); + setActiveConversationId(newId); + setCurrentChatName(name); + } else { + // Conversation existante + await chatHistoryStorage.updateMessages(activeConversationId, storageChatMessages); + const currentConversation = await chatHistoryStorage.getConversation(activeConversationId); + if (model && currentConversation && currentConversation.model !== model) { + await chatHistoryStorage.updateConversation(activeConversationId, { model }); + } + } + return true; + } catch (error) { + console.error('Erreur lors de la sauvegarde de la session de chat:', error); + return false; + } + }, + [activeConversationId], + ); + + return { + chatHistory, + activeConversationId, + currentChatName, + setActiveConversationId, // Peut être retiré si la gestion est entièrement internalisée + setCurrentChatName, // Peut être retiré si la gestion est entièrement internalisée + loadConversation, + deleteConversation, + createNewConversation, + renameCurrentConversation, + renameConversationInHistory, + addMessageToCurrentConversation, + saveCurrentChatSession, + }; +} From b8fdab96d03572dbcb7b80fb6002f0751a4fb7b0 Mon Sep 17 00:00:00 2001 From: ASSEF Andrew Date: Thu, 22 May 2025 13:36:03 +0200 Subject: [PATCH 6/9] fix: update ChatPanel logic for conditional rendering and enable compression in config - Modified ChatPanel to conditionally render based on the onToggleHistory prop, improving user interaction. - Enabled compression in repomix.config.json for optimized configuration settings. --- pages/main/src/features/canvas/components/ChatPanel.tsx | 4 ++-- repomix.config.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/main/src/features/canvas/components/ChatPanel.tsx b/pages/main/src/features/canvas/components/ChatPanel.tsx index 6ab6b4a..57e7d46 100644 --- a/pages/main/src/features/canvas/components/ChatPanel.tsx +++ b/pages/main/src/features/canvas/components/ChatPanel.tsx @@ -135,7 +135,7 @@ const ChatPanel: React.FC = ({ activeViewMode, setActiveViewMode Chat
- {activeViewMode === 'chat' && ( + {onToggleHistory && ( )} {/* Placeholder for alignment if history button is hidden */} - {activeViewMode === 'canvas' &&
} + {!onToggleHistory &&
}
{activeViewMode === 'canvas' && diff --git a/repomix.config.json b/repomix.config.json index b1617ee..d522d4e 100644 --- a/repomix.config.json +++ b/repomix.config.json @@ -5,7 +5,7 @@ "directoryStructure": true, "removeComments": false, "showLineNumbers": false, - "compress": false + "compress": true }, "ignore": { "customPatterns": ["**/*.svg"] From d8802ae8442039c868bb3dfbde866c854b139c92 Mon Sep 17 00:00:00 2001 From: ASSEF Andrew Date: Thu, 22 May 2025 14:15:28 +0200 Subject: [PATCH 7/9] feat: introduce ChatArtifact and ChatArtifactWithHistory interfaces for enhanced chat management - Added new interfaces for ChatArtifact and ChatArtifactWithHistory to structure chat artifacts and their history. - Updated CanvasView to handle currentArtifact more effectively during conversation loading and saving. - Enhanced useDodaiCanvasHistory hook to manage artifact conversion between storage and canvas formats, improving data handling. --- .../storage/lib/impl/chat-history-storage.ts | 14 +++++ pages/main/src/features/canvas/CanvasView.tsx | 6 +- .../canvas/hooks/useDodaiCanvasHistory.ts | 60 +++++++++++++++++-- 3 files changed, 73 insertions(+), 7 deletions(-) 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/pages/main/src/features/canvas/CanvasView.tsx b/pages/main/src/features/canvas/CanvasView.tsx index f0db398..54f8223 100644 --- a/pages/main/src/features/canvas/CanvasView.tsx +++ b/pages/main/src/features/canvas/CanvasView.tsx @@ -59,7 +59,7 @@ const CanvasViewContent = () => { const result = await loadConversation(id); if (result.success && result.messages) { setMessages(result.messages); - setCurrentArtifact(null); + setCurrentArtifact(result.artifact || null); if (result.model) { setSelectedDodaiModel(result.model); } @@ -131,7 +131,7 @@ const CanvasViewContent = () => { console.log('[CanvasView] Registering onChatTurnEnd handler.'); setOnChatTurnEnd((finalMessages, modelUsed) => { console.log('[CanvasView] onChatTurnEnd triggered. Saving session.'); - saveCurrentChatSession(finalMessages, modelUsed || undefined); + saveCurrentChatSession(finalMessages, currentArtifact, modelUsed || undefined); }); // Cleanup: Unregister handler when component unmounts or dependencies change @@ -139,7 +139,7 @@ const CanvasViewContent = () => { console.log('[CanvasView] Unregistering onChatTurnEnd handler.'); setOnChatTurnEnd(null); }; - }, [setOnChatTurnEnd, saveCurrentChatSession]); // Dependencies for registering the handler + }, [setOnChatTurnEnd, saveCurrentChatSession, currentArtifact]); // Added currentArtifact as a dependency return (
diff --git a/pages/main/src/features/canvas/hooks/useDodaiCanvasHistory.ts b/pages/main/src/features/canvas/hooks/useDodaiCanvasHistory.ts index 9810ee7..ff9fdd4 100644 --- a/pages/main/src/features/canvas/hooks/useDodaiCanvasHistory.ts +++ b/pages/main/src/features/canvas/hooks/useDodaiCanvasHistory.ts @@ -4,8 +4,10 @@ import { chatHistoryStorage, type ChatConversation, // Sera utilisé dans Omit type ChatMessage, + type ChatArtifact, + type ChatArtifactWithHistory, } from '@extension/storage'; -import type { Message } from '../types'; // Importer le type Message spécifique au Canvas +import type { Message, ArtifactV3, ArtifactMarkdownV3 } from '../types'; // Importer le type Message spécifique au Canvas /** * Hook pour gérer l'historique des conversations spécifiquement pour Dodai Canvas. @@ -51,15 +53,51 @@ export function useDodaiCanvasHistory() { })); }; + const canvasArtifactToStorage = (artifact: ArtifactV3): ChatArtifactWithHistory => { + return { + currentIndex: artifact.currentIndex, + contents: artifact.contents.map(content => { + const markdownContent = content as ArtifactMarkdownV3; + return { + type: markdownContent.type, + title: markdownContent.title, + fullMarkdown: markdownContent.fullMarkdown, + }; + }), + }; + }; + + const storageArtifactToCanvas = (artifact: ChatArtifactWithHistory): ArtifactV3 => { + return { + currentIndex: artifact.currentIndex, + contents: artifact.contents.map(content => { + return { + type: content.type, + title: content.title, + fullMarkdown: content.fullMarkdown, + }; + }), + }; + }; + const loadConversation = useCallback( - async (id: string): Promise<{ success: boolean; messages?: Message[]; model?: string; error?: string }> => { + async (id: string): Promise<{ success: boolean; messages?: Message[]; artifact?: ArtifactV3 | null; model?: string; error?: string }> => { try { const conversation = await chatHistoryStorage.getConversation(id); if (conversation) { setActiveConversationId(conversation.id); setCurrentChatName(conversation.name); const canvasMessages = storageMessagesToCanvas(conversation.messages, conversation.id); - return { success: true, messages: canvasMessages, model: conversation.model }; + let canvasArtifact = null; + if (conversation.artifact) { + canvasArtifact = storageArtifactToCanvas(conversation.artifact); + } + return { + success: true, + messages: canvasMessages, + artifact: canvasArtifact, + model: conversation.model + }; } return { success: false, error: 'Conversation non trouvée.' }; } catch (error) { @@ -115,6 +153,7 @@ export function useDodaiCanvasHistory() { name: 'Nouvelle conversation', // Nom par défaut, pourrait être généré plus tard messages: [assistantChatMessage], model: model, // Modèle global pour la conversation + artifact: null, // Pas d'artefact au départ }; try { @@ -218,8 +257,13 @@ export function useDodaiCanvasHistory() { }; const saveCurrentChatSession = useCallback( - async (messages: Message[], model?: string): Promise => { + async (messages: Message[], currentArtifact?: ArtifactV3 | null, model?: string): Promise => { const storageChatMessages = canvasMessagesToStorage(messages); + let storageArtifact = null; + if (currentArtifact) { + storageArtifact = canvasArtifactToStorage(currentArtifact); + } + try { if (!activeConversationId) { // Nouvelle conversation @@ -228,6 +272,7 @@ export function useDodaiCanvasHistory() { name, messages: storageChatMessages, model, + artifact: storageArtifact, }; const newId = await chatHistoryStorage.addConversation(newConversationData); setActiveConversationId(newId); @@ -235,6 +280,13 @@ export function useDodaiCanvasHistory() { } else { // Conversation existante await chatHistoryStorage.updateMessages(activeConversationId, storageChatMessages); + + // Update artifact + await chatHistoryStorage.updateConversation(activeConversationId, { + artifact: storageArtifact + }); + + // Update model if changed const currentConversation = await chatHistoryStorage.getConversation(activeConversationId); if (model && currentConversation && currentConversation.model !== model) { await chatHistoryStorage.updateConversation(activeConversationId, { model }); From 218dd62b250c62f24abe4efa011c3fd52b85537f Mon Sep 17 00:00:00 2001 From: drewano Date: Thu, 22 May 2025 23:18:52 +0200 Subject: [PATCH 8/9] fix: update configuration and enhance chat response handling - Disabled compression in repomix.config.json for better compatibility. - Added STREAM_START message handling in agent-service.ts to notify UI when streaming begins. - Improved chat response generation logic to provide clearer user feedback based on artifact generation success or failure. - Updated streaming-service.ts to include a new simple chat stream type for better message handling. - Refactored MainLayout.tsx to streamline chat session saving and removed unused conversation creation logic. - Adjusted CanvasView.tsx to determine panel visibility based on artifact mode and streaming state. - Removed deprecated TextChatView component and its associated hook for a cleaner codebase. --- .../src/background/services/agent-service.ts | 99 ++- .../background/services/streaming-service.ts | 3 +- .../main/src/components/layout/MainLayout.tsx | 26 +- pages/main/src/features/canvas/CanvasView.tsx | 82 +-- .../features/canvas/components/ChatPanel.tsx | 81 ++- .../features/canvas/contexts/DodaiContext.tsx | 662 ++++++++++++------ .../text-chat/components/TextChatView.tsx | 51 -- .../text-chat/hooks/useSimpleTextChat.ts | 229 ------ repomix.config.json | 2 +- 9 files changed, 593 insertions(+), 642 deletions(-) delete mode 100644 pages/main/src/features/text-chat/components/TextChatView.tsx delete mode 100644 pages/main/src/features/text-chat/hooks/useSimpleTextChat.ts 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 169df9f..c615152 100644 --- a/chrome-extension/src/background/services/streaming-service.ts +++ b/chrome-extension/src/background/services/streaming-service.ts @@ -41,7 +41,8 @@ export class StreamingService { port.name.startsWith('ai_streaming_') || port.name.startsWith('rag_streaming_') || port.name.startsWith('dodai_canvas_artifact_stream_') || - port.name.startsWith('simple_text_chat_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/pages/main/src/components/layout/MainLayout.tsx b/pages/main/src/components/layout/MainLayout.tsx index e2141fc..2825691 100644 --- a/pages/main/src/components/layout/MainLayout.tsx +++ b/pages/main/src/components/layout/MainLayout.tsx @@ -16,11 +16,10 @@ const MainLayout: React.FC = () => { resetChatAndArtifact, messages: dodaiContextMessages, selectedDodaiModel: dodaiContextSelectedModel, - setMessages: dodaiContextSetMessages, } = useDodai(); const { notes, addNote, getNote } = useNotes(); const { handleCreateNewNote } = useNoteSelection(notes, getNote, addNote); - const { saveCurrentChatSession, createNewConversation } = useDodaiCanvasHistory(); + const { saveCurrentChatSession } = useDodaiCanvasHistory(); // Define individual button configurations const newCanvasButton: NavItemProps = { @@ -32,33 +31,14 @@ const MainLayout: React.FC = () => { await resetChatAndArtifact(async () => { if (dodaiContextMessages.length > 0) { console.log('[MainLayout] Saving current session before reset...'); - await saveCurrentChatSession(dodaiContextMessages, dodaiContextSelectedModel || undefined); + await saveCurrentChatSession(dodaiContextMessages, null, dodaiContextSelectedModel || undefined); console.log('[MainLayout] Current session saved.'); } else { console.log('[MainLayout] No active session to save before reset.'); } }); - console.log('[MainLayout] Creating new conversation in history...'); - const newChatData = await createNewConversation( - "Nouvelle conversation. Comment puis-je vous aider aujourd'hui ?", - dodaiContextSelectedModel || undefined, - ); - - if (newChatData.success && newChatData.initialMessages) { - console.log('[MainLayout] New conversation created in history, setting initial messages in context.'); - dodaiContextSetMessages(newChatData.initialMessages); - } else { - console.error('[MainLayout] Failed to create new conversation in history:', newChatData.error); - dodaiContextSetMessages([ - { - id: 'fallback-new-chat', - role: 'assistant', - content: 'Impossible de créer une nouvelle session, veuillez réessayer.', - timestamp: Date.now(), - }, - ]); - } + console.log('[MainLayout] New Canvas session started - ready for user input.'); navigate('/canvas'); }, diff --git a/pages/main/src/features/canvas/CanvasView.tsx b/pages/main/src/features/canvas/CanvasView.tsx index 54f8223..04deb97 100644 --- a/pages/main/src/features/canvas/CanvasView.tsx +++ b/pages/main/src/features/canvas/CanvasView.tsx @@ -6,7 +6,6 @@ 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 TextChatView from '../text-chat/components/TextChatView'; import DodaiCanvasHistoryPanel from './components/DodaiCanvasHistoryPanel'; import { useDodaiCanvasHistory } from './hooks/useDodaiCanvasHistory'; import { v4 as uuidv4 } from 'uuid'; @@ -14,12 +13,13 @@ import { v4 as uuidv4 } from 'uuid'; const CanvasViewContent = () => { const { currentArtifact, - selectedDodaiModel, setMessages, setCurrentArtifact, setSelectedDodaiModel, resetChatAndArtifact, setOnChatTurnEnd, + isArtifactModeActive, + isStreamingArtifact, } = useDodai(); const { chatHistory, @@ -27,17 +27,16 @@ const CanvasViewContent = () => { loadConversation, deleteConversation, saveCurrentChatSession, - createNewConversation, renameConversationInHistory, } = useDodaiCanvasHistory(); const { notes } = useNotes(); const tagData = useTagGraph(notes); const [activeTag, setActiveTag] = useState(null); - const [activeViewMode, setActiveViewMode] = useState<'canvas' | 'chat'>('canvas'); const [showHistoryPanel, setShowHistoryPanel] = useState(false); - const showArtifactPanel = !!currentArtifact; + // Determine what to show in the right panel + const shouldShowArtifactPanel = isArtifactModeActive && (currentArtifact !== null || isStreamingArtifact); const handleTagSelect = (tag: string) => { setActiveTag(tag); @@ -89,24 +88,13 @@ const CanvasViewContent = () => { timestamp: Date.now(), }, ]); - // Optionnel: Créer automatiquement une nouvelle conversation vide dans l'historique ? - // Pour l'instant, on laisse l'utilisateur choisir ou le contexte se réinitialiser. } - // Pas besoin de fermer le panneau ici, car la liste se mettra à jour - // et si la conv active a été supprimée, l'état du chat est déjà réinitialisé. } else { console.error('[CanvasView] Failed to delete conversation:', result.error); // Afficher une notification à l'utilisateur ici si nécessaire } }, - [ - deleteConversation, - resetChatAndArtifact, - setMessages, - createNewConversation, - setSelectedDodaiModel, - selectedDodaiModel, - ], + [deleteConversation, resetChatAndArtifact, setMessages], ); const handleRenameConversation = useCallback( @@ -143,47 +131,29 @@ const CanvasViewContent = () => { return (
- {activeViewMode === 'canvas' ? ( - - -
- -
-
- - -
- {showArtifactPanel ? ( - - ) : ( - - )} -
-
-
- ) : ( -
-
- + + +
+
-
- + + + +
+ {shouldShowArtifactPanel ? ( + + ) : ( + + )}
-
- )} +
+
+ {showHistoryPanel && ( void; onToggleHistory?: () => void; }; -const ChatPanel: React.FC = ({ activeViewMode, setActiveViewMode, onToggleHistory }) => { - const { messages, chatInput, setChatInput, isLoading, sendPromptAndGenerateArtifact } = useDodai(); +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: React.FC = ({ activeViewMode, setActiveViewMode e.preventDefault(); const currentInput = (promptToSend || chatInput || initialHubPrompt).trim(); if (!currentInput) return; - await sendPromptAndGenerateArtifact(currentInput); + await sendMessage(currentInput); setInitialHubPrompt(''); }; @@ -115,26 +114,28 @@ const ChatPanel: React.FC = ({ activeViewMode, setActiveViewMode
+
+ {onToggleHistory && ( )} - {/* Placeholder for alignment if history button is hidden */} {!onToggleHistory &&
}
- {activeViewMode === 'canvas' && - (messages.length === 0 && !isLoading ? ( - renderInitialHubView() - ) : ( - <> -
- {messages.map(message => ( - - ))} -
-
- -
- -
- - ))} + {messages.length === 0 && !isLoading ? ( + renderInitialHubView() + ) : ( + <> +
+ {messages.map(message => ( + + ))} +
+
+ +
+ +
+ + )}
); }; diff --git a/pages/main/src/features/canvas/contexts/DodaiContext.tsx b/pages/main/src/features/canvas/contexts/DodaiContext.tsx index 4445754..087592e 100644 --- a/pages/main/src/features/canvas/contexts/DodaiContext.tsx +++ b/pages/main/src/features/canvas/contexts/DodaiContext.tsx @@ -22,9 +22,12 @@ interface DodaiContextType { isLoading: boolean; setIsLoading: React.Dispatch>; isStreamingArtifact: boolean; // Added for artifact streaming state + isArtifactModeActive: boolean; // NEW: State for artifact vs simple chat mode + setIsArtifactModeActive: (active: boolean) => void; // NEW: Setter for mode toggle selectedDodaiModel: string | null; // Added for Dodai Canvas specific model setSelectedDodaiModel: (model: string | null) => void; // Added setter sendPromptAndGenerateArtifact: (prompt: string) => Promise; + sendMessage: (prompt: string) => Promise; // NEW: Unified send function updateCurrentArtifactContent: (newMarkdown: string) => void; modifyCurrentArtifact: (promptSuffix: string, currentMarkdown: string) => Promise; // Nouvelle fonction cancelCurrentStreaming: () => void; // New function to cancel streaming @@ -44,9 +47,12 @@ const defaultContext: DodaiContextType = { isLoading: false, setIsLoading: () => {}, isStreamingArtifact: false, // Added default value + isArtifactModeActive: true, // NEW: Default to artifact mode + setIsArtifactModeActive: () => {}, // NEW: Default setter selectedDodaiModel: null, // Initial state setSelectedDodaiModel: () => {}, // Default setter sendPromptAndGenerateArtifact: async () => {}, + sendMessage: async () => {}, // NEW: Default for unified send updateCurrentArtifactContent: () => {}, modifyCurrentArtifact: async () => {}, // Valeur par défaut cancelCurrentStreaming: () => {}, // Default for cancel @@ -69,12 +75,14 @@ export const DodaiProvider: React.FC = ({ children }) => { const [artifactHistory, setArtifactHistory] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isStreamingArtifact, setIsStreamingArtifact] = useState(false); // Specific state for artifact streaming + const [isArtifactModeActive, setIsArtifactModeActive] = useState(true); // NEW: Mode toggle state const [selectedDodaiModel, setSelectedDodaiModel] = useState(null); // State for selected model const streamingPort = useRef(null); const streamingPortId = useRef(null); const currentStreamingPrompt = useRef(null); // To store the prompt that initiated streaming const onChatTurnEndCallbackRef = useRef<((finalMessages: Message[], modelUsed?: string) => void) | null>(null); + const currentAssistantMessageId = useRef(null); // NEW: For simple chat streaming const setOnChatTurnEnd = useCallback((handler: ((finalMessages: Message[], modelUsed?: string) => void) | null) => { onChatTurnEndCallbackRef.current = handler; @@ -101,6 +109,7 @@ export const DodaiProvider: React.FC = ({ children }) => { setIsLoading(false); // General loading setIsStreamingArtifact(false); // Specific artifact streaming loading currentStreamingPrompt.current = null; + currentAssistantMessageId.current = null; // NEW: Reset assistant message ID }, []); useEffect(() => { @@ -127,259 +136,292 @@ export const DodaiProvider: React.FC = ({ children }) => { }, [cleanupStreamingConnection]); // Fonction pour envoyer le prompt et générer/modifier l'artefact - const sendPromptAndGenerateArtifact = async (prompt: string) => { - if (!prompt.trim()) return; - if (isLoading || isStreamingArtifact) { - console.warn("[DodaiCanvas] Tentative d'envoi de prompt pendant une opération en cours."); - return; - } + const sendPromptAndGenerateArtifact = useCallback( + async (prompt: string) => { + if (!prompt.trim()) return; + if (isLoading || isStreamingArtifact) { + console.warn("[DodaiCanvas] Tentative d'envoi de prompt pendant une opération en cours."); + return; + } - currentStreamingPrompt.current = prompt; + currentStreamingPrompt.current = prompt; - const userMessage: Message = { - id: uuidv4(), - role: 'user', - content: prompt, - timestamp: Date.now(), - }; + const userMessage: Message = { + id: uuidv4(), + role: 'user', + content: prompt, + timestamp: Date.now(), + }; - const assistantPlaceholderId = uuidv4(); - const assistantPlaceholderMessage: Message = { - id: assistantPlaceholderId, - role: 'assistant', - content: "Génération de l'artefact en cours...", // Placeholder pendant la génération - timestamp: Date.now() + 1, - }; + const assistantPlaceholderId = uuidv4(); + const assistantPlaceholderMessage: Message = { + id: assistantPlaceholderId, + role: 'assistant', + content: "Génération de l'artefact en cours...", // Placeholder pendant la génération + timestamp: Date.now() + 1, + }; - setMessages(prev => [...prev, userMessage, assistantPlaceholderMessage]); - setChatInput(''); - setIsLoading(true); - setIsStreamingArtifact(true); - - // 1. Initialiser la connexion de streaming - const uniquePortId = `dodai_canvas_artifact_stream_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; - streamingPortId.current = uniquePortId; - const port = chrome.runtime.connect({ name: uniquePortId }); - streamingPort.current = port; - - // 2. Configurer les écouteurs de port - port.onMessage.addListener((message: GenerateDodaiCanvasArtifactStreamResponse) => { - switch (message.type) { - case StreamEventType.STREAM_START: - console.log("[DodaiCanvas] Début du streaming d'artefact", message.model); - // Initialiser l'artefact placeholder - setCurrentArtifact({ - currentIndex: 0, - contents: [ - { - type: 'text', // Default to text - title: `En cours: ${generateTitleFromMarkdown(currentStreamingPrompt.current || prompt)}`, - fullMarkdown: '', - }, - ], - }); - setMessages(prev => - prev.map(msg => - msg.id === assistantPlaceholderId - ? { - ...msg, - content: `Génération avec ${message.model || 'le modèle par défaut'} commencée...`, - model: message.model, // Sauvegarder le modèle ici aussi - } - : msg, - ), - ); - break; - - case StreamEventType.STREAM_CHUNK: - if (message.chunk) { - setCurrentArtifact(prevArtifact => { - if (!prevArtifact) return null; // Should not happen if STREAM_START was handled - const currentContent = prevArtifact.contents[0] as ArtifactMarkdownV3; - const newMarkdown = currentContent.fullMarkdown + message.chunk; - return { - ...prevArtifact, - contents: [ - { - ...currentContent, - fullMarkdown: newMarkdown, - title: generateTitleFromMarkdown(newMarkdown), // Update title as content grows - }, - ], - }; + setMessages(prev => [...prev, userMessage, assistantPlaceholderMessage]); + setChatInput(''); + setIsLoading(true); + setIsStreamingArtifact(true); + + // 1. Initialiser la connexion de streaming + const uniquePortId = `dodai_canvas_artifact_stream_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; + streamingPortId.current = uniquePortId; + const port = chrome.runtime.connect({ name: uniquePortId }); + streamingPort.current = port; + + // 2. Configurer les écouteurs de port + port.onMessage.addListener((message: GenerateDodaiCanvasArtifactStreamResponse) => { + switch (message.type) { + case StreamEventType.STREAM_START: + console.log("[DodaiCanvas] Début du streaming d'artefact", message.model); + // Initialiser l'artefact placeholder + setCurrentArtifact({ + currentIndex: 0, + contents: [ + { + type: 'text', // Default to text + title: `En cours: ${generateTitleFromMarkdown(currentStreamingPrompt.current || prompt)}`, + fullMarkdown: '', + }, + ], }); - } - break; + setMessages(prev => + prev.map(msg => + msg.id === assistantPlaceholderId + ? { + ...msg, + content: `Génération avec ${message.model || 'le modèle par défaut'} commencée...`, + model: message.model, // Sauvegarder le modèle ici aussi + } + : msg, + ), + ); + break; + + case StreamEventType.STREAM_CHUNK: + if (message.chunk) { + setCurrentArtifact(prevArtifact => { + if (!prevArtifact) return null; // Should not happen if STREAM_START was handled + const currentContent = prevArtifact.contents[0] as ArtifactMarkdownV3; + const newMarkdown = currentContent.fullMarkdown + message.chunk; + return { + ...prevArtifact, + contents: [ + { + ...currentContent, + fullMarkdown: newMarkdown, + title: generateTitleFromMarkdown(newMarkdown), // Update title as content grows + }, + ], + }; + }); + } + break; - case StreamEventType.STREAM_END: - console.log("[DodaiCanvas] Fin du streaming d'artefact, succès:", message.success); - if (message.success) { - if (currentArtifact) { - setArtifactHistory(prev => [...prev, currentArtifact]); + case StreamEventType.STREAM_END: + console.log("[DodaiCanvas] Fin du streaming d'artefact, succès:", message.success); + if (message.success) { + if (currentArtifact) { + setArtifactHistory(prev => [...prev, currentArtifact]); + } + // Ne mettre à jour le message que si aucune réponse chat n'a été reçue + setMessages(prev => { + const hasReceivedChatResponse = prev.some( + msg => + msg.id === assistantPlaceholderId && + msg.content !== "Génération de l'artefact en cours..." && + !msg.content.includes('Génération avec') && + !msg.content.includes('Artefact généré avec succès !'), + ); + + if (hasReceivedChatResponse) { + // Une réponse chat a déjà été reçue, ne pas écraser + // Mais quand même appeler le callback pour signaler la fin + onChatTurnEndCallbackRef.current?.(prev, message.model); + return prev; + } + + const updatedMessages = prev.map((msg: Message) => + msg.id === assistantPlaceholderId + ? { + ...msg, + content: `Artefact généré avec succès ! (Modèle: ${message.model || 'inconnu'})\nVous pouvez le consulter dans le panneau de droite.`, + model: message.model, + isStreaming: false, + } + : msg, + ); + + // Appeler le callback avec les messages mis à jour + onChatTurnEndCallbackRef.current?.(updatedMessages, message.model); + return updatedMessages; + }); + cleanupStreamingConnection(); + } else { + setMessages(prev => + prev.map(msg => + msg.id === assistantPlaceholderId + ? { + ...msg, + content: `Erreur lors de la génération : ${message.error || 'Erreur inconnue'}`, + } + : msg, + ), + ); + setCurrentArtifact(prev => + prev + ? { + ...prev, + contents: [{ ...(prev.contents[0] as ArtifactMarkdownV3), title: 'Erreur de génération' }], + } + : null, + ); + cleanupStreamingConnection(); } - const finalMessages = messages.map((msg: Message) => - msg.id === assistantPlaceholderId && !msg.content.includes('Réponse chat reçue') - ? { - ...msg, - content: `Artefact généré avec succès ! (Modèle: ${message.model || 'inconnu'})\nVous pouvez le consulter dans le panneau de droite.`, - model: message.model, - isStreaming: false, - } - : msg, - ); - setMessages(finalMessages); - onChatTurnEndCallbackRef.current?.(finalMessages, message.model); - } else { + break; + + case StreamEventType.ARTIFACT_CHAT_RESPONSE: { + console.log('[DodaiCanvas] Réponse chat reçue:', message.chatResponse); + setMessages(prev => { + const updatedMessages = prev.map((msg: Message) => + msg.id === assistantPlaceholderId + ? { + ...msg, + content: message.chatResponse + ? `${message.chatResponse}` + : `Artefact généré avec succès ! (Modèle: ${message.model || 'inconnu'})\nVous pouvez le consulter dans le panneau de droite.`, + model: message.model, + isStreaming: false, + } + : msg, + ); + // Appeler le callback avec les messages mis à jour + onChatTurnEndCallbackRef.current?.(updatedMessages, message.model); + return updatedMessages; + }); + // Note: cleanupStreamingConnection sera appelé dans STREAM_END + break; + } + case StreamEventType.STREAM_ERROR: { + console.error("[DodaiCanvas] Erreur de streaming d'artefact:", message.error); setMessages(prev => prev.map(msg => msg.id === assistantPlaceholderId ? { ...msg, - content: `Erreur lors de la génération : ${message.error || 'Erreur inconnue'}`, + content: `Erreur de streaming : ${message.error || 'Erreur inconnue'}`, + model: message.model, + isStreaming: false, } : msg, ), ); - setCurrentArtifact(prev => - prev - ? { - ...prev, - contents: [{ ...(prev.contents[0] as ArtifactMarkdownV3), title: 'Erreur de génération' }], - } - : null, - ); - cleanupStreamingConnection(); // Nettoyer en cas d'erreur de STREAM_END + cleanupStreamingConnection(); + break; + } + default: { + console.warn("[DodaiCanvas] Message de streaming d'artefact inconnu:", message); } - break; - - case StreamEventType.ARTIFACT_CHAT_RESPONSE: { - console.log('[DodaiCanvas] Réponse chat reçue:', message.chatResponse); - const updatedMessages = messages.map((msg: Message) => - msg.id === assistantPlaceholderId - ? { - ...msg, - content: message.chatResponse - ? `${message.chatResponse} (Modèle: ${message.model || 'inconnu'})` - : `Artefact généré avec succès ! (Modèle: ${message.model || 'inconnu'})\nVous pouvez le consulter dans le panneau de droite.`, - model: message.model, - isStreaming: false, - } - : msg, - ); - setMessages(updatedMessages); - onChatTurnEndCallbackRef.current?.(updatedMessages, message.model); - cleanupStreamingConnection(); - break; } - case StreamEventType.STREAM_ERROR: { - console.error("[DodaiCanvas] Erreur de streaming d'artefact:", message.error); + }); + + port.onDisconnect.addListener(() => { + console.log("[DodaiCanvas] Port de streaming d'artefact déconnecté"); + // Only consider it an error if it was not a clean STREAM_END + if (isLoading && isStreamingArtifact) { setMessages(prev => prev.map(msg => - msg.id === assistantPlaceholderId + msg.id === assistantPlaceholderId && msg.content.includes('...') ? { ...msg, - content: `Erreur de streaming : ${message.error || 'Erreur inconnue'}`, - model: message.model, - isStreaming: false, + content: "Connexion perdue pendant la génération de l'artefact.", } : msg, ), ); - cleanupStreamingConnection(); - break; - } - default: { - console.warn("[DodaiCanvas] Message de streaming d'artefact inconnu:", message); } - } - }); + cleanupStreamingConnection(); // Ensure cleanup happens + }); - port.onDisconnect.addListener(() => { - console.log("[DodaiCanvas] Port de streaming d'artefact déconnecté"); - // Only consider it an error if it was not a clean STREAM_END - if (isLoading && isStreamingArtifact) { + // 3. Envoyer la requête au background + try { + const historyToSend: ChatHistoryMessage[] = messages + .filter(msg => msg.id !== assistantPlaceholderId) // Exclude current placeholder + .map(msg => ({ + role: msg.role === 'user' ? 'user' : 'assistant', + content: msg.content, + })); + + // Wait a bit for the port to be registered in the background service + setTimeout(() => { + chrome.runtime.sendMessage( + { + type: MessageType.GENERATE_DODAI_CANVAS_ARTIFACT_STREAM_REQUEST, + payload: { + prompt: prompt, + history: historyToSend, + portId: uniquePortId, + modelName: selectedDodaiModel, // Pass selected model + }, + }, + response => { + if (chrome.runtime.lastError) { + console.error( + "[DodaiCanvas] Erreur lors de l'envoi du message de streaming:", + chrome.runtime.lastError, + ); + setMessages(prev => + prev.map(msg => + msg.id === assistantPlaceholderId + ? { + ...msg, + content: `Erreur de communication (envoi): ${chrome.runtime.lastError?.message || 'Inconnue'}`, + } + : msg, + ), + ); + cleanupStreamingConnection(); + return; + } + // The initial response from sendMessage is just an ack or immediate error, not the stream itself. + if (response && !response.success) { + console.error('[DodaiCanvas] Le background a refusé la requête de streaming:', response.error); + setMessages(prev => + prev.map(msg => + msg.id === assistantPlaceholderId + ? { + ...msg, + content: `Erreur (refus background): ${response.error || 'Inconnue'}`, + } + : msg, + ), + ); + cleanupStreamingConnection(); + } + // else: Streaming setup was successful on background side, waiting for port messages. + }, + ); + }, 100); // 100ms delay to ensure port registration + } catch (error) { + console.error("[DodaiCanvas] Erreur lors de la préparation de l'envoi du message de streaming:", error); setMessages(prev => prev.map(msg => - msg.id === assistantPlaceholderId && msg.content.includes('...') + msg.id === assistantPlaceholderId ? { ...msg, - content: "Connexion perdue pendant la génération de l'artefact.", + content: `Erreur (préparation): ${error instanceof Error ? error.message : String(error)}`, } : msg, ), ); + cleanupStreamingConnection(); } - cleanupStreamingConnection(); // Ensure cleanup happens - }); - - // 3. Envoyer la requête au background - try { - const historyToSend: ChatHistoryMessage[] = messages - .filter(msg => msg.id !== assistantPlaceholderId) // Exclude current placeholder - .map(msg => ({ - role: msg.role === 'user' ? 'user' : 'assistant', - content: msg.content, - })); - - chrome.runtime.sendMessage( - { - type: MessageType.GENERATE_DODAI_CANVAS_ARTIFACT_STREAM_REQUEST, - payload: { - prompt: prompt, - history: historyToSend, - portId: uniquePortId, - modelName: selectedDodaiModel, // Pass selected model - }, - }, - response => { - if (chrome.runtime.lastError) { - console.error("[DodaiCanvas] Erreur lors de l'envoi du message de streaming:", chrome.runtime.lastError); - setMessages(prev => - prev.map(msg => - msg.id === assistantPlaceholderId - ? { - ...msg, - content: `Erreur de communication (envoi): ${chrome.runtime.lastError?.message || 'Inconnue'}`, - } - : msg, - ), - ); - cleanupStreamingConnection(); - return; - } - // The initial response from sendMessage is just an ack or immediate error, not the stream itself. - if (response && !response.success) { - console.error('[DodaiCanvas] Le background a refusé la requête de streaming:', response.error); - setMessages(prev => - prev.map(msg => - msg.id === assistantPlaceholderId - ? { - ...msg, - content: `Erreur (refus background): ${response.error || 'Inconnue'}`, - } - : msg, - ), - ); - cleanupStreamingConnection(); - } - // else: Streaming setup was successful on background side, waiting for port messages. - }, - ); - } catch (error) { - console.error("[DodaiCanvas] Erreur lors de la préparation de l'envoi du message de streaming:", error); - setMessages(prev => - prev.map(msg => - msg.id === assistantPlaceholderId - ? { - ...msg, - content: `Erreur (préparation): ${error instanceof Error ? error.message : String(error)}`, - } - : msg, - ), - ); - cleanupStreamingConnection(); - } - }; + }, + [isLoading, isStreamingArtifact, messages, selectedDodaiModel, currentArtifact, cleanupStreamingConnection], + ); // Fonction pour mettre à jour le contenu de l'artefact actuel via BlockNote const updateCurrentArtifactContent = (newMarkdown: string) => { @@ -538,6 +580,189 @@ export const DodaiProvider: React.FC = ({ children }) => { setIsLoading(false); }; + // NEW: Function for simple chat messaging (adapted from useSimpleTextChat) + const sendSimpleChatMessage = useCallback( + async (prompt: string) => { + if (!prompt.trim()) return; + + currentStreamingPrompt.current = prompt; + + const userMessage: Message = { + id: uuidv4(), + role: 'user', + content: prompt, + timestamp: Date.now(), + }; + + const assistantMsgId = uuidv4(); + currentAssistantMessageId.current = assistantMsgId; + const assistantPlaceholderMessage: Message = { + id: assistantMsgId, + role: 'assistant', + content: '', // Start with empty content, will be filled by stream + timestamp: Date.now() + 1, + isStreaming: true, + }; + + setMessages(prev => [...prev, userMessage, assistantPlaceholderMessage]); + setChatInput(''); + setIsLoading(true); + + // Setup streaming port + const uniquePortId = `simple_chat_stream_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; + streamingPortId.current = uniquePortId; + const port = chrome.runtime.connect({ name: uniquePortId }); + streamingPort.current = port; + + port.onMessage.addListener((msg: GenerateDodaiCanvasArtifactStreamResponse) => { + switch (msg.type) { + case StreamEventType.STREAM_START: + console.log('[DodaiCanvas] Simple chat stream started:', msg.model); + setMessages(prev => prev.map(m => (m.id === assistantMsgId ? { ...m, model: msg.model, content: '' } : m))); + break; + case StreamEventType.STREAM_CHUNK: + if (msg.chunk) { + setMessages(prev => + prev.map(m => + m.id === assistantMsgId ? { ...m, content: m.content + msg.chunk, isStreaming: true } : m, + ), + ); + } + break; + case StreamEventType.STREAM_END: { + console.log('[DodaiCanvas] Simple chat stream ended, success:', msg.success); + setMessages(prev => prev.map(m => (m.id === assistantMsgId ? { ...m, isStreaming: false } : m))); + + if (!msg.success) { + setMessages(prev => + prev.map(m => + m.id === assistantMsgId + ? { + ...m, + content: m.content + `\nErreur: ${msg.error || 'Erreur inconnue'}`, + isStreaming: false, + } + : m, + ), + ); + } + + // Call onChatTurnEnd for simple chat + onChatTurnEndCallbackRef.current?.( + messages.map(m => (m.id === assistantMsgId ? { ...m, isStreaming: false, model: msg.model } : m)), + msg.model, + ); + cleanupStreamingConnection(); + break; + } + case StreamEventType.STREAM_ERROR: + console.error('[DodaiCanvas] Simple chat stream error:', msg.error); + setMessages(prev => + prev.map(m => + m.id === assistantMsgId + ? { + ...m, + content: `Erreur de streaming: ${msg.error || 'Erreur inconnue'}`, + isStreaming: false, + } + : m, + ), + ); + cleanupStreamingConnection(); + break; + default: + console.warn('[DodaiCanvas] Unknown simple chat stream message:', msg); + } + }); + + port.onDisconnect.addListener(() => { + if (isLoading) { + console.warn('[DodaiCanvas] Simple chat port disconnected unexpectedly.'); + setMessages(prev => + prev.map(m => + m.id === currentAssistantMessageId.current && m.isStreaming + ? { ...m, content: m.content + '\n(Connexion perdue)', isStreaming: false } + : m, + ), + ); + } + cleanupStreamingConnection(); + }); + + const chatHistoryForPayload = messages + .filter(m => m.id !== assistantMsgId) // Exclude current placeholder + .map(m => ({ role: m.role, content: m.content })); + + // Wait longer for the port to be registered in the background service + setTimeout(() => { + chrome.runtime.sendMessage( + { + type: MessageType.AI_CHAT_REQUEST, + payload: { + message: prompt, + chatHistory: chatHistoryForPayload, + streamHandler: true, + portId: uniquePortId, + modelName: selectedDodaiModel, // Pass the model name + }, + }, + response => { + if (chrome.runtime.lastError) { + console.error('[DodaiCanvas] Simple chat SendMessage error:', chrome.runtime.lastError); + setMessages(prev => + prev.map(m => + m.id === assistantMsgId + ? { + ...m, + content: `Erreur (envoi): ${chrome.runtime.lastError?.message || 'Inconnue'}`, + isStreaming: false, + } + : m, + ), + ); + cleanupStreamingConnection(); + return; + } + if (response && !response.success && !response.streaming) { + console.error('[DodaiCanvas] Background refused simple chat request:', response.error); + setMessages(prev => + prev.map(m => + m.id === assistantMsgId + ? { + ...m, + content: `Erreur (refus BG): ${response.error || 'Inconnue'}`, + isStreaming: false, + } + : m, + ), + ); + cleanupStreamingConnection(); + } + }, + ); + }, 500); // Increased delay to 500ms + }, + [messages, isLoading, selectedDodaiModel, cleanupStreamingConnection], + ); + + // NEW: Unified send function that routes to artifact or simple chat based on mode + const sendMessage = useCallback( + async (prompt: string) => { + if (!prompt.trim()) return; + if (isLoading || isStreamingArtifact) { + console.warn("[DodaiCanvas] Tentative d'envoi de message pendant une opération en cours."); + return; + } + + if (isArtifactModeActive) { + await sendPromptAndGenerateArtifact(prompt); + } else { + await sendSimpleChatMessage(prompt); + } + }, + [isArtifactModeActive, isLoading, isStreamingArtifact, sendPromptAndGenerateArtifact, sendSimpleChatMessage], + ); + // Implementation for resetChatAndArtifact const resetChatAndArtifact = useCallback( async (onBeforeResetCallback?: () => Promise) => { @@ -552,6 +777,8 @@ export const DodaiProvider: React.FC = ({ children }) => { setChatInput(''); setIsLoading(false); setIsStreamingArtifact(false); + // Keep the current mode active when resetting (don't force artifact mode) + // setIsArtifactModeActive(true); // Removed to preserve user's current mode choice if (streamingPort.current) { cleanupStreamingConnection(); @@ -573,9 +800,12 @@ export const DodaiProvider: React.FC = ({ children }) => { isLoading, setIsLoading, isStreamingArtifact, + isArtifactModeActive, // NEW: Expose artifact mode state + setIsArtifactModeActive, // NEW: Expose artifact mode setter selectedDodaiModel, setSelectedDodaiModel, sendPromptAndGenerateArtifact, + sendMessage, // NEW: Unified send function updateCurrentArtifactContent, modifyCurrentArtifact, cancelCurrentStreaming, diff --git a/pages/main/src/features/text-chat/components/TextChatView.tsx b/pages/main/src/features/text-chat/components/TextChatView.tsx deleted file mode 100644 index aae012a..0000000 --- a/pages/main/src/features/text-chat/components/TextChatView.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import type React from 'react'; -// import type { Message } from '../../canvas/types'; // No longer directly needed here -import { ChatMessage } from '../../canvas/components/ChatMessage'; -import { ChatInput } from '../../canvas/components/ChatInput'; -import { useSimpleTextChat } from '../hooks/useSimpleTextChat'; // Import the new hook - -// TextChatViewProps is no longer needed as props come from the hook -// interface TextChatViewProps { -// messages: Message[]; -// chatInput: string; -// setChatInput: (value: string) => void; -// handleSubmit: (e: React.FormEvent, promptToSend?: string) => Promise; -// isLoading: boolean; -// messagesEndRef: React.RefObject; -// } - -const TextChatView: React.FC = () => { - const { messages, chatInput, setChatInput, handleSubmit, isLoading, messagesEndRef } = useSimpleTextChat(); - - // Static example messages are removed as the hook handles messages - // const exampleMessages: Message[] = messages.length === 0 && !isLoading ? [ - // { id: '1', role: 'assistant', content: 'Bonjour! Ceci est une vue de chat textuel.', timestamp: Date.now() }, - // { id: '2', role: 'user', content: 'Super! Comment ça marche?', timestamp: Date.now() + 1000 }, - // { id: '3', role: 'assistant', content: 'Vous pouvez taper votre message ci-dessous.', timestamp: Date.now() + 2000 }, - // ] : messages; - - return ( -
- {/* Messages area */} -
- {messages.map(message => ( - - ))} -
-
- - {/* Input area */} -
- -
-
- ); -}; - -export default TextChatView; diff --git a/pages/main/src/features/text-chat/hooks/useSimpleTextChat.ts b/pages/main/src/features/text-chat/hooks/useSimpleTextChat.ts deleted file mode 100644 index b53ea22..0000000 --- a/pages/main/src/features/text-chat/hooks/useSimpleTextChat.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { useState, useCallback, useRef, useEffect } from 'react'; -import { v4 as uuidv4 } from 'uuid'; -import type { Message } from '../../canvas/types'; // Assuming Message type is in canvas/types -import { - MessageType, - StreamEventType, - type ChatHistoryMessage, - type GenerateDodaiCanvasArtifactStreamResponse, // More specific type for port messages -} from '../../../../../../chrome-extension/src/background/types'; -import { dodaiCanvasConfigStorage } from '@extension/storage'; // Import storage - -export interface UseSimpleTextChatReturn { - messages: Message[]; - chatInput: string; - isLoading: boolean; - messagesEndRef: React.RefObject; // Allow null for initial ref value - setChatInput: React.Dispatch>; - handleSubmit: (e: React.FormEvent, promptToSend?: string) => Promise; -} - -export function useSimpleTextChat(): UseSimpleTextChatReturn { - const [messages, setMessages] = useState([]); - const [chatInput, setChatInput] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const streamingPort = useRef(null); - const streamingPortId = useRef(null); - const messagesEndRef = useRef(null); - const currentAssistantMessageId = useRef(null); - - const cleanupStreamingConnection = useCallback(() => { - if (streamingPort.current) { - try { - streamingPort.current.disconnect(); - } catch (e) { - console.warn('[SimpleTextChat] Error disconnecting port:', e); - } - streamingPort.current = null; - streamingPortId.current = null; - } - setIsLoading(false); - currentAssistantMessageId.current = null; - }, []); - - useEffect(() => { - return () => { - cleanupStreamingConnection(); - }; - }, [cleanupStreamingConnection]); - - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages, isLoading]); - - const handleSubmit = useCallback( - async (e: React.FormEvent, promptToSend?: string) => { - e.preventDefault(); - const currentInput = (promptToSend || chatInput).trim(); - if (!currentInput || isLoading) return; - - const userMessage: Message = { - id: uuidv4(), - role: 'user', - content: currentInput, - timestamp: Date.now(), - }; - setMessages(prev => [...prev, userMessage]); - setChatInput(''); - setIsLoading(true); - - const assistantMsgId = uuidv4(); - currentAssistantMessageId.current = assistantMsgId; - const assistantPlaceholderMessage: Message = { - id: assistantMsgId, - role: 'assistant', - content: '', // Start with empty content, will be filled by stream - timestamp: Date.now() + 1, - isStreaming: true, - }; - setMessages(prev => [...prev, assistantPlaceholderMessage]); - - const uniquePortId = `simple_text_chat_stream_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; - streamingPortId.current = uniquePortId; - const port = chrome.runtime.connect({ name: uniquePortId }); - streamingPort.current = port; - - // Read selected model from storage for the request - let modelToUseForRequest: string | undefined; - try { - const canvasSettings = await dodaiCanvasConfigStorage.get(); - modelToUseForRequest = canvasSettings.selectedModel || undefined; - } catch (err) { - console.warn('[SimpleTextChat] Could not read selected model from dodaiCanvasConfigStorage', err); - // Proceed without a specific model, background will use its default - } - - port.onMessage.addListener((msg: GenerateDodaiCanvasArtifactStreamResponse) => { - switch (msg.type) { - case StreamEventType.STREAM_START: - console.log('[SimpleTextChat] Stream started:', msg.model); - setMessages(prev => prev.map(m => (m.id === assistantMsgId ? { ...m, model: msg.model, content: '' } : m))); - break; - case StreamEventType.STREAM_CHUNK: - if (msg.chunk) { - setMessages(prev => - prev.map(m => - m.id === assistantMsgId ? { ...m, content: m.content + msg.chunk, isStreaming: true } : m, - ), - ); - } - break; - case StreamEventType.STREAM_END: - console.log('[SimpleTextChat] Stream ended, success:', msg.success); - setMessages(prev => prev.map(m => (m.id === assistantMsgId ? { ...m, isStreaming: false } : m))); - if (!msg.success) { - setMessages(prev => - prev.map(m => - m.id === assistantMsgId - ? { - ...m, - content: m.content + `\nErreur: ${msg.error || 'Erreur inconnue'}`, - isStreaming: false, - } - : m, - ), - ); - } - cleanupStreamingConnection(); - break; - case StreamEventType.STREAM_ERROR: - console.error('[SimpleTextChat] Stream error:', msg.error); - setMessages(prev => - prev.map(m => - m.id === assistantMsgId - ? { - ...m, - content: `Erreur de streaming: ${msg.error || 'Erreur inconnue'}`, - isStreaming: false, - } - : m, - ), - ); - cleanupStreamingConnection(); - break; - default: - console.warn('[SimpleTextChat] Unknown stream message:', msg); - } - }); - - port.onDisconnect.addListener(() => { - if (isLoading) { - // Disconnected unexpectedly - console.warn('[SimpleTextChat] Port disconnected unexpectedly.'); - setMessages(prev => - prev.map(m => - m.id === currentAssistantMessageId.current && m.isStreaming - ? { ...m, content: m.content + '\n(Connexion perdue)', isStreaming: false } - : m, - ), - ); - } - cleanupStreamingConnection(); - }); - - const chatHistoryForPayload: ChatHistoryMessage[] = messages - .filter(m => m.id !== assistantMsgId) // Exclude current placeholder/streaming message - .map(m => ({ role: m.role, content: m.content })); - - chrome.runtime.sendMessage( - { - type: MessageType.AI_CHAT_REQUEST, - payload: { - message: currentInput, - chatHistory: chatHistoryForPayload, - streamHandler: true, - portId: uniquePortId, - modelName: modelToUseForRequest, // Pass the model name - // pageContent: undefined, // Not needed for simple chat - }, - }, - response => { - if (chrome.runtime.lastError) { - console.error('[SimpleTextChat] SendMessage error:', chrome.runtime.lastError); - setMessages(prev => - prev.map(m => - m.id === assistantMsgId - ? { - ...m, - content: `Erreur (envoi): ${chrome.runtime.lastError?.message || 'Inconnue'}`, - isStreaming: false, - } - : m, - ), - ); - cleanupStreamingConnection(); - return; - } - if (response && !response.success && !response.streaming) { - // If it wasn't a streaming setup success, and not a general success either - console.error('[SimpleTextChat] Background refused request:', response.error); - setMessages(prev => - prev.map(m => - m.id === assistantMsgId - ? { - ...m, - content: `Erreur (refus BG): ${response.error || 'Inconnue'}`, - isStreaming: false, - } - : m, - ), - ); - cleanupStreamingConnection(); - } - // If response.streaming is true, we wait for port messages. - }, - ); - }, - [chatInput, isLoading, messages, cleanupStreamingConnection], // Added messages to dependency array for chatHistoryForPayload - ); - - return { - messages, - chatInput, - isLoading, - messagesEndRef, - setChatInput, - handleSubmit, - }; -} diff --git a/repomix.config.json b/repomix.config.json index d522d4e..b1617ee 100644 --- a/repomix.config.json +++ b/repomix.config.json @@ -5,7 +5,7 @@ "directoryStructure": true, "removeComments": false, "showLineNumbers": false, - "compress": true + "compress": false }, "ignore": { "customPatterns": ["**/*.svg"] From 585fbec09fe9cc51201f9a3421ca68a33b9ecb76 Mon Sep 17 00:00:00 2001 From: drewano Date: Fri, 23 May 2025 14:30:58 +0200 Subject: [PATCH 9/9] feat: enhance chat session management and UI interactions - Updated MainLayout to save current chat sessions with the current artifact and reset active sessions appropriately. - Improved CanvasView to handle new conversation creation and reset chat state, enhancing user experience. - Added a new button in DodaiCanvasHistoryPanel for initiating new conversations, improving accessibility. - Enhanced useDodaiCanvasHistory to support saving sessions as new and resetting active sessions, streamlining chat management. - Added logging for better debugging and tracking of chat session states and actions. --- .../main/src/components/layout/MainLayout.tsx | 11 ++- pages/main/src/features/canvas/CanvasView.tsx | 18 ++++- .../components/DodaiCanvasHistoryPanel.tsx | 14 +++- .../features/canvas/contexts/DodaiContext.tsx | 5 ++ .../canvas/hooks/useDodaiCanvasHistory.ts | 73 +++++++++++++++---- 5 files changed, 101 insertions(+), 20 deletions(-) diff --git a/pages/main/src/components/layout/MainLayout.tsx b/pages/main/src/components/layout/MainLayout.tsx index 2825691..54b2cfb 100644 --- a/pages/main/src/components/layout/MainLayout.tsx +++ b/pages/main/src/components/layout/MainLayout.tsx @@ -16,10 +16,11 @@ const MainLayout: React.FC = () => { resetChatAndArtifact, messages: dodaiContextMessages, selectedDodaiModel: dodaiContextSelectedModel, + currentArtifact: dodaiContextCurrentArtifact, } = useDodai(); const { notes, addNote, getNote } = useNotes(); const { handleCreateNewNote } = useNoteSelection(notes, getNote, addNote); - const { saveCurrentChatSession } = useDodaiCanvasHistory(); + const { resetActiveSession, saveCurrentChatSessionAsNew } = useDodaiCanvasHistory(); // Define individual button configurations const newCanvasButton: NavItemProps = { @@ -31,10 +32,16 @@ const MainLayout: React.FC = () => { await resetChatAndArtifact(async () => { if (dodaiContextMessages.length > 0) { console.log('[MainLayout] Saving current session before reset...'); - await saveCurrentChatSession(dodaiContextMessages, null, dodaiContextSelectedModel || undefined); + 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(); } }); diff --git a/pages/main/src/features/canvas/CanvasView.tsx b/pages/main/src/features/canvas/CanvasView.tsx index 04deb97..955a097 100644 --- a/pages/main/src/features/canvas/CanvasView.tsx +++ b/pages/main/src/features/canvas/CanvasView.tsx @@ -28,6 +28,7 @@ const CanvasViewContent = () => { deleteConversation, saveCurrentChatSession, renameConversationInHistory, + resetActiveSession, } = useDodaiCanvasHistory(); const { notes } = useNotes(); @@ -56,6 +57,7 @@ const CanvasViewContent = () => { 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); @@ -114,12 +116,23 @@ const CanvasViewContent = () => { [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.'); - saveCurrentChatSession(finalMessages, currentArtifact, modelUsed || undefined); + 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 @@ -162,6 +175,7 @@ const CanvasViewContent = () => { onDeleteConversation={handleDeleteConversation} onRenameConversation={handleRenameConversation} onClose={() => setShowHistoryPanel(false)} + onNewConversation={handleNewConversation} /> )}
diff --git a/pages/main/src/features/canvas/components/DodaiCanvasHistoryPanel.tsx b/pages/main/src/features/canvas/components/DodaiCanvasHistoryPanel.tsx index 7223f40..84c01ed 100644 --- a/pages/main/src/features/canvas/components/DodaiCanvasHistoryPanel.tsx +++ b/pages/main/src/features/canvas/components/DodaiCanvasHistoryPanel.tsx @@ -1,7 +1,7 @@ import type React from 'react'; import { useState } from 'react'; import type { ChatConversation } from '@extension/storage'; -import { XIcon, Trash2Icon, Edit3Icon, CheckIcon, XCircleIcon } from 'lucide-react'; +import { XIcon, Trash2Icon, Edit3Icon, CheckIcon, XCircleIcon, PlusIcon } from 'lucide-react'; interface DodaiCanvasHistoryPanelProps { chatHistory: ChatConversation[]; @@ -9,6 +9,7 @@ interface DodaiCanvasHistoryPanelProps { onLoadConversation: (id: string) => void; onDeleteConversation: (id: string) => void; onRenameConversation: (id: string, newName: string) => Promise; + onNewConversation?: () => void; onClose: () => void; } @@ -21,6 +22,7 @@ export const DodaiCanvasHistoryPanel: React.FC = ( onLoadConversation, onDeleteConversation, onRenameConversation, + onNewConversation, onClose, }) => { const [editingConversationId, setEditingConversationId] = useState(null); @@ -51,7 +53,15 @@ export const DodaiCanvasHistoryPanel: React.FC = ( {/* En-tête du panneau */}

Historique des Chats

-
+
+ {onNewConversation && ( + + )}