summaryrefslogtreecommitdiff
path: root/app/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/pages')
-rw-r--r--app/src/pages/_layout.tsx39
-rw-r--r--app/src/pages/about.tsx35
-rw-r--r--app/src/pages/categorize.tsx171
-rw-r--r--app/src/pages/index.tsx72
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;
+};