Skip to content

Commit

Permalink
✨ feat: Add cache support for LLM models and improve article summary UI
Browse files Browse the repository at this point in the history
- Added `RunInCache` function to cache LLM model list results for 1 hour
- Updated `ListModels` method to use cached results
- Enhanced article summary component with better UI/UX:
  - Added reading time indicator
  - Improved layout and styling
  - Simplified regenerate summary functionality
- Added regenerate summary option to article actions dropdown
- Updated language selection in summary settings to use a predefined list
- Integrated article summary component into content reader
  • Loading branch information
vaayne committed Jan 1, 2025
1 parent 999eaee commit a20d1b9
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 108 deletions.
15 changes: 15 additions & 0 deletions internal/pkg/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,18 @@ func Get[T any](ctx context.Context, c Cache, key CacheKey) (*T, bool) {
MustUnmarshaler(b, &value)
return &value, true
}

func RunInCache[T any](ctx context.Context, c Cache, key CacheKey, expiration time.Duration, f func() (*T, error)) (*T, error) {
data, ok := Get[T](ctx, c, key)
if ok {
return data, nil
}

data, err := f()
if err != nil {
return nil, err
}

c.SetWithContext(ctx, key, data, expiration)
return data, nil
}
28 changes: 16 additions & 12 deletions internal/pkg/llms/llm.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"recally/internal/pkg/cache"
"recally/internal/pkg/logger"
"recally/internal/pkg/tools"
"strings"
Expand Down Expand Up @@ -56,18 +57,21 @@ func New(baseUrl, apiKey string) *LLM {
}

func (l *LLM) ListModels(ctx context.Context) ([]Model, error) {
models, err := l.client.ListModels(ctx)
if err != nil {
return nil, err
}
data := make([]Model, 0, len(models.Models))
for _, m := range models.Models {
data = append(data, Model{
ID: m.ID,
Name: m.ID,
})
}
return data, nil
models, err := cache.RunInCache[[]Model](ctx, cache.MemCache, cache.NewCacheKey("llm", "list-models"), time.Hour, func() (*[]Model, error) {
models, err := l.client.ListModels(ctx)
if err != nil {
return nil, err
}
data := make([]Model, 0, len(models.Models))
for _, m := range models.Models {
data = append(data, Model{
ID: m.ID,
Name: m.ID,
})
}
return &data, nil
})
return *models, err
}

func (l *LLM) ListTools(ctx context.Context) ([]tools.BaseTool, error) {
Expand Down
16 changes: 14 additions & 2 deletions web/src/components/article/article-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Chrome, Database, Globe, RefreshCw, Trash2 } from "lucide-react";
import { Bot, Chrome, Database, Globe, RefreshCw, Trash2 } from "lucide-react";

