diff options
author | polwex <polwex@sortug.com> | 2025-07-16 10:07:06 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-07-16 10:07:06 +0700 |
commit | ff3078e93411c3467d797258744a7f17a7dbdf0a (patch) | |
tree | b13df65dbae32a3298afc0ada39b73a7d8aa469d /app/src/pages | |
parent | b26f4a03e15feddeb4bee8c06cd5078c1a54e5c5 (diff) |
m
Diffstat (limited to 'app/src/pages')
-rw-r--r-- | app/src/pages/_layout.tsx | 39 | ||||
-rw-r--r-- | app/src/pages/about.tsx | 35 | ||||
-rw-r--r-- | app/src/pages/categorize.tsx | 171 | ||||
-rw-r--r-- | app/src/pages/index.tsx | 72 |
4 files changed, 317 insertions, 0 deletions
diff --git a/app/src/pages/_layout.tsx b/app/src/pages/_layout.tsx new file mode 100644 index 0000000..6d227c9 --- /dev/null +++ b/app/src/pages/_layout.tsx @@ -0,0 +1,39 @@ +import '../styles.css'; + +import type { ReactNode } from 'react'; + +import { Header } from '../components/header'; +import { Footer } from '../components/footer'; + +type RootLayoutProps = { children: ReactNode }; + +export default async function RootLayout({ children }: RootLayoutProps) { + const data = await getData(); + + return ( + <div className="font-['Nunito']"> + <meta name="description" content={data.description} /> + <link rel="icon" type="image/png" href={data.icon} /> + <Header /> + <main className="m-6 flex items-center *:min-h-64 *:min-w-64 lg:m-0 lg:min-h-svh lg:justify-center"> + {children} + </main> + <Footer /> + </div> + ); +} + +const getData = async () => { + const data = { + description: 'An internet website!', + icon: '/images/favicon.png', + }; + + return data; +}; + +export const getConfig = async () => { + return { + render: 'static', + } as const; +}; diff --git a/app/src/pages/about.tsx b/app/src/pages/about.tsx new file mode 100644 index 0000000..c641af0 --- /dev/null +++ b/app/src/pages/about.tsx @@ -0,0 +1,35 @@ +import { Link } from "waku"; +import { listObsidian } from "../lib/categorization"; + +export default async function AboutPage() { + const data = await getData(); + + return ( + <div> + <title>{data.title}</title> + <h1 className="text-4xl font-bold tracking-tight">{data.headline}</h1> + <p>{data.body}</p> + <Link to="/" className="mt-4 inline-block underline"> + Return home + </Link> + </div> + ); +} + +const getData = async () => { + const obsidian = await listObsidian(); + + const data = { + title: "About", + headline: "About Waku", + body: "The minimal React framework", + }; + + return data; +}; + +export const getConfig = async () => { + return { + render: "static", + } as const; +}; diff --git a/app/src/pages/categorize.tsx b/app/src/pages/categorize.tsx new file mode 100644 index 0000000..d91d8c1 --- /dev/null +++ b/app/src/pages/categorize.tsx @@ -0,0 +1,171 @@ +import * as Bun from "bun"; +import { Suspense } from "react"; +import { TwitterApiService, TwitterBookmark } from "../lib/twitter-api"; +import { + userCategories, + type CategorizationResponse, +} from "../lib/categorization"; +import { LLMService } from "../lib/llm-service"; +import ProcessedBookmark from "../components/cat/Entry"; + +interface CategorizePageProps { + bookmarks: Awaited<ReturnType<TwitterApiService["fetchAllBookmarks"]>>; + currentBookmarkIndex: number; + categorization?: CategorizationResponse; + error?: string; +} + +async function CategorizationFetcher({ + bookmarks, + currentIndex, +}: { + bookmarks: TwitterBookmark[]; + currentIndex: number; +}) { + if (currentIndex >= bookmarks.length) { + return ( + <div className="text-center py-12"> + <h2 className="text-2xl font-bold text-gray-900 mb-4"> + All Bookmarks Categorized! + </h2> + <p className="text-gray-600 mb-6"> + You've successfully categorized all your bookmarks. + </p> + <a + href="/" + className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700" + > + Back to Bookmarks + </a> + </div> + ); + } + + const bookmark = bookmarks[currentIndex]; + const llmRes = await callLLM(bookmark!); + if ("error" in llmRes) + return ( + <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg"> + Error categorizing bookmark: {llmRes.error} + </div> + ); + return ( + <div className=""> + <ProcessedBookmark + bookmark={bookmark!} + categorization={llmRes.ok} + currentIndex={currentIndex} + totalCount={bookmarks.length} + /> + </div> + ); +} + +function LoadingSpinner() { + return ( + <div className="flex items-center justify-center py-12"> + <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> + <span className="ml-3 text-gray-600"> + Loading your Twitter bookmarks... + </span> + </div> + ); +} + +export default async function CategorizePage(props: any) { + // console.log({ props }); + const params = new URLSearchParams(props.query); + const currentIndex = Number(params.get("idx") || "0"); + const cookie = Bun.env.TWATTER_COKI; + + if (!cookie) { + return ( + <div className="text-red-600 text-center py-12"> + Missing Twitter cookie configuration + </div> + ); + } + const twbookmarks = await getTwData(cookie); + if ("error" in twbookmarks) { + return ( + <div className="text-red-600 text-center py-12"> + Error fetching Twatter bookmarks + </div> + ); + } + // const currentIndex = parseInt(searchParams.index || '0', 10); + const totalCount = twbookmarks.ok.length; + + return ( + <div className="min-h-screen bg-gray-50 py-8"> + <div className="max-w-4xl mx-auto px-4"> + <title>Categorize Bookmarks - SORMARK</title> + + <div className="mb-8"> + <h1 className="text-4xl font-bold tracking-tight mb-4"> + Categorize Bookmarks + </h1> + <p className="text-lg text-gray-600"> + Review and categorize your Twitter bookmarks one by one + </p> + </div> + + <Suspense fallback={<LoadingSpinner />}> + <> + <div className="bg-blue-600 text-white p-4"> + <p className="text-blue-100 mt-1"> + Bookmark {currentIndex + 1} of {totalCount} + </p> + <div className="w-full bg-blue-800 rounded-full h-2 mt-2"> + <div + className="bg-white h-2 rounded-full transition-all duration-300" + style={{ + width: `${((currentIndex + 1) / totalCount) * 100}%`, + }} + ></div> + </div> + </div> + <CategorizationFetcher + bookmarks={twbookmarks.ok} + currentIndex={currentIndex} + /> + </> + </Suspense> + </div> + </div> + ); +} + +import bmarks from "../lib/testData.json"; +async function getTwData(cookie: string) { + try { + // const twitterService = new TwitterApiService(cookie); + // const bookmarks = await twitterService.fetchAllBookmarks(); + // return { ok: bookmarks }; + return { ok: bmarks } as any; + } catch (error) { + return { error: `${error}` }; + } +} + +async function callLLM(bookmark: TwitterBookmark) { + const apiKey = Bun.env.GEMINI_API_KEY!; + try { + const llmService = new LLMService(apiKey); + + const categorization = await llmService.categorizeBookmark({ + bookmark, + userCategories, + }); + + return { ok: categorization }; + } catch (error) { + return { error: `${error}` }; + } +} + +export const getConfig = async () => { + return { + render: "dynamic", + } as const; +}; diff --git a/app/src/pages/index.tsx b/app/src/pages/index.tsx new file mode 100644 index 0000000..99202e5 --- /dev/null +++ b/app/src/pages/index.tsx @@ -0,0 +1,72 @@ +import * as Bun from "bun"; +import { Suspense } from "react"; +import { TwitterApiService } from "../lib/twitter-api"; +import { BookmarkList } from "../components/bookmark-list"; + +async function BookmarkFetcher() { + const cookie = Bun.env.TWATTER_COKI; + + if (!cookie) { + return ( + <div className="text-red-600">Missing Twitter cookie configuration</div> + ); + } + + try { + const twitterService = new TwitterApiService(cookie); + const bookmarks = await twitterService.fetchAllBookmarks(); + const file = Bun.file("testData.json"); + await file.write(JSON.stringify(bookmarks)); + + return ( + <div className="space-y-6"> + <div className="bg-white border border-gray-200 rounded-lg p-6"> + <h2 className="text-xl font-semibold mb-4">Your Bookmarks</h2> + <BookmarkList bookmarks={bookmarks} /> + </div> + </div> + ); + } catch (error) { + return ( + <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg"> + Error loading bookmarks:{" "} + {error instanceof Error ? error.message : "Unknown error"} + </div> + ); + } +} + +function LoadingSpinner() { + return ( + <div className="flex items-center justify-center py-12"> + <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> + <span className="ml-3 text-gray-600"> + Loading your Twitter bookmarks... + </span> + </div> + ); +} + +export default async function HomePage() { + return ( + <div className="max-w-4xl mx-auto"> + <title>SORMARK - Twitter Bookmark Manager</title> + <div className="mb-8"> + <h1 className="text-4xl font-bold tracking-tight mb-4">SORMARK</h1> + <p className="text-lg text-gray-600"> + Your Twitter bookmark manager powered by AI + </p> + </div> + + <Suspense fallback={<LoadingSpinner />}> + <BookmarkFetcher /> + </Suspense> + </div> + ); +} + +export const getConfig = async () => { + return { + render: "static", + } as const; +}; |