From 931471ec77ce3dba7d5743629cae8f50b996d171 Mon Sep 17 00:00:00 2001 From: bobby abbott Date: Tue, 27 Jan 2026 16:22:20 -0800 Subject: [PATCH 1/3] Stabilize UI flickering, add chat suggestions, and enable deep search queries Frontend: - Add static chat suggestions for Vietnam economy, Nvidia vs Intel, and commodities/Ukraine - Fix chart iframe reloading by accumulating embeds in state (persist across renders) - Add stable state for report and research question to prevent flickering during streaming - Each StableEmbed now has its own resize handler to fix whitespace issues - Add CSS rules for chat layout stability Backend: - Enable deep queries (ENABLE_DEEP_QUERIES = True) so prediction market searches run - Add superlative/ranking query examples to GenerateDataQuestions tool - Update prompt to generate "which X has the highest Y" style queries Co-Authored-By: Claude Opus 4.5 --- agents/python/src/lib/chat.py | 30 ++++--- agents/python/src/lib/search.py | 6 +- src/app/Main.tsx | 36 ++++++-- src/app/globals.css | 20 +++++ src/components/MarkdownRenderer.tsx | 127 +++++++++++++++++++++++++--- src/components/ResearchCanvas.tsx | 34 ++++++-- 6 files changed, 215 insertions(+), 38 deletions(-) diff --git a/agents/python/src/lib/chat.py b/agents/python/src/lib/chat.py index 8bfa597..7d186da 100644 --- a/agents/python/src/lib/chat.py +++ b/agents/python/src/lib/chat.py @@ -48,15 +48,20 @@ def GenerateDataQuestions(questions: List[DataQuestion]): # pylint: disable=inv """ Generate 3-6 data-focused questions to search Tako's knowledge base. - Create a diverse set of questions with different complexity levels: - - 2-3 basic questions (search_effort='fast') for straightforward data lookups - - 1-2 complex questions (search_effort='deep') for in-depth analysis + Create a diverse set of questions: + - 2-4 basic questions (search_effort='fast') for straightforward data lookups AND superlative/ranking queries - 0-1 prediction market questions (search_effort='deep') about forecasts, probabilities, or future outcomes + IMPORTANT - Include superlative/ranking queries (use fast search): + - "Which countries have the highest GDP per capita?" + - "Which cities have the highest rent?" + - "What are the top 10 companies by market cap?" + Example: [ {"question": "China GDP since 1960", "search_effort": "fast", "query_type": "basic"}, - {"question": "Compare year-over-year growth in exports for east asian countries", "search_effort": "deep", "query_type": "complex"}, + {"question": "Which countries have the highest inflation rates in 2024?", "search_effort": "fast", "query_type": "basic"}, + {"question": "Compare exports for east asian countries", "search_effort": "fast", "query_type": "basic"}, {"question": "What are prediction market odds for China invading Taiwan in 2025?", "search_effort": "deep", "query_type": "prediction_market"} ] """ @@ -143,17 +148,20 @@ async def chat_node( if ENABLE_DEEP_QUERIES: data_questions_instructions = """2. THEN: Use GenerateDataQuestions to create 3-6 data-focused questions with varied complexity: - 2-3 BASIC questions (fast search) for straightforward data: "Country X GDP 2020-2024" - - 1-2 COMPLEX questions (deep search) for analytical insights: "What factors drove X's growth?" - - 0-1 PREDICTION MARKET question (deep search) if relevant: "What are odds for X in 2025?" + - 1-2 COMPLEX questions (deep search) for analytical insights + - 0-1 PREDICTION MARKET question (deep search) if relevant: "What are prediction market odds for X in 2025?" - Use the entities, metrics, cohorts, and time periods listed in the knowledge base context above when available - Prefer exact entity/metric names from the knowledge base context for better search results""" else: - data_questions_instructions = """2. THEN: Use GenerateDataQuestions to create 2-4 BASIC data-focused questions (fast search only): - - Focus on straightforward data lookups: "Country X GDP 2020-2024" + data_questions_instructions = """2. THEN: Use GenerateDataQuestions to create 3-5 data-focused questions: + - 2-4 BASIC questions (fast search) for data lookups, comparisons, AND superlative/ranking queries: + * Data lookups: "Country X GDP 2020-2024" + * Superlatives: "Which cities have the highest rent?", "Which countries have the lowest unemployment?" + * Rankings: "Top 10 companies by market cap" + * Comparisons: "Compare GDP growth of X vs Y" + - 0-1 PREDICTION MARKET question (deep search) if relevant: "What are prediction market odds for X in 2025?" - Use the entities, metrics, cohorts, and time periods listed in the knowledge base context above when available - - Prefer exact entity/metric names from the knowledge base context for better search results - - 0-1 PREDICTION MARKET question if relevant: "What are prediction market odds for X in 2025?" - - Note: Deep/complex queries are currently disabled""" + - Prefer exact entity/metric names from the knowledge base context for better search results""" # Add status update for query analysis state["logs"] = state.get("logs", []) diff --git a/agents/python/src/lib/search.py b/agents/python/src/lib/search.py index 272e07b..ecc4979 100644 --- a/agents/python/src/lib/search.py +++ b/agents/python/src/lib/search.py @@ -23,7 +23,7 @@ # Configuration -MAX_WEB_SEARCHES = 3 +MAX_WEB_SEARCHES = 1 MAX_TOTAL_RESOURCES = 10 # Maximum total resources to prevent context bloat class ResourceInput(BaseModel): @@ -108,9 +108,9 @@ async def search_node(state: AgentState, config: RunnableConfig): fast_questions = [q for q in data_questions if isinstance(q, dict) and q.get("search_effort") == "fast"] deep_questions = [q for q in data_questions if isinstance(q, dict) and q.get("search_effort") == "deep"] - # Filter out deep queries if disabled + # Filter out deep queries if disabled, BUT always allow prediction_market queries if not ENABLE_DEEP_QUERIES: - deep_questions = [] + deep_questions = [q for q in deep_questions if q.get("query_type") == "prediction_market"] # Add logs for both web and Tako searches for query in queries: diff --git a/src/app/Main.tsx b/src/app/Main.tsx index 50a75c6..ccdee9c 100644 --- a/src/app/Main.tsx +++ b/src/app/Main.tsx @@ -3,11 +3,37 @@ import { useModelSelectorContext } from "@/lib/model-selector-provider"; import { AgentState } from "@/lib/types"; import { useCoAgent } from "@copilotkit/react-core"; import { CopilotChat } from "@copilotkit/react-ui"; -import { useCopilotChatSuggestions } from "@copilotkit/react-ui"; import { ChatInputWithModelSelector } from "@/components/ChatInputWithModelSelector"; import Split from "react-split"; import React from "react"; +const CHAT_SUGGESTIONS = [ + { + title: "Vietnam's Economy", + message: "Tell me about Vietnam's economy", + }, + { + title: "Nvidia vs Intel", + message: "Compare the performance of Nvidia and Intel over the last 10 years", + }, + { + title: "Commodities Prices", + message: "How have commodities prices moved since the Global Financial Crisis?", + }, + { + title: "Global Military Spending", + message: "What is the trend of global military spending since the Ukraine war started?", + }, + { + title: "Performance of AI companies", + message: "How has the performance of AI company's stock prices and funding evolved since 2020 (also include traffic trends)?", + }, + { + title: "Rent & Inflation", + message: "How has the rent, inflation, and average wages trended in top US cities?", + } +]; + export default function Main() { const { model, agent } = useModelSelectorContext(); const { state, setState } = useCoAgent({ @@ -21,10 +47,6 @@ export default function Main() { }, }); - useCopilotChatSuggestions({ - instructions: "Lifespan of penguins", - }); - return (