import { Button } from "@/components/ui/button";
import {
Expand All @@ -20,12 +20,14 @@ export type FetcherType = "http" | "jina" | "browser";
interface ArticleActionsProps {
onDelete: () => Promise<void>;
onRefetch: (type: FetcherType) => Promise<void>;
onRegenerateSummary: () => Promise<void>;
isLoading: boolean;
}

interface RefreshDropdownMenuProps {
isLoading: boolean;
onRefetch: (type: FetcherType) => Promise<void>;
onRegenerateSummary: () => Promise<void>;
}

const RefreshDropdownMenu = (props: RefreshDropdownMenuProps) => {
Expand Down Expand Up @@ -55,6 +57,11 @@ const RefreshDropdownMenu = (props: RefreshDropdownMenuProps) => {
>
<Chrome className="mr-2 h-4 w-4" /> Browser Fetcher
</DropdownMenuItem>
<DropdownMenuItem
onClick={async () => await props.onRegenerateSummary()}
>
<Bot className="mr-2 h-4 w-4" /> Genrate Summary
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
Expand All @@ -63,11 +70,16 @@ const RefreshDropdownMenu = (props: RefreshDropdownMenuProps) => {
export const ArticleActions: React.FC<ArticleActionsProps> = ({
onDelete,
onRefetch,
onRegenerateSummary,
isLoading,
}) => {
return (
<div className="flex justify-end flex-wrap items-center gap-1 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 py-1">
<RefreshDropdownMenu isLoading={isLoading} onRefetch={onRefetch} />
<RefreshDropdownMenu
isLoading={isLoading}
onRefetch={onRefetch}
onRegenerateSummary={onRegenerateSummary}
/>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
Expand Down
123 changes: 52 additions & 71 deletions web/src/components/article/article-summary.tsx
Original file line number Diff line number Diff line change
@@ -1,93 +1,74 @@
import MarkdownRenderer from "@/components/markdown-render";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ChevronDown, RefreshCw } from "lucide-react";
import { ChevronsUpDown, Clock, RefreshCw } from "lucide-react";
import { useState } from "react";

interface ArticleSummaryProps {
summary: string;
onRegenerateSummary: () => Promise<void>;
isLoading?: boolean;
onRegenerate?: () => Promise<void>;
}

export const ArticleSummary: React.FC<ArticleSummaryProps> = ({
export const ArticleSummary = ({
summary,
onRegenerateSummary,
isLoading,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
onRegenerate,
}: ArticleSummaryProps) => {
const [isOpen, setIsOpen] = useState(true);
const [isRegenerating, setIsRegenerating] = useState(false);
const readingTime = Math.ceil(summary.split(/\s+/).length / 200); // Approx. reading time in minutes

const handleRegenerate = async () => {
if (!onRegenerate) return;
setIsRegenerating(true);
await onRegenerate();
setIsRegenerating(false);
};

return (
<Card className="bg-secondary/50 border-none shadow-sm">
<Card className="mb-6 bg-muted/50">
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<div className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="ml-2 inline-flex items-center rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary">
AI Generated Summary
</span>
</div>
<div className="flex items-center gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="transition-all hover:scale-105"
onClick={async () => {
setIsGenerating(true);
await onRegenerateSummary();
setIsGenerating(false);
}}
disabled={isLoading || isGenerating}
>
<RefreshCw
className={`h-4 w-4 ${isLoading || isGenerating ? "animate-spin" : ""}`}
/>
</Button>
</TooltipTrigger>
<TooltipContent>Regenerate summary</TooltipContent>
</Tooltip>
</TooltipProvider>

<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm">
<ChevronDown
className={`h-4 w-4 transition-transform duration-200 ${
isOpen ? "transform rotate-180" : ""
}`}
/>
</Button>
</CollapsibleTrigger>
</div>
<div className="flex items-center justify-between px-4 py-2">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">AI Summary</h2>
<span className="flex items-center text-xs text-muted-foreground">
<Clock className="mr-1 h-3 w-3" />
{readingTime} min read
</span>
</div>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-9 p-0">
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">Toggle summary</span>
</Button>
</CollapsibleTrigger>
</div>

<CollapsibleContent>
<div className="px-6 pb-6">
<Card className="bg-background p-4 shadow-sm">
{isLoading || isGenerating ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : (
<div className="prose dark:prose-invert prose-sm max-w-none">
<MarkdownRenderer content={summary} />
</div>
)}
</Card>
</div>
<CardContent className="pt-2">
<div className="prose dark:prose-invert prose-sm max-w-none prose-h1:text-xl">
<MarkdownRenderer content={summary} />
</div>
</CardContent>
{onRegenerate && (
<CardFooter className="justify-end py-2">
<Button
variant="ghost"
size="sm"
onClick={handleRegenerate}
disabled={isRegenerating}
>
<RefreshCw
className={`mr-2 h-4 w-4 ${isRegenerating ? "animate-spin" : ""}`}
/>
Regenerate
</Button>
</CardFooter>
)}
</CollapsibleContent>
</Collapsible>
</Card>
Expand Down
32 changes: 14 additions & 18 deletions web/src/components/content-reader.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import type { FetcherType } from "@/components/article/article-actions";
import { ArticleHeader } from "@/components/article/article-header";
import { ArticleSummary } from "@/components/article/article-summary";
import MarkdownRenderer from "@/components/markdown-render";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import type { Bookmark as BookmarkType } from "@/lib/apis/bookmarks";
import type React from "react";
import { Separator } from "./ui/separator";
Expand All @@ -18,7 +13,10 @@ interface ArticleReaderProps {
onRegenerateSummary?: (id: string) => Promise<void>;
}

export const ArticleReader: React.FC<ArticleReaderProps> = ({ bookmark }) => {
export const ArticleReader: React.FC<ArticleReaderProps> = ({
bookmark,
onRegenerateSummary,
}) => {
return (
<>
<ArticleHeader
Expand All @@ -27,19 +25,17 @@ export const ArticleReader: React.FC<ArticleReaderProps> = ({ bookmark }) => {
publishedAt={bookmark.created_at}
/>

<Separator className="my-2" />
<Separator className="my-4" />

{bookmark.summary && (
<Accordion type="single" collapsible className="mb-8">
<AccordionItem value="summary">
<AccordionTrigger>AI Summary</AccordionTrigger>
<AccordionContent>
<div className="prose dark:prose-invert prose-lg">
<MarkdownRenderer content={bookmark.summary} />
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<ArticleSummary
summary={bookmark.summary}
onRegenerate={
onRegenerateSummary
? () => onRegenerateSummary(bookmark.id)
: undefined
}
/>
)}

{/* Main Content */}
Expand Down
18 changes: 13 additions & 5 deletions web/src/components/settings/summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ import { useLLMs } from "@/lib/apis/llm";
import { useUsers } from "@/lib/apis/users";
import { useState } from "react";

const supportedLanguages = [
{ id: "en", name: "English" },
{ id: "zh", name: "Chinese" },
{ id: "es", name: "Spanish" },
{ id: "fr", name: "French" },
{ id: "de", name: "German" },
];

export function SummarySettings() {
const { updateSettings } = useUsers();
const { user } = useUser();
Expand Down Expand Up @@ -100,11 +108,11 @@ export function SummarySettings() {
<SelectValue placeholder="Select language" />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">English</SelectItem>
<SelectItem value="zh">Chinese</SelectItem>
<SelectItem value="es">Spanish</SelectItem>
<SelectItem value="fr">French</SelectItem>
<SelectItem value="de">German</SelectItem>
{supportedLanguages.map((l) => (
<SelectItem key={l.id} value={l.name}>
{l.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
Expand Down
22 changes: 22 additions & 0 deletions web/src/pages/bookmark-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,27 @@ export default function BookmarkDetailPage({ id }: { id: string }) {
}
};

const handleRegenerateSummary = async () => {
try {
setIsLoading(true);
await refreshBookmark(bookmark.id, {
regenerate_summary: true,
});
toast({
title: "Success",
description: "Summary regenerated successfully",
});
} catch (error) {
toast({
title: "Error",
description: "Failed to regenerate summary",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
};

const handleDelete = async () => {
try {
setIsLoading(true);
Expand Down Expand Up @@ -94,6 +115,7 @@ export default function BookmarkDetailPage({ id }: { id: string }) {
<ArticleActions
onDelete={async () => setShowDeleteDialog(true)}
onRefetch={handleRefetch}
onRegenerateSummary={handleRegenerateSummary}
isLoading={isLoading}
/>
</div>
Expand Down

0 comments on commit a20d1b9

Please sign in to comment.