From 4fc3949f0cf4efc98f6e89a7f232552fdbd826b4 Mon Sep 17 00:00:00 2001
From: Krishna Agarwal
Date: Fri, 15 May 2026 19:46:26 +0800
Subject: [PATCH 1/2] updated changes
---
research-sentry/.env.example | 5 +
research-sentry/.env.local.example | 7 -
research-sentry/.gitignore | 2 +-
research-sentry/README.md | 270 +-
.../app/api/citations/track/route.ts | 4 +-
research-sentry/app/api/conversation/route.ts | 13 +-
research-sentry/app/api/search/text/route.ts | 75 +-
research-sentry/app/api/search/voice/route.ts | 19 +-
research-sentry/app/page.tsx | 83 +-
research-sentry/components/PaperCard.tsx | 149 +-
.../components/PaperComparison.tsx | 2 +-
research-sentry/components/PaperSummary.tsx | 120 +-
research-sentry/image.png | Bin 474587 -> 0 bytes
research-sentry/lib/citation-tracker.ts | 122 +-
research-sentry/lib/comparator.ts | 57 +-
research-sentry/lib/conversation.ts | 60 +-
research-sentry/lib/intent-parser.ts | 33 +-
research-sentry/lib/search.ts | 286 +-
research-sentry/lib/summarizer.ts | 45 +-
research-sentry/lib/tinyfish.ts | 143 +-
research-sentry/next-env.d.ts | 2 +-
research-sentry/package-lock.json | 2745 -----------------
research-sentry/package.json | 7 +-
23 files changed, 761 insertions(+), 3488 deletions(-)
create mode 100644 research-sentry/.env.example
delete mode 100644 research-sentry/.env.local.example
delete mode 100644 research-sentry/image.png
delete mode 100644 research-sentry/package-lock.json
diff --git a/research-sentry/.env.example b/research-sentry/.env.example
new file mode 100644
index 000000000..2039caeaf
--- /dev/null
+++ b/research-sentry/.env.example
@@ -0,0 +1,5 @@
+# TinyFish API key — https://agent.tinyfish.ai/api-keys
+TINYFISH_API_KEY=your-tinyfish-api-key
+
+# OpenAI API key (LLM + Whisper transcription) — https://platform.openai.com/api-keys
+OPENAI_API_KEY=your-openai-api-key
diff --git a/research-sentry/.env.local.example b/research-sentry/.env.local.example
deleted file mode 100644
index e201a97ec..000000000
--- a/research-sentry/.env.local.example
+++ /dev/null
@@ -1,7 +0,0 @@
-# Rename to .env.local and add your keys
-
-# TinyFish API key for web agent automation (https://tinyfish.ai)
-TINYFISH_API_KEY=your-tinyfish-key-here
-
-# OpenAI API key for GPT-4o intent parsing and Whisper transcription
-OPENAI_API_KEY=sk-your-key-here
diff --git a/research-sentry/.gitignore b/research-sentry/.gitignore
index 5f52bd74a..872816aed 100644
--- a/research-sentry/.gitignore
+++ b/research-sentry/.gitignore
@@ -1,5 +1,5 @@
node_modules
.next
-.env*.local
+.env.local
.vercel
*.tsbuildinfo
diff --git a/research-sentry/README.md b/research-sentry/README.md
index bdae958ab..75fb72f09 100644
--- a/research-sentry/README.md
+++ b/research-sentry/README.md
@@ -1,176 +1,182 @@
# Research Sentry
+**Live: https://cookbook-research-sentry.vercel.app/**
-**A voice-first academic research co-pilot** that scans live portals (ArXiv, PubMed, Semantic Scholar, IEEE Xplore, Google Scholar, SSRN, CORE, DOAJ) to assemble verified paper metadata and summaries. It uses the **TinyFish Web Agent** to automate multi-step portal navigation and extract structured results in real time.
+**Voice-first academic research co-pilot — AI agents scrape 8 live research portals in parallel and assemble verified paper metadata in real time.**
-Live: https://cookbook-research-sentry.vercel.app/
+Speak or type a research query. Research Sentry parses your intent, dispatches one TinyFish browser agent per academic portal simultaneously, aggregates and deduplicates the results, and streams them back as each portal completes. Then ask follow-up questions, compare papers side-by-side, track citations, or export BibTeX.
-Demo video: https://cookbook-research-sentry.vercel.app/
-
----
+## Architecture
-## How It Works
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Browser (Client) │
+│ │
+│ VoiceRecorder / SearchInterface → ResultsGrid │
+│ ConversationInterface → PaperComparison → CitationTracker │
+│ TinyFishAgentTerminal (live agent log) │
+└──────────────────────────┬──────────────────────────────────┘
+ │
+ ┌────────────┼─────────────┐
+ ▼ ▼ ▼
+ /api/search/text /api/search/voice /api/compare
+ /api/summarize /api/conversation /api/citations/track
+ /api/export/bibtex
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ lib/tinyfish.ts │
+│ │
+│ runTinyFishAutomation(url, goal, stealth?) │
+│ Throws TinyFishError with typed codes: │
+│ MISSING_API_KEY | RUN_FAILED | TIMEOUT | │
+│ STREAM_ERROR | NO_RESULT │
+│ │
+│ client.agent.stream({ url, goal, browser_profile }) │
+│ onComplete → RunStatus.COMPLETED → return result │
+│ → RunStatus.FAILED → throw RUN_FAILED │
+└──────────────────────────┬──────────────────────────────────┘
+ │ Promise.allSettled (x8 parallel)
+ ┌─────────┬───────┼────────┬─────────┐
+ ▼ ▼ ▼ ▼ ▼
+ ArXiv PubMed Semantic Google IEEE
+ Scholar Scholar Xplore
+ + SSRN + CORE + DOAJ
+
+Google Scholar + IEEE Xplore use browser_profile: 'stealth'
+```
-1. **Voice / text input** -- speak or type your research query.
-2. **GPT-4o parses intent** -- OpenAI extracts topic, keywords, and target sources from your query.
-3. **TinyFish agents scrape 8 academic portals in parallel** -- each portal gets its own headless browser session via the TinyFish API.
-4. **Results aggregated & deduplicated** -- papers from every source are merged, normalized, and ranked by citation count.
-5. **Summarize, compare, export** -- ask follow-up questions, compare papers side-by-side, track citations, or export BibTeX.
+### OpenAI usage
----
+```
+lib/intent-parser.ts → parse topic, keywords, sources from query
+lib/summarizer.ts → summarize individual papers
+lib/comparator.ts → structured methodology/results comparison
+lib/conversation.ts → conversational follow-up answers
+lib/citation-tracker.ts → citation velocity and impact prediction
+lib/whisper.ts → speech-to-text via OpenAI's Whisper endpoint
+```
## Key Features
-- **Voice input** -- record a question and Whisper transcribes it into a search query.
-- **Multi-source search** -- scrapes ArXiv, PubMed, Semantic Scholar, Google Scholar, IEEE Xplore, SSRN, CORE, and DOAJ simultaneously.
-- **Paper comparison** -- select papers and get a structured methodology/results comparison via GPT-4o.
-- **Citation tracking** -- monitor a paper's citation velocity and predicted impact.
-- **BibTeX export** -- download selected papers as a `.bib` file.
-- **Conversational follow-ups** -- ask the AI assistant questions about your results.
-
----
-
-## TinyFish API Usage
-
-The core integration lives in `lib/tinyfish.ts`. Here is the SSE call that drives every search:
-
-```ts
-const res = await fetch("https://agent.tinyfish.ai/v1/automation/run-sse", {
- method: "POST",
- headers: {
- "X-API-Key": process.env.TINYFISH_API_KEY!,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- url,
- goal,
- browser_profile: stealth ? "stealth" : "lite",
- }),
-});
-
-// Parse the SSE stream
-const reader = res.body!.getReader();
-const decoder = new TextDecoder();
-let buffer = "";
-let result = null;
-
-while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split("\n");
- buffer = lines.pop() || "";
- for (const line of lines) {
- if (line.startsWith("data: ")) {
- const event = JSON.parse(line.slice(6));
- if (event.type === "COMPLETE") result = event.resultJson;
- }
- }
-}
-```
+- **Voice input** — record a question, OpenAI Whisper transcribes it into a search query
+- **Multi-source search** — 8 portals scraped simultaneously: ArXiv, PubMed, Semantic Scholar, Google Scholar, IEEE Xplore, SSRN, CORE, DOAJ
+- **Paper comparison** — structured methodology/results comparison across selected papers
+- **Citation tracking** — monitor citation velocity and predicted impact
+- **BibTeX export** — download selected papers as a `.bib` file
+- **Conversational follow-ups** — ask the AI assistant questions about your results
+- **Live agent terminal** — watch each TinyFish agent's progress in real time
----
+## Scraping Flow
-## Tech Stack
+1. User speaks or types a research query
+2. OpenAI (`intent-parser.ts`) extracts topic, keywords, and target sources
+3. One TinyFish agent fires per portal — all in parallel via `Promise.allSettled`
+4. Each agent navigates the portal's live DOM with a tight, focused goal prompt
+5. Results stream back to the aggregator as each agent completes
+6. `aggregator.ts` deduplicates and ranks by citation count
+7. Results appear in the UI as portals finish — no waiting for the slowest one
-| Layer | Technology |
-|-------|-----------|
-| Framework | Next.js 14 (App Router) |
-| Web scraping | TinyFish API (SSE) |
-| LLM | OpenAI GPT-4o |
-| Speech-to-text | OpenAI Whisper |
-| Styling | Tailwind CSS |
-| Icons | Lucide React |
+## Setup
----
+### Prerequisites
-## Setup
+- Node.js 18+
+- TinyFish API key
+- OpenAI API key
+
+### Environment Variables
```bash
-# 1. Install dependencies
-npm install
+cp .env.example .env.local
+```
-# 2. Create your env file
-cp .env.local.example .env.local
+Then fill in:
-# 3. Add your API keys to .env.local
-# TINYFISH_API_KEY -- get one at https://tinyfish.ai
-# OPENAI_API_KEY -- get one at https://platform.openai.com
+```env
+# TinyFish (required) — https://agent.tinyfish.ai/api-keys
+TINYFISH_API_KEY=your-tinyfish-key-here
-# 4. Start the dev server
-npm run dev
+# Google OpenAI (required) — https://platform.openai.com/api-keys
+OPENAI_API_KEY=your-openai-api-key
```
-Open http://localhost:3000 to use the app.
+### Install & Run
+
+```bash
+npm install
+npm run dev
+```
----
+Open http://localhost:3000
-## Folder Structure
+## Project Structure
```
research-sentry/
├── app/
-│ ├── api/
-│ │ ├── citations/track/route.ts # Citation velocity analysis
-│ │ ├── compare/route.ts # Paper comparison endpoint
-│ │ ├── conversation/route.ts # Conversational follow-ups
-│ │ ├── emails/extract/route.ts # Author email extraction
-│ │ ├── export/bibtex/route.ts # BibTeX export
-│ │ ├── health/route.ts # Health check
-│ │ ├── search/text/route.ts # Text search endpoint
-│ │ ├── search/voice/route.ts # Voice search endpoint
-│ │ └── summarize/route.ts # Paper summarization
-│ ├── globals.css
│ ├── layout.tsx
-│ └── page.tsx # Main UI
+│ ├── page.tsx # Main UI
+│ ├── globals.css
+│ └── api/
+│ ├── citations/track/route.ts # Citation velocity analysis
+│ ├── compare/route.ts # Paper comparison
+│ ├── conversation/route.ts # Conversational follow-ups
+│ ├── emails/extract/route.ts # Author email extraction
+│ ├── export/bibtex/route.ts # BibTeX export
+│ ├── health/route.ts # Health check
+│ ├── search/text/route.ts # Text search
+│ ├── search/voice/route.ts # Voice search
+│ └── summarize/route.ts # Paper summarization
├── components/
+│ ├── SearchInterface.tsx
+│ ├── VoiceRecorder.tsx
+│ ├── ResultsGrid.tsx
+│ ├── PaperCard.tsx
+│ ├── PaperComparison.tsx
+│ ├── PaperSummary.tsx
│ ├── CitationTracker.tsx
│ ├── ConversationInterface.tsx
│ ├── CoPilotMode.tsx
+│ ├── WorkflowSelector.tsx
+│ ├── TinyFishAgentTerminal.tsx # Live agent log display
│ ├── ErrorMessage.tsx
-│ ├── LoadingSpinner.tsx
-│ ├── PaperCard.tsx
-│ ├── PaperComparison.tsx
-│ ├── PaperSummary.tsx
-│ ├── ResultsGrid.tsx
-│ ├── SearchInterface.tsx
-│ ├── TinyFishAgentTerminal.tsx # Live agent log display
-│ ├── VoiceRecorder.tsx
-│ └── WorkflowSelector.tsx
+│ └── LoadingSpinner.tsx
├── hooks/
│ └── useVoiceCommands.ts
├── lib/
-│ ├── aggregator.ts # Deduplication & ranking
+│ ├── tinyfish.ts # TinyFish agent client (typed errors)
+│ ├── intent-parser.ts # OpenAI — query intent parsing
+│ ├── summarizer.ts # OpenAI — paper summarization
+│ ├── comparator.ts # OpenAI — paper comparison
+│ ├── conversation.ts # OpenAI — conversational follow-ups
+│ ├── citation-tracker.ts # OpenAI — citation velocity
+│ ├── whisper.ts # OpenAI Whisper — speech-to-text
+│ ├── aggregator.ts # Deduplication & ranking
+│ ├── search.ts # Multi-source search orchestration
+│ ├── workflows.ts
│ ├── audio-utils.ts
-│ ├── citation-tracker.ts
-│ ├── comparator.ts
-│ ├── conversation.ts
│ ├── email-utils.ts
-│ ├── intent-parser.ts # GPT-4o query parsing
│ ├── pdf-utils.ts
-│ ├── search.ts # Multi-source search engine
-│ ├── summarizer.ts
-│ ├── tinyfish.ts # TinyFish SSE client
-│ ├── types.ts
-│ └── workflows.ts
-└── .env.local.example
+│ └── types.ts
+├── .env.example
+└── package.json
```
----
+## Constraint Checklist
-## Architecture
+| Constraint | Status |
+|---|---|
+| External database used? | NO (pure in-memory) |
+| Scraping parallel? | YES (`Promise.allSettled` across 8 portals) |
+| Bot-protected sites handled? | YES (Google Scholar + IEEE use `browser_profile: 'stealth'`) |
+| SDK errors surfaced? | YES (typed `TinyFishError` with code — no silent `null` returns) |
+| Voice input? | YES (OpenAI Whisper transcription) |
+| BibTeX export? | YES |
-```mermaid
-graph TD
- User((User)) -->|Voice/Text| UI[Search Interface]
- UI -->|Intent| Parser[Intent Parser GPT-4o]
- Parser -->|Plan| Engine[Search Engine]
- Engine -->|Dispatch| Agent1[TinyFish Agent: ArXiv]
- Engine -->|Dispatch| Agent2[TinyFish Agent: PubMed]
- Engine -->|Dispatch| Agent3[TinyFish Agent: Scholar]
- Agent1 -->|Scraping| Web[Live Web DOM]
- Agent2 -->|Scraping| Web
- Agent3 -->|Scraping| Web
- Web -->|Result| Aggregator[Synthesis & Deduplication]
- Aggregator -->|JSON Payload| UI
- UI -->|Visuals| Terminal[Live Log Terminal]
-```
+## Tech Stack
+
+- **Framework:** Next.js (App Router), TypeScript, Tailwind CSS
+- **Browser Agents:** TinyFish SDK (`client.agent.stream`)
+- **LLM: OpenAI (gpt-4o-mini) + Speech-to-text: OpenAI Whisper
+- **Icons:** Lucide React
+- **Deployment:** Vercel
diff --git a/research-sentry/app/api/citations/track/route.ts b/research-sentry/app/api/citations/track/route.ts
index 4586046ec..75580335d 100644
--- a/research-sentry/app/api/citations/track/route.ts
+++ b/research-sentry/app/api/citations/track/route.ts
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
-import { analyzeCitationTrend } from '@/lib/citation-tracker';
+import { analyzeCitationNetwork } from '@/lib/citation-tracker';
export async function POST(req: NextRequest) {
try {
@@ -9,7 +9,7 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: 'Paper data required' }, { status: 400 });
}
- const trackedData = await analyzeCitationTrend(paper);
+ const trackedData = await analyzeCitationNetwork(paper);
// In a real app, we would save this to a database here
diff --git a/research-sentry/app/api/conversation/route.ts b/research-sentry/app/api/conversation/route.ts
index f5dbe5064..67bdb9df4 100644
--- a/research-sentry/app/api/conversation/route.ts
+++ b/research-sentry/app/api/conversation/route.ts
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
-import { generateConversationResponse } from '@/lib/conversation';
+import { continueConversation, Message } from '@/lib/conversation';
export const maxDuration = 60;
@@ -11,9 +11,16 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: 'Invalid history format' }, { status: 400 });
}
- const response = await generateConversationResponse(history, context);
+ // Build messages array — prepend context as system message if provided
+ const messages: Message[] = [];
+ if (context) {
+ messages.push({ role: 'system', content: `Research context: ${JSON.stringify(context)}` });
+ }
+ messages.push(...(history as Message[]));
+
+ const response = await continueConversation(messages);
- return NextResponse.json(response);
+ return NextResponse.json({ response });
} catch (error) {
console.error('Conversation API Error:', error);
return NextResponse.json({ error: 'Failed to generate response' }, { status: 500 });
diff --git a/research-sentry/app/api/search/text/route.ts b/research-sentry/app/api/search/text/route.ts
index 2a34e21f8..f5ea0bfa5 100644
--- a/research-sentry/app/api/search/text/route.ts
+++ b/research-sentry/app/api/search/text/route.ts
@@ -1,13 +1,76 @@
-import { NextRequest, NextResponse } from 'next/server';
+import { NextRequest } from 'next/server';
import { parseSearchIntent } from '@/lib/intent-parser';
-import { searchResearchPapers } from '@/lib/search';
+import { scrapeSourceStreaming } from '@/lib/search';
+import type { SearchCriteria, SourceType } from '@/lib/types';
export const maxDuration = 300;
+const DEFAULT_SOURCES: SourceType[] = ['arxiv', 'pubmed', 'semantic_scholar'];
+
export async function POST(req: NextRequest) {
const { query, sources } = await req.json();
- const criteria = await parseSearchIntent(query);
- if (sources) criteria.sources = sources;
- const results = await searchResearchPapers(criteria);
- return NextResponse.json(results);
+
+ // Parse intent — returns { keywords, topics, authors, yearFrom, yearTo, intent }
+ const parsed = await parseSearchIntent(query);
+
+ // Build a proper SearchCriteria — topic is the original query or first keyword/topic
+ const topic: string =
+ query ||
+ (Array.isArray(parsed.topics) && parsed.topics[0]) ||
+ (Array.isArray(parsed.keywords) && parsed.keywords[0]) ||
+ query;
+
+ const criteria: SearchCriteria = {
+ topic,
+ keywords: Array.isArray(parsed.keywords) ? parsed.keywords : [query],
+ sources: (sources as SourceType[]) || DEFAULT_SOURCES,
+ maxResults: 20,
+ fullPrompt: query,
+ };
+
+ const encoder = new TextEncoder();
+ const sseData = (payload: unknown) => encoder.encode(`data: ${JSON.stringify(payload)}\n\n`);
+
+ const stream = new ReadableStream({
+ async start(controller) {
+ let closed = false;
+ const safeEnqueue = (payload: unknown) => {
+ if (!closed) controller.enqueue(sseData(payload));
+ };
+ const safeClose = () => {
+ if (!closed) { closed = true; controller.close(); }
+ };
+
+ safeEnqueue({ type: 'SEARCH_STARTED', total: criteria.sources.length, query: topic });
+
+ const perSourceTimeoutMs = 40_000;
+ let totalFound = 0;
+
+ const tasks = criteria.sources.map((source: SourceType) =>
+ scrapeSourceStreaming(source, criteria, perSourceTimeoutMs)
+ .then(papers => {
+ totalFound += papers.length;
+ safeEnqueue({ type: 'SOURCE_COMPLETE', source, papers, count: papers.length });
+ })
+ .catch(err => {
+ console.error(`[Search/${source}] Failed:`, err?.message);
+ safeEnqueue({ type: 'SOURCE_ERROR', source, error: err?.message });
+ })
+ );
+
+ await Promise.allSettled(tasks);
+
+ safeEnqueue({ type: 'SEARCH_COMPLETE', totalFound });
+ safeClose();
+ },
+ });
+
+ return new Response(stream, {
+ headers: {
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache, no-transform',
+ 'Connection': 'keep-alive',
+ 'X-Accel-Buffering': 'no',
+ },
+ });
}
diff --git a/research-sentry/app/api/search/voice/route.ts b/research-sentry/app/api/search/voice/route.ts
index cee48ebd4..ce8dff2ac 100644
--- a/research-sentry/app/api/search/voice/route.ts
+++ b/research-sentry/app/api/search/voice/route.ts
@@ -2,9 +2,12 @@ import { NextRequest, NextResponse } from 'next/server';
import { transcribeAudio } from '@/lib/whisper';
import { parseSearchIntent } from '@/lib/intent-parser';
import { searchResearchPapers } from '@/lib/search';
+import { SearchCriteria, SourceType } from '@/lib/types';
export const maxDuration = 300;
+const DEFAULT_SOURCES: SourceType[] = ['arxiv', 'pubmed', 'semantic_scholar', 'google_scholar', 'ieee'];
+
export async function POST(req: NextRequest) {
const form = await req.formData();
const audio = form.get('audio');
@@ -13,7 +16,21 @@ export async function POST(req: NextRequest) {
}
const buffer = Buffer.from(await audio.arrayBuffer());
const transcript = await transcribeAudio(buffer);
- const criteria = await parseSearchIntent(transcript);
+ const intent = await parseSearchIntent(transcript);
+
+ // Map intent parser output to SearchCriteria
+ const criteria: SearchCriteria = {
+ topic: intent.topics?.[0] || intent.keywords?.[0] || transcript,
+ keywords: intent.keywords || [],
+ sources: DEFAULT_SOURCES,
+ maxResults: 20,
+ fullPrompt: transcript,
+ dateRange: (intent.yearFrom || intent.yearTo) ? {
+ from: intent.yearFrom ? String(intent.yearFrom) : undefined,
+ to: intent.yearTo ? String(intent.yearTo) : undefined,
+ } : undefined,
+ };
+
const results = await searchResearchPapers(criteria);
return NextResponse.json({ ...results, transcript });
}
diff --git a/research-sentry/app/page.tsx b/research-sentry/app/page.tsx
index 9f2227e7e..3c41bb64c 100644
--- a/research-sentry/app/page.tsx
+++ b/research-sentry/app/page.tsx
@@ -45,10 +45,14 @@ export default function Home() {
setSearchSources(sources);
setAgentLogs([]);
setAgentComplete(false);
+ setResults(null);
addAgentLog('TinyFish Agent initialized. Connecting to browser instance...', 'info');
addAgentLog(`Targeting [${sources.join(', ')}] for discovery.`, 'info');
+ // Accumulate papers across sources as they stream in
+ const allPapers: import('@/lib/types').ResearchPaper[] = [];
+
try {
const response = await fetch('/api/search/text', {
method: 'POST',
@@ -56,15 +60,45 @@ export default function Home() {
body: JSON.stringify({ query, sources }),
});
- if (!response.ok) {
- addAgentLog(`Search request failed: ${response.statusText}`, 'error');
- throw new Error(`Search failed: ${response.statusText}`);
+ if (!response.ok) throw new Error(`Search failed: ${response.statusText}`);
+ if (!response.body) throw new Error('No response body');
+
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split('\n');
+ buffer = lines.pop() ?? '';
+
+ for (const line of lines) {
+ if (!line.startsWith('data: ')) continue;
+ let event: Record;
+ try { event = JSON.parse(line.slice(6)); } catch { continue; }
+
+ if (event.type === 'SEARCH_STARTED') {
+ addAgentLog(`Searching ${event.total} sources in parallel...`, 'info');
+ } else if (event.type === 'SOURCE_COMPLETE') {
+ const papers = (event.papers as import('@/lib/types').ResearchPaper[]) ?? [];
+ addAgentLog(`${event.source}: found ${papers.length} papers.`, papers.length > 0 ? 'success' : 'info');
+ if (papers.length > 0) {
+ allPapers.push(...papers);
+ // Show results immediately as each source completes
+ setResults({ query, papers: [...allPapers], totalFound: allPapers.length });
+ }
+ } else if (event.type === 'SOURCE_ERROR') {
+ addAgentLog(`${event.source}: failed — ${event.error}`, 'error');
+ } else if (event.type === 'SEARCH_COMPLETE') {
+ addAgentLog(`Discovery complete. Found ${allPapers.length} papers total.`, 'success');
+ setAgentComplete(true);
+ setLoading(false);
+ }
+ }
}
-
- const data = await response.json();
- addAgentLog(`Discovery complete. Found ${data.totalFound ?? 0} papers.`, 'success');
- setAgentComplete(true);
- setResults(data);
} catch (err) {
addAgentLog(err instanceof Error ? err.message : 'An error occurred', 'error');
setAgentComplete(true);
@@ -183,7 +217,7 @@ export default function Home() {
Your AI Research Co-Pilot
- Search academic papers using your voice or text. Powered by OpenAI, GPT-4, and TinyFish Web Agent.
+ Search academic papers using your voice or text. Powered by OpenAI and TinyFish Web Agent.
{/* Features badges */}
@@ -289,7 +323,7 @@ export default function Home() {
/>
- {/* Loading State */}
+ {/* Loading State — terminal visible while searching */}
{loading && (
@@ -303,10 +337,29 @@ export default function Home() {
-
-
-
Compiling findings from 8 research nodes
-
+ {/* Show results as they arrive while other agents run */}
+ {results && results.papers.length > 0 ? (
+
+
+
+
+ Live results — {results.papers.length} papers found so far, more agents still running…
+
+
+ setTrackingPaperId(id)}
+ />
+
+ ) : (
+
+
+
Compiling findings from research nodes…
+
+ )}
)}
@@ -350,7 +403,7 @@ export default function Home() {
)}
- {/* Results */}
+ {/* Final Results — shown after all agents complete */}
{!loading && results && (