From 53e592bc2f8ed0ce5aefd1ab3e6fa0b4ff9f9730 Mon Sep 17 00:00:00 2001 From: bobby abbott Date: Tue, 27 Jan 2026 21:18:38 -0800 Subject: [PATCH 1/5] Improve canvas state management --- agents/python/src/lib/chat.py | 25 ++++++++++++++++++++----- src/components/ResearchCanvas.tsx | 26 +++++++++++++++++++++----- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/agents/python/src/lib/chat.py b/agents/python/src/lib/chat.py index 948ffbd..79be303 100644 --- a/agents/python/src/lib/chat.py +++ b/agents/python/src/lib/chat.py @@ -75,14 +75,11 @@ async def chat_node( """ logger.info("=== CHAT_NODE: Starting execution ===") + # Note: report is NOT in emit_intermediate_state to prevent flicker + # The report is only emitted once charts are injected config = copilotkit_customize_config( config, emit_intermediate_state=[ - { - "state_key": "report", - "tool": "WriteReport", - "tool_argument": "report", - }, { "state_key": "research_question", "tool": "WriteResearchQuestion", @@ -236,8 +233,16 @@ async def chat_node( ai_message = cast(AIMessage, response) if ai_message.tool_calls: if ai_message.tool_calls[0]["name"] == "WriteReport": + # Add progress indicator for report generation + state["logs"].append({"message": "Writing research report...", "done": False}) + await copilotkit_emit_state(config, state) + report = ai_message.tool_calls[0]["args"].get("report", "") + # Mark report writing as done + state["logs"][-1]["done"] = True + await copilotkit_emit_state(config, state) + # Clean up: Remove any markdown image links that the LLM incorrectly added import re external_domains = r'(tradingeconomics|worldbank|imf|fred|ourworldindata|statista)' @@ -253,6 +258,8 @@ async def chat_node( # Second pass: Inject charts at appropriate positions processed_report = report if tako_charts_map: + state["logs"].append({"message": "Inserting data visualizations...", "done": False}) + await copilotkit_emit_state(config, state) # Build chart list for injection prompt chart_list = "\n".join([f"- {title}" for title in tako_charts_map.keys()]) @@ -327,6 +334,14 @@ async def replace_marker(match): logger.info(f"Injected {len([r for r in replacements if r[2]])} charts into report") + # Mark chart injection as done + state["logs"][-1]["done"] = True + await copilotkit_emit_state(config, state) + + # Clear logs before showing final report + state["logs"] = [] + await copilotkit_emit_state(config, state) + return Command( goto="chat_node", update={ diff --git a/src/components/ResearchCanvas.tsx b/src/components/ResearchCanvas.tsx index 7667264..c7cb565 100644 --- a/src/components/ResearchCanvas.tsx +++ b/src/components/ResearchCanvas.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useRef } from "react"; import { ChevronDown } from "lucide-react"; import { Textarea } from "@/components/ui/textarea"; import { @@ -32,10 +32,26 @@ export function ResearchCanvas() { }, }); - // Use state values directly - the agent state is the source of truth - const resources = state.resources || []; - const report = state.report || ""; - const researchQuestion = state.research_question || ""; + // Use refs to persist last known good values (prevents flicker on partial state updates) + const lastResourcesRef = useRef([]); + const lastReportRef = useRef(""); + const lastQuestionRef = useRef(""); + + // Update refs when we get valid values, use ref values as fallback + if (state.resources && state.resources.length > 0) { + lastResourcesRef.current = state.resources; + } + if (state.report && state.report.length > 0) { + lastReportRef.current = state.report; + } + if (state.research_question && state.research_question.length > 0) { + lastQuestionRef.current = state.research_question; + } + + // Use current state if available, otherwise fall back to last known good value + const resources = (state.resources && state.resources.length > 0) ? state.resources : lastResourcesRef.current; + const report = (state.report && state.report.length > 0) ? state.report : lastReportRef.current; + const researchQuestion = (state.research_question && state.research_question.length > 0) ? state.research_question : lastQuestionRef.current; useCoAgentStateRender({ name: agent, From f86b39559472401cd9dc17f72cb3ca979e08fc34 Mon Sep 17 00:00:00 2001 From: bobby abbott Date: Tue, 27 Jan 2026 21:25:00 -0800 Subject: [PATCH 2/5] dont write report to chat --- agents/python/src/lib/chat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agents/python/src/lib/chat.py b/agents/python/src/lib/chat.py index 79be303..a8c65e5 100644 --- a/agents/python/src/lib/chat.py +++ b/agents/python/src/lib/chat.py @@ -208,8 +208,8 @@ async def chat_node( You should use the search tool to get resources before answering the user's question. Use the content and descriptions from both Tako charts and web resources to inform your report. - If you finished writing the report, ask the user proactively for next steps, changes etc, make it engaging. - To write the report, you should use the WriteReport tool. Never EVER respond with the report, only use the tool. + To write the report, you should use the WriteReport tool. Never EVER respond with the report content, only use the tool. + After writing the report, send a brief (1-2 sentence) follow-up asking if the user wants any changes or has questions. Do NOT summarize or repeat the report content in the chat. This is the research question: {research_question} @@ -351,7 +351,7 @@ async def replace_marker(match): ai_message, ToolMessage( tool_call_id=ai_message.tool_calls[0]["id"], - content="Report written.", + content="Report written successfully. Now send a brief follow-up message asking if the user wants any changes or has questions. Do NOT repeat the report content.", ), ], }, From 0404f787cbb7547a36180fb292b715a189b68728 Mon Sep 17 00:00:00 2001 From: bobby abbott Date: Tue, 27 Jan 2026 21:31:25 -0800 Subject: [PATCH 3/5] Fix iframe reloading by rendering outside ReactMarkdown tree Restructure MarkdownRenderer to render iframes as siblings to ReactMarkdown components instead of inside its DOM tree. This prevents iframe remounting when markdown content changes, since ReactMarkdown re-renders no longer affect the iframe components. Co-Authored-By: Claude Opus 4.5 --- src/components/MarkdownRenderer.tsx | 296 +++++++++++++--------------- 1 file changed, 139 insertions(+), 157 deletions(-) diff --git a/src/components/MarkdownRenderer.tsx b/src/components/MarkdownRenderer.tsx index 0f574c3..8c4655b 100644 --- a/src/components/MarkdownRenderer.tsx +++ b/src/components/MarkdownRenderer.tsx @@ -9,12 +9,6 @@ interface MarkdownRendererProps { content: string; } -interface ExtractedEmbed { - id: string; - html: string; - src: string; -} - // Extract the iframe src from an HTML block function extractIframeSrc(html: string): string | null { const srcMatch = html.match(/src=["']([^"']+)["']/); @@ -31,185 +25,173 @@ function generateEmbedId(src: string): string { } } -// Module-level cache for embeds to persist across re-renders -const embedCache = new Map(); +// Memoized components object +const markdownComponents = { + h1: ({ node, ...props }: any) => ( +

+ ), + h2: ({ node, ...props }: any) => ( +

+ ), + h3: ({ node, ...props }: any) => ( +

+ ), + p: ({ node, ...props }: any) => ( +

+ ), + ul: ({ node, ...props }: any) => ( +