summaryrefslogtreecommitdiff
path: root/app/src/components/cat/Entry.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/components/cat/Entry.tsx')
-rw-r--r--app/src/components/cat/Entry.tsx214
1 files changed, 214 insertions, 0 deletions
diff --git a/app/src/components/cat/Entry.tsx b/app/src/components/cat/Entry.tsx
new file mode 100644
index 0000000..1ded145
--- /dev/null
+++ b/app/src/components/cat/Entry.tsx
@@ -0,0 +1,214 @@
+import {
+ CategorizationResponse,
+ userCategories,
+} from "../../lib/categorization";
+import { TwitterApiService, TwitterBookmark } from "../../lib/twitter-api";
+
+export default function ({
+ bookmark,
+ categorization,
+ currentIndex,
+ totalCount,
+}: {
+ bookmark: TwitterBookmark;
+ categorization: CategorizationResponse;
+ currentIndex: number;
+ totalCount: number;
+}) {
+ async function processEntry() {
+ "use server";
+ const cookie = Bun.env.TWATTER_COKI;
+ const api = new TwitterApiService(cookie!);
+ await api.removeBookmark(bookmark.id);
+ }
+
+ return (
+ <div className="bg-white rounded-lg shadow-lg overflow-hidden">
+ {/* Bookmark Content */}
+ <div className="p-6 border-b">
+ <div className="flex items-start space-x-3 mb-4">
+ <img
+ src={bookmark.author.avatar}
+ alt={bookmark.author.name}
+ className="w-12 h-12 rounded-full"
+ />
+ <div>
+ <h3 className="font-semibold text-gray-900">
+ {bookmark.author.name}
+ </h3>
+ <p className="text-gray-600">@{bookmark.author.username}</p>
+ <p className="text-sm text-gray-500">
+ {new Date(bookmark.createdAt).toLocaleDateString()}
+ </p>
+ </div>
+ </div>
+
+ <div className="prose max-w-none mb-4">
+ <p className="whitespace-pre-wrap">{bookmark.text}</p>
+ </div>
+
+ {bookmark.media.pics.length > 0 && (
+ <div className="mb-4">
+ <h4 className="text-sm font-semibold text-gray-700 mb-2">Images</h4>
+ <div className="grid grid-cols-3 gap-2">
+ {bookmark.media.pics.map((url, index) => (
+ <img
+ key={index}
+ src={url}
+ alt={`Bookmark image ${index + 1}`}
+ className="rounded-lg border"
+ />
+ ))}
+ </div>
+ </div>
+ )}
+
+ <div className="flex flex-wrap gap-2">
+ {bookmark.hashtags.map((tag) => (
+ <span
+ key={tag}
+ className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-sm"
+ >
+ #{tag}
+ </span>
+ ))}
+ </div>
+ </div>
+
+ {/* LLM Analysis */}
+ <div className="p-6 border-b bg-gray-50">
+ <h3 className="text-lg font-semibold mb-3">AI Analysis</h3>
+ <p className="text-sm text-gray-600 mb-3">{categorization.summary}</p>
+
+ <div className="mb-4">
+ <h4 className="text-sm font-semibold mb-2">Key Topics</h4>
+ <div className="flex flex-wrap gap-2">
+ {categorization.keyTopics.map((topic) => (
+ <span
+ key={topic}
+ className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-sm"
+ >
+ {topic}
+ </span>
+ ))}
+ </div>
+ </div>
+
+ <div>
+ <h4 className="text-sm font-semibold mb-2">Suggested Categories</h4>
+ {categorization.suggestedCategories.map((suggestion, index) => (
+ <div key={index} className="mb-2 p-2 bg-white rounded border">
+ <div className="flex items-center justify-between mb-1">
+ <span className="font-medium">
+ {suggestion.categories.join(", ")}
+ </span>
+ <span className="text-sm text-gray-600">
+ {(suggestion.confidence * 100).toFixed(0)}%
+ </span>
+ </div>
+ <p className="text-sm text-gray-600">{suggestion.reasoning}</p>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {/* Category Selection */}
+ <form method="POST" action="/api/save-categorization">
+ <div className="p-6">
+ <h3 className="text-lg font-semibold mb-3">Select Categories</h3>
+
+ <div className="mb-4">
+ <h4 className="text-sm font-semibold mb-2">User Categories</h4>
+ <div className="flex flex-wrap gap-2">
+ {userCategories.map((category) => (
+ <label
+ key={category.name}
+ className="flex items-center cursor-pointer"
+ >
+ <input
+ type="checkbox"
+ name="categories"
+ value={category.name}
+ defaultChecked={categorization.suggestedCategories[0]?.categories.includes(
+ category.name,
+ )}
+ className="mr-2"
+ />
+ <span className="px-3 py-1 bg-gray-200 text-gray-700 rounded-full text-sm">
+ {category.name}
+ </span>
+ </label>
+ ))}
+ </div>
+ </div>
+
+ {categorization.newCategories.length > 0 && (
+ <div className="mb-4">
+ <h4 className="text-sm font-semibold mb-2">
+ New Category Suggestions
+ </h4>
+ <div className="flex flex-wrap gap-2">
+ {categorization.newCategories.map((category) => (
+ <label
+ key={category}
+ className="flex items-center cursor-pointer"
+ >
+ <input
+ type="checkbox"
+ name="categories"
+ value={category}
+ className="mr-2"
+ />
+ <span className="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm">
+ {category}
+ </span>
+ </label>
+ ))}
+ </div>
+ </div>
+ )}
+
+ <div className="mb-4">
+ <label className="block text-sm font-semibold mb-2">
+ Custom Categories
+ </label>
+ <input
+ type="text"
+ name="customCategories"
+ placeholder="Add custom categories, separated by commas"
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+ </div>
+
+ <input type="hidden" name="bookmarkId" value={bookmark.id} />
+ <input type="hidden" name="nextIndex" value={currentIndex + 1} />
+
+ <div className="flex space-x-4">
+ {currentIndex > 0 && (
+ <a
+ href={`/categorize?idx=${currentIndex - 1}`}
+ className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
+ >
+ Previous
+ </a>
+ )}
+ <button
+ type="submit"
+ className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700"
+ >
+ Save & Next
+ </button>
+ {currentIndex + 1 < totalCount && (
+ <a
+ href={`/categorize?idx=${currentIndex + 1}`}
+ className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
+ >
+ Skip
+ </a>
+ )}
+ </div>
+ </div>
+ </form>
+ </div>
+ );
+}