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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions app/proxy/error.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useEffect} from "react";
import { useEffect, useState } from "react";
import { kv } from "@vercel/kv";
import TopBar from "@/components/top-bar";
import UnderlineLink from "@/components/underline-link";
import { track } from '@vercel/analytics';
Expand Down Expand Up @@ -63,4 +64,52 @@ export default function Error({
</div>
</div>
);
}
}

export function StreamingText({
response,
url,
}: {
response: Response;
url: string;
}) {
const [text, setText] = useState("");

useEffect(() => {
if (!response.body) return;

const reader = response.body.getReader();
const decoder = new TextDecoder();
let completion = "";

const pump = async () => {
const { done, value } = await reader.read();
if (done) {
await kv.set(url, completion);
return;
}
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n").filter((line) => line.trim() !== "");
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (data === "[DONE]") continue;
try {
const parsed = JSON.parse(data);
const delta = parsed.choices?.[0]?.delta?.content;
if (typeof delta === "string") {
completion += delta;
setText((prev) => prev + delta);
}
} catch {
// ignore malformed JSON
}
}
Comment on lines +91 to +107

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

SSE messages split across chunks will fail to parse.

The code splits each chunk by \n, but a single SSE data: line can span multiple chunks. For example, if one chunk ends mid-JSON (data: {"choi) and the next starts with the remainder (ces":...}\n), the parser will discard both fragments as malformed.

Buffer incomplete lines and only parse when a complete line ending in \n is received.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/proxy/error.tsx` around lines 91 - 107, The issue is that SSE data lines
can span multiple chunks, so splitting by newline on each chunk independently
will fail to parse incomplete lines. Maintain a buffer variable to store
incomplete lines that don't end with a newline. When processing each chunk in
the decoder.decode loop, append the decoded chunk to the buffer, then split the
buffer by newline. For each line, only parse it if it's complete; if the last
line doesn't end with a newline, store it back in the buffer for the next chunk.
This ensures complete JSON data is available before calling JSON.parse(data) on
the delta extraction logic.

pump();
};

pump();
}, [response, url]);
Comment on lines +78 to +112

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

kv.set inside useEffect runs on the client where @vercel/kv credentials are unavailable.

error.tsx is a Client Component (error boundaries must be), so useEffect executes in the browser. @vercel/kv requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables which are not exposed to the client by default. This will either silently fail or require exposing secrets via NEXT_PUBLIC_ prefix (a security risk).

Consider moving the KV persistence to a Server Action or API route that the client calls upon completion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/proxy/error.tsx` around lines 78 - 112, The kv.set call inside the
useEffect pump function cannot access Vercel KV credentials because error.tsx is
a Client Component executing in the browser. Move the kv.set(url, completion)
call from the pump function to a Server Action or API route. When the pump
function completes (when done is true), call this Server Action or API route
from the client, passing the url and completion string as parameters. This
ensures KV persistence happens on the server where credentials are available.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing useEffect cleanup will leak the stream reader on unmount.

If the component unmounts while streaming, the reader continues running and may attempt state updates on an unmounted component. Return a cleanup function that cancels the reader.

🛠️ Suggested fix
   useEffect(() => {
     if (!response.body) return;
+    let cancelled = false;
 
     const reader = response.body.getReader();
     const decoder = new TextDecoder();
     let completion = "";
 
     const pump = async () => {
       const { done, value } = await reader.read();
-      if (done) {
+      if (done || cancelled) {
         await kv.set(url, completion);
         return;
       }
       // ... rest of pump
-      pump();
+      if (!cancelled) pump();
     };
 
     pump();
+
+    return () => {
+      cancelled = true;
+      reader.cancel();
+    };
   }, [response, url]);
📝 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.

Suggested change
useEffect(() => {
if (!response.body) return;
const reader = response.body.getReader();
const decoder = new TextDecoder();
let completion = "";
const pump = async () => {
const { done, value } = await reader.read();
if (done) {
await kv.set(url, completion);
return;
}
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n").filter((line) => line.trim() !== "");
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (data === "[DONE]") continue;
try {
const parsed = JSON.parse(data);
const delta = parsed.choices?.[0]?.delta?.content;
if (typeof delta === "string") {
completion += delta;
setText((prev) => prev + delta);
}
} catch {
// ignore malformed JSON
}
}
pump();
};
pump();
}, [response, url]);
useEffect(() => {
if (!response.body) return;
let cancelled = false;
const reader = response.body.getReader();
const decoder = new TextDecoder();
let completion = "";
const pump = async () => {
const { done, value } = await reader.read();
if (done || cancelled) {
await kv.set(url, completion);
return;
}
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n").filter((line) => line.trim() !== "");
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (data === "[DONE]") continue;
try {
const parsed = JSON.parse(data);
const delta = parsed.choices?.[0]?.delta?.content;
if (typeof delta === "string") {
completion += delta;
setText((prev) => prev + delta);
}
} catch {
// ignore malformed JSON
}
}
if (!cancelled) pump();
};
pump();
return () => {
cancelled = true;
reader.cancel();
};
}, [response, url]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/proxy/error.tsx` around lines 78 - 112, The useEffect hook that reads
from the response body stream is missing a cleanup function, which can cause the
reader to continue operating and attempt state updates after the component
unmounts. Add a return statement at the end of the useEffect (after the pump
function definition and call) that returns a cleanup function. Inside this
cleanup function, call the cancel method on the reader variable to properly
abort the stream reading when the component unmounts or the effect dependencies
change.


return <>{text}</>;
}
24 changes: 8 additions & 16 deletions app/proxy/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Configuration, OpenAIApi } from "openai-edge";
import { OpenAIStream } from "ai";
import { StreamingText } from "./error";
import { kv } from "@vercel/kv";
import { Tokens } from "ai/react";
import { Suspense } from "react";
import Image from "next/image";
import { Skeleton } from "@/components/ui/skeleton";
Expand All @@ -14,9 +12,6 @@ export const runtime = "edge";
import { Source, getData } from "@/lib/data";


const apiConfig = new Configuration({
apiKey: process.env.OPENAI_API_KEY!,
});

type Article = {
title: string;
Expand Down Expand Up @@ -47,7 +42,7 @@ export default async function Page({
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}) {
const headersList = headers();
const headersList = await headers();
const ip = headersList.get("x-real-ip") || "default_ip";

const url =
Expand Down Expand Up @@ -253,19 +248,16 @@ async function Wrapper({
}),
});

// Convert the response into a friendly text-stream
const stream = OpenAIStream(response, {
async onCompletion(completion) {
await kv.set(url, completion);
// await kv.expire(prompt, 60 * 10);
},
});

if (!response.ok) {
return "Well this sucks. Looks like I ran out of money to pay for summaries. Please be patient until a benevolent sponsor gives me either cash or sweet sweet OpenAI credits. If you would like to be that sponsor, feel free to reach out to contact@smry.ai!";
}

return <Tokens stream={stream} />;
if (!response.body) {
return "No response body received from the API.";
}

// Stream the response body through the client-side Tokens replacement.
return <StreamingText response={response} url={url} />;
Comment on lines +255 to +260

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Vercel AI SDK v5 streamText streaming response Next.js

💡 Result:

In Vercel AI SDK v5, the streamText function is the primary method for streaming AI responses in Next.js applications [1][2]. It is designed to handle text generation and tool-calling loops efficiently by returning a StreamTextResult object, which provides built-in methods to return streaming HTTP responses [1][3]. To implement a streaming response in a Next.js App Router API route (route handler), follow this pattern: 1. Import streamText and necessary helpers from the ai package [3][4]. 2. Create a POST request handler that extracts the required input (e.g., messages) from the request body [3][4]. 3. Call streamText with your model configuration [3][4]. 4. Return the result using one of the helper methods provided on the result object, such as toUIMessageStreamResponse or toTextStreamResponse [1][5]. Example implementation for a chat endpoint: import { convertToModelMessages, streamText, type UIMessage } from 'ai'; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json; const result = streamText({ model: 'openai/gpt-4o', // Replace with your model messages: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse; } Key changes and notes in v5: - Migration: The convertToCoreMessages function from previous versions has been renamed to convertToModelMessages [6]. - Helper Methods: The result object provides specialized methods for different streaming protocols. toUIMessageStreamResponse is standard for UI-integrated chat, while toTextStreamResponse is used for simple text streams [1][5]. - Multi-step control: The maxSteps parameter used in earlier versions has been replaced with the more flexible stopWhen parameter to control tool-calling loops [6][2]. - Error Handling: streamText suppresses errors by default to prevent server crashes, sending them as part of the stream. You can use the onError callback to log these errors [1]. - Performance: You can optionally define maxDuration in your route to handle longer generations (e.g., export const maxDuration = 30;) [5].

Citations:


🏁 Script executed:

# Read the relevant file and surrounding context
cat -n app/proxy/page.tsx | sed -n '240,275p'

Repository: caffo/SMRY

Length of output: 1201


🏁 Script executed:

# Find and inspect the StreamingText component definition
fd -t f 'StreamingText' --or -g '*StreamingText*'

Repository: caffo/SMRY

Length of output: 299


🏁 Script executed:

# Search for StreamingText component definition in the codebase
rg -t tsx -t ts "component.*StreamingText|function StreamingText|const StreamingText" -A 10

Repository: caffo/SMRY

Length of output: 82


🏁 Script executed:

# Find StreamingText component
find . -type f -name "*Stream*" | head -20

Repository: caffo/SMRY

Length of output: 2067


🏁 Script executed:

# Search for StreamingText in the codebase
rg "StreamingText" -t js -t ts

Repository: caffo/SMRY

Length of output: 245


🏁 Script executed:

# Read the StreamingText component definition
cat -n app/proxy/error.tsx

Repository: caffo/SMRY

Length of output: 4450


Response object cannot be serialized from Server Component to Client Component.

Wrapper is an async Server Component, and StreamingText is a Client Component (marked with "use client" and uses useState/useEffect). In Next.js App Router, props passed from server to client must be JSON-serializable. The native Response object with its ReadableStream body is not serializable—this will cause a runtime error.

Consider:

  1. Using an API route that returns a streaming response, consumed directly by StreamingText via fetch
  2. Using AI SDK v5's streaming utilities (e.g., streamText with toUIMessageStreamResponse) which handle server-to-client streaming properly
  3. Converting the entire flow to stay server-side using React's streaming patterns
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/proxy/page.tsx` around lines 255 - 260, The Response object being passed
from the Server Component Wrapper to the Client Component StreamingText cannot
be serialized across the server-client boundary in Next.js App Router. Remove
the Response object from the props passed to StreamingText and instead refactor
to use one of these approaches: create an API route that returns the streaming
response and have StreamingText fetch it directly using fetch, migrate to use AI
SDK v5's streaming utilities like streamText with toUIMessageStreamResponse
which handle server-to-client streaming properly, or refactor the entire flow to
remain server-side using React's streaming patterns rather than passing the
Response to a Client Component.

} catch (error) {
return `Well this sucks. Looks like an unexpected error occured, so no summary for this site :( No I won't tell you the error, that is private. Really, you insist? Fine. The error is ${error} Happy?`;
}
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ export function Dashboard() {
{json.segments.map((segment, index) => (
<div
key={index}
ref={(el) => (segmentRefs.current[index] = el)}
ref={(el) => { segmentRefs.current[index] = el; }}
className="p-4 prose lg:prose-lg"
>
{/* {parse( */}
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@upstash/ratelimit": "^0.4.4",
"@vercel/analytics": "^1.1.1",
"@vercel/kv": "^1.0.0",
"ai": "^2.2.21",
"ai": "^5.0.52",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"compromise": "^14.10.0",
Expand All @@ -31,7 +31,7 @@
"jsdom": "^22.1.0",
"lucide-react": "^0.290.0",
"marked": "^10.0.0",
"next": "14.0.0",
"next": "16.2.5",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check React version to ensure compatibility with Next.js 16

# Extract React and React-DOM versions from package.json
echo "Checking React versions in package.json..."
cat package.json | jq -r '.dependencies | {"react": .react, "react-dom": ."react-dom"}'

# Check if versions are compatible with Next 16 (requires React 19+)
react_version=$(cat package.json | jq -r '.dependencies.react // empty')
if [[ -n "$react_version" ]]; then
  echo "Found React version: $react_version"
  if [[ "$react_version" =~ ^(\^|~)?18\. ]] || [[ "$react_version" =~ ^(\^|~)?17\. ]]; then
    echo "❌ ERROR: React $react_version is incompatible with Next.js 16.2.5"
    echo "Next.js 15+ requires React 19 minimum"
    exit 1
  elif [[ "$react_version" =~ ^(\^|~)?19\. ]] || [[ "$react_version" =~ ^(\^|~)?[2-9][0-9]\. ]]; then
    echo "✓ React version appears compatible"
  fi
fi

Repository: caffo/SMRY

Length of output: 274


🌐 Web query:

Next.js 16 React version requirements minimum compatibility

💡 Result:

Next.js 16 requires React 19.2 or higher for its App Router functionality, as it leverages the latest React Canary release [1][2][3]. When upgrading to or installing Next.js 16, the official documentation instructs users to install the latest versions of react and react-dom using their package manager [1][4][5][6]: npm install next@latest react@latest react-dom@latest Key points regarding React version compatibility in Next.js 16: - App Router: The App Router internally utilizes the React Canary channel to access the latest features, including React 19.2 [1][2][3]. - Manual Installation: While Next.js requires these specific dependencies for its internal routing mechanisms, you must explicitly declare react and react-dom in your project's package.json [7][8]. - TypeScript: If using TypeScript, you are also required to upgrade @types/react and @types/react-dom to their latest versions to maintain compatibility [1][3][5]. For optimal stability and access to all features, you should always use the versions of React specified by the latest tag provided in the official Next.js installation and upgrade documentation [1][6].

Citations:


🏁 Script executed:

# Search for Next.js version requirements and documentation in common sources
curl -s "https://nextjs.org/docs" 2>/dev/null | grep -i "react.*19\|react.*18" | head -20 || echo "Could not fetch documentation"

Repository: caffo/SMRY

Length of output: 50367


Upgrade React to version 19 or higher for Next.js 16.2.5 compatibility.

Next.js 16 requires React 19.2 or higher. The project currently uses React ^18.2.0, which is incompatible and will cause build or runtime failures. Update react and react-dom in package.json to React 19 or later, and update @types/react and @types/react-dom to their latest versions if using TypeScript.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 34, The Next.js version 16.2.5 requires React 19.2 or
higher for compatibility, but the current project dependencies specify react and
react-dom as ^18.2.0. Update the react and react-dom package versions in
package.json from ^18.2.0 to version 19 or later (e.g., ^19.0.0 or a specific
version like 19.2.0 or higher). Additionally, if the project uses TypeScript,
update the `@types/react` and `@types/react-dom` packages to their latest versions
compatible with React 19 to ensure type definitions match the runtime version.

"node-html-markdown": "^1.3.0",
"node-html-parser": "^6.1.11",
"openai": "^4.17.0",
Expand Down
Loading