aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/web/app/dashboard/tags/page.tsx75
-rw-r--r--packages/trpc/routers/tags.ts77
2 files changed, 112 insertions, 40 deletions
diff --git a/apps/web/app/dashboard/tags/page.tsx b/apps/web/app/dashboard/tags/page.tsx
index f87b0fcc..2ba1074c 100644
--- a/apps/web/app/dashboard/tags/page.tsx
+++ b/apps/web/app/dashboard/tags/page.tsx
@@ -1,8 +1,13 @@
import Link from "next/link";
-import { redirect } from "next/navigation";
import { Separator } from "@/components/ui/separator";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
import { api } from "@/server/api/client";
-import { getServerAuthSession } from "@/server/auth";
+import { Info } from "lucide-react";
function TagPill({ name, count }: { name: string; count: number }) {
return (
@@ -16,30 +21,62 @@ function TagPill({ name, count }: { name: string; count: number }) {
}
export default async function TagsPage() {
- const session = await getServerAuthSession();
- if (!session) {
- redirect("/");
- }
-
- let tags = (await api.tags.list()).tags;
+ let allTags = (await api.tags.list()).tags;
// Sort tags by usage desc
- tags = tags.sort((a, b) => b.count - a.count);
+ allTags = allTags.sort((a, b) => b.count - a.count);
+
+ const humanTags = allTags.filter((t) => (t.countAttachedBy.human ?? 0) > 0);
+ const aiTags = allTags.filter((t) => (t.countAttachedBy.human ?? 0) == 0);
- let tagPill;
- if (tags.length) {
- tagPill = tags.map((t) => (
- <TagPill key={t.id} name={t.name} count={t.count} />
- ));
- } else {
- tagPill = "No Tags";
- }
+ const tagsToPill = (tags: typeof allTags) => {
+ let tagPill;
+ if (tags.length) {
+ tagPill = tags.map((t) => (
+ <TagPill key={t.id} name={t.name} count={t.count} />
+ ));
+ } else {
+ tagPill = "No Tags";
+ }
+ return tagPill;
+ };
return (
- <div className="space-y-3">
+ <div className="space-y-3 rounded-md border bg-background p-4">
<span className="text-2xl">All Tags</span>
<Separator />
- <div className="flex flex-wrap gap-3">{tagPill}</div>
+
+ <span className="flex items-center gap-2">
+ <p className="text-lg">Your Tags</p>
+ <TooltipProvider delayDuration={0}>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Info size={20} />
+ </TooltipTrigger>
+ <TooltipContent>
+ <p>Tags that were attached at least once by you</p>
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ </span>
+ <div className="flex flex-wrap gap-3">{tagsToPill(humanTags)}</div>
+
+ <Separator />
+
+ <span className="flex items-center gap-2">
+ <p className="text-lg">AI Tags</p>
+ <TooltipProvider delayDuration={0}>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Info className="text-gray-100" size={20} />
+ </TooltipTrigger>
+ <TooltipContent>
+ <p>Tags that were only attached automatically (by AI)</p>
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ </span>
+ <div className="flex flex-wrap gap-3">{tagsToPill(aiTags)}</div>
</div>
);
}
diff --git a/packages/trpc/routers/tags.ts b/packages/trpc/routers/tags.ts
index f6ce752d..e84f52ac 100644
--- a/packages/trpc/routers/tags.ts
+++ b/packages/trpc/routers/tags.ts
@@ -5,7 +5,9 @@ import { z } from "zod";
import { bookmarks, bookmarkTags, tagsOnBookmarks } from "@hoarder/db/schema";
import type { Context } from "../index";
+import type { ZAttachedByEnum } from "../types/tags";
import { authedProcedure, router } from "../index";
+import { zAttachedByEnumSchema } from "../types/tags";
function conditionFromInput(
input: { tagName: string } | { tagId: string },
@@ -55,6 +57,13 @@ const ensureTagOwnership = experimental_trpcMiddleware<{
return opts.next();
});
+const zTagSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ count: z.number(),
+ countAttachedBy: z.record(zAttachedByEnumSchema, z.number()),
+});
+
export const tagsAppRouter = router({
get: authedProcedure
.input(
@@ -68,20 +77,14 @@ export const tagsAppRouter = router({
}),
),
)
- .output(
- z.object({
- id: z.string(),
- name: z.string(),
- bookmarks: z.array(z.string()),
- }),
- )
+ .output(zTagSchema)
.use(ensureTagOwnership)
.query(async ({ input, ctx }) => {
const res = await ctx.db
.select({
id: bookmarkTags.id,
name: bookmarkTags.name,
- bookmarkId: bookmarks.id,
+ attachedBy: tagsOnBookmarks.attachedBy,
})
.from(bookmarkTags)
.leftJoin(tagsOnBookmarks, eq(bookmarkTags.id, tagsOnBookmarks.tagId))
@@ -98,10 +101,21 @@ export const tagsAppRouter = router({
throw new TRPCError({ code: "NOT_FOUND" });
}
+ const countAttachedBy = res.reduce<Record<ZAttachedByEnum, number>>(
+ (acc, curr) => {
+ if (curr.attachedBy) {
+ acc[curr.attachedBy]++;
+ }
+ return acc;
+ },
+ { ai: 0, human: 0 },
+ );
+
return {
id: res[0].id,
name: res[0].name,
- bookmarks: res.flatMap((t) => (t.bookmarkId ? [t.bookmarkId] : [])),
+ count: Object.values(countAttachedBy).reduce((s, a) => s + a, 0),
+ countAttachedBy,
};
}),
delete: authedProcedure
@@ -133,26 +147,47 @@ export const tagsAppRouter = router({
list: authedProcedure
.output(
z.object({
- tags: z.array(
- z.object({
- id: z.string(),
- name: z.string(),
- count: z.number(),
- }),
- ),
+ tags: z.array(zTagSchema),
}),
)
.query(async ({ ctx }) => {
- const tags = await ctx.db
+ const res = await ctx.db
.select({
id: tagsOnBookmarks.tagId,
name: bookmarkTags.name,
+ attachedBy: tagsOnBookmarks.attachedBy,
count: count(),
})
.from(tagsOnBookmarks)
- .where(eq(bookmarkTags.userId, ctx.user.id))
- .groupBy(tagsOnBookmarks.tagId)
- .innerJoin(bookmarkTags, eq(bookmarkTags.id, tagsOnBookmarks.tagId));
- return { tags };
+ .groupBy(tagsOnBookmarks.tagId, tagsOnBookmarks.attachedBy)
+ .innerJoin(bookmarkTags, eq(bookmarkTags.id, tagsOnBookmarks.tagId))
+ .leftJoin(bookmarks, eq(tagsOnBookmarks.bookmarkId, bookmarks.id))
+ .where(
+ and(
+ eq(bookmarkTags.userId, ctx.user.id),
+ eq(bookmarks.archived, false),
+ ),
+ );
+
+ const tags = res.reduce<Record<string, z.infer<typeof zTagSchema>>>(
+ (acc, row) => {
+ if (!(row.id in acc)) {
+ acc[row.id] = {
+ id: row.id,
+ name: row.name,
+ count: 0,
+ countAttachedBy: {
+ ai: 0,
+ human: 0,
+ },
+ };
+ }
+ acc[row.id].count++;
+ acc[row.id].countAttachedBy[row.attachedBy]!++;
+ return acc;
+ },
+ {},
+ );
+ return { tags: Object.values(tags) };
}),
});