From 45081dcb472f6aa197d6ebdc129485f77fe3ee88 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 23 Nov 2025 11:20:18 +0000 Subject: feat(mobile): Add AI summary field to mobile bookmark info (#2157) * feat: Add AI summary field to mobile bookmark info page Add a new AI summary section to the mobile bookmark info page that allows users to: - Generate AI summaries for link bookmarks - View existing summaries with expand/collapse functionality - Regenerate summaries with the refresh button - Delete summaries The implementation is inspired by the web app's AI summary feature and includes: - Purple-themed styling to match the AI branding - Loading states for all actions - Toast notifications for success/error feedback - Support for dark mode - Only displays for LINK type bookmarks The summary is rendered using markdown and appears prominently in the bookmark info page, positioned between the title editor and tags section. * refactor: Simplify AI summary styling to match normal fields Remove purple border and background from the AI summary display to make it look more like a standard field. The summary now uses the default card background from InfoSection, making it visually consistent with other fields on the page. The purple "Summarize with AI" button is retained for the generate action. * make the expand button more clear --------- Co-authored-by: Claude --- .../mobile/app/dashboard/bookmarks/[slug]/info.tsx | 154 ++++++++++++++++++++- 1 file changed, 153 insertions(+), 1 deletion(-) (limited to 'apps/mobile') diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx index 1781ec74..15e2a082 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx @@ -1,11 +1,12 @@ import React from "react"; -import { Alert, Pressable, View } from "react-native"; +import { ActivityIndicator, Alert, Pressable, View } from "react-native"; import { KeyboardAwareScrollView, KeyboardGestureArea, } from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { router, Stack, useLocalSearchParams } from "expo-router"; +import BookmarkTextMarkdown from "@/components/bookmarks/BookmarkTextMarkdown"; import TagPill from "@/components/bookmarks/TagPill"; import FullPageError from "@/components/FullPageError"; import { Button } from "@/components/ui/Button"; @@ -17,10 +18,12 @@ import { Skeleton } from "@/components/ui/Skeleton"; import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import { cn } from "@/lib/utils"; +import { ChevronUp, RefreshCw, Sparkles, Trash2 } from "lucide-react-native"; import { useAutoRefreshingBookmarkQuery, useDeleteBookmark, + useSummarizeBookmark, useUpdateBookmark, } from "@karakeep/shared-react/hooks/bookmarks"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; @@ -138,6 +141,154 @@ function NotesEditor({ ); } +function AISummarySection({ bookmark }: { bookmark: ZBookmark }) { + const { toast } = useToast(); + const [isExpanded, setIsExpanded] = React.useState(false); + + const { mutate: summarize, isPending: isSummarizing } = useSummarizeBookmark({ + onSuccess: () => { + toast({ + message: "Summary generated successfully!", + showProgress: false, + }); + }, + onError: () => { + toast({ + message: "Failed to generate summary", + showProgress: false, + }); + }, + }); + + const { mutate: resummarize, isPending: isResummarizing } = + useSummarizeBookmark({ + onSuccess: () => { + toast({ + message: "Summary regenerated successfully!", + showProgress: false, + }); + }, + onError: () => { + toast({ + message: "Failed to regenerate summary", + showProgress: false, + }); + }, + }); + + const { mutate: updateBookmark, isPending: isDeletingSummary } = + useUpdateBookmark({ + onSuccess: () => { + toast({ + message: "Summary deleted!", + showProgress: false, + }); + }, + onError: () => { + toast({ + message: "Failed to delete summary", + showProgress: false, + }); + }, + }); + + // Only show for LINK bookmarks + if (bookmark.content.type !== BookmarkTypes.LINK) { + return null; + } + + // If there's a summary, show it + if (bookmark.summary) { + return ( + + + + + {!isExpanded && ( + setIsExpanded(true)} + className="rounded-md bg-gray-100 py-2 dark:bg-gray-800" + > + + Show more + + + )} + {isExpanded && ( + + resummarize({ bookmarkId: bookmark.id })} + disabled={isResummarizing} + className="rounded-full bg-gray-200 p-2 dark:bg-gray-700" + > + {isResummarizing ? ( + + ) : ( + + )} + + + updateBookmark({ bookmarkId: bookmark.id, summary: null }) + } + disabled={isDeletingSummary} + className="rounded-full bg-gray-200 p-2 dark:bg-gray-700" + > + {isDeletingSummary ? ( + + ) : ( + + )} + + setIsExpanded(false)} + className="rounded-full bg-gray-200 p-2 dark:bg-gray-700" + > + + + + )} + + ); + } + + // If no summary, show button to generate one + return ( + + summarize({ bookmarkId: bookmark.id })} + disabled={isSummarizing} + className="rounded-lg bg-purple-500 p-3 dark:bg-purple-600" + > + + {isSummarizing ? ( + <> + + + Generating summary... + + + ) : ( + <> + Summarize with AI + + + )} + + + + ); +} + const ViewBookmarkPage = () => { const insets = useSafeAreaInsets(); const { slug } = useLocalSearchParams(); @@ -266,6 +417,7 @@ const ViewBookmarkPage = () => { } isPending={isEditPending} /> +