Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 37 additions & 15 deletions chrome-extension/src/background/handlers/message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export class MessageHandler {
string,
(message: BaseRuntimeMessage, sender: chrome.runtime.MessageSender) => Promise<unknown>
> = {
[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),
Expand All @@ -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) =>
Expand Down Expand Up @@ -422,8 +422,12 @@ export class MessageHandler {
/**
* Gestionnaire pour les requêtes de chat avec l'agent AI
*/
private async handleAiChatRequest(message: AIChatRequestMessage): Promise<ChatResponse> {
private async handleAiChatRequest(
message: AIChatRequestMessage,
sender?: chrome.runtime.MessageSender,
): Promise<ChatResponse> {
logger.debug('Reçu AI_CHAT_REQUEST', message.payload);
logger.debug('Request sender:', sender);

// Vérifier si on veut du streaming
const {
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<RagChatResponse> {
private async handleRagChatRequest(
message: RagChatRequestMessage,
sender?: chrome.runtime.MessageSender,
): Promise<RagChatResponse> {
logger.debug('Reçu RAG_CHAT_REQUEST', message.payload);
const { message: userInput, chatHistory = [], streamHandler = false, portId, selectedModel } = message.payload;

Expand Down Expand Up @@ -1075,6 +1096,7 @@ Instruction de modification: ${prompt}`;
};
}
const { port } = streamingPortInfo;
let effectiveModelName: string | undefined;

try {
const isReady = await agentService.isAgentReady();
Expand All @@ -1089,12 +1111,12 @@ Instruction de modification: ${prompt}`;

const history = chatHistoryPayload ? convertChatHistory(chatHistoryPayload) : [];
const settings = await aiAgentStorage.get();
const modelToUse = modelNameFromPayload || settings.selectedModel;
effectiveModelName = modelNameFromPayload || settings.selectedModel;

port.postMessage({ type: StreamEventType.STREAM_START, model: modelToUse });
port.postMessage({ type: StreamEventType.STREAM_START, model: effectiveModelName });

agentService
.streamArtifactGeneration(prompt, history, port, modelToUse)
.streamArtifactGeneration(prompt, history, port, effectiveModelName)
.then(() => {
logger.debug('[DodaiCanvasStream] Streaming terminé avec succès par agentService pour le port', portId);
})
Expand All @@ -1105,7 +1127,7 @@ Instruction de modification: ${prompt}`;
type: StreamEventType.STREAM_ERROR,
error: error instanceof Error ? error.message : "Erreur inconnue durant le streaming d'artefact",
});
port.postMessage({ type: StreamEventType.STREAM_END, success: false, model: modelToUse });
port.postMessage({ type: StreamEventType.STREAM_END, success: false, model: effectiveModelName });
} catch (portError) {
logger.warn(
"[DodaiCanvasStream] Impossible d'envoyer l'erreur sur le port après échec agentService:",
Expand All @@ -1122,7 +1144,7 @@ Instruction de modification: ${prompt}`;
type: StreamEventType.STREAM_ERROR,
error: error instanceof Error ? error.message : "Erreur inconnue avant le lancement du streaming d'artefact",
});
port.postMessage({ type: StreamEventType.STREAM_END, success: false, model: modelToUse });
port.postMessage({ type: StreamEventType.STREAM_END, success: false, model: effectiveModelName });
} catch (portError) {
logger.warn("[DodaiCanvasStream] Impossible d'envoyer l'erreur sur le port après erreur majeure:", portError);
}
Expand Down
99 changes: 75 additions & 24 deletions chrome-extension/src/background/services/agent-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export class StreamingService {
if (
port.name.startsWith('ai_streaming_') ||
port.name.startsWith('rag_streaming_') ||
port.name.startsWith('dodai_canvas_artifact_stream_')
port.name.startsWith('dodai_canvas_artifact_stream_') ||
port.name.startsWith('simple_text_chat_stream_') ||
port.name.startsWith('simple_chat_stream_')
) {
const portId = port.name;
logger.debug(`Nouvelle connexion de streaming établie: ${portId}`);
Expand Down
14 changes: 14 additions & 0 deletions packages/storage/lib/impl/chat-history-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +32,7 @@ export interface ChatConversation {
createdAt: number;
updatedAt: number;
model?: string;
artifact?: ChatArtifactWithHistory | null;
}

// État initial pour l'historique des conversations
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/lib/components/DodaiSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ const DodaiSidebar: React.FC<DodaiSidebarProps> = ({

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}`;
};

Expand Down
43 changes: 32 additions & 11 deletions pages/main/src/components/layout/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,50 @@ import { BookText, LayoutDashboard, PlusCircle, FilePlus2 } from 'lucide-react';
import { useDodai } from '@src/features/canvas/contexts/DodaiContext';
import { useNotes } from '@src/features/notes/hooks/useNotes';
import { useNoteSelection } from '@src/features/notes/hooks/useNoteSelection';
import { useDodaiCanvasHistory } from '@src/features/canvas/hooks/useDodaiCanvasHistory';

const MainLayout: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();

const { resetChatAndArtifact } = useDodai();
const {
resetChatAndArtifact,
messages: dodaiContextMessages,
selectedDodaiModel: dodaiContextSelectedModel,
currentArtifact: dodaiContextCurrentArtifact,
} = useDodai();
const { notes, addNote, getNote } = useNotes();
const { handleCreateNewNote } = useNoteSelection(notes, getNote, addNote);
const { resetActiveSession, saveCurrentChatSessionAsNew } = useDodaiCanvasHistory();

// Define individual button configurations
const newCanvasButton: NavItemProps = {
id: 'new-canvas',
label: 'Nouveau Canvas',
icon: <PlusCircle size={20} />,
onClick: () => {
resetChatAndArtifact();
navigate('/canvas'); // Navigate to ensure canvas view is shown
onClick: async () => {
console.log('[MainLayout] New Canvas button clicked.');
await resetChatAndArtifact(async () => {
if (dodaiContextMessages.length > 0) {
console.log('[MainLayout] Saving current session before reset...');
await saveCurrentChatSessionAsNew(
dodaiContextMessages,
dodaiContextCurrentArtifact,
dodaiContextSelectedModel || undefined,
);
console.log('[MainLayout] Current session saved.');
resetActiveSession();
} else {
console.log('[MainLayout] No active session to save before reset.');
resetActiveSession();
}
});

console.log('[MainLayout] New Canvas session started - ready for user input.');

navigate('/canvas');
},
isActive: false, // Action buttons are not typically "active"
isActive: false,
title: 'Commencer un nouveau Canvas',
};

Expand All @@ -43,7 +68,7 @@ const MainLayout: React.FC = () => {
icon: <FilePlus2 size={20} />,
onClick: async () => {
await handleCreateNewNote(null);
navigate('/notes'); // Ensure notes view and list are active/updated
navigate('/notes');
},
isActive: false,
title: 'Créer une nouvelle note',
Expand All @@ -62,16 +87,12 @@ const MainLayout: React.FC = () => {
const isCanvasPath = location.pathname.startsWith('/canvas');
const isNotesPath = location.pathname.startsWith('/notes');

// Determine which "New" button to show based on the active path
// If neither, or on a different path, default to newCanvas or make a specific choice.
let primaryNewButton = newCanvasButton; // Default
let primaryNewButton = newCanvasButton;
if (isCanvasPath) {
primaryNewButton = newCanvasButton;
} else if (isNotesPath) {
primaryNewButton = newNoteButton;
} else {
// Fallback if on a path other than /canvas or /notes
// Decide what the default "+" button should be. For instance, new canvas.
primaryNewButton = newCanvasButton;
}

Expand Down
Loading
Loading