diff options
| author | Mohamed Bassem <me@mbassem.com> | 2024-10-27 00:12:11 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2024-10-27 00:12:11 +0000 |
| commit | 731d2dfbea39aa140ccb6d2d2cabd49186320299 (patch) | |
| tree | 2311d04b5dc61102c63d4e4ec9c7c97b359faad6 /apps | |
| parent | 3e727f7ba3ad157ca1ccc6100711266cae1bde23 (diff) | |
| download | karakeep-731d2dfbea39aa140ccb6d2d2cabd49186320299.tar.zst | |
feature: Add a summarize with AI button for links
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx | 151 | ||||
| -rw-r--r-- | apps/web/components/dashboard/preview/BookmarkPreview.tsx | 2 | ||||
| -rw-r--r-- | apps/workers/openaiWorker.ts | 9 |
3 files changed, 159 insertions, 3 deletions
diff --git a/apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx b/apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx new file mode 100644 index 00000000..c1f3a18a --- /dev/null +++ b/apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx @@ -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> + ); + } +} diff --git a/apps/web/components/dashboard/preview/BookmarkPreview.tsx b/apps/web/components/dashboard/preview/BookmarkPreview.tsx index 5e2764f4..e37c4b86 100644 --- a/apps/web/components/dashboard/preview/BookmarkPreview.tsx +++ b/apps/web/components/dashboard/preview/BookmarkPreview.tsx @@ -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"; @@ -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} /> diff --git a/apps/workers/openaiWorker.ts b/apps/workers/openaiWorker.ts index b1394f73..4fe74f44 100644 --- a/apps/workers/openaiWorker.ts +++ b/apps/workers/openaiWorker.ts @@ -180,6 +180,7 @@ async function inferTagsFromImage( ), metadata.contentType, base64, + { json: true }, ); } @@ -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( @@ -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}`, ); |
