-
Notifications
You must be signed in to change notification settings - Fork 106
Add Silicon Signal cookbook project #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
📝 WalkthroughWalkthroughThis pull request introduces two new Next.js 14/16 applications to the repository. Research Sentry is a research paper discovery and analysis platform featuring voice/text search, AI-powered summarization, paper comparison, citation tracking, and conversational analysis with support for multiple academic sources (ArXiv, PubMed, Semantic Scholar). Silicon Signal is a semiconductor supply chain tracker that performs real-time risk assessment and availability analysis for electronic components across multiple vendors, with historical trend visualization and technical intelligence gathering. Both applications include complete frontend components, API route handlers, utility libraries, configuration files, TypeScript types, and dependency manifests. The main README was also updated with an entry documenting Silicon Signal. Sequence Diagram(s)sequenceDiagram
actor User
participant UI as Research Sentry UI
participant API as Next.js API Routes
participant Parser as Search Intent Parser
participant Scrapers as Multi-Source Scrapers
participant OpenAI as OpenAI Services
participant Cache as Aggregator/Cache
User->>UI: Enter search query (text/voice)
UI->>API: POST /api/search/text or /api/search/voice
API->>Parser: parseSearchIntent(query)
Parser->>OpenAI: Parse to structured criteria
OpenAI-->>Parser: SearchCriteria
Parser-->>API: Parsed intent
API->>Scrapers: searchResearchPapers(criteria)
par Parallel Sources
Scrapers->>Scrapers: scrapeArxiv
Scrapers->>Scrapers: scrapePubMed
Scrapers->>Scrapers: scrapeSemanticScholar
Scrapers->>Scrapers: scrapeGoogleScholar
end
Scrapers->>Cache: aggregateAndDeduplicate results
Cache-->>Scrapers: merged papers
Scrapers-->>API: SearchResult
API-->>UI: Results with papers
UI->>User: Display results grid
User->>UI: Select paper & request summary
UI->>API: POST /api/summarize
API->>OpenAI: Generate summary (gpt-4o)
OpenAI-->>API: Summary text
API-->>UI: { summary }
UI->>User: Show summary + synthesize speech
sequenceDiagram
actor User
participant Dashboard as Silicon Signal Dashboard
participant API as /api/scan
participant Cache as History Store
participant Crawler as Puppeteer Crawler
participant DOM as DOM Extractor
participant Mino as Mino Automation
participant Scoring as Risk Scorer
User->>Dashboard: Enter part number + manufacturer
Dashboard->>API: POST /api/scan
API->>Cache: getLastSnapshot(partNumber)
Cache-->>API: Previous snapshot (if exists)
API->>Crawler: Launch browser
Crawler->>Crawler: Search & navigate sources
Crawler->>DOM: Extract signals (availability, price, lifecycle)
DOM-->>Crawler: Parsed data
par Parallel Source Fetching
Crawler->>Crawler: Fetch ArXiv, Semantic Scholar, Distributors
end
alt No data from browser
Crawler->>Mino: runMinoAutomation (fallback)
Mino-->>Crawler: Automated extraction results
end
Crawler-->>API: Raw signals from all sources
API->>Scoring: Compute risk (signals + history + changes)
Scoring->>Scoring: Heuristic scoring (lifecycle, lead time, availability)
Scoring-->>API: RiskAnalysis { score, level, reasoning }
API->>Cache: saveSnapshot(partNumber, snapshot)
Cache-->>API: Snapshot persisted
API-->>Dashboard: ScanResult { signals, risk, sources, history }
Dashboard->>User: Display risk badge, trends, vendor intelligence
🚥 Pre-merge checks | ✅ 1 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 13
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🤖 Fix all issues with AI agents
In `@research-sentry/app/api/search/text/route.ts`:
- Around line 7-12: Validate and guard inputs and add error handling in the POST
handler: ensure the parsed body contains a non-empty string "query" (return a
400 NextResponse when missing/invalid) and ensure "sources" is either an array
or omitted (coerce/validate before using or default to undefined/empty array so
you never call sources.map inside searchResearchPapers with a non-array). Wrap
the async calls to parseSearchIntent and searchResearchPapers in a try/catch,
log or include the error message, and return appropriate NextResponse error
responses (400 for validation errors, 500 for unexpected internal errors) so
failures in parseSearchIntent or searchResearchPapers don’t crash the server;
when valid, assign criteria.sources only after validating/coercing sources and
return NextResponse.json(results).
In `@research-sentry/app/api/search/voice/route.ts`:
- Around line 8-18: The POST route currently accepts any File and doesn't handle
runtime errors; add basic input validation and robust error handling: in the
POST handler validate the uploaded File's type against an allowed MIME list
(e.g., 'audio/wav', 'audio/mp3', 'audio/mpeg', 'audio/x-wav') and check size
against a MAX_AUDIO_BYTES constant, returning NextResponse.json with status 400
if invalid, then wrap the transcription/search flow (transcribeAudio,
parseSearchIntent, searchResearchPapers) in a try/catch and return a 500 JSON
error on exceptions; ensure successful responses still include transcript and
results via NextResponse.json.
In `@research-sentry/lib/aggregator.ts`:
- Around line 4-10: The deduplication currently keeps the first entry per
generated key which can discard better records; update the loop that builds seen
(iterating over all) to compare candidates by citations (p.citations) and
replace the stored value when a new item has higher citations (or better
metadata) before finalizing results; use the same key generation logic
(p.title.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 80)) and update the
Map entry in the seen variable when the incoming p has more citations than the
existing seen.get(key).
In `@research-sentry/lib/citation-tracker.ts`:
- Around line 28-75: In analyzeCitationTrend, validate and sanitize the parsed
`analysis` object before constructing the TrackedPaper: ensure
`analysis.velocity` is a finite number (coerce/parse and round or throw),
confirm `analysis.trend` is one of the allowed values ("up","stable","down")
otherwise throw, and verify `analysis.impactProjections` exists with numeric
`nextYear` and `fiveYear` fields (coerce/parse or throw). If any required field
is missing or invalid, throw a clear error indicating which field failed
validation; use these validated values when building and returning the
TrackedPaper
(id/paperTitle/originalCitationCount/currentCitationCount/lastChecked/velocity/trend/impactProjections).
Also add guarding around JSON.parse of `choice.message.content` in
analyzeCitationTrend to include which field was invalid in the thrown error.
In `@research-sentry/lib/intent-parser.ts`:
- Around line 21-28: The JSON parsing of res.choices[0].message.content is
unguarded and uses non-null assertions which can throw on malformed or
unexpected API responses; update the logic that builds the returned object (the
block that assigns parsed and returns
topic/keywords/sources/maxResults/fullPrompt) to first validate that res,
res.choices, res.choices[0].message and its content exist, then wrap JSON.parse
in a try-catch to handle parse errors, and on failure fall back to safe defaults
(e.g., topic = query, keywords = [], sources = ['arxiv','semantic_scholar'],
fullPrompt = query, maxResults = 20) while emitting a descriptive error/log
message that includes the raw content or error; ensure you reference and protect
access to res.choices[0].message.content and the parsed variable so downstream
code never receives undefined.
In `@research-sentry/lib/pdf-utils.ts`:
- Around line 94-116: The hostname check in isSafeHttpUrl/isPrivateIp only
inspects the literal hostname and misses DNS-rebinding cases; add an async DNS
resolution step that looks up all A/AAAA addresses for the parsed URL hostname
(using node:dns/promises lookup with { all: true }) and validate each resolved
IP with the existing isPrivateIp(host) helper, throwing or rejecting the request
if any resolved address is private; implement this as a new async helper (e.g.,
assertPublicHostname or verifyResolvedAddresses) and call it from fetchPdfText
immediately after URL parsing and before performing fetch, keeping existing
isSafeHttpUrl/isPrivateIp logic for literal checks but augmenting with the
resolved-address check.
- Around line 34-45: The current logic uses res.arrayBuffer() which can load the
entire PDF into memory before the second size check; replace that with streaming
read of res.body and enforce options?.maxBytes (maxBytes) per-chunk to prevent
unbounded memory use: after validating content-length (if present) keep that
check, then obtain a stream reader from res.body (res.body?.getReader()),
iterate reading chunks, accumulate into a Buffer/array up to maxBytes, and throw
an Error(`PDF too large to parse (...)`) as soon as the accumulated size exceeds
maxBytes; finally assemble the accumulated chunks into bytes (replacing the
Buffer.from(await res.arrayBuffer()) usage) and ensure the reader/body is
properly cancelled/closed on error.
In `@research-sentry/voice-research-project.txt`:
- Around line 410-419: The POST handler builds BibTeX by concatenating paper
fields directly (see POST, bib, and p.title/p.authors/p.url/p.publishedDate)
which can produce invalid LaTeX; add a helper (e.g., escapeTex or
sanitizeBibField) that escapes LaTeX special characters (& % $ # _ { } ~ ^ \)
and apply it to title, each author name before joining, year/publishedDate and
URL when constructing the entry so the generated bib string is safe and valid.
In `@silicon-signal/next.config.ts`:
- Around line 1-10: The config uses __dirname (which is undefined in ESM);
update turbopack.root to compute the directory from import.meta.url instead:
convert the current path.resolve(__dirname) usage to derive the directory using
the ESM pattern (e.g., fileURLToPath(new URL(import.meta.url)) and path.dirname)
and then pass that computed directory into nextConfig.turbopack.root; locate the
nextConfig object and the turbopack.root assignment to make this change.
In `@silicon-signal/src/app/api/scan/route.ts`:
- Line 641: The page navigation calls (page.goto) currently use very long
per-navigation timeouts (100000 ms) which can make a single scan exceed
serverless limits; update the page.goto calls (searchUrl1, and the other
occurrences referenced around the same block) to use a much shorter
per-navigation timeout (e.g., 10–30s) or remove the explicit timeout and instead
set a conservative page.setDefaultNavigationTimeout, and rely on the overall
SCAN_TIMEOUT_MS to enforce the full scan limit; change the timeout values passed
to page.goto (and any other page.goto calls near the same code block)
accordingly so individual navigations don't consume excessive time while the
global SCAN_TIMEOUT_MS remains the authoritative timeout.
- Around line 6-62: Remove the duplicated local interfaces (RiskAnalysis,
ScanResult, SourceSignal, SignalSummary, ConfidenceInfo) from route.ts and
import them from the shared types module (the exported definitions in
silicon-signal/src/types.ts); update any local references to use the imported
types (e.g., ScanResult, RiskAnalysis, SourceSignal, SignalSummary,
ConfidenceInfo) so there is a single source of truth and no duplicate type
declarations in route.ts.
In `@silicon-signal/src/components/Header.tsx`:
- Around line 1-11: The Header component uses window.location.href inside the
clickable div (onClick={() => window.location.href = '/'}), which will break
server rendering; replace this with Next.js client-side navigation by importing
Link from 'next/link' and using a <Link> around the logo/title instead of the
onClick handler (remove window access and the onClick). Keep Header as a server
component (no 'use client') and ensure the Link wraps the existing inner
elements (the icon and span) so navigation works without a full reload.
In `@silicon-signal/src/lib/store.ts`:
- Line 4: currentHistoryFile is module-level mutable state and is being
reassigned in saveSnapshot which can race with getHistory; change saveSnapshot
to stop mutating currentHistoryFile and instead return the written file path (or
accept an explicit target path parameter), update callers to use the returned
path when calling getHistory or other consumers, and remove any reassignment of
the variable in saveSnapshot; reference saveSnapshot and getHistory to locate
the changes and remove reliance on the module-level currentHistoryFile variable.
🟡 Minor comments (31)
research-sentry/README.md-8-9 (1)
8-9:⚠️ Potential issue | 🟡 MinorDemo video link appears to be a placeholder.
The "Demo video" section links to the live app URL rather than an actual video. Either update this with the actual demo video link or remove/clarify this section.
silicon-signal/src/lib/store.ts-27-79 (1)
27-79:⚠️ Potential issue | 🟡 MinorNo file locking: concurrent writes may corrupt data.
Multiple concurrent scan requests writing to the same history file can interleave reads and writes, potentially corrupting the JSON or losing snapshots. Consider using a file-locking library (e.g.,
proper-lockfile) or an atomic write pattern (write to temp file, then rename).silicon-signal/src/app/api/scan/route.ts-524-524 (1)
524-524:⚠️ Potential issue | 🟡 MinorModule-level cache is unreliable in serverless environments.
scanCacheis a module-levelMap. In serverless environments (Vercel, AWS Lambda), each invocation may or may not share the same module instance, making cache behavior unpredictable. Additionally, concurrent requests could have race conditions when accessing/modifying the cache.For production use, consider an external cache (Redis, Upstash) or accept that caching is best-effort in serverless.
silicon-signal/src/components/VendorIntelligence.tsx-4-9 (1)
4-9:⚠️ Potential issue | 🟡 MinorHardcoded values may mislead users.
All sources display fixed values:
type: 'Distributor',reliability: 98.4, andstatus: 'Online'. Combined with the always-present "Verified" and "LIVE" badges (lines 27-28), this creates an impression of verified real-time data when these are actually static placeholders.Consider either:
- Deriving these values from actual source metadata (if available in
source_signals)- Clearly indicating these are placeholder/baseline values in the UI
silicon-signal/README.md-8-9 (1)
8-9:⚠️ Potential issue | 🟡 MinorSection heading is misleading.
The heading says "Demo video" but the content references an image file (
image.png). Consider renaming to "Demo" or "Screenshot", or replace with an actual video/GIF.research-sentry/lib/mino.ts-60-90 (1)
60-90:⚠️ Potential issue | 🟡 MinorPotential data loss: remaining buffer not processed after stream ends.
When the stream ends (
done === true), any data remaining inbuffer(i.e., a final line without a trailing newline) is not processed. If the server sends theCOMPLETEevent as the last line without a trailing newline, the result will be lost.🔧 Proposed fix to process remaining buffer
while (true) { const { done, value } = await reader.read(); - if (done) break; + if (done) { + // Process any remaining data in buffer + if (buffer.trim().startsWith('data: ')) { + try { + const eventData = JSON.parse(buffer.trim().slice(6)); + if (eventData.type === 'COMPLETE' || eventData.type === 'complete') { + result = eventData.resultJson || eventData.result || eventData.data; + } + } catch { /* ignore */ } + } + break; + } buffer += decoder.decode(value, { stream: true });research-sentry/hooks/useVoiceCommands.ts-98-139 (1)
98-139:⚠️ Potential issue | 🟡 MinorMissing cleanup for event handlers.
The effect assigns handlers to
speechRecognition.onresult,onerror, andonendbut doesn't clean them up when dependencies change or on unmount. This can cause memory leaks or unexpected behavior if the effect re-runs.🔧 Add cleanup function
useEffect(() => { if (!speechRecognition) return; speechRecognition.onresult = (event: SpeechRecognitionEvent) => { // ... handler code }; speechRecognition.onerror = (event: SpeechRecognitionErrorEvent) => { // ... handler code }; speechRecognition.onend = () => { // ... handler code }; + return () => { + speechRecognition.onresult = null; + speechRecognition.onerror = null; + speechRecognition.onend = null; + }; }, [speechRecognition, isListening, processCommand]);silicon-signal/src/components/RiskBadge.tsx-15-15 (1)
15-15:⚠️ Potential issue | 🟡 MinorSilent fallback for unknown levels.
If an unknown level is passed, the badge will display the unknown level text (e.g., "CRITICAL RISK") but with MEDIUM styling, which could be misleading. Consider either restricting the type or handling unknown levels explicitly.
silicon-signal/src/components/ScanForm.tsx-23-26 (1)
23-26:⚠️ Potential issue | 🟡 MinorForm submission allows whitespace-only input.
The check
if (part)will pass for whitespace-only strings like" ". Consider trimming the input before validation.Proposed fix
const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (part) onScan(part, mfr); + const trimmedPart = part.trim(); + if (trimmedPart) onScan(trimmedPart, mfr.trim() || undefined); };research-sentry/lib/whisper.ts-3-3 (1)
3-3:⚠️ Potential issue | 🟡 MinorNon-null assertion on environment variable can cause unclear runtime errors.
If
OPENAI_API_KEYis not set, the app will fail with an unhelpful error from the OpenAI client. Consider adding validation or a descriptive error.Proposed fix
-const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! }); +const apiKey = process.env.OPENAI_API_KEY; +if (!apiKey) { + throw new Error('OPENAI_API_KEY environment variable is required'); +} +const openai = new OpenAI({ apiKey });research-sentry/app/api/compare/route.ts-8-10 (1)
8-10:⚠️ Potential issue | 🟡 MinorAdd
Array.isArraycheck to prevent runtime errors.If
papersis a non-array truthy value (e.g., an object), accessingpapers.lengthwill returnundefinedand the comparison< 2will pass, potentially causing downstream errors.Proposed fix
- if (!papers || papers.length < 2) { + if (!Array.isArray(papers) || papers.length < 2) { return NextResponse.json({ error: 'Select at least 2 papers to compare' }, { status: 400 }); }research-sentry/app/layout.tsx-5-6 (1)
5-6:⚠️ Potential issue | 🟡 MinorAdd
langto the root<html>element.This improves accessibility and avoids Next.js warnings.
✅ Suggested fix
-export default function RootLayout({ children }: { children: React.ReactNode }) { - return <html><body>{children}</body></html>; -} +export default function RootLayout({ children }: { children: React.ReactNode }) { + return <html lang="en"><body>{children}</body></html>; +}research-sentry/app/api/conversation/route.ts-6-16 (1)
6-16:⚠️ Potential issue | 🟡 MinorValidate history entries (role/content) before invoking the model.
historyis only checked for array shape; invalid roles or non-string content can break the OpenAI call. Add a role allow‑list and basic content validation (and optionally trim length).✅ Suggested fix
export async function POST(req: NextRequest) { try { const { history, context } = await req.json(); if (!history || !Array.isArray(history)) { return NextResponse.json({ error: 'Invalid history format' }, { status: 400 }); } + const allowedRoles = new Set(['user', 'assistant', 'system']); + const sanitized = history + .filter((m: any) => m && allowedRoles.has(m.role) && typeof m.content === 'string') + .slice(-50); + if (sanitized.length === 0) { + return NextResponse.json({ error: 'Invalid history format' }, { status: 400 }); + } - const response = await generateConversationResponse(history, context); + const response = await generateConversationResponse(sanitized, context); return NextResponse.json(response);research-sentry/app/api/export/bibtex/route.ts-18-25 (1)
18-25:⚠️ Potential issue | 🟡 MinorValidate individual paper entries before accessing fields.
papersis validated as an array, but null/primitive entries will throw onp.title, and non-string author items will serialize as[object Object]. Add a per-item guard and normalize authors to avoid server errors and malformed BibTeX.✅ Suggested fix
- const bib = papers.map((p: any, i: number) => { + if (!papers.every((p: any) => p && typeof p === 'object')) { + return NextResponse.json({ error: 'Invalid paper entry' }, { status: 400 }); + } + const bib = papers.map((p: any, i: number) => { const key = 'paper' + i; + const authors = Array.isArray(p.authors) + ? p.authors + .map((a: any) => (typeof a === 'string' ? a : a?.name)) + .filter(Boolean) + .join(' and ') + : ''; return '@article{' + key + ',\n title={' + escapeBibtex(p.title) + - '},\n author={' + escapeBibtex(p.authors?.join(' and ') || '') + + '},\n author={' + escapeBibtex(authors) + '},\n year={' + yearFrom(p.publishedDate) + '},\n url={' + escapeBibtex(p.url) + '}\n}'; }).join('\n\n');research-sentry/components/AudioPlayer.tsx-34-40 (1)
34-40:⚠️ Potential issue | 🟡 MinorGuard against
NaNin progress calculation.Before audio metadata loads,
durationmay beNaNor0, causingprogressto becomeNaN. This could result in unexpected rendering behavior.🛡️ Suggested fix
const handleTimeUpdate = () => { if (audioRef.current) { const current = audioRef.current.currentTime; const duration = audioRef.current.duration; - setProgress((current / duration) * 100); + if (duration && !isNaN(duration)) { + setProgress((current / duration) * 100); + } } };research-sentry/components/CitationTracker.tsx-18-35 (1)
18-35:⚠️ Potential issue | 🟡 MinorUseEffect dependency on object reference may cause unnecessary refetches.
Using
paper(an object) as a dependency will trigger the effect on every render if the parent recreates the paper object. Consider usingpaper.idinstead for a stable dependency.🐛 Suggested fix
useEffect(() => { const fetchData = async () => { + setLoading(true); try { const res = await fetch('/api/citations/track', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paper }), }); const result = await res.json(); setData(result); } catch (e) { console.error(e); } finally { setLoading(false); } }; fetchData(); - }, [paper]); + }, [paper.id]);Note: You'll also need to ensure the
paperreference is captured in the fetch call. Alternatively, use a memoization strategy in the parent component.research-sentry/components/CitationTracker.tsx-21-29 (1)
21-29:⚠️ Potential issue | 🟡 MinorMissing error state and response validation.
The component doesn't check
res.okbefore parsing JSON and has no error state to display to users. If the API fails, the user sees nothing (returnsnullat line 46).🛡️ Suggested improvement
+ const [error, setError] = useState<string | null>(null); useEffect(() => { const fetchData = async () => { + setError(null); try { const res = await fetch('/api/citations/track', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paper }), }); + if (!res.ok) { + throw new Error('Failed to fetch citation data'); + } const result = await res.json(); setData(result); } catch (e) { console.error(e); + setError(e instanceof Error ? e.message : 'An error occurred'); } finally { setLoading(false); } }; fetchData(); }, [paper]); + + if (error) { + return ( + <div className="p-6 bg-red-900/20 border border-red-700 rounded-xl"> + <p className="text-red-400">{error}</p> + </div> + ); + }research-sentry/components/AudioPlayer.tsx-24-32 (1)
24-32:⚠️ Potential issue | 🟡 MinorToggle play state may desync if
play()fails.The
isPlayingstate is toggled immediately regardless of whetherplay()succeeds. Ifplay()fails (e.g., due to browser autoplay restrictions), the UI will show playing while audio is actually paused.🐛 Suggested fix
const togglePlay = () => { if (!audioRef.current) return; if (isPlaying) { audioRef.current.pause(); + setIsPlaying(false); } else { - audioRef.current.play(); + audioRef.current.play().catch(() => {/* autoplay blocked */}); + setIsPlaying(true); + return; } - setIsPlaying(!isPlaying); };Or handle the promise properly:
const togglePlay = () => { if (!audioRef.current) return; if (isPlaying) { audioRef.current.pause(); setIsPlaying(false); } else { audioRef.current.play() .then(() => setIsPlaying(true)) .catch(() => setIsPlaying(false)); } };research-sentry/lib/summarizer.ts-24-30 (1)
24-30:⚠️ Potential issue | 🟡 MinorNon-null assertion on response content could cause runtime errors.
Similar to
comparator.ts, if the OpenAI API returns an unexpected response, the!assertion will cause a crash.🛡️ Proposed fix with safer handling
const response = await openai.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: prompt }], }); - return response.choices[0].message.content!; + const content = response.choices[0]?.message?.content; + if (!content) { + throw new Error('No content in OpenAI response'); + } + return content; }research-sentry/lib/comparator.ts-40-41 (1)
40-41:⚠️ Potential issue | 🟡 MinorUnsafe JSON parsing and non-null assertion on response content.
JSON.parsewill throw if the response content is not valid JSON, and the non-null assertion (!) onmessage.contentcould cause runtime errors if the API returns an unexpected response.🛡️ Proposed fix with safer parsing and validation
- const content = JSON.parse(response.choices[0].message.content!); - return content; + const rawContent = response.choices[0]?.message?.content; + if (!rawContent) { + throw new Error('No content in OpenAI response'); + } + + let content: ComparisonResult; + try { + content = JSON.parse(rawContent); + } catch { + throw new Error('Failed to parse comparison response as JSON'); + } + + if (!Array.isArray(content.points) || typeof content.summary !== 'string') { + throw new Error('Invalid comparison response structure'); + } + + return content;research-sentry/lib/search.ts-267-296 (1)
267-296:⚠️ Potential issue | 🟡 MinorClamp
maxResultsto avoid negative slicing.
Negative values makeslice(0, -n)drop trailing items unexpectedly. Guard to>= 0.🛠️ Suggested clamp
- return { - query: criteria.topic, - papers: papers.slice(0, criteria.maxResults), - totalFound: papers.length, - }; + const maxResults = Math.max(0, criteria.maxResults ?? 0); + return { + query: criteria.topic, + papers: papers.slice(0, maxResults), + totalFound: papers.length, + };research-sentry/lib/search.ts-80-98 (1)
80-98:⚠️ Potential issue | 🟡 MinorHandle non-200 responses before parsing ArXiv XML.
If ArXiv returns an error page (rate-limit, outage), the regex parser will treat it as results. Return early whenres.okis false.🛠️ Suggested guard
- const res = await fetch(`https://export.arxiv.org/api/query?search_query=all:${query}&start=0&max_results=10`); + const res = await fetch(`https://export.arxiv.org/api/query?search_query=all:${query}&start=0&max_results=10`); + if (!res.ok) return []; const xml = await res.text();research-sentry/app/api/emails/extract/route.ts-185-199 (1)
185-199:⚠️ Potential issue | 🟡 MinorGuard against non-object
paperpayloads.
Ifpaperis missing or not an object,paper.pdfUrlwill throw and the route returns 500. Consider validating and returning a 400 early.🛡️ Suggested payload validation
- const { paper } = await req.json(); - if (!paper) return NextResponse.json({ error: 'Paper data required' }, { status: 400 }); + const body = await req.json().catch(() => null); + const paper = body?.paper; + if (!paper || typeof paper !== 'object') { + return NextResponse.json({ error: 'Paper data required', authors: [] }, { status: 400 }); + }research-sentry/components/CoPilotMode.tsx-14-21 (1)
14-21:⚠️ Potential issue | 🟡 MinorReset
currentIndexwhenpaperslist shrinks.When a new search yields fewer papers,
currentIndexcan exceed the array bounds, causing the component to display "No papers" instead of gracefully resetting to the first paper. Add an effect to reset the index and summary state when papers.length decreases.🛠️ Suggested implementation
const currentPaper = papers[currentIndex]; + + useEffect(() => { + if (currentIndex >= papers.length && papers.length > 0) { + setCurrentIndex(0); + setIsReading(false); + setSummaryText(''); + } + }, [papers.length, currentIndex]);research-sentry/components/PaperCard.tsx-36-66 (1)
36-66:⚠️ Potential issue | 🟡 MinorClear the AbortController timeout in all code paths.
If the fetch fails beforewindow.clearTimeout()on line 50 executes, the timer persists and accumulates across repeated calls, unnecessarily aborting a dead controller later.🛠️ Suggested cleanup
window.clearTimeout(timeout); const data = (await res.json()) as { authors?: AuthorInfo[]; error?: string }; - } catch (e) { + } catch (e) { setAuthorError(e instanceof Error ? e.message : 'Failed to extract author information'); setAuthors([]); } finally { + window.clearTimeout(timeout); setIsExtractingAuthors(false); }research-sentry/voice-research-project.txt-61-64 (1)
61-64:⚠️ Potential issue | 🟡 MinorMarkdown syntax error in README section.
The code block closing marker has a typo:
Finstead of``` ``.📝 Fix the markdown syntax
```bash vercel --prod -``F +```research-sentry/voice-research-project.txt-251-253 (1)
251-253:⚠️ Potential issue | 🟡 MinorNon-null assertion on response body could throw.
res.body!.getReader()assumes the body is always present, but it could benullif the request fails or the server returns an empty response.🛡️ Suggested fix with guard
+ if (!res.body) { + throw new Error('No response body from Mino API'); + } const reader = res.body.getReader();research-sentry/voice-research-project.txt-351-353 (1)
351-353:⚠️ Potential issue | 🟡 MinorMissing
langattribute on<html>element.The layout's
<html>tag is missing thelangattribute, which is important for accessibility and SEO.📝 Suggested fix
export default function RootLayout({ children }: { children: React.ReactNode }) { - return <html><body>{children}</body></html>; + return <html lang="en"><body>{children}</body></html>; }research-sentry/voice-research-project.txt-265-270 (1)
265-270:⚠️ Potential issue | 🟡 MinorSilent error swallowing in SSE parsing.
The empty
catch {}block on Line 268 silently ignores JSON parse errors, which could mask malformed SSE events from the Mino API.🛡️ Suggested fix: log parse errors
try { const event = JSON.parse(line.slice(6)); if (event.type === 'COMPLETE') result = event.resultJson; - } catch {} + } catch (e) { + console.warn('Failed to parse SSE event:', line, e); + }research-sentry/app/page.tsx-229-231 (1)
229-231:⚠️ Potential issue | 🟡 MinorPotential runtime error with non-null assertion on
find()result.The
find()call on Line 230 uses a non-null assertion (!), but iftrackingPaperIdreferences a paper that no longer exists in results, this will passundefinedtoCitationTracker.🛡️ Suggested fix with guard
- <CitationTracker - paper={results.papers.find(p => p.id === trackingPaperId)!} - /> + {(() => { + const paper = results.papers.find(p => p.id === trackingPaperId); + return paper ? <CitationTracker paper={paper} /> : null; + })()}Alternatively, close the modal if the paper isn't found:
{trackingPaperId && results && ( + results.papers.find(p => p.id === trackingPaperId) && ( <div className="fixed inset-0 z-[60] ..."> ... </div> + ) )}research-sentry/components/ConversationInterface.tsx-86-104 (1)
86-104:⚠️ Potential issue | 🟡 MinorMissing response status check and user feedback on voice transcription failure.
The voice input handler doesn't check
response.okbefore parsing JSON, and errors are silently logged without user feedback.🛡️ Suggested fix with proper error handling
const handleVoiceInput = async (audioBlob: Blob) => { - // Ideally we transcribe this first or send audio to conversation API - // For now, simpler implementation: Transcribe via existing voice API then send text try { const formData = new FormData(); formData.append('audio', audioBlob, 'voice_chat.webm'); - const res = await fetch('/api/search/voice', { // Reusing voice endpoint for transcription mostly + const res = await fetch('/api/search/voice', { method: 'POST', body: formData, }); + if (!res.ok) { + throw new Error('Voice transcription failed'); + } const data = await res.json(); if (data.transcript) { handleSend(data.transcript); + } else { + throw new Error('No transcript received'); } } catch (e) { console.error(e); + const errorMsg: Message = { + id: crypto.randomUUID(), + role: 'system', + content: "Sorry, I couldn't process the voice input. Please try again.", + timestamp: Date.now(), + }; + setMessages(prev => [...prev, errorMsg]); } };
| 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add request validation and error handling to avoid crashes.
query can be missing/empty and sources can be non-array, which will break searchResearchPapers (sources.map). Also, parseSearchIntent/searchResearchPapers errors are unhandled.
✅ Suggested fix
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);
+ try {
+ const { query, sources } = await req.json();
+ if (typeof query !== 'string' || !query.trim()) {
+ return NextResponse.json({ error: 'query is required' }, { status: 400 });
+ }
+ const criteria = await parseSearchIntent(query);
+ if (sources != null) {
+ if (!Array.isArray(sources) || !sources.every((s) => typeof s === 'string')) {
+ return NextResponse.json({ error: 'sources must be a string[]' }, { status: 400 });
+ }
+ criteria.sources = sources;
+ }
+ const results = await searchResearchPapers(criteria);
+ return NextResponse.json(results);
+ } catch (error) {
+ console.error('Search API Error:', error);
+ return NextResponse.json({ error: 'Failed to search' }, { status: 500 });
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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); | |
| export async function POST(req: NextRequest) { | |
| try { | |
| const { query, sources } = await req.json(); | |
| if (typeof query !== 'string' || !query.trim()) { | |
| return NextResponse.json({ error: 'query is required' }, { status: 400 }); | |
| } | |
| const criteria = await parseSearchIntent(query); | |
| if (sources != null) { | |
| if (!Array.isArray(sources) || !sources.every((s) => typeof s === 'string')) { | |
| return NextResponse.json({ error: 'sources must be a string[]' }, { status: 400 }); | |
| } | |
| criteria.sources = sources; | |
| } | |
| const results = await searchResearchPapers(criteria); | |
| return NextResponse.json(results); | |
| } catch (error) { | |
| console.error('Search API Error:', error); | |
| return NextResponse.json({ error: 'Failed to search' }, { status: 500 }); | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@research-sentry/app/api/search/text/route.ts` around lines 7 - 12, Validate
and guard inputs and add error handling in the POST handler: ensure the parsed
body contains a non-empty string "query" (return a 400 NextResponse when
missing/invalid) and ensure "sources" is either an array or omitted
(coerce/validate before using or default to undefined/empty array so you never
call sources.map inside searchResearchPapers with a non-array). Wrap the async
calls to parseSearchIntent and searchResearchPapers in a try/catch, log or
include the error message, and return appropriate NextResponse error responses
(400 for validation errors, 500 for unexpected internal errors) so failures in
parseSearchIntent or searchResearchPapers don’t crash the server; when valid,
assign criteria.sources only after validating/coercing sources and return
NextResponse.json(results).
| export async function POST(req: NextRequest) { | ||
| const form = await req.formData(); | ||
| const audio = form.get('audio'); | ||
| if (!audio || !(audio instanceof File)) { | ||
| return NextResponse.json({ error: 'audio file is required' }, { status: 400 }); | ||
| } | ||
| const buffer = Buffer.from(await audio.arrayBuffer()); | ||
| const transcript = await transcribeAudio(buffer); | ||
| const criteria = await parseSearchIntent(transcript); | ||
| const results = await searchResearchPapers(criteria); | ||
| return NextResponse.json({ ...results, transcript }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle transcription/search errors and validate audio input.
This route doesn’t catch errors from transcribeAudio or searchResearchPapers, and it accepts any file type/size. Add a try/catch and basic MIME/size validation to prevent runtime failures and misuse.
✅ Suggested fix
export async function POST(req: NextRequest) {
- const form = await req.formData();
- const audio = form.get('audio');
- if (!audio || !(audio instanceof File)) {
- return NextResponse.json({ error: 'audio file is required' }, { status: 400 });
- }
- const buffer = Buffer.from(await audio.arrayBuffer());
- const transcript = await transcribeAudio(buffer);
- const criteria = await parseSearchIntent(transcript);
- const results = await searchResearchPapers(criteria);
- return NextResponse.json({ ...results, transcript });
+ try {
+ const form = await req.formData();
+ const audio = form.get('audio');
+ if (!audio || !(audio instanceof File)) {
+ return NextResponse.json({ error: 'audio file is required' }, { status: 400 });
+ }
+ if (!audio.type.startsWith('audio/')) {
+ return NextResponse.json({ error: 'audio file must be audio/*' }, { status: 400 });
+ }
+ if (audio.size > 10 * 1024 * 1024) {
+ return NextResponse.json({ error: 'audio file too large' }, { status: 413 });
+ }
+ const buffer = Buffer.from(await audio.arrayBuffer());
+ const transcript = await transcribeAudio(buffer);
+ const criteria = await parseSearchIntent(transcript);
+ const results = await searchResearchPapers(criteria);
+ return NextResponse.json({ ...results, transcript });
+ } catch (error) {
+ console.error('Voice Search API Error:', error);
+ return NextResponse.json({ error: 'Failed to process audio' }, { status: 500 });
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function POST(req: NextRequest) { | |
| const form = await req.formData(); | |
| const audio = form.get('audio'); | |
| if (!audio || !(audio instanceof File)) { | |
| return NextResponse.json({ error: 'audio file is required' }, { status: 400 }); | |
| } | |
| const buffer = Buffer.from(await audio.arrayBuffer()); | |
| const transcript = await transcribeAudio(buffer); | |
| const criteria = await parseSearchIntent(transcript); | |
| const results = await searchResearchPapers(criteria); | |
| return NextResponse.json({ ...results, transcript }); | |
| export async function POST(req: NextRequest) { | |
| try { | |
| const form = await req.formData(); | |
| const audio = form.get('audio'); | |
| if (!audio || !(audio instanceof File)) { | |
| return NextResponse.json({ error: 'audio file is required' }, { status: 400 }); | |
| } | |
| if (!audio.type.startsWith('audio/')) { | |
| return NextResponse.json({ error: 'audio file must be audio/*' }, { status: 400 }); | |
| } | |
| if (audio.size > 10 * 1024 * 1024) { | |
| return NextResponse.json({ error: 'audio file too large' }, { status: 413 }); | |
| } | |
| const buffer = Buffer.from(await audio.arrayBuffer()); | |
| const transcript = await transcribeAudio(buffer); | |
| const criteria = await parseSearchIntent(transcript); | |
| const results = await searchResearchPapers(criteria); | |
| return NextResponse.json({ ...results, transcript }); | |
| } catch (error) { | |
| console.error('Voice Search API Error:', error); | |
| return NextResponse.json({ error: 'Failed to process audio' }, { status: 500 }); | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@research-sentry/app/api/search/voice/route.ts` around lines 8 - 18, The POST
route currently accepts any File and doesn't handle runtime errors; add basic
input validation and robust error handling: in the POST handler validate the
uploaded File's type against an allowed MIME list (e.g., 'audio/wav',
'audio/mp3', 'audio/mpeg', 'audio/x-wav') and check size against a
MAX_AUDIO_BYTES constant, returning NextResponse.json with status 400 if
invalid, then wrap the transcription/search flow (transcribeAudio,
parseSearchIntent, searchResearchPapers) in a try/catch and return a 500 JSON
error on exceptions; ensure successful responses still include transcript and
results via NextResponse.json.
| const all = results.flat(); | ||
| const seen = new Map(); | ||
| for (const p of all) { | ||
| const key = p.title.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 80); | ||
| if (key && !seen.has(key)) seen.set(key, p); | ||
| } | ||
| return Array.from(seen.values()).sort((a, b) => (b.citations || 0) - (a.citations || 0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep the best duplicate, not just the first.
Choosing the first entry can discard higher‑citation metadata and corrupt the final ranking. Consider replacing with the best candidate (e.g., highest citations).
🔧 Suggested fix
- const seen = new Map();
+ const seen = new Map<string, ResearchPaper>();
for (const p of all) {
const key = p.title.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 80);
- if (key && !seen.has(key)) seen.set(key, p);
+ if (!key) continue;
+ const existing = seen.get(key);
+ const pCitations = p.citations ?? 0;
+ const eCitations = existing?.citations ?? 0;
+ if (!existing || pCitations > eCitations) {
+ seen.set(key, p);
+ }
}🤖 Prompt for AI Agents
In `@research-sentry/lib/aggregator.ts` around lines 4 - 10, The deduplication
currently keeps the first entry per generated key which can discard better
records; update the loop that builds seen (iterating over all) to compare
candidates by citations (p.citations) and replace the stored value when a new
item has higher citations (or better metadata) before finalizing results; use
the same key generation logic (p.title.toLowerCase().replace(/[^a-z0-9]/g,
'').slice(0, 80)) and update the Map entry in the seen variable when the
incoming p has more citations than the existing seen.get(key).
| export async function analyzeCitationTrend(paper: ResearchPaper): Promise<TrackedPaper> { | ||
| const openai = getOpenAI(); | ||
| // Simulating citation analysis with AI since we don't have historical data access in this demo | ||
| const prompt = `Analyze the potential citation impact of this research paper: | ||
| Title: "${paper.title}" | ||
| Current Citations: ${paper.citations || 0} | ||
| Published: ${paper.publishedDate} | ||
| Source: ${paper.source} | ||
|
|
||
| Estimate the "Citation Velocity" (citations/month) and predict impact. | ||
| Return JSON: | ||
| { | ||
| "velocity": number, | ||
| "trend": "up" | "stable" | "down", | ||
| "impactProjections": { "nextYear": number, "fiveYear": number } | ||
| } | ||
| `; | ||
|
|
||
| const response = await openai.chat.completions.create({ | ||
| model: 'gpt-4o', | ||
| messages: [{ role: 'user', content: prompt }], | ||
| response_format: { type: 'json_object' }, | ||
| }); | ||
|
|
||
| const choice = response.choices?.[0]; | ||
| if (!choice) { | ||
| throw new Error('OpenAI returned no choices'); | ||
| } | ||
| if (choice.finish_reason === 'length') { | ||
| throw new Error('OpenAI response was truncated'); | ||
| } | ||
| let analysis: any; | ||
| try { | ||
| analysis = JSON.parse(choice.message.content ?? '{}'); | ||
| } catch (error) { | ||
| throw new Error('OpenAI returned invalid JSON'); | ||
| } | ||
|
|
||
| return { | ||
| id: paper.id, | ||
| paperTitle: paper.title, | ||
| originalCitationCount: paper.citations || 0, | ||
| currentCitationCount: paper.citations || 0, // In real app, this updates | ||
| lastChecked: Date.now(), | ||
| velocity: analysis.velocity, | ||
| trend: analysis.trend, | ||
| impactProjections: analysis.impactProjections | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
fd -e ts -e tsx citation-trackerRepository: tinyfish-io/tinyfish-cookbook
Length of output: 112
🏁 Script executed:
cat research-sentry/lib/citation-tracker.ts | head -100Repository: tinyfish-io/tinyfish-cookbook
Length of output: 2437
🏁 Script executed:
# Search for TrackedPaper type definition
rg "type TrackedPaper|interface TrackedPaper" -A 15Repository: tinyfish-io/tinyfish-cookbook
Length of output: 1132
🏁 Script executed:
# Search for ResearchPaper type definition
rg "type ResearchPaper|interface ResearchPaper" -A 10Repository: tinyfish-io/tinyfish-cookbook
Length of output: 1327
Validate AI JSON fields before building TrackedPaper.
All fields in the TrackedPaper interface are required; OpenAI's JSON mode can return missing or invalid fields (e.g., velocity as a string, trend with unexpected values, or missing impactProjections structure). Directly assigning these values violates the contract and breaks consumers. Add field validation with clear error handling.
🛡️ Suggested validation hardening
- let analysis: any;
+ let analysis: any;
try {
analysis = JSON.parse(choice.message.content ?? '{}');
} catch (error) {
throw new Error('OpenAI returned invalid JSON');
}
+ const velocity = Number(analysis?.velocity);
+ const trend = analysis?.trend;
+ const impact = analysis?.impactProjections ?? {};
+ const nextYear = Number(impact?.nextYear);
+ const fiveYear = Number(impact?.fiveYear);
+ if (
+ !Number.isFinite(velocity) ||
+ !['up', 'stable', 'down'].includes(trend) ||
+ !Number.isFinite(nextYear) ||
+ !Number.isFinite(fiveYear)
+ ) {
+ throw new Error('OpenAI returned incomplete analysis');
+ }
return {
id: paper.id,
paperTitle: paper.title,
originalCitationCount: paper.citations || 0,
currentCitationCount: paper.citations || 0, // In real app, this updates
lastChecked: Date.now(),
- velocity: analysis.velocity,
- trend: analysis.trend,
- impactProjections: analysis.impactProjections
+ velocity,
+ trend,
+ impactProjections: { nextYear, fiveYear }
};🤖 Prompt for AI Agents
In `@research-sentry/lib/citation-tracker.ts` around lines 28 - 75, In
analyzeCitationTrend, validate and sanitize the parsed `analysis` object before
constructing the TrackedPaper: ensure `analysis.velocity` is a finite number
(coerce/parse and round or throw), confirm `analysis.trend` is one of the
allowed values ("up","stable","down") otherwise throw, and verify
`analysis.impactProjections` exists with numeric `nextYear` and `fiveYear`
fields (coerce/parse or throw). If any required field is missing or invalid,
throw a clear error indicating which field failed validation; use these
validated values when building and returning the TrackedPaper
(id/paperTitle/originalCitationCount/currentCitationCount/lastChecked/velocity/trend/impactProjections).
Also add guarding around JSON.parse of `choice.message.content` in
analyzeCitationTrend to include which field was invalid in the thrown error.
| const parsed = JSON.parse(res.choices[0].message.content!); | ||
| return { | ||
| topic: parsed.searchKeywords || query, | ||
| keywords: [], | ||
| sources: parsed.sources || ['arxiv', 'semantic_scholar'], | ||
| maxResults: 20, | ||
| fullPrompt: parsed.refinedAgenticGoal || query | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling for API response and JSON parsing.
The code uses non-null assertions on res.choices[0].message.content and performs JSON.parse without a try-catch. If the API returns an unexpected response or malformed JSON, this will throw an unhelpful error.
🛡️ Suggested fix with error handling
- const parsed = JSON.parse(res.choices[0].message.content!);
- return {
- topic: parsed.searchKeywords || query,
- keywords: [],
- sources: parsed.sources || ['arxiv', 'semantic_scholar'],
- maxResults: 20,
- fullPrompt: parsed.refinedAgenticGoal || query
- };
+ const content = res.choices[0]?.message?.content;
+ if (!content) {
+ return {
+ topic: query,
+ keywords: [],
+ sources: ['arxiv', 'semantic_scholar'],
+ maxResults: 20,
+ fullPrompt: query
+ };
+ }
+
+ let parsed;
+ try {
+ parsed = JSON.parse(content);
+ } catch {
+ return {
+ topic: query,
+ keywords: [],
+ sources: ['arxiv', 'semantic_scholar'],
+ maxResults: 20,
+ fullPrompt: query
+ };
+ }
+
+ return {
+ topic: parsed.searchKeywords || query,
+ keywords: [],
+ sources: parsed.sources || ['arxiv', 'semantic_scholar'],
+ maxResults: 20,
+ fullPrompt: parsed.refinedAgenticGoal || query
+ };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const parsed = JSON.parse(res.choices[0].message.content!); | |
| return { | |
| topic: parsed.searchKeywords || query, | |
| keywords: [], | |
| sources: parsed.sources || ['arxiv', 'semantic_scholar'], | |
| maxResults: 20, | |
| fullPrompt: parsed.refinedAgenticGoal || query | |
| }; | |
| const content = res.choices[0]?.message?.content; | |
| if (!content) { | |
| return { | |
| topic: query, | |
| keywords: [], | |
| sources: ['arxiv', 'semantic_scholar'], | |
| maxResults: 20, | |
| fullPrompt: query | |
| }; | |
| } | |
| let parsed; | |
| try { | |
| parsed = JSON.parse(content); | |
| } catch { | |
| return { | |
| topic: query, | |
| keywords: [], | |
| sources: ['arxiv', 'semantic_scholar'], | |
| maxResults: 20, | |
| fullPrompt: query | |
| }; | |
| } | |
| return { | |
| topic: parsed.searchKeywords || query, | |
| keywords: [], | |
| sources: parsed.sources || ['arxiv', 'semantic_scholar'], | |
| maxResults: 20, | |
| fullPrompt: parsed.refinedAgenticGoal || query | |
| }; |
🤖 Prompt for AI Agents
In `@research-sentry/lib/intent-parser.ts` around lines 21 - 28, The JSON parsing
of res.choices[0].message.content is unguarded and uses non-null assertions
which can throw on malformed or unexpected API responses; update the logic that
builds the returned object (the block that assigns parsed and returns
topic/keywords/sources/maxResults/fullPrompt) to first validate that res,
res.choices, res.choices[0].message and its content exist, then wrap JSON.parse
in a try-catch to handle parse errors, and on failure fall back to safe defaults
(e.g., topic = query, keywords = [], sources = ['arxiv','semantic_scholar'],
fullPrompt = query, maxResults = 20) while emitting a descriptive error/log
message that includes the raw content or error; ensure you reference and protect
access to res.choices[0].message.content and the parsed variable so downstream
code never receives undefined.
| import type { NextConfig } from "next"; | ||
| import path from "path"; | ||
|
|
||
| const nextConfig: NextConfig = { | ||
| turbopack: { | ||
| root: path.resolve(__dirname), | ||
| }, | ||
| }; | ||
|
|
||
| export default nextConfig; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find and display package.json to check Next.js version
find . -name "package.json" -path "*/silicon-signal/*" | head -5Repository: tinyfish-io/tinyfish-cookbook
Length of output: 102
🏁 Script executed:
#!/bin/bash
# Get Next.js version
cat ./silicon-signal/package.json | jq '.dependencies.next // .devDependencies.next' 2>/dev/null || echo "Not found"Repository: tinyfish-io/tinyfish-cookbook
Length of output: 81
🏁 Script executed:
#!/bin/bash
# Check if there are any other config files or documentation
ls -la ./silicon-signal/ | grep -E "(next|tsconfig|build)"Repository: tinyfish-io/tinyfish-cookbook
Length of output: 203
🌐 Web query:
Next.js 16 __dirname in next.config.ts ES module
💡 Result:
In an ES module next.config.ts, Node doesn’t provide __dirname. Use import.meta.url + fileURLToPath():
// next.config.ts (ESM)
import type { NextConfig } from "next";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const nextConfig: NextConfig = {
webpack(config) {
// example: resolve a path relative to this config file
config.resolve.alias["@root"] = resolve(__dirname);
return config;
},
};
export default nextConfig;This is the standard Node ESM replacement for __dirname/__filename. (nodejs.org)
Notes specific to Next.js 16 config files:
- If you “just need the project root”,
process.cwd()is often enough (and avoids the ESM__dirnameissue). next.config.jsis a regular Node module; if you want ESM without TypeScript, usenext.config.mjs. (nextjs.org)- For
next.config.ts, Next.js notes module resolution is limited to CommonJS unless you’re using Node’s native TypeScript resolver (Node v22.10.0+ with the feature enabled), which is what enables native ESM behavior innext.config.ts. (nextjs.org)
Citations:
- 1: https://nodejs.org/download/release/v12.8.1/docs/api/esm.html?utm_source=openai
- 2: https://nextjs.org/docs/app/api-reference/config/next-config-js?utm_source=openai
- 3: https://nextjs.org/docs/app/api-reference/config/typescript?utm_source=openai
🌐 Web query:
Next.js config file __dirname available CommonJS shim
💡 Result:
- In
next.config.js(CommonJS),__dirnameis available because it’s a regular Node.js CommonJS module (module.exports = …). [1] - In
next.config.mjs(ESM),__dirnameis not defined (same for__filename,require, etc.). [2][1]
ESM “CommonJS shim” for __dirname (use in next.config.mjs / next.config.ts):
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
export default {
// you can now use __dirname here
}[2]
If you also need require inside ESM:
import { createRequire } from 'module'
const require = createRequire(import.meta.url)[2]
Sources: Next.js config docs (updated Nov 4, 2025) [1]; Node.js ESM docs on __dirname/__filename equivalents [2].
🏁 Script executed:
cat -n ./silicon-signal/next.config.tsRepository: tinyfish-io/tinyfish-cookbook
Length of output: 327
Fix __dirname usage in ES module context.
__dirname is not defined in ES modules. Since next.config.ts is treated as an ES module, this will cause a runtime error. Use the standard ESM approach with import.meta.url:
♻️ ESM-compatible fix
import type { NextConfig } from "next";
import path from "path";
+import { fileURLToPath } from "url";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
const nextConfig: NextConfig = {
turbopack: {
root: path.resolve(__dirname),
},
};
export default nextConfig;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import type { NextConfig } from "next"; | |
| import path from "path"; | |
| const nextConfig: NextConfig = { | |
| turbopack: { | |
| root: path.resolve(__dirname), | |
| }, | |
| }; | |
| export default nextConfig; | |
| import type { NextConfig } from "next"; | |
| import path from "path"; | |
| import { fileURLToPath } from "url"; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const nextConfig: NextConfig = { | |
| turbopack: { | |
| root: path.resolve(__dirname), | |
| }, | |
| }; | |
| export default nextConfig; |
🤖 Prompt for AI Agents
In `@silicon-signal/next.config.ts` around lines 1 - 10, The config uses __dirname
(which is undefined in ESM); update turbopack.root to compute the directory from
import.meta.url instead: convert the current path.resolve(__dirname) usage to
derive the directory using the ESM pattern (e.g., fileURLToPath(new
URL(import.meta.url)) and path.dirname) and then pass that computed directory
into nextConfig.turbopack.root; locate the nextConfig object and the
turbopack.root assignment to make this change.
| interface RiskAnalysis { | ||
| score: number; | ||
| level: string; | ||
| reasoning: string; | ||
| } | ||
|
|
||
| interface ScanResult { | ||
| part_number: string; | ||
| manufacturer: string; | ||
| lifecycle_status: string; | ||
| lead_time_weeks?: number; | ||
| lead_time_days?: number; | ||
| moq?: number; | ||
| availability?: string; | ||
| timestamp: string; | ||
| last_time_buy_date?: string; | ||
| pcn_summary?: string; | ||
| risk: RiskAnalysis; | ||
| evidence_links: string[]; | ||
| price_estimate?: string; | ||
| sources?: string[]; | ||
| sources_checked?: string[]; | ||
| sources_blocked?: string[]; | ||
| source_signals?: SourceSignal[]; | ||
| signals?: SignalSummary; | ||
| confidence?: ConfidenceInfo; | ||
| scanned_at?: string; | ||
| scan_duration_ms?: number; | ||
| scan_timed_out?: boolean; | ||
| agent_logs?: string[]; | ||
| history?: { timestamp: string; score: number }[]; | ||
| } | ||
|
|
||
| interface SourceSignal { | ||
| name: string; | ||
| url: string; | ||
| ok: boolean; | ||
| blocked: boolean; | ||
| availability?: string; | ||
| lifecycle_status?: string; | ||
| lead_time_weeks?: number; | ||
| price_estimate?: string; | ||
| } | ||
|
|
||
| interface SignalSummary { | ||
| availability: string; | ||
| lifecycle_status: string; | ||
| lead_time_weeks?: number; | ||
| price_estimate?: string; | ||
| } | ||
|
|
||
| interface ConfidenceInfo { | ||
| score: number; | ||
| level: string; | ||
| sources: number; | ||
| signals: number; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Duplicate type definitions; import from shared types instead.
RiskAnalysis, ScanResult, SourceSignal, SignalSummary, and ConfidenceInfo are defined locally here but already exist in silicon-signal/src/types.ts. This duplication can lead to type drift if one is updated without the other.
♻️ Import types instead of redefining
import { NextResponse } from 'next/server';
import puppeteer from 'puppeteer';
import type { Browser } from 'puppeteer';
import { saveSnapshot, getLastSnapshot, getHistory, HistoricalSnapshot } from '@/lib/store';
+import type { RiskAnalysis, ScanResult, SourceSignal, SignalSummary, ConfidenceInfo } from '@/types';
-interface RiskAnalysis {
- score: number;
- level: string;
- reasoning: string;
-}
-
-interface ScanResult {
- // ... remove all duplicated interfaces
-}🤖 Prompt for AI Agents
In `@silicon-signal/src/app/api/scan/route.ts` around lines 6 - 62, Remove the
duplicated local interfaces (RiskAnalysis, ScanResult, SourceSignal,
SignalSummary, ConfidenceInfo) from route.ts and import them from the shared
types module (the exported definitions in silicon-signal/src/types.ts); update
any local references to use the imported types (e.g., ScanResult, RiskAnalysis,
SourceSignal, SignalSummary, ConfidenceInfo) so there is a single source of
truth and no duplicate type declarations in route.ts.
| const query1 = `${part_number} price stock distributor`; | ||
| const searchUrl1 = `https://duckduckgo.com/?q=${encodeURIComponent(query1)}&ia=web`; | ||
|
|
||
| await page.goto(searchUrl1, { waitUntil: 'networkidle2', timeout: 100000 }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very long individual timeouts (100s) may cause request timeouts.
Each page.goto has a 100-second timeout, and there are multiple navigations per scan. A single scan could theoretically take 10+ minutes, likely exceeding serverless function limits (Vercel default is 10s, max 300s for Pro). Consider reducing individual timeouts and relying more on the overall SCAN_TIMEOUT_MS.
🔧 Reduce individual page timeouts
- await page.goto(searchUrl1, { waitUntil: 'networkidle2', timeout: 100000 });
+ await page.goto(searchUrl1, { waitUntil: 'networkidle2', timeout: 30000 });Apply similar reductions to other page.goto calls. The overall SCAN_TIMEOUT_MS will still enforce the total time limit.
Also applies to: 650-650, 664-664
🤖 Prompt for AI Agents
In `@silicon-signal/src/app/api/scan/route.ts` at line 641, The page navigation
calls (page.goto) currently use very long per-navigation timeouts (100000 ms)
which can make a single scan exceed serverless limits; update the page.goto
calls (searchUrl1, and the other occurrences referenced around the same block)
to use a much shorter per-navigation timeout (e.g., 10–30s) or remove the
explicit timeout and instead set a conservative
page.setDefaultNavigationTimeout, and rely on the overall SCAN_TIMEOUT_MS to
enforce the full scan limit; change the timeout values passed to page.goto (and
any other page.goto calls near the same code block) accordingly so individual
navigations don't consume excessive time while the global SCAN_TIMEOUT_MS
remains the authoritative timeout.
| import { CheckSquare } from 'lucide-react'; | ||
|
|
||
| export default function Header() { | ||
| return ( | ||
| <header className="fixed top-0 left-0 right-0 h-16 border-b border-white/5 bg-background/80 backdrop-blur-md z-50 flex items-center justify-between px-6"> | ||
| <div className="flex items-center gap-3 cursor-pointer" onClick={() => window.location.href = '/'}> | ||
| <div className="w-8 h-8 rounded bg-gradient-to-br from-white/10 to-transparent border border-white/10 flex items-center justify-center"> | ||
| <CheckSquare className="w-5 h-5 text-accent" /> | ||
| </div> | ||
| <span className="font-semibold tracking-tight text-lg">SiliconSignal</span> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -name "Header.tsx" -type fRepository: tinyfish-io/tinyfish-cookbook
Length of output: 115
🏁 Script executed:
cat -n silicon-signal/src/components/Header.tsxRepository: tinyfish-io/tinyfish-cookbook
Length of output: 1023
🌐 Web query:
Next.js App Router server components window access error ReferenceError
💡 Result:
In the Next.js App Router, pages/layouts/components are Server Components by default, and Server Components can’t access browser APIs like window—so referencing window (directly or via a dependency) triggers ReferenceError: window is not defined. [1]
Fix patterns
- Move
windowcode into a Client Component
// app/components/Thing.tsx
'use client'
import { useEffect, useState } from 'react'
export default function Thing() {
const [w, setW] = useState<number | null>(null)
useEffect(() => {
setW(window.innerWidth)
}, [])
return <div>{w}</div>
}Use 'use client' when you need browser APIs/state/effects. [2]
- If a third‑party library touches
windowat import time, dynamically load it on the client
- Important:
ssr: falseis not supported in Server Components—put this in a Client Component. [3]
// app/components/ClientOnlyChart.tsx
'use client'
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./Chart'), { ssr: false })
export default function ClientOnlyChart() {
return <Chart />
}This avoids evaluating the module during SSR, which commonly causes the error. [4]
- Guard with
typeof window !== 'undefined'(useful for small one-offs)
if (typeof window !== 'undefined') {
// safe
}But prefer (1) / (2) for React UI code. [5]
Replace window.location.href with Next.js Link.
The Header component is a Server Component (no 'use client' directive), and accessing the window object will cause a ReferenceError during server rendering. Use the Link component for navigation instead, which handles client-side routing without full page reloads.
✅ Suggested fix
import { CheckSquare } from 'lucide-react';
+import Link from 'next/link';
export default function Header() {
return (
<header className="fixed top-0 left-0 right-0 h-16 border-b border-white/5 bg-background/80 backdrop-blur-md z-50 flex items-center justify-between px-6">
- <div className="flex items-center gap-3 cursor-pointer" onClick={() => window.location.href = '/'}>
+ <Link href="/" className="flex items-center gap-3 cursor-pointer">
<div className="w-8 h-8 rounded bg-gradient-to-br from-white/10 to-transparent border border-white/10 flex items-center justify-center">
<CheckSquare className="w-5 h-5 text-accent" />
</div>
<span className="font-semibold tracking-tight text-lg">SiliconSignal</span>
- </div>
+ </Link>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { CheckSquare } from 'lucide-react'; | |
| export default function Header() { | |
| return ( | |
| <header className="fixed top-0 left-0 right-0 h-16 border-b border-white/5 bg-background/80 backdrop-blur-md z-50 flex items-center justify-between px-6"> | |
| <div className="flex items-center gap-3 cursor-pointer" onClick={() => window.location.href = '/'}> | |
| <div className="w-8 h-8 rounded bg-gradient-to-br from-white/10 to-transparent border border-white/10 flex items-center justify-center"> | |
| <CheckSquare className="w-5 h-5 text-accent" /> | |
| </div> | |
| <span className="font-semibold tracking-tight text-lg">SiliconSignal</span> | |
| </div> | |
| import { CheckSquare } from 'lucide-react'; | |
| import Link from 'next/link'; | |
| export default function Header() { | |
| return ( | |
| <header className="fixed top-0 left-0 right-0 h-16 border-b border-white/5 bg-background/80 backdrop-blur-md z-50 flex items-center justify-between px-6"> | |
| <Link href="/" className="flex items-center gap-3 cursor-pointer"> | |
| <div className="w-8 h-8 rounded bg-gradient-to-br from-white/10 to-transparent border border-white/10 flex items-center justify-center"> | |
| <CheckSquare className="w-5 h-5 text-accent" /> | |
| </div> | |
| <span className="font-semibold tracking-tight text-lg">SiliconSignal</span> | |
| </Link> |
🤖 Prompt for AI Agents
In `@silicon-signal/src/components/Header.tsx` around lines 1 - 11, The Header
component uses window.location.href inside the clickable div (onClick={() =>
window.location.href = '/'}), which will break server rendering; replace this
with Next.js client-side navigation by importing Link from 'next/link' and using
a <Link> around the logo/title instead of the onClick handler (remove window
access and the onClick). Keep Header as a server component (no 'use client') and
ensure the Link wraps the existing inner elements (the icon and span) so
navigation works without a full reload.
| import fs from 'fs'; | ||
| import path from 'path'; | ||
|
|
||
| let currentHistoryFile = path.join(process.cwd(), 'data', 'history.json'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Module-level mutable state can cause race conditions.
currentHistoryFile is mutated during saveSnapshot (line 61) and read during getHistory. In serverless environments where multiple requests may share the same module instance, or during concurrent requests, this shared mutable state can lead to unpredictable behavior where one request's fallback path affects another request's read path.
🔧 Proposed fix: avoid mutating module-level state
-let currentHistoryFile = path.join(process.cwd(), 'data', 'history.json');
+const PRIMARY_HISTORY_FILE = path.join(process.cwd(), 'data', 'history.json');
+const TMP_HISTORY_FILE = path.join('/tmp', 'history.json');
// In saveSnapshot, track success without mutating module state
// In getHistory, always check both locations in orderAlternatively, return the successfully-written file path from saveSnapshot and pass it explicitly if needed.
Also applies to: 60-61
🤖 Prompt for AI Agents
In `@silicon-signal/src/lib/store.ts` at line 4, currentHistoryFile is
module-level mutable state and is being reassigned in saveSnapshot which can
race with getHistory; change saveSnapshot to stop mutating currentHistoryFile
and instead return the written file path (or accept an explicit target path
parameter), update callers to use the returned path when calling getHistory or
other consumers, and remove any reassignment of the variable in saveSnapshot;
reference saveSnapshot and getHistory to locate the changes and remove reliance
on the module-level currentHistoryFile variable.
|
Closing in favor of #29 (clean PR with only Silicon Signal changes). |
No description provided.