Skip to content

Commit 7dc6f91

Browse files
authored
Add post AI summary (#97)
* add post AI summary * summarise articles * add error handling * fix button UI
1 parent e25495b commit 7dc6f91

File tree

2 files changed

+117
-1
lines changed

2 files changed

+117
-1
lines changed

src/api/GeminiClient.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
const API_KEY = import.meta.env.VITE_GEMINI_API_KEY;
2+
3+
const isValidUrl = (text: string) => {
4+
try {
5+
new URL(text);
6+
return true;
7+
} catch (e) {
8+
return false;
9+
}
10+
};
11+
12+
export const summarizeText = async (text: string) => {
13+
const controller = new AbortController();
14+
const timeoutId = setTimeout(() => controller.abort(), 10000);
15+
16+
try {
17+
let promptText = "";
18+
if (isValidUrl(text)) {
19+
promptText = `Summarise the article at the following URL in a sentence: ${text}`;
20+
} else {
21+
promptText = `Summarise the following paragraph in a sentence: ${text}`;
22+
}
23+
24+
const response = await fetch(
25+
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent',
26+
{
27+
method: 'POST',
28+
headers: {
29+
'Content-Type': 'application/json',
30+
'X-goog-api-key': API_KEY,
31+
},
32+
body: JSON.stringify({
33+
contents: [
34+
{
35+
parts: [
36+
{
37+
text: promptText,
38+
},
39+
],
40+
},
41+
],
42+
generationConfig: {
43+
temperature: 0.5,
44+
topP: 0.95,
45+
},
46+
}),
47+
signal: controller.signal,
48+
}
49+
);
50+
51+
clearTimeout(timeoutId);
52+
53+
if (!response.ok) {
54+
throw new Error('Failed to summarize text');
55+
}
56+
57+
const data = await response.json();
58+
const summary = data.candidates?.[0]?.content?.parts?.[0]?.text;
59+
if (!summary) {
60+
throw new Error('Invalid response format from Gemini API');
61+
}
62+
return summary;
63+
} catch (error) {
64+
throw error;
65+
}
66+
};
67+

src/components/Post.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,39 @@ import PostLock from "./PostLock";
1616
import PollData from "./PollData";
1717
import ExternalLink from "./ExternalLink";
1818
import UpvotePercentageLabel from "./UpvotePercentageLabel";
19+
import { useState } from "react";
20+
import { summarizeText as summarizeTextApi } from "../api/GeminiClient";
1921

2022
const PostComponent = ({ post }: { post: Post }) => {
2123
document.title = he.decode(post.title);
24+
const [summarizedText, setSummarizedText] = useState("");
25+
const [isSummarizing, setIsSummarizing] = useState(false);
26+
const [summarizationError, setSummarizationError] = useState("");
27+
28+
const summarizeText = async () => {
29+
setIsSummarizing(true);
30+
setSummarizationError("");
31+
try {
32+
let textToSummarize = "";
33+
if (post.url_overridden_by_dest && !isImage(post.url_overridden_by_dest)) {
34+
textToSummarize = post.url_overridden_by_dest;
35+
} else {
36+
textToSummarize = post?.selftext_html ?? "";
37+
}
38+
39+
if (textToSummarize.trim() === "") {
40+
setSummarizationError("No text available to summarize.");
41+
return;
42+
}
43+
44+
const summary = await summarizeTextApi(textToSummarize);
45+
setSummarizedText(summary);
46+
} catch (error) {
47+
setSummarizationError("Failed to summarize. Please try again.");
48+
} finally {
49+
setIsSummarizing(false);
50+
}
51+
};
2252

2353
return (
2454
<div
@@ -122,7 +152,26 @@ const PostComponent = ({ post }: { post: Post }) => {
122152
)}
123153
</a>
124154
<PostLock locked={post.locked} />
125-
<PostStats score={post.score} num_comments={post.num_comments} />
155+
<div className="flex justify-between items-center">
156+
<PostStats score={post.score} num_comments={post.num_comments} />
157+
<button
158+
onClick={summarizeText}
159+
className="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 mt-2"
160+
>
161+
{isSummarizing ? "Summarizing..." : "✨ AI Summarize"}
162+
</button>
163+
</div>
164+
{summarizationError && (
165+
<div className="text-sm mt-3 p-2 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-200 rounded-md">
166+
<p>{summarizationError}</p>
167+
</div>
168+
)}
169+
{summarizedText && (
170+
<div className="text-sm mt-4 p-4 bg-gray-100 dark:bg-gray-800 rounded-md border border-gray-300 dark:border-gray-600">
171+
<h3 className="font-bold mb-2">✨ AI Summary</h3>
172+
<p>{summarizedText}</p>
173+
</div>
174+
)}
126175
</div>
127176
);
128177
};

0 commit comments

Comments
 (0)