@@ -74,7 +96,10 @@ export default function Main() {
setTimeout(resolve, 30)); }} + suggestions={CHAT_SUGGESTIONS} labels={{ initial: "Hi! How can I assist you with your research today?", }} diff --git a/src/app/globals.css b/src/app/globals.css index 3ccf0f2..ed12d5e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -132,3 +132,23 @@ body { .copilotKitInput > .copilotKitInputControls > button:not([disabled]) { color: var(--copilot-kit-secondary-color); } + +/* Stabilize chat layout to prevent flickering */ +.copilotKitChat { + display: flex; + flex-direction: column; + height: 100%; + max-height: 100%; + overflow: hidden; +} + +.copilotKitMessages { + flex: 1; + min-height: 0; + overflow-y: auto; +} + +/* Ensure suggestions don't cause layout shift */ +.copilotKitSuggestions { + min-height: 44px; +} diff --git a/src/components/MarkdownRenderer.tsx b/src/components/MarkdownRenderer.tsx index 6837d65..67a3d48 100644 --- a/src/components/MarkdownRenderer.tsx +++ b/src/components/MarkdownRenderer.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect } from "react"; +import React, { useEffect, useRef, useState, useCallback } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import rehypeRaw from "rehype-raw"; @@ -9,20 +9,72 @@ 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=["']([^"']+)["']/); + return srcMatch ? srcMatch[1] : null; +} + +// Generate a stable ID from the iframe src +function generateEmbedId(src: string): string { + try { + const url = new URL(src); + return `embed-${url.pathname.replace(/[^a-zA-Z0-9]/g, "-")}`; + } catch { + return `embed-${src.replace(/[^a-zA-Z0-9]/g, "-").slice(0, 50)}`; + } +} + export function MarkdownRenderer({ content }: MarkdownRendererProps) { + // Persist embeds across renders - once an embed is found, it stays + const [embeds, setEmbeds] = useState>(new Map()); + const [processedContent, setProcessedContent] = useState(""); + + // Process content and extract embeds + useEffect(() => { + const embedPattern = /[\s\S]*?<\/html>/gi; + let processed = content; + const matches = content.match(embedPattern) || []; + + // Add any new embeds we find + const newEmbeds = new Map(embeds); + let hasNewEmbeds = false; + + for (const match of matches) { + const src = extractIframeSrc(match); + if (src) { + const id = generateEmbedId(src); + if (!newEmbeds.has(id)) { + newEmbeds.set(id, { id, html: match, src }); + hasNewEmbeds = true; + } + // Replace with placeholder + processed = processed.replace(match, `
`); + } + } + + if (hasNewEmbeds) { + setEmbeds(newEmbeds); + } + setProcessedContent(processed); + }, [content]); // Note: embeds intentionally not in deps - we only add, never remove + // Listen for Tako chart resize messages useEffect(() => { const handleTakoResize = (event: MessageEvent) => { const data = event.data; - - // Early return if not a Tako resize message if (data.type !== "tako::resize") return; - // Find and resize the iframe that sent this message - const iframes = document.querySelectorAll("iframe"); + const iframes = document.querySelectorAll("iframe[data-tako-embed]"); for (const iframe of iframes) { if (iframe.contentWindow === event.source) { - iframe.style.height = `${data.height + 4}px`; + iframe.style.height = `${data.height}px`; break; } } @@ -30,7 +82,10 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) { window.addEventListener("message", handleTakoResize); return () => window.removeEventListener("message", handleTakoResize); - }, [content]); + }, []); + + // Memoized embed lookup function + const getEmbed = useCallback((id: string) => embeds.get(id), [embeds]); return (
@@ -38,7 +93,6 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) { remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} components={{ - // Custom renderer for HTML elements to allow iframes // eslint-disable-next-line @typescript-eslint/no-unused-vars h1: ({ node, ...props }) => (

@@ -78,7 +132,6 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) { ), // eslint-disable-next-line @typescript-eslint/no-unused-vars code: ({ node, className, children, ...props }) => { - // If there's a className with language-, it's a code block const isCodeBlock = className?.includes("language-"); if (!isCodeBlock) { return ( @@ -99,18 +152,68 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) { ); }, + // Render embed placeholders with stable iframe components + // eslint-disable-next-line @typescript-eslint/no-unused-vars + div: ({ node, ...props }) => { + const embedId = (node?.properties?.["dataEmbedPlaceholder"] as string) || + (props as Record)["data-embed-placeholder"] as string | undefined; + if (embedId) { + const embed = getEmbed(embedId); + if (embed) { + return ; + } + } + return
; + }, // eslint-disable-next-line @typescript-eslint/no-unused-vars iframe: ({ node, ...props }) => (