Skip to content

Commit

Permalink
feature: Add a summarize with AI button for links
Browse files Browse the repository at this point in the history
  • Loading branch information
MohamedBassem committed Oct 27, 2024
1 parent 3e727f7 commit 731d2df
Show file tree
Hide file tree
Showing 12 changed files with 1,536 additions and 11 deletions.
151 changes: 151 additions & 0 deletions apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React from "react";
import { ActionButton } from "@/components/ui/action-button";
import { Button } from "@/components/ui/button";
import LoadingSpinner from "@/components/ui/spinner";
import { toast } from "@/components/ui/use-toast";
import { cn } from "@/lib/utils";
import {
ChevronDown,
ChevronUp,
Loader2,
RefreshCw,
Trash2,
} from "lucide-react";

import {
useSummarizeBookmark,
useUpdateBookmark,
} from "@hoarder/shared-react/hooks/bookmarks";
import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks";

function AISummary({
bookmarkId,
summary,
}: {
bookmarkId: string;
summary: string;
}) {
const [isExpanded, setIsExpanded] = React.useState(false);
const { mutate: resummarize, isPending: isResummarizing } =
useSummarizeBookmark({
onError: () => {
toast({
description: "Something went wrong",
variant: "destructive",
});
},
});
const { mutate: updateBookmark, isPending: isUpdatingBookmark } =
useUpdateBookmark({
onError: () => {
toast({
description: "Something went wrong",
variant: "destructive",
});
},
});
return (
<div className="w-full p-1">
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
className={`
relative overflow-hidden rounded-lg p-4
transition-all duration-300 ease-in-out
${isExpanded ? "h-auto" : "h-[4.5em] cursor-pointer"}
bg-gradient-to-r from-purple-400 via-pink-500 to-red-500 p-[2px]
`}
onClick={() => !isExpanded && setIsExpanded(true)}
>
<div className="h-full rounded-lg bg-background p-3">
<p
className={`text-sm text-gray-700 dark:text-gray-300 ${!isExpanded && "line-clamp-3"}`}
>
{summary}
</p>
{!isExpanded && (
<div className="absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-white to-transparent dark:from-gray-800" />
)}
<span className="absolute bottom-2 right-2 flex gap-2">
{isExpanded && (
<>
<ActionButton
variant="none"
size="none"
spinner={<LoadingSpinner className="size-4" />}
className="rounded-full bg-gray-200 p-1 text-gray-600 dark:bg-gray-700 dark:text-gray-400"
aria-label={isExpanded ? "Collapse" : "Expand"}
loading={isResummarizing}
onClick={() => resummarize({ bookmarkId })}
>
<RefreshCw size={16} />
</ActionButton>
<ActionButton
size="none"
variant="none"
spinner={<LoadingSpinner className="size-4" />}
className="rounded-full bg-gray-200 p-1 text-gray-600 dark:bg-gray-700 dark:text-gray-400"
aria-label={isExpanded ? "Collapse" : "Expand"}
loading={isUpdatingBookmark}
onClick={() => updateBookmark({ bookmarkId, summary: null })}
>
<Trash2 size={16} />
</ActionButton>
</>
)}
<button
className="rounded-full bg-gray-200 p-1 text-gray-600 dark:bg-gray-700 dark:text-gray-400"
aria-label={isExpanded ? "Collapse" : "Expand"}
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</button>
</span>
</div>
</div>
</div>
);
}

export default function SummarizeBookmarkArea({
bookmark,
}: {
bookmark: ZBookmark;
}) {
const { mutate, isPending } = useSummarizeBookmark({
onError: () => {
toast({
description: "Something went wrong",
variant: "destructive",
});
},
});

if (bookmark.content.type !== BookmarkTypes.LINK) {
return null;
}

if (bookmark.summary) {
return <AISummary bookmarkId={bookmark.id} summary={bookmark.summary} />;
} else {
return (
<div className="flex w-full items-center gap-4">
<Button
onClick={() => mutate({ bookmarkId: bookmark.id })}
className={cn(
`relative w-full overflow-hidden bg-opacity-30 bg-gradient-to-r from-blue-400 via-purple-500 to-pink-500 transition-all duration-300`,
isPending ? "text-transparent" : "text-gray-300",
)}
disabled={isPending}
>
{isPending && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="animate-gradient-x background-animate h-full w-full bg-gradient-to-r from-blue-400 via-purple-500 to-pink-500"></div>
<Loader2 className="absolute h-5 w-5 animate-spin text-white" />
</div>
)}
<span className="relative z-10">Summarize with AI</span>
</Button>
</div>
);
}
}
2 changes: 2 additions & 0 deletions apps/web/components/dashboard/preview/BookmarkPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "@hoarder/shared-react/utils/bookmarkUtils";
import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks";

import SummarizeBookmarkArea from "../bookmarks/SummarizeBookmarkArea";
import ActionBar from "./ActionBar";
import { AssetContentSection } from "./AssetContentSection";
import AttachmentBox from "./AttachmentBox";
Expand Down Expand Up @@ -137,6 +138,7 @@ export default function BookmarkPreview({
</div>

<CreationTime createdAt={bookmark.createdAt} />
<SummarizeBookmarkArea bookmark={bookmark} />
<div className="flex items-center gap-4">
<p className="text-sm text-gray-400">Tags</p>
<BookmarkTagsEditor bookmark={bookmark} />
Expand Down
9 changes: 6 additions & 3 deletions apps/workers/openaiWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ async function inferTagsFromImage(
),
metadata.contentType,
base64,
{ json: true },
);
}

Expand Down Expand Up @@ -235,14 +236,16 @@ async function inferTagsFromPDF(
`Content: ${pdfParse.text}`,
serverConfig.inference.contextLength,
);
return inferenceClient.inferFromText(prompt);
return inferenceClient.inferFromText(prompt, { json: true });
}

async function inferTagsFromText(
bookmark: NonNullable<Awaited<ReturnType<typeof fetchBookmark>>>,
inferenceClient: InferenceClient,
) {
return await inferenceClient.inferFromText(await buildPrompt(bookmark));
return await inferenceClient.inferFromText(await buildPrompt(bookmark), {
json: true,
});
}

async function inferTags(
Expand Down Expand Up @@ -290,7 +293,7 @@ async function inferTags(

return tags;
} catch (e) {
const responseSneak = response.response.substr(0, 20);
const responseSneak = response.response.substring(0, 20);
throw new Error(
`[inference][${jobId}] The model ignored our prompt and didn't respond with the expected JSON: ${JSON.stringify(e)}. Here's a sneak peak from the response: ${responseSneak}`,
);
Expand Down
1 change: 1 addition & 0 deletions packages/db/drizzle/0031_yummy_famine.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `bookmarks` ADD `summary` text;
Loading

0 comments on commit 731d2df

Please sign in to comment.