diff --git a/.gitignore b/.gitignore index c070cb1..5e7a59e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules +node_modules /.pnp .pnp.js .yarn/install-state.gz diff --git a/apps/api/package.json b/apps/api/package.json index 255d8ca..691651d 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -28,7 +28,7 @@ "@scalar/hono-api-reference": "^0.5.100", "@tensorflow-models/universal-sentence-encoder": "^1.3.3", "@tensorflow/tfjs-node": "^4.20.0", - "@trigger.dev/sdk": "3.0.0-beta.51", + "@trigger.dev/sdk": "3.0.0-beta.52", "ai": "^3.2.19", "cheerio": "^1.0.0-rc.12", "drizzle-orm": "^0.32.0", diff --git a/apps/api/src/lib/ai/llm.ts b/apps/api/src/lib/ai/llm.ts index 6a72054..5cdf838 100644 --- a/apps/api/src/lib/ai/llm.ts +++ b/apps/api/src/lib/ai/llm.ts @@ -36,7 +36,7 @@ export const summarizeDocument = async (document: string) => { const { text } = await generateText({ model: openai(LATEST_MODEL), system: "", - prompt: `summarize the following document in 1-2 paragraphs:\n${document}`, + prompt: `summarize the following document in 2-3 paragraphs of 1-2 sentences:\n${document}`, }); return text; diff --git a/apps/api/src/lib/scraper/fetchSite.ts b/apps/api/src/lib/scraper/fetchSite.ts index 30af36f..228930f 100644 --- a/apps/api/src/lib/scraper/fetchSite.ts +++ b/apps/api/src/lib/scraper/fetchSite.ts @@ -2,7 +2,6 @@ import { env } from '@/env' export async function fetchSite(url: string) { try { - console.log('fetching', url) const resp = await fetch(`https://r.jina.ai/${url}`, { headers: { Accept: 'application/json', diff --git a/apps/api/src/modules/api/handlers/queueScrape.ts b/apps/api/src/modules/api/handlers/queueScrape.ts index ef3cd6e..2e92cf8 100644 --- a/apps/api/src/modules/api/handlers/queueScrape.ts +++ b/apps/api/src/modules/api/handlers/queueScrape.ts @@ -37,6 +37,9 @@ export const queueScrape = new OpenAPIHono().openapi( method: 'post', path: '/', request: { + query: z.object({ + retry: z.boolean().optional().default(false), + }), body: { content: { 'application/json': { @@ -84,6 +87,7 @@ export const queueScrape = new OpenAPIHono().openapi( // @ts-ignore async (c) => { const { url } = c.req.valid('json') + const { retry } = c.req.valid('query') if (!c.var.user?.id) { return c.json({ message: 'Not logged in' }, 401) @@ -93,8 +97,7 @@ export const queueScrape = new OpenAPIHono().openapi( return c.json({ message: 'This site is not yet supported' }, 400) } - const links = await createLinkRepository() - + const links = createLinkRepository() const link = await links.createLink({ url, userId: c.var.user?.id, @@ -104,7 +107,7 @@ export const queueScrape = new OpenAPIHono().openapi( return c.json({ message: 'Something went wrong' }, 400) } - if (!link.summary) { + if (retry || !link.summary) { await scrapeWebsite.trigger({ url, link: link.id, diff --git a/apps/ui/package.json b/apps/ui/package.json index 73e98b8..5edcaf5 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -49,6 +49,7 @@ "sanitize-html": "^2.13.0", "set-cookie-parser": "^2.6.0", "sonner": "^1.5.0", + "swr": "^2.2.5", "tailwind-merge": "^2.4.0", "tailwindcss-animate": "^1.0.7", "ts-pattern": "^5.2.0", diff --git a/apps/ui/src/app/(app)/page.tsx b/apps/ui/src/app/(app)/page.tsx index 96cdbf8..1b369f3 100644 --- a/apps/ui/src/app/(app)/page.tsx +++ b/apps/ui/src/app/(app)/page.tsx @@ -1,3 +1,3 @@ export default function Dashboard() { - return

Dashboard

+ return

Dashboard

} diff --git a/apps/ui/src/app/layout.tsx b/apps/ui/src/app/layout.tsx index bc67c04..698e660 100644 --- a/apps/ui/src/app/layout.tsx +++ b/apps/ui/src/app/layout.tsx @@ -4,24 +4,13 @@ import { ThemeProvider } from '@/components/theme-provider' import { Dialog } from '@/components/ui/dialog' import { Toaster } from '@/components/ui/sonner' import { TooltipProvider } from '@/components/ui/tooltip' +import { fontSans } from '@/lib/fonts' import { cn } from '@/lib/utils' import type { Metadata, Viewport } from 'next' -import { Fragment_Mono, Instrument_Sans } from 'next/font/google' - -const fontSans = Instrument_Sans({ - subsets: ['latin'], - variable: '--font-sans', -}) - -const _ = Fragment_Mono({ - subsets: ['latin'], - variable: '--font-mono', - weight: ['400'], -}) export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', + title: 'Read it Later', + description: 'Bookmark your favorite articles and read them later with AI.', } export const viewport: Viewport = { diff --git a/apps/ui/src/components/DialogLinkCreate/DialogLinkCreate.tsx b/apps/ui/src/components/DialogLinkCreate/DialogLinkCreate.tsx index b501913..8a37e6f 100644 --- a/apps/ui/src/components/DialogLinkCreate/DialogLinkCreate.tsx +++ b/apps/ui/src/components/DialogLinkCreate/DialogLinkCreate.tsx @@ -20,9 +20,10 @@ import { import { Input } from '@/components/ui/input' import { createUrl } from '@/actions/create-url' import { zodResolver } from '@hookform/resolvers/zod' -import { redirect } from 'next/navigation' +import { useRouter } from 'next/navigation' import { useForm } from 'react-hook-form' import { z } from 'zod' +import { useState } from 'react' const linkCreateSchema = z.object({ url: z.preprocess( @@ -32,6 +33,9 @@ const linkCreateSchema = z.object({ }) export const DialogLinkCreate = () => { + const router = useRouter() + const [isSubmitting, setIsSubmitting] = useState(false) + const form = useForm>({ resolver: zodResolver(linkCreateSchema), defaultValues: { @@ -40,16 +44,16 @@ export const DialogLinkCreate = () => { }) const submit = async (values: z.infer) => { + setIsSubmitting(true) try { await createUrl(values) + router.push('/bookmarks') + router.refresh() } catch (_) { console.error('Failed to create URL') - return } finally { - close() - // const router = useRouter() - - redirect('/bookmarks') + form.reset() + setIsSubmitting(false) } } @@ -57,9 +61,7 @@ export const DialogLinkCreate = () => { New Bookmark - - Add a new bookmark manually. You can also use the bookmarklet. - + Add a new bookmark manually.
@@ -84,7 +86,9 @@ export const DialogLinkCreate = () => { - +
diff --git a/apps/ui/src/components/LinkList/LinkList.tsx b/apps/ui/src/components/LinkList/LinkList.tsx index f2dc024..6644df1 100644 --- a/apps/ui/src/components/LinkList/LinkList.tsx +++ b/apps/ui/src/components/LinkList/LinkList.tsx @@ -11,12 +11,14 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { SearchIcon } from 'lucide-react' import { createPaginationQueryString } from '@/lib/utils' +import type { LinkStatus } from '@/components/StatusIcon/StatusIcon' interface Link { id: string title: string url: string summary: string + status: LinkStatus tags: { key: string; label: string }[] createdAt: string } @@ -66,6 +68,7 @@ export async function LinkList({ links, pagination }: LinkListProps) { summary={link.summary} tags={link.tags} createdAt={link.createdAt} + status={link.status} /> ))} diff --git a/apps/ui/src/components/LinkList/LinkListItem.tsx b/apps/ui/src/components/LinkList/LinkListItem.tsx index 39ab897..6b2b650 100644 --- a/apps/ui/src/components/LinkList/LinkListItem.tsx +++ b/apps/ui/src/components/LinkList/LinkListItem.tsx @@ -1,3 +1,4 @@ +import { StatusIcon, type LinkStatus } from '@/components/StatusIcon/StatusIcon' import { formatRelative } from 'date-fns' import Link from 'next/link' import type { MouseEventHandler } from 'react' @@ -9,6 +10,7 @@ interface LinkListItemProps { title: string url: string summary: string + status: LinkStatus tags?: { key: string; label: string }[] createdAt: string } @@ -16,8 +18,15 @@ interface LinkListItemProps { export const LinkListItem = (props: LinkListItemProps) => { return ( -

{props.title}

-
{props.url}
+
+

+ {props.title ?? props.url} +

+ +
+
+ {props.title ? props.url : null} +
Added {formatRelative(props.createdAt, new Date())}
diff --git a/apps/ui/src/components/StatusIcon/StatusIcon.tsx b/apps/ui/src/components/StatusIcon/StatusIcon.tsx new file mode 100644 index 0000000..3ed7655 --- /dev/null +++ b/apps/ui/src/components/StatusIcon/StatusIcon.tsx @@ -0,0 +1,32 @@ +import { + CircleCheckIcon, + CircleDashedIcon, + CircleDotDashedIcon, + CircleXIcon, +} from 'lucide-react' +import { match } from 'ts-pattern' + +export type LinkStatus = 'submitted' | 'processing' | 'completed' | 'error' +export function StatusIcon({ status }: { status: LinkStatus }) { + return match(status) + .with('submitted', () => ( + + + + )) + .with('processing', () => ( + + + + )) + .with('completed', () => ( + + + + )) + .otherwise(() => ( + + + + )) +} diff --git a/apps/ui/src/components/StatusIcon/index.ts b/apps/ui/src/components/StatusIcon/index.ts new file mode 100644 index 0000000..317228c --- /dev/null +++ b/apps/ui/src/components/StatusIcon/index.ts @@ -0,0 +1 @@ +export * from './StatusIcon' \ No newline at end of file diff --git a/apps/ui/src/lib/api/calls/getLinksForUser.ts b/apps/ui/src/lib/api/calls/getLinksForUser.ts index 9e122b6..88949f1 100644 --- a/apps/ui/src/lib/api/calls/getLinksForUser.ts +++ b/apps/ui/src/lib/api/calls/getLinksForUser.ts @@ -27,6 +27,12 @@ export async function getLinksForUser(props: GetLinkProps) { const response = await client.api.v1.links.$get({ query, + },{ + init: { + next: { + revalidate: 100 + } + } }) if (!response.ok) { diff --git a/apps/ui/src/lib/fonts.ts b/apps/ui/src/lib/fonts.ts new file mode 100644 index 0000000..a4355eb --- /dev/null +++ b/apps/ui/src/lib/fonts.ts @@ -0,0 +1,12 @@ +import { Fragment_Mono, Instrument_Sans } from 'next/font/google' + +export const fontSans = Instrument_Sans({ + subsets: ['latin'], + variable: '--font-sans', +}) + +export const fontMono = Fragment_Mono({ + subsets: ['latin'], + variable: '--font-mono', + weight: ['400'], +}) \ No newline at end of file diff --git a/apps/ui/src/lib/url.ts b/apps/ui/src/lib/url.ts index c77de2d..f46e54e 100644 --- a/apps/ui/src/lib/url.ts +++ b/apps/ui/src/lib/url.ts @@ -1,6 +1,6 @@ export function getBaseDomain(url: string): string { - const hostname = new URL(url).hostname - const parts = hostname.split('.') + const hostname = new URL(url).hostname; + const parts = hostname.split("."); - return parts.length > 2 ? parts.slice(-2).join('.') : hostname + return parts.length > 2 ? parts.slice(-2).join(".") : hostname; } diff --git a/bun.lockb b/bun.lockb index 5b3bd6f..0c81aef 100755 Binary files a/bun.lockb and b/bun.lockb differ