From a57a9a167017be146c4626968f33a4b5f2d29042 Mon Sep 17 00:00:00 2001 From: Conner Ow Date: Thu, 25 Apr 2024 09:34:54 -0500 Subject: [PATCH] feat: Change Requests --- .prettierrc | 1 - app/about/sections/conclusion.tsx | 4 +- app/about/sections/header.tsx | 16 +- app/about/sections/inspiration.tsx | 10 +- app/about/sections/intro.tsx | 59 +-- app/admin/actions/remove-contributor.ts | 24 +- app/admin/actions/remove-llm.ts | 34 +- app/admin/actions/remove-vote.ts | 34 +- .../actions/update-pending-contributor.ts | 32 +- app/admin/content.tsx | 4 +- app/admin/page.tsx | 4 +- app/api/auth/[...nextauth]/route.ts | 16 +- app/api/meta-search/route.ts | 12 +- app/api/search/route.ts | 42 +- app/api/search/types.ts | 6 +- app/changes/actions/get-change-requests.ts | 29 ++ app/changes/components/ChangeRequestItem.tsx | 252 ++++++++++ app/changes/components/actions/cast-vote.ts | 168 +++++++ app/changes/content.tsx | 54 +++ app/changes/page.tsx | 20 + app/compare/content.tsx | 12 +- app/compare/error.tsx | 10 +- app/compare/item/boolean-table.tsx | 4 +- app/compare/item/index.tsx | 22 +- app/compare/item/numeric-chart.tsx | 18 +- app/compare/item/string-table.tsx | 4 +- app/compare/item/tables.tsx | 10 +- app/compare/llms.tsx | 52 +- app/compare/page.tsx | 26 +- app/compare/search.tsx | 44 +- app/compare/search/route.ts | 32 +- app/compare/search/types.ts | 2 +- app/compare/sidebar.tsx | 38 +- app/compare/state.ts | 22 +- app/compare/types.ts | 50 +- app/contribute/cta.tsx | 12 +- app/contribute/function.tsx | 32 +- app/contribute/header.tsx | 24 +- app/contribute/intro.tsx | 2 +- app/contribute/join/actions.ts | 12 +- app/contribute/join/content.tsx | 24 +- app/contribute/waitlist/content.tsx | 18 +- app/contribute/waitlist/page.tsx | 4 +- app/home/selector.tsx | 16 +- app/layout.tsx | 2 +- app/llms/actions.ts | 70 ++- app/llms/components/FieldTable.tsx | 12 +- app/llms/components/LLMItem.tsx | 36 +- app/llms/components/LLMOverlay/Fields.tsx | 142 ++++++ app/llms/components/LLMOverlay/Header.tsx | 2 +- .../components/LLMOverlay/VoteSection.tsx | 188 -------- app/llms/components/LLMOverlay/Votes.tsx | 445 +++++++++++------- .../LLMOverlay/dialogs/RequestAdd.tsx | 130 +++++ .../LLMOverlay/dialogs/RequestChange.tsx | 117 +++++ .../LLMOverlay/dialogs/RequestDeletion.tsx | 120 +++++ .../LLMOverlay/dialogs/actions/request-add.ts | 115 +++++ .../dialogs/actions/request-change.ts | 78 +++ .../dialogs/actions/request-deletion.ts | 67 +++ app/llms/components/LLMOverlay/index.tsx | 212 +++++---- app/llms/components/LLMSearchFilter.tsx | 12 +- app/llms/content.tsx | 22 +- app/llms/llm/route.ts | 39 +- app/login/content.tsx | 26 +- app/metadata.ts | 14 +- app/page.tsx | 26 +- app/submit/actions.ts | 64 +-- app/submit/components/MetaField.tsx | 117 ++--- app/submit/content.tsx | 30 +- app/submit/page.tsx | 4 +- components/NumberInput.tsx | 6 +- components/container.tsx | 16 +- components/llm-icon.tsx | 10 +- components/navbar.tsx | 26 +- components/overflow.tsx | 6 +- components/providers/CurrentUserProvider.tsx | 2 +- components/providers/QueryClientProvider.tsx | 4 +- components/scroll-section.tsx | 4 +- components/ui/button.tsx | 10 +- components/ui/checkbox.tsx | 4 +- components/ui/dialog.tsx | 53 +++ components/ui/dropdown-menu.tsx | 18 +- components/ui/flex.tsx | 32 +- components/ui/input.tsx | 10 +- components/ui/popover.tsx | 2 +- components/ui/radio-group.tsx | 6 +- components/ui/select.tsx | 34 +- components/ui/sheet.tsx | 12 +- components/ui/skeleton.tsx | 2 +- components/ui/slider.tsx | 8 +- components/ui/switch.tsx | 4 +- components/ui/tabs.tsx | 6 +- components/ui/text.tsx | 28 +- components/ui/textarea.tsx | 10 +- components/ui/tooltip.tsx | 2 +- hooks/useElementSize.ts | 2 +- lib/comparison.ts | 26 +- lib/consensus.ts | 16 +- lib/env.ts | 2 +- lib/gradients.ts | 6 +- lib/numbers.ts | 6 +- lib/server/email.ts | 4 +- lib/server/utils/session.ts | 4 +- next.config.js | 8 +- postcss.config.js | 4 +- .../migration.sql | 47 ++ .../migration.sql | 2 + .../20240420123648_newnote/migration.sql | 2 + .../migration.sql | 8 + .../migration.sql | 11 + prisma/schema.prisma | 92 +++- prisma/seed/changes.ts | 113 +++++ prisma/seed/index.ts | 2 + prisma/seed/llms.ts | 20 +- prisma/seed/meta.ts | 14 +- prisma/seed/users.ts | 28 +- tailwind.config.ts | 28 +- 116 files changed, 2864 insertions(+), 1297 deletions(-) create mode 100644 app/changes/actions/get-change-requests.ts create mode 100644 app/changes/components/ChangeRequestItem.tsx create mode 100644 app/changes/components/actions/cast-vote.ts create mode 100644 app/changes/content.tsx create mode 100644 app/changes/page.tsx create mode 100644 app/llms/components/LLMOverlay/Fields.tsx delete mode 100644 app/llms/components/LLMOverlay/VoteSection.tsx create mode 100644 app/llms/components/LLMOverlay/dialogs/RequestAdd.tsx create mode 100644 app/llms/components/LLMOverlay/dialogs/RequestChange.tsx create mode 100644 app/llms/components/LLMOverlay/dialogs/RequestDeletion.tsx create mode 100644 app/llms/components/LLMOverlay/dialogs/actions/request-add.ts create mode 100644 app/llms/components/LLMOverlay/dialogs/actions/request-change.ts create mode 100644 app/llms/components/LLMOverlay/dialogs/actions/request-deletion.ts create mode 100644 components/ui/dialog.tsx create mode 100644 prisma/migrations/20240406162436_change_requests/migration.sql create mode 100644 prisma/migrations/20240418132219_realign_changereqs/migration.sql create mode 100644 prisma/migrations/20240420123648_newnote/migration.sql create mode 100644 prisma/migrations/20240420144500_nocomment_crv/migration.sql create mode 100644 prisma/migrations/20240423131712_change_req_meta/migration.sql create mode 100644 prisma/seed/changes.ts diff --git a/.prettierrc b/.prettierrc index 738126a..0465684 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,6 @@ { "semi": false, "singleQuote": false, - "trailingComma": "none", "bracketSpacing": true, "bracketSameLine": false, "arrowParens": "avoid" diff --git a/app/about/sections/conclusion.tsx b/app/about/sections/conclusion.tsx index ec00209..620135d 100644 --- a/app/about/sections/conclusion.tsx +++ b/app/about/sections/conclusion.tsx @@ -14,8 +14,8 @@ export default function Conclusion() { tokens.colors.accent.dimmer + "75", tokens.colors.accent.dimmer + "25 30%", tokens.colors.transparent + " 50%", - tokens.colors.transparent - ) + tokens.colors.transparent, + ), }} center className="border-t-2 border-default" diff --git a/app/about/sections/header.tsx b/app/about/sections/header.tsx index c6fe217..0fde6c8 100644 --- a/app/about/sections/header.tsx +++ b/app/about/sections/header.tsx @@ -9,7 +9,7 @@ import { useCallback } from "react" const toHex = (num: number): string => num.toString(16).padStart(2, "0") export default function Header({ - percentage + percentage, }: { percentage: MotionValue }) { @@ -19,33 +19,33 @@ export default function Header({ 0, tokens.colors.root + `${toHex(100 + p * 100)}`, tokens.colors.root + `${toHex(50 + p * 100)} ${10 + p * 10}%`, - `transparent ${20 + p * 20}%` + `transparent ${20 + p * 20}%`, ), gr.radial( `circle at 50% ${100 - 90 * p}%`, tokens.colors.red[500] + "55", tokens.colors.red[500] + "25 30%", "transparent 70%", - "transparent" + "transparent", ), gr.rRadial( `circle at 50% ${150 - 90 * p}%`, ...gr.stack( ["transparent", 400 - 150 * p], - [tokens.colors.outline.dimmest + "25", 404 - 150 * p] - ) - ) + [tokens.colors.outline.dimmest + "25", 404 - 150 * p], + ), + ), ) }, []) const smoothPercentage = useSpring(percentage, { - mass: 0.05 + mass: 0.05, }) const background = useTransform( smoothPercentage, [0, 1], - [backgroundImage(0), backgroundImage(1)] + [backgroundImage(0), backgroundImage(1)], ) const translateY = useTransform(smoothPercentage, [0, 1], ["0%", "-50%"]) diff --git a/app/about/sections/inspiration.tsx b/app/about/sections/inspiration.tsx index 93e2ad6..d15fb56 100644 --- a/app/about/sections/inspiration.tsx +++ b/app/about/sections/inspiration.tsx @@ -15,14 +15,14 @@ export default function Inspiration() { tokens.colors.accent.dimmest + "45", tokens.colors.accent.dimmest + "15 20%", tokens.colors.transparent + " 50%", - tokens.colors.transparent + tokens.colors.transparent, ), gr.radial( "circle at 100% 50%", tokens.colors.accent.dimmest + "45", tokens.colors.accent.dimmest + "15 20%", tokens.colors.transparent + " 50%", - tokens.colors.transparent + tokens.colors.transparent, ), gr.linear( 0, @@ -32,9 +32,9 @@ export default function Inspiration() { tokens.colors.accent.dimmest + "45", tokens.colors.accent.dimmest + "15", tokens.colors.root + " 70%", - tokens.colors.root - ) - ) + tokens.colors.root, + ), + ), }} className="border-t-2 border-default" > diff --git a/app/about/sections/intro.tsx b/app/about/sections/intro.tsx index edb7dc6..e6e6df8 100644 --- a/app/about/sections/intro.tsx +++ b/app/about/sections/intro.tsx @@ -9,12 +9,12 @@ import { MotionValue, useSpring, useTransform } from "framer-motion" import { styled } from "react-tailwind-variants" export default function Intro({ - percentage + percentage, }: { percentage: MotionValue }) { const smoothPercentage = useSpring(percentage, { - mass: 0.05 + mass: 0.05, }) const slideWidgetsX = useTransform(smoothPercentage, [0, 1], ["0", "-100%"]) @@ -27,8 +27,8 @@ export default function Intro({ tokens.colors.default + "ac", tokens.colors.default + "87 20%", tokens.colors.root + " 85%", - tokens.colors.root - ) + tokens.colors.root, + ), }} center className="border-t-2 border-default" @@ -53,7 +53,7 @@ export default function Intro({ gap={4} center style={{ - translateX: slideWidgetsX + translateX: slideWidgetsX, }} > @@ -149,9 +152,9 @@ export default function Intro({ const { WidgetGutter, WidgetContainer } = { WidgetGutter: styled("div", { - base: "w-full px-4 h-[256px] relative" + base: "w-full px-4 h-[256px] relative", }), WidgetContainer: styled(MotionFlex, { - base: "absolute top-0 left-0 *:shrink-0" - }) + base: "absolute top-0 left-0 *:shrink-0", + }), } diff --git a/app/admin/actions/remove-contributor.ts b/app/admin/actions/remove-contributor.ts index fcfcf2a..32f2c5e 100644 --- a/app/admin/actions/remove-contributor.ts +++ b/app/admin/actions/remove-contributor.ts @@ -10,12 +10,12 @@ import { EmbedBuilder, WebhookClient } from "discord.js" import { z } from "zod" const webhook = new WebhookClient({ - url: process.env.DISCORD_WEBHOOK_URL_ADMIN + url: process.env.DISCORD_WEBHOOK_URL_ADMIN, }) const removeContributorInput = z.object({ userId: z.number(), - reason: z.string().min(3).max(255) + reason: z.string().min(3).max(255), }) export type RemoveContributorInput = z.infer @@ -33,8 +33,8 @@ export async function removeContributor(e: RemoveContributorInput) { const contributor = await prisma.user.findFirst({ where: { - id: userId - } + id: userId, + }, }) if (!contributor) throw new Error("Contributor not found") @@ -48,8 +48,8 @@ export async function removeContributor(e: RemoveContributorInput) { await prisma.user.update({ where: { id: userId }, data: { - role: UserRole.user - } + role: UserRole.user, + }, }) await resend.emails.send({ @@ -60,10 +60,10 @@ export async function removeContributor(e: RemoveContributorInput) { html: baseEmail({ title: "Your contributor status has been removed", paragraphs: [ - `Your contributor status has been revoked by an administrator for this reason: "${reason}". If you have any questions or if you believe this was done in error, you may respond directly to this email.` + `Your contributor status has been revoked by an administrator for this reason: "${reason}". If you have any questions or if you believe this was done in error, you may respond directly to this email.`, ], - buttonLinks: [] - }) + buttonLinks: [], + }), }) const embed = new EmbedBuilder() @@ -72,16 +72,16 @@ export async function removeContributor(e: RemoveContributorInput) { .setColor(0xef4444) await webhook.send({ - embeds: [embed] + embeds: [embed], }) return { - success: true + success: true, } } catch (error) { return { success: false, - message: formatError(error) + message: formatError(error), } } } diff --git a/app/admin/actions/remove-llm.ts b/app/admin/actions/remove-llm.ts index 52aba6d..53b05c5 100644 --- a/app/admin/actions/remove-llm.ts +++ b/app/admin/actions/remove-llm.ts @@ -9,12 +9,12 @@ import { EmbedBuilder, WebhookClient } from "discord.js" import { z } from "zod" const webhook = new WebhookClient({ - url: process.env.DISCORD_WEBHOOK_URL_ADMIN + url: process.env.DISCORD_WEBHOOK_URL_ADMIN, }) const removeLLMInput = z.object({ llmId: z.number(), - reason: z.string().min(3).max(255) + reason: z.string().min(3).max(255), }) export type RemoveLLMInput = z.infer @@ -32,31 +32,31 @@ export async function removeLLM(e: RemoveLLMInput) { const llm = await prisma.lLM.findFirst({ where: { - id: llmId + id: llmId, }, include: { fields: true, votes: true, - user: true - } + user: true, + }, }) if (!llm) throw new Error("LLM not found") await prisma.field.deleteMany({ where: { - llmId - } + llmId, + }, }) await prisma.vote.deleteMany({ where: { - llmId - } + llmId, + }, }) await prisma.lLM.delete({ - where: { id: llmId } + where: { id: llmId }, }) await resend.emails.send({ @@ -67,32 +67,32 @@ export async function removeLLM(e: RemoveLLMInput) { html: baseEmail({ title: `Your LLM "${llm.name}" has been removed`, paragraphs: [ - `Your LLM with the name "${llm.name}" has been removed from LLM Arena by an administrator for this reason: "${reason}". If you have any questions or if you believe this was done in error, you may respond directly to this email.` + `Your LLM with the name "${llm.name}" has been removed from LLM Arena by an administrator for this reason: "${reason}". If you have any questions or if you believe this was done in error, you may respond directly to this email.`, ], - buttonLinks: [] - }) + buttonLinks: [], + }), }) const embed = new EmbedBuilder() .setTitle(`[LLM Removed] ${llm.name}`) .setFields({ name: "Reason", - value: reason + value: reason, }) .setDescription(`${llm.votes.length} votes • ${llm.fields.length} fields`) .setColor(0xef4444) await webhook.send({ - embeds: [embed] + embeds: [embed], }) return { - success: true + success: true, } } catch (error) { return { success: false, - message: formatError(error) + message: formatError(error), } } } diff --git a/app/admin/actions/remove-vote.ts b/app/admin/actions/remove-vote.ts index e06a4f8..41f6692 100644 --- a/app/admin/actions/remove-vote.ts +++ b/app/admin/actions/remove-vote.ts @@ -10,12 +10,12 @@ import { EmbedBuilder, WebhookClient } from "discord.js" import { z } from "zod" const webhook = new WebhookClient({ - url: process.env.DISCORD_WEBHOOK_URL_ADMIN + url: process.env.DISCORD_WEBHOOK_URL_ADMIN, }) const removeVoteInput = z.object({ voteId: z.number(), - reason: z.string().min(3).max(255) + reason: z.string().min(3).max(255), }) export type RemoveVoteInput = z.infer @@ -33,27 +33,27 @@ export async function removeVote(e: RemoveVoteInput) { const vote = await prisma.vote.findFirst({ where: { - id: voteId + id: voteId, }, include: { - user: true - } + user: true, + }, }) if (!vote?.comment) throw new Error("Cannot remove a vote lacking a comment") await prisma.vote.delete({ - where: { id: voteId } + where: { id: voteId }, }) const llm = await prisma.lLM.findFirst({ where: { - id: vote.llmId + id: vote.llmId, }, include: { - votes: true - } + votes: true, + }, }) if (!llm) throw new Error("LLM not found") @@ -62,11 +62,11 @@ export async function removeVote(e: RemoveVoteInput) { await prisma.lLM.update({ where: { - id: llm.id + id: llm.id, }, data: { - status: consensus.status - } + status: consensus.status, + }, }) const embed = new EmbedBuilder() @@ -75,24 +75,24 @@ export async function removeVote(e: RemoveVoteInput) { .setDescription(`"${vote.comment}"`) .setFields({ name: "Reason", - value: reason + value: reason, }) .setFooter({ - text: `- ${vote.user.handle}` + text: `- ${vote.user.handle}`, }) .setColor(0xef4444) await webhook.send({ - embeds: [embed] + embeds: [embed], }) return { - success: true + success: true, } } catch (error) { return { success: false, - message: formatError(error) + message: formatError(error), } } } diff --git a/app/admin/actions/update-pending-contributor.ts b/app/admin/actions/update-pending-contributor.ts index 3c9b492..bce2455 100644 --- a/app/admin/actions/update-pending-contributor.ts +++ b/app/admin/actions/update-pending-contributor.ts @@ -10,16 +10,16 @@ import { EmbedBuilder, WebhookClient } from "discord.js" import { z } from "zod" const webhook = new WebhookClient({ - url: process.env.DISCORD_WEBHOOK_URL_ADMIN + url: process.env.DISCORD_WEBHOOK_URL_ADMIN, }) const updatePendingContributorInput = z.object({ userId: z.number(), - status: z.nativeEnum(VoteStatus) + status: z.nativeEnum(VoteStatus), }) export async function updatePendingContributor( - input: z.infer + input: z.infer, ): Promise< | { success: true @@ -40,7 +40,7 @@ export async function updatePendingContributor( const { status, userId } = updatePendingContributorInput.parse(input) const userToUpdate = await prisma.user.findFirst({ - where: { id: userId } + where: { id: userId }, }) if (!userToUpdate) throw new Error("User not found") @@ -52,8 +52,8 @@ export async function updatePendingContributor( where: { id: userId }, data: { role: - status === VoteStatus.approve ? UserRole.contributor : UserRole.user - } + status === VoteStatus.approve ? UserRole.contributor : UserRole.user, + }, }) if (status === VoteStatus.approve) { @@ -67,15 +67,15 @@ export async function updatePendingContributor( paragraphs: [ "Congratulations! We've approved your request to become a contributor to LLM Arena.", 'To get started, check out the Contributor Guide', - `Join the Discord Server to stay up-to-date on announcements, updates, and more.` + `Join the Discord Server to stay up-to-date on announcements, updates, and more.`, ], buttonLinks: [ { href: "https://github.com/IroncladDev/llm-arena/blob/main/docs/contributor-guide.md", - text: "Start Contributing" - } - ] - }) + text: "Start Contributing", + }, + ], + }), }) } else { await resend.emails.send({ @@ -87,22 +87,22 @@ export async function updatePendingContributor( title: "Contribution Request Denied", paragraphs: [ "Thanks for your interest in contributing to LLM Arena. At the moment we've decided not to move forward with your request.", - "If you have any questions, you may respond to this email directly." + "If you have any questions, you may respond to this email directly.", ], - buttonLinks: [] - }) + buttonLinks: [], + }), }) } const embed = new EmbedBuilder() .setTitle( - `[Contributor ${status === VoteStatus.approve ? "Accepted" : "Rejected"}] ${userToUpdate.handle}` + `[Contributor ${status === VoteStatus.approve ? "Accepted" : "Rejected"}] ${userToUpdate.handle}`, ) .setURL(`https://github.com/${userToUpdate.handle}`) .setColor(status === VoteStatus.approve ? 0x34d399 : 0xef4444) await webhook.send({ - embeds: [embed] + embeds: [embed], }) return { success: true } diff --git a/app/admin/content.tsx b/app/admin/content.tsx index 71cddd9..3d07747 100644 --- a/app/admin/content.tsx +++ b/app/admin/content.tsx @@ -44,7 +44,7 @@ export default function AdminPage({ waitlist }: { waitlist: Array }) { const UserRow = ({ user, - setUsers + setUsers, }: { user: User setUsers: React.Dispatch> @@ -52,7 +52,7 @@ const UserRow = ({ const submit = async (status: VoteStatus) => { const res = await updatePendingContributor({ status, - userId: user.id + userId: user.id, }) if (res.success) { diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 79e1ebf..d73d441 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -16,8 +16,8 @@ export default async function Admin() { const waitlist = await prisma.user.findMany({ where: { - role: "pending" - } + role: "pending", + }, }) return diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index dd9eb6d..8c7ce3e 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -6,8 +6,8 @@ const authOptions: AuthOptions = { providers: [ GithubProvider({ clientId: process.env.GITHUB_ID!, - clientSecret: process.env.GITHUB_SECRET! - }) + clientSecret: process.env.GITHUB_SECRET!, + }), ], callbacks: { async signIn({ user, account, profile }) { @@ -17,8 +17,8 @@ const authOptions: AuthOptions = { const existingUser = await prisma.user.findFirst({ where: { - handle - } + handle, + }, }) if (existingUser) return true @@ -27,8 +27,8 @@ const authOptions: AuthOptions = { data: { handle, provider: account.provider as "github", - email: user.email as string - } + email: user.email as string, + }, }) if (newUser) { @@ -36,8 +36,8 @@ const authOptions: AuthOptions = { } else { return false } - } - } + }, + }, } const handler = NextAuth(authOptions) diff --git a/app/api/meta-search/route.ts b/app/api/meta-search/route.ts index 4281e04..5b017e1 100644 --- a/app/api/meta-search/route.ts +++ b/app/api/meta-search/route.ts @@ -15,9 +15,9 @@ export async function GET(request: Request) { const mostUsed = await prisma.metaProperty.findMany({ orderBy: { - useCount: "desc" + useCount: "desc", }, - take: 10 + take: 10, }) if (!query) { @@ -27,13 +27,13 @@ export async function GET(request: Request) { const results = await prisma.metaProperty.findMany({ where: { name: { - contains: query - } + contains: query, + }, }, orderBy: { - useCount: "desc" + useCount: "desc", }, - take: 10 + take: 10, }) return Response.json(results) diff --git a/app/api/search/route.ts b/app/api/search/route.ts index 33d6c3b..3069ada 100644 --- a/app/api/search/route.ts +++ b/app/api/search/route.ts @@ -33,22 +33,22 @@ export async function POST(req: Request) { searchBy.name && { name: advanced ? { - search: query + search: query, } : { contains: query, - mode: "insensitive" - } + mode: "insensitive", + }, }, searchBy.sourceDescription && { sourceDescription: advanced ? { - search: query + search: query, } : { contains: query, - mode: "insensitive" - } + mode: "insensitive", + }, }, searchBy.fields && { fields: { @@ -56,44 +56,44 @@ export async function POST(req: Request) { metaProperty: { name: advanced ? { - search: query + search: query, } : { contains: query, - mode: "insensitive" - } - } - } - } - } - ].filter(Boolean) as Array + mode: "insensitive", + }, + }, + }, + }, + }, + ].filter(Boolean) as Array, }, take: Math.min(limit, 100), skip, include: { fields: { include: { - metaProperty: true - } + metaProperty: true, + }, }, votes: true, - user: true + user: true, }, orderBy: { - createdAt: status === LLMStatus.pending ? "asc" : "desc" - } + createdAt: status === LLMStatus.pending ? "asc" : "desc", + }, }) return Response.json({ success: true, - data: results + data: results, }) } catch (err) { console.error(err) return Response.json({ success: false, - error: formatError(err) + error: formatError(err), }) } } diff --git a/app/api/search/types.ts b/app/api/search/types.ts index 514bb0a..62344a0 100644 --- a/app/api/search/types.ts +++ b/app/api/search/types.ts @@ -10,13 +10,13 @@ export const searchInput = z.object({ searchBy: z.object({ name: z.boolean().default(true), sourceDescription: z.boolean().default(true), - fields: z.boolean().default(true) - }) + fields: z.boolean().default(true), + }), }) export type SearchInput = z.infer -export type LLMWithRelations = LLM & { +export type LLMWithRelations = LLM & { fields: Array votes: Array user: User diff --git a/app/changes/actions/get-change-requests.ts b/app/changes/actions/get-change-requests.ts new file mode 100644 index 0000000..4ee96db --- /dev/null +++ b/app/changes/actions/get-change-requests.ts @@ -0,0 +1,29 @@ +"use server" +import { ChangeRequestWithVote } from "@/app/llms/components/LLMOverlay" +import prisma from "@/lib/server/prisma" + +export async function getChangeRequests(): Promise< + Array +> { + const changeRequests = await prisma.changeRequest.findMany({ + where: { + status: "pending", + }, + include: { + user: true, + votes: { + include: { + user: true, + }, + }, + field: true, + metaProperty: true, + llm: true, + }, + orderBy: { + id: "asc", + }, + }) + + return changeRequests +} diff --git a/app/changes/components/ChangeRequestItem.tsx b/app/changes/components/ChangeRequestItem.tsx new file mode 100644 index 0000000..468a69c --- /dev/null +++ b/app/changes/components/ChangeRequestItem.tsx @@ -0,0 +1,252 @@ +import { ChangeRequestWithVote } from "@/app/llms/components/LLMOverlay" +import { Table } from "@/app/llms/components/LLMOverlay/Fields" +import { useCurrentUser } from "@/components/providers/CurrentUserProvider" +import { Button } from "@/components/ui/button" +import Flex from "@/components/ui/flex" +import Text from "@/components/ui/text" +import determineConsensus from "@/lib/consensus" +import { abbrNumber } from "@/lib/numbers" +import { ChangeRequestType, MetaPropertyType, VoteStatus } from "@prisma/client" +import { + GitPullRequestIcon, + PlusIcon, + ThumbsDownIcon, + ThumbsUpIcon, + TrashIcon, +} from "lucide-react" +import { usePathname, useRouter } from "next/navigation" +import { styled } from "react-tailwind-variants" +import { castVote } from "./actions/cast-vote" + +export default function ChangeRequestItem({ + refetch, + request, + isOnLLM = false, +}: { + request: ChangeRequestWithVote + refetch: () => void + isOnLLM?: boolean +}) { + const user = useCurrentUser() + const router = useRouter() + const pathname = usePathname() + + const { approvals, rejections } = determineConsensus(request.votes) + + const userVote = request.votes.find(x => x.user.id === user?.id) + const hasUserVoted = Boolean(userVote) + + const requestValue = request.newValue ?? request.field?.value + const requestNote = request.newNote ?? request.field?.note + + const handleCastVote = async (action: VoteStatus) => { + const res = await castVote({ + changeRequestId: request.id, + action, + }) + + if (!res.success) { + alert(res.message) + + return + } + + refetch() + } + + return ( + + + + {request.type === ChangeRequestType.add ? ( + + ) : request.type === ChangeRequestType.edit ? ( + + ) : ( + + )} + + + + {request.user.handle} + {" "} + requested to {request.type} {request.metaProperty.name}{" "} + {isOnLLM ? null : ( + <> + on{" "} + + router.push(pathname + "?llm=" + request.llm.id, { + scroll: false, + }) + } + > + {request.llm.name} + + + )} + + handleCastVote("approve")} + > + + {approvals} + + handleCastVote("reject")} + > + + {rejections} + + + + + {request.type === "edit" && request.field && ( + + + + + {request.metaProperty.name} + + {request.field.note && ( + + {request.field.note} + + )} + + + + + + {request.metaProperty.type} + + + + + + + {request.metaProperty.type === MetaPropertyType.Number + ? abbrNumber(Number(request.field.value)) + : request.field.value} + + + + + )} + + + + + {request.metaProperty.name} + + {requestNote && ( + + {requestNote} + + )} + + + + + + {request.metaProperty.type} + + + + + + + {request.metaProperty.type === MetaPropertyType.Number + ? abbrNumber(Number(requestValue)) + : requestValue} + + + + + + + + {request.sourceDescription} + + + ) +} + +const { Container, VoteButton, TypeIndicator } = { + Container: styled("div", { + base: "flex flex-col gap-2 rounded-lg bg-higher p-2", + }), + VoteButton: styled(Button, { + base: "gap-1", + variants: { + status: { + approved: + "border-emerald-500 focus:border-emerald-500 text-emerald-500", + rejected: "border-rose-500 focus:border-rose-500 text-rose-500", + default: "", + deselected: "opacity-50", + }, + }, + }), + TypeIndicator: styled("div", { + base: "rounded-full p-1.5 w-7 h-7 flex items-center justify-center", + variants: { + status: { + [ChangeRequestType.add]: "bg-emerald-500/15 text-emerald-500", + [ChangeRequestType.edit]: "bg-yellow-500/15 text-yellow-500", + [ChangeRequestType.delete]: "bg-rose-500/15 text-rose-500", + }, + }, + }), +} diff --git a/app/changes/components/actions/cast-vote.ts b/app/changes/components/actions/cast-vote.ts new file mode 100644 index 0000000..74e3d19 --- /dev/null +++ b/app/changes/components/actions/cast-vote.ts @@ -0,0 +1,168 @@ +"use server" + +import determineConsensus from "@/lib/consensus" +import { siteUrl } from "@/lib/env" +import { formatError } from "@/lib/errors" +import prisma from "@/lib/server/prisma" +import { requireContributorOrAdmin } from "@/lib/server/utils/auth" +import { getSession } from "@/lib/server/utils/session" +import { LLMStatus, VoteStatus } from "@prisma/client" +import { EmbedBuilder, WebhookClient } from "discord.js" +import { z } from "zod" + +const webhook = new WebhookClient({ + url: process.env.DISCORD_WEBHOOK_URL_PUBLIC, +}) + +const castVoteInput = z.object({ + changeRequestId: z.number(), + action: z.nativeEnum(VoteStatus), +}) + +type CastVoteReturn = + | { + success: true + } + | { + success: false + message: string + } + +export async function castVote( + input: z.infer, +): Promise { + const res = await getSession() + + try { + if (!res?.user) throw new Error("Unauthorized") + + const { user } = res + requireContributorOrAdmin(user) + + const { changeRequestId, action } = castVoteInput.parse(input) + + const request = await prisma.changeRequest.findFirst({ + where: { + id: changeRequestId, + }, + include: { + votes: true, + metaProperty: { + select: { + name: true, + }, + }, + llm: { + select: { + name: true, + }, + }, + }, + }) + + if (!request) throw new Error("Change Request not found") + + if (request.status !== LLMStatus.pending) + throw new Error("Change Request Has already been " + request.status) + + const existingVote = await prisma.changeRequestVote.findFirst({ + where: { + changeRequestId, + userId: user.id, + }, + }) + + if (existingVote) + throw new Error("You have already voted on this Change Request") + + const newVote = await prisma.changeRequestVote.create({ + data: { + changeRequestId, + userId: user.id, + status: action, + }, + }) + + if (!newVote) throw new Error("Failed to cast vote") + + const consensus = determineConsensus([...request.votes, newVote]) + + if (consensus.status !== request.status) + await prisma.changeRequest.update({ + where: { + id: changeRequestId, + }, + data: { + status: consensus.status, + }, + }) + + if (consensus.status === "approved") { + if (request.type === "delete" && request.fieldId) { + await prisma.field.delete({ + where: { + id: request.fieldId, + }, + }) + } else if ( + request.type === "edit" && + request.fieldId && + (request.newNote || request.newValue) + ) { + await prisma.field.update({ + where: { id: request.fieldId }, + data: { + ...(request.newNote + ? { + note: request.newNote, + } + : {}), + ...(request.newValue + ? { + value: request.newValue, + } + : {}), + }, + }) + } else if (request.newValue) { + await prisma.field.create({ + data: { + llmId: request.llmId, + note: request.newNote, + value: request.newValue, + metaPropertyId: request.metaPropertyId, + }, + }) + } + } + + if (consensus.status !== LLMStatus.pending) { + const allVotes = [...request.votes, newVote] + + const embed = new EmbedBuilder() + .setTitle( + `[Change Request ${consensus.status}] ${request.type} ${request.metaProperty.name} on ${request.llm.name}`, + ) + .setURL(new URL("/llms?llm=" + request.llmId, siteUrl).toString()) + .setDescription( + `${consensus.status} after ${allVotes.filter(v => v.status === VoteStatus.approve).length} approvals, ${allVotes.filter(v => v.status === VoteStatus.reject).length} rejections.`, + ) + .setColor(consensus.status === "approved" ? 0x34d399 : 0xef4444) + + await webhook.send({ + embeds: [embed], + }) + } + + return { + success: true, + } + } catch (e) { + console.error(e) + + return { + success: false, + message: formatError(e), + } + } +} diff --git a/app/changes/content.tsx b/app/changes/content.tsx new file mode 100644 index 0000000..327abf4 --- /dev/null +++ b/app/changes/content.tsx @@ -0,0 +1,54 @@ +"use client" + +import { Container } from "@/components/container" +import Navbar from "@/components/navbar" +import Flex from "@/components/ui/flex" +import Text from "@/components/ui/text" +import { useState } from "react" +import { styled } from "react-tailwind-variants" +import LLMOverlay, { + ChangeRequestWithVote, +} from "../llms/components/LLMOverlay" +import { getChangeRequests } from "./actions/get-change-requests" +import ChangeRequestItem from "./components/ChangeRequestItem" + +export default function ChangesPage({ + initialRequests, +}: { + initialRequests: ChangeRequestWithVote[] +}) { + const [changeRequests, setChangeRequests] = + useState(initialRequests) + + const refetch = async () => { + setChangeRequests(await getChangeRequests()) + } + + return ( + + + +
+ + Change Requests + +
+ + {changeRequests.map(r => ( + + ))} + +
+ +
+ ) +} + +const { Content, Header } = { + Content: styled("div", { + base: "flex flex-col gap-4 max-w-2xl self-center min-h-screen h-full w-full p-4", + }), + Header: styled("div", { + base: "flex items-center gap-4 justify-between pb-4 border-b-2 border-outline-dimmest", + }), +} diff --git a/app/changes/page.tsx b/app/changes/page.tsx new file mode 100644 index 0000000..088c8f3 --- /dev/null +++ b/app/changes/page.tsx @@ -0,0 +1,20 @@ +import { getSession } from "@/lib/server/utils/session" +import { notFound, redirect } from "next/navigation" +import { getChangeRequests } from "./actions/get-change-requests" +import ChangesPage from "./content" + +export default async function LLMs() { + const { user } = await getSession() + + if (!user) { + return redirect("/login") + } + + if (user.role !== "admin" && user.role !== "contributor") { + return notFound() + } + + const initialRequests = await getChangeRequests() + + return +} diff --git a/app/compare/content.tsx b/app/compare/content.tsx index c7eb3d6..79ced64 100644 --- a/app/compare/content.tsx +++ b/app/compare/content.tsx @@ -16,7 +16,7 @@ import { useURLState } from "./state" import { LLMWithMetadata, ModeEnum } from "./types" export default function Content({ - initialLLMs + initialLLMs, }: { initialLLMs: Array }) { @@ -47,7 +47,7 @@ export default function Content({ llms={llms} setLLMs={items => { set({ - llms: items.map(x => x.id).join(",") + llms: items.map(x => x.id).join(","), }) setLLMs(items) }} @@ -99,12 +99,12 @@ export default function Content({ const { ContentContainer, SidebarButton, Footer } = { ContentContainer: styled("div", { - base: "flex flex-col gap-4 grow relative" + base: "flex flex-col gap-4 grow relative", }), SidebarButton: styled(Button, { - base: "absolute top-2 left-2 md:hidden" + base: "absolute top-2 left-2 md:hidden", }), Footer: styled("footer", { - base: "flex items-center justify-between p-4 gap-2 bg-gradient-to-b from-root/0 via-root to-root absolute bottom-0 left-0 right-0" - }) + base: "flex items-center justify-between p-4 gap-2 bg-gradient-to-b from-root/0 via-root to-root absolute bottom-0 left-0 right-0", + }), } diff --git a/app/compare/error.tsx b/app/compare/error.tsx index 32c2bce..a76352d 100644 --- a/app/compare/error.tsx +++ b/app/compare/error.tsx @@ -9,7 +9,7 @@ import Link from "next/link" import { styled } from "react-tailwind-variants" export default function Error({ - error: { message } + error: { message }, }: { error: Error & { digest?: string } }) { @@ -22,8 +22,8 @@ export default function Error({ tokens.colors.default, tokens.colors.default + " 25%", tokens.colors.transparent + " 70%", - tokens.colors.transparent - ) + tokens.colors.transparent, + ), }} > @@ -43,6 +43,6 @@ export default function Error({ const { Content } = { Content: styled("div", { - base: "flex flex-col gap-4 p-6 border-2 rounded-xl border-outline-dimmest bg-root/50 shadow-xl max-w-[360px] w-full items-center" - }) + base: "flex flex-col gap-4 p-6 border-2 rounded-xl border-outline-dimmest bg-root/50 shadow-xl max-w-[360px] w-full items-center", + }), } diff --git a/app/compare/item/boolean-table.tsx b/app/compare/item/boolean-table.tsx index 5fe2238..bfe9bb7 100644 --- a/app/compare/item/boolean-table.tsx +++ b/app/compare/item/boolean-table.tsx @@ -6,14 +6,14 @@ import { FilterEnum, themeData } from "../types" import Table from "./tables" export default function BooleanTable({ - field + field, }: { field: ComparableFieldGroup }) { const { theme, filters } = useURLState() const { - foreground: [fg1] + foreground: [fg1], } = themeData[theme] const fields = filters.includes(FilterEnum.nullFields) diff --git a/app/compare/item/index.tsx b/app/compare/item/index.tsx index 7eebc89..1b5b9f8 100644 --- a/app/compare/item/index.tsx +++ b/app/compare/item/index.tsx @@ -15,7 +15,7 @@ import StringTable from "./string-table" export default function CompareItem({ field, - rect + rect, }: { field: ComparableFieldGroup rect?: DOMRect @@ -26,7 +26,7 @@ export default function CompareItem({ const { view, theme, setOmmittedField, mode, padding } = useURLState() const { - foreground: [, fg2] + foreground: [, fg2], } = themeData[theme] return ( @@ -40,9 +40,9 @@ export default function CompareItem({ flexGrow: 0, flexShrink: 0, width: customWidth, - flexBasis: customWidth + flexBasis: customWidth, } - : {}) + : {}), }} ref={ref} > @@ -96,17 +96,17 @@ const { Container, OptionButton, DragHandle, DragHandleBar } = { variants: { variant: { grid: "", - list: "self-center" - } - } + list: "self-center", + }, + }, }), OptionButton: styled("button", { - base: "absolute top-2 right-2 opacity-0 group-hover/item:opacity-100 transition-opacity" + base: "absolute top-2 right-2 opacity-0 group-hover/item:opacity-100 transition-opacity", }), DragHandle: styled(MotionDiv, { - base: "flex items-center justify-center w-2 h-full absolute top-0 bottom-0 opacity-0 group-hover/item:opacity-50 transition-opacity right-0" + base: "flex items-center justify-center w-2 h-full absolute top-0 bottom-0 opacity-0 group-hover/item:opacity-50 transition-opacity right-0", }), DragHandleBar: styled("div", { - base: "bg-foreground-dimmest/50 rounded-full w-[2px] h-12 cursor-ew-resize active:bg-foreground-dimmest/25" - }) + base: "bg-foreground-dimmest/50 rounded-full w-[2px] h-12 cursor-ew-resize active:bg-foreground-dimmest/25", + }), } diff --git a/app/compare/item/numeric-chart.tsx b/app/compare/item/numeric-chart.tsx index 1e331ac..77a5521 100644 --- a/app/compare/item/numeric-chart.tsx +++ b/app/compare/item/numeric-chart.tsx @@ -9,14 +9,14 @@ import { FilterEnum, themeData } from "../types" import Table from "./tables" export default function NumericChart({ - field + field, }: { field: ComparableFieldGroup }) { const { theme, filters } = useURLState() const { - foreground: [fg1, fg2] + foreground: [fg1, fg2], } = themeData[theme] const rows = filters.includes(FilterEnum.nullFields) @@ -58,7 +58,7 @@ export default function NumericChart({ borderColor: value === null ? tokens.colors.foreground.dimmest + "9f" - : fg1 + : fg1, }} /> - filter.includes(FilterEnum.standalone) ? true : l.nonNullCount > 1 + filter.includes(FilterEnum.standalone) ? true : l.nonNullCount > 1, ) .filter(l => filter.includes(FilterEnum.number) ? true - : l.type !== MetaPropertyType.Number + : l.type !== MetaPropertyType.Number, ) .filter(l => filter.includes(FilterEnum.string) ? true - : l.type !== MetaPropertyType.String + : l.type !== MetaPropertyType.String, ) .filter(l => filter.includes(FilterEnum.boolean) ? true - : l.type !== MetaPropertyType.Boolean + : l.type !== MetaPropertyType.Boolean, ) .filter(l => - filter.includes(FilterEnum.nullFields) ? true : l.nonNullCount > 0 + filter.includes(FilterEnum.nullFields) ? true : l.nonNullCount > 0, ) .filter(l => !ommitted.includes(l.name)) @@ -71,7 +71,7 @@ const LLMContainer = forwardRef< style={{ background: gr.radial(...background), width: containerWidth, - borderColor: fg2 + borderColor: fg2, }} ref={ref} > @@ -84,7 +84,7 @@ const LLMContainer = forwardRef< if (!box) return let newWidth = Math.round( - (box.left + box.width / 2 - info.point.x) * 2 + padding + (box.left + box.width / 2 - info.point.x) * 2 + padding, ) containerWidth.set(newWidth) @@ -100,12 +100,12 @@ const LLMContainer = forwardRef< paddingLeft: springPadding, paddingRight: springPadding, // not animating since it would cause issues with animating the gap - paddingBottom: spacing + paddingBottom: spacing, }} > @@ -131,7 +131,7 @@ const LLMContainer = forwardRef< paddingRight: springPadding, paddingBottom: springPadding, // gap can't be animated, using the direct state value - gap: spacing + gap: spacing, }} > {items.map((x, i) => ( @@ -147,7 +147,7 @@ const LLMContainer = forwardRef< if (!box) return const newWidth = Math.round( - (info.point.x - (box.left + box.width / 2)) * 2 + padding + (info.point.x - (box.left + box.width / 2)) * 2 + padding, ) containerWidth.set(newWidth) @@ -171,41 +171,41 @@ const { DisplayContainer, HeaderWidget, DragHandle, - DragHandleBar + DragHandleBar, } = { Container: styled(MotionDiv, { - base: "relative grow basis-0" + base: "relative grow basis-0", }), Overflow: styled("div", { - base: "absolute left-0 top-1/2 -translate-y-1/2 overflow-y-auto max-h-full w-full flex justify-center px-4 py-8 pb-[96px]" + base: "absolute left-0 top-1/2 -translate-y-1/2 overflow-y-auto max-h-full w-full flex justify-center px-4 py-8 pb-[96px]", }), DisplayContainer: styled(MotionDiv, { - base: "flex flex-col border-2 rounded-xl max-w-[1600px] min-w-[360px] h-full bg-root relative group/outer" + base: "flex flex-col border-2 rounded-xl max-w-[1600px] min-w-[360px] h-full bg-root relative group/outer", }), HeaderWidget: styled("div", { - base: "flex flex-col gap-2 border-2 rounded-lg bg-root/50 w-full p-4" + base: "flex flex-col gap-2 border-2 rounded-lg bg-root/50 w-full p-4", }), ItemsContainer: styled(MotionDiv, { base: "flex rounded-xl w-full h-full", variants: { view: { grid: "flex-row flex-wrap", - list: "flex-col" - } - } + list: "flex-col", + }, + }, }), DragHandle: styled(MotionDiv, { base: "flex items-center justify-center md:opacity-0 group-hover/outer:opacity-100 cursor-ew-resize z-10 absolute top-0 bottom-0 h-full transition-opacity", variants: { position: { left: "left-0", - right: "right-0" - } - } + right: "right-0", + }, + }, }), DragHandleBar: styled("div", { - base: "bg-white/10 rounded-full w-[4px] h-24 cursor-ew-resize active:bg-white/5 transition-colors" - }) + base: "bg-white/10 rounded-full w-[4px] h-24 cursor-ew-resize active:bg-white/5 transition-colors", + }), } export default LLMContainer diff --git a/app/compare/page.tsx b/app/compare/page.tsx index ae7462a..287745c 100644 --- a/app/compare/page.tsx +++ b/app/compare/page.tsx @@ -4,7 +4,7 @@ import Content from "./content" import { optionsSchema, OptionsType } from "./state" export default async function ComparePage({ - searchParams + searchParams, }: { searchParams: OptionsType }) { @@ -20,17 +20,17 @@ export default async function ComparePage({ const llms = await prisma.lLM.findMany({ where: { id: { - in: ids - } + in: ids, + }, }, include: { fields: { include: { - metaProperty: true - } + metaProperty: true, + }, }, - user: true - } + user: true, + }, }) return @@ -40,7 +40,7 @@ export default async function ComparePage({ } export async function generateMetadata({ - searchParams + searchParams, }: { searchParams: OptionsType }) { @@ -50,9 +50,9 @@ export async function generateMetadata({ const llms = await prisma.lLM.findMany({ where: { id: { - in: params.llms.split(",").map(Number) - } - } + in: params.llms.split(",").map(Number), + }, + }, }) let title: string @@ -78,12 +78,12 @@ export async function generateMetadata({ return { title, - description + description, } } catch { return { title: "Error", - description: "An error occurred" + description: "An error occurred", } } } diff --git a/app/compare/search.tsx b/app/compare/search.tsx index cd730a9..3de5c31 100644 --- a/app/compare/search.tsx +++ b/app/compare/search.tsx @@ -29,7 +29,7 @@ const LLMSearch = forwardRef< const res = await fetch(searchUrl) return await res.json() - } + }, }) const sortSelected = (llm: LLMWithMetadata) => @@ -44,7 +44,7 @@ const LLMSearch = forwardRef< highlightedIndex, getItemProps, setInputValue, - reset + reset, } = useCombobox({ items, defaultHighlightedIndex: 0, @@ -62,7 +62,7 @@ const LLMSearch = forwardRef< setInputValue("") reset() } - } + }, }) return ( @@ -103,57 +103,57 @@ const { Item, ItemInfo, ItemIcon, - EmptyState + EmptyState, } = { Search: styled("div", { - base: "flex relative basis-0 w-full min-w-0 grow" + base: "flex relative basis-0 w-full min-w-0 grow", }), DownshiftPopover: styled("div", { base: "w-full rounded-lg border-2 border-outline-dimmer/75 absolute top-12 left-0 bg-default z-10 shadow-lg", variants: { inputSize: { default: "top-12", - lg: "top-14" + lg: "top-14", }, loading: { - true: "animate-pulse" - } + true: "animate-pulse", + }, }, defaultVariants: { - inputSize: "default" - } + inputSize: "default", + }, }), ItemsContainer: styled("ul", { base: "flex-col hidden", variants: { visible: { - true: "flex" - } - } + true: "flex", + }, + }, }), Item: styled("li", { base: "flex gap-2 justify-between p-2 first:rounded-t-md last:rounded-b-md", variants: { highlighted: { - true: "bg-higher" - } - } + true: "bg-higher", + }, + }, }), ItemInfo: styled("div", { - base: "flex gap-2 items-center" + base: "flex gap-2 items-center", }), ItemIcon: styled(Hexagon, { base: "w-4 h-4", variants: { selected: { true: "text-accent-dimmer fill-accent-dimmest/50", - false: "text-foreground-dimmest" - } - } + false: "text-foreground-dimmest", + }, + }, }), EmptyState: styled("div", { - base: "flex justify-center items-center p-4" - }) + base: "flex justify-center items-center p-4", + }), } export default LLMSearch diff --git a/app/compare/search/route.ts b/app/compare/search/route.ts index 31e935a..708c00d 100644 --- a/app/compare/search/route.ts +++ b/app/compare/search/route.ts @@ -15,20 +15,20 @@ export async function GET(req: Request) { where: { status: LLMStatus.approved, id: { - notIn: idsToExclude + notIn: idsToExclude, }, OR: [ { name: { contains: query, - mode: "insensitive" - } + mode: "insensitive", + }, }, { sourceDescription: { contains: query, - mode: "insensitive" - } + mode: "insensitive", + }, }, { fields: { @@ -36,23 +36,23 @@ export async function GET(req: Request) { metaProperty: { name: { contains: query, - mode: "insensitive" - } - } - } - } - } - ].filter(Boolean) as Array + mode: "insensitive", + }, + }, + }, + }, + }, + ].filter(Boolean) as Array, }, take: 10, include: { fields: { include: { - metaProperty: true - } + metaProperty: true, + }, }, - user: true - } + user: true, + }, }) return Response.json(results) diff --git a/app/compare/search/types.ts b/app/compare/search/types.ts index 3b6eecd..f8f9754 100644 --- a/app/compare/search/types.ts +++ b/app/compare/search/types.ts @@ -1,7 +1,7 @@ import { z } from "zod" export const searchInput = z.object({ - query: z.string() + query: z.string(), }) export type SearchInput = z.infer diff --git a/app/compare/sidebar.tsx b/app/compare/sidebar.tsx index 0bd5607..89351f8 100644 --- a/app/compare/sidebar.tsx +++ b/app/compare/sidebar.tsx @@ -5,7 +5,7 @@ import { DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, - DropdownMenuTrigger + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import Flex from "@/components/ui/flex" import { @@ -13,13 +13,13 @@ import { SelectContent, SelectItem, SelectTrigger, - SelectValue + SelectValue, } from "@/components/ui/select" import { Slider, SliderRange, SliderThumb, - SliderTrack + SliderTrack, } from "@/components/ui/slider" import Text from "@/components/ui/text" import gr from "@/lib/gradients" @@ -34,7 +34,7 @@ import { PencilIcon, ShareIcon, XIcon, - icons + icons, } from "lucide-react" import { createElement, useEffect, useState } from "react" import { styled } from "react-tailwind-variants" @@ -48,7 +48,7 @@ import { ViewEnum, filterData, themeData, - viewData + viewData, } from "./types" export default function Sidebar({ @@ -72,7 +72,7 @@ export default function Sidebar({ padding, spacing, ommitted, - setOmmittedField + setOmmittedField, } = useURLState() const [hasActed, setHasActed] = useState(false) @@ -94,8 +94,8 @@ export default function Sidebar({ await navigator.clipboard.write([ new ClipboardItem({ - "image/png": blob - }) + "image/png": blob, + }), ]) setHasActed(true) @@ -165,7 +165,7 @@ export default function Sidebar({ {createElement(icons[option.icon], { - className: "w-4 h-4" + className: "w-4 h-4", })} {option.label} @@ -202,7 +202,7 @@ export default function Sidebar({ onSelect={() => setFilterValue( key as FilterEnum, - !filters.includes(key as FilterEnum) + !filters.includes(key as FilterEnum), ) } key={i} @@ -409,27 +409,27 @@ const { ThemeColor, ShareOption, OmmittedField, - LLMListItem + LLMListItem, } = { SidebarContainer: styled("div", { base: "flex flex-col gap-4 p-4 bg-default border-r-2 border-outline-dimmer max-md:border-r-0 md:max-w-[320px] max-w-screen max-md:absolute max-md:inset-0 h-screen overflow-y-auto grow z-10", variants: { open: { true: "", - false: "max-md:hidden" - } - } + false: "max-md:hidden", + }, + }, }), ThemeColor: styled("div", { - base: "w-4 h-4 rounded-full shrink-0" + base: "w-4 h-4 rounded-full shrink-0", }), ShareOption: styled(DropdownMenuItem, { - base: "flex gap-2 items-center pl-2" + base: "flex gap-2 items-center pl-2", }), OmmittedField: styled("button", { - base: "flex gap-1 items-center border border-outline-dimmer rounded-md px-2 py-1 hover:border-accent-dimmer transition-colors text-foreground-dimmer hover:text-accent" + base: "flex gap-1 items-center border border-outline-dimmer rounded-md px-2 py-1 hover:border-accent-dimmer transition-colors text-foreground-dimmer hover:text-accent", }), LLMListItem: styled("div", { - base: "flex items-center gap-2 border-2 border-outline-dimmest rounded-lg px-2 py-1" - }) + base: "flex items-center gap-2 border-2 border-outline-dimmest rounded-lg px-2 py-1", + }), } diff --git a/app/compare/state.ts b/app/compare/state.ts index 404bd18..ed4835b 100644 --- a/app/compare/state.ts +++ b/app/compare/state.ts @@ -20,11 +20,11 @@ export const optionsSchema = z.object({ return ( options.length === 0 || options.every(option => - Object.values(FilterEnum).includes(option as FilterEnum) + Object.values(FilterEnum).includes(option as FilterEnum), ) ) }, - { message: "Invalid filter option" } + { message: "Invalid filter option" }, ) .optional(), // Omit particular meta fields @@ -38,14 +38,14 @@ export const optionsSchema = z.object({ return fields.length === 0 || fields.every(x => /[a-z0-9\_]*/.test(x)) }, { - message: "Invalid ommitted field" - } + message: "Invalid ommitted field", + }, ) .optional(), mode: z.nativeEnum(ModeEnum).optional(), padding: z.string().optional(), spacing: z.string().optional(), - width: z.string().optional() + width: z.string().optional(), }) export type OptionsType = z.infer @@ -59,15 +59,15 @@ export function useURLState() { theme = ThemeEnum.crimson, view = ViewEnum.grid, filter = [FilterEnum.number, FilterEnum.string, FilterEnum.boolean].join( - "," + ",", ), mode = ModeEnum.view, padding = "24", spacing = "16", width = "1600", - omit + omit, }: OptionsType = Object.fromEntries( - searchParams.entries() + searchParams.entries(), ) as unknown as OptionsType const set = (arg: Partial, replace: boolean = false) => { @@ -101,7 +101,7 @@ export function useURLState() { url.searchParams.set( "filter", - fts.length > 0 ? fts.filter(x => !!FilterEnum[x]).join(",") : "" + fts.length > 0 ? fts.filter(x => !!FilterEnum[x]).join(",") : "", ) router.push(url.pathname + url.search, { scroll: false }) @@ -116,7 +116,7 @@ export function useURLState() { url.searchParams.set( "omit", - omtd.length > 0 ? omtd.filter(x => x.length > 0).join(",") : "" + omtd.length > 0 ? omtd.filter(x => x.length > 0).join(",") : "", ) router.push(url.pathname + url.search, { scroll: false }) @@ -136,6 +136,6 @@ export function useURLState() { setOmmittedField, padding: Number(padding), spacing: Number(spacing), - width: Number(width) + width: Number(width), } } diff --git a/app/compare/types.ts b/app/compare/types.ts index 3af119f..ab0f9dc 100644 --- a/app/compare/types.ts +++ b/app/compare/types.ts @@ -9,7 +9,7 @@ export type LLMWithMetadata = LLM & { export enum ViewEnum { grid = "grid", - list = "list" + list = "list", } export enum ThemeEnum { @@ -18,7 +18,7 @@ export enum ThemeEnum { amber = "amber", cerulean = "cerulean", amethyst = "amethyst", - evergreen = "evergreen" + evergreen = "evergreen", } export enum FilterEnum { @@ -26,12 +26,12 @@ export enum FilterEnum { string = "string", boolean = "boolean", nullFields = "nullFields", - standalone = "standalone" + standalone = "standalone", } export enum ModeEnum { view = "view", - edit = "edit" + edit = "edit", } interface ControlOption { @@ -48,35 +48,35 @@ interface ThemeOption { export const viewData: Record = { [ViewEnum.grid]: { icon: "LayoutGrid", - label: "Grid" + label: "Grid", }, [ViewEnum.list]: { icon: "StretchHorizontal", - label: "List" - } + label: "List", + }, } export const filterData: Record = { [FilterEnum.number]: { icon: "Hash", - label: "Number" + label: "Number", }, [FilterEnum.string]: { icon: "Type", - label: "String" + label: "String", }, [FilterEnum.boolean]: { icon: "ThumbsUp", - label: "Boolean" + label: "Boolean", }, [FilterEnum.nullFields]: { icon: "CircleDashed", - label: "Null Fields" + label: "Null Fields", }, [FilterEnum.standalone]: { icon: "BarChart", - label: "Standalone" - } + label: "Standalone", + }, } export const themeData: Record = { @@ -85,9 +85,9 @@ export const themeData: Record = { background: [ "circle at 75% -10%", tokens.colors.accent.dimmest, - tokens.colors.root + tokens.colors.root, ], - label: "Crimson" + label: "Crimson", }, [ThemeEnum.amber]: { foreground: [tokens.colors.amber[500], tokens.colors.amber[600]], @@ -97,14 +97,14 @@ export const themeData: Record = { tokens.colors.yellow[700] + " 30%", tokens.colors.yellow[900] + " 50%", tokens.colors.root + " 90%", - tokens.colors.root + tokens.colors.root, ], - label: "Amber" + label: "Amber", }, [ThemeEnum.evergreen]: { foreground: [tokens.colors.green[500], tokens.colors.green[700]], background: [tokens.colors.emerald[800], tokens.colors.emerald[950]], - label: "Evergreen" + label: "Evergreen", }, [ThemeEnum.cerulean]: { foreground: [tokens.colors.teal[500], tokens.colors.teal[700]], @@ -113,9 +113,9 @@ export const themeData: Record = { tokens.colors.teal[800], tokens.colors.teal[900] + " 20%", tokens.colors.cyan[900] + " 70%", - tokens.colors.cyan[950] + tokens.colors.cyan[950], ], - label: "Cerulean" + label: "Cerulean", }, [ThemeEnum.amethyst]: { foreground: [tokens.colors.indigo[500], tokens.colors.indigo[700]], @@ -125,9 +125,9 @@ export const themeData: Record = { tokens.colors.violet[800] + "95 20%", tokens.colors.blue[950] + " 70%", tokens.colors.blue[800] + " 90%", - tokens.colors.blue[500] + tokens.colors.blue[500], ], - label: "Amethyst" + label: "Amethyst", }, [ThemeEnum.midnight]: { foreground: [tokens.colors.outline.default, tokens.colors.outline.dimmer], @@ -135,8 +135,8 @@ export const themeData: Record = { "circle at 75% -10%", tokens.colors.highest, tokens.colors.default, - tokens.colors.root + tokens.colors.root, ], - label: "Midnight" - } + label: "Midnight", + }, } diff --git a/app/contribute/cta.tsx b/app/contribute/cta.tsx index a9599b1..8ce5085 100644 --- a/app/contribute/cta.tsx +++ b/app/contribute/cta.tsx @@ -15,16 +15,16 @@ export default function Cta() { gr.radial( `circle at 50% 100%`, tokens.colors.red[500] + "35", - "transparent" + "transparent", ), gr.rRadial( "circle at 50% 110%", ...gr.stack( ["transparent", 300], - [tokens.colors.outline.dimmest + "85", 302] - ) - ) - ) + [tokens.colors.outline.dimmest + "85", 302], + ), + ), + ), }} > @@ -44,5 +44,5 @@ export default function Cta() { } const Content = styled(Flex, { - base: "max-w-screen-md max-md:max-w-screen max-md:px-4 py-16 self-center" + base: "max-w-screen-md max-md:max-w-screen max-md:px-4 py-16 self-center", }) diff --git a/app/contribute/function.tsx b/app/contribute/function.tsx index eba74dd..f323df0 100644 --- a/app/contribute/function.tsx +++ b/app/contribute/function.tsx @@ -8,7 +8,7 @@ import { MotionValue, useSpring, useTransform } from "framer-motion" import { styled } from "react-tailwind-variants" export default function Function({ - percentage + percentage, }: { percentage: MotionValue }) { @@ -18,7 +18,7 @@ export default function Function({ const translateX = useTransform( smoothPercentage, [0, 1], - ["calc(0px - 0%)", `calc(${box?.width || 768}px - 100%)`] + ["calc(0px - 0%)", `calc(${box?.width || 768}px - 100%)`], ) return ( @@ -40,9 +40,9 @@ export default function Function({ style={{ backgroundImage: gr.merge( gr.linear(0, tokens.colors.root + "85", "transparent"), - "url(/images/1-data.png)" + "url(/images/1-data.png)", ), - backgroundColor: tokens.colors.default + backgroundColor: tokens.colors.default, }} /> @@ -58,9 +58,9 @@ export default function Function({ style={{ backgroundImage: gr.merge( gr.linear(0, tokens.colors.root + "85", "transparent"), - "url(/images/2-src.png)" + "url(/images/2-src.png)", ), - backgroundColor: tokens.colors.default + backgroundColor: tokens.colors.default, }} /> @@ -75,9 +75,9 @@ export default function Function({ style={{ backgroundImage: gr.merge( gr.linear(0, tokens.colors.root + "85", "transparent"), - "url(/images/3-approval.png)" + "url(/images/3-approval.png)", ), - backgroundColor: tokens.colors.default + backgroundColor: tokens.colors.default, }} /> @@ -93,9 +93,9 @@ export default function Function({ style={{ backgroundImage: gr.merge( gr.linear(0, tokens.colors.root + "85", "transparent"), - "url(/images/4-comparison.png)" + "url(/images/4-comparison.png)", ), - backgroundColor: tokens.colors.root + backgroundColor: tokens.colors.root, }} /> @@ -108,18 +108,18 @@ export default function Function({ const { Content, StepContainer, Steps, Step, Preview } = { Content: styled(Flex, { - base: "max-w-screen-md self-center py-16 px-4" + base: "max-w-screen-md self-center py-16 px-4", }), StepContainer: styled("div", { - base: "grow relative" + base: "grow relative", }), Steps: styled(MotionDiv, { - base: "absolute top-0 left-0 w-auto h-full flex gap-4" + base: "absolute top-0 left-0 w-auto h-full flex gap-4", }), Step: styled("div", { - base: "h-full border-2 border-outline-dimmest rounded-xl p-8 grow min-w-[360px] w-[60vw] max-w-[600px] flex flex-col gap-4" + base: "h-full border-2 border-outline-dimmest rounded-xl p-8 grow min-w-[360px] w-[60vw] max-w-[600px] flex flex-col gap-4", }), Preview: styled("div", { - base: "grow bg-center bg-cover bg-contain bg-no-repeat rounded-xl p-2 rounded-lg border border-outline-dimmest" - }) + base: "grow bg-center bg-cover bg-contain bg-no-repeat rounded-xl p-2 rounded-lg border border-outline-dimmest", + }), } diff --git a/app/contribute/header.tsx b/app/contribute/header.tsx index 16db016..8252211 100644 --- a/app/contribute/header.tsx +++ b/app/contribute/header.tsx @@ -9,7 +9,7 @@ import { useCallback } from "react" import { styled } from "react-tailwind-variants" export default function Header({ - percentage + percentage, }: { percentage: MotionValue }) { @@ -23,33 +23,33 @@ export default function Header({ gr.radial( `circle at 50% ${lightY}%`, tokens.colors.red[500] + "35", - "transparent" + "transparent", ), gr.rLinear( -5 + rotateFactor, ...gr.stack( ["transparent", 200 + sizeFactor], - [tokens.colors.outline.dimmest, 202 + sizeFactor] - ) + [tokens.colors.outline.dimmest, 202 + sizeFactor], + ), ), gr.rLinear( -95 + rotateFactor, ...gr.stack( ["transparent", 200 + sizeFactor], - [tokens.colors.outline.dimmest, 202 + sizeFactor] - ) - ) + [tokens.colors.outline.dimmest, 202 + sizeFactor], + ), + ), ) }, []) const smoothPercentage = useSpring(percentage, { - mass: 0.05 + mass: 0.05, }) const background = useTransform( smoothPercentage, [0, 1], - [backgroundImage(0), backgroundImage(1)] + [backgroundImage(0), backgroundImage(1)], ) const translateY = useTransform(smoothPercentage, [0, 1], ["0%", "-100%"]) @@ -82,9 +82,9 @@ export default function Header({ const { Content, DownButton } = { Content: styled(Flex, { - base: "max-w-screen-md max-md:max-w-screen max-md:p-4 self-center" + base: "max-w-screen-md max-md:max-w-screen max-md:p-4 self-center", }), DownButton: styled(Button, { - base: "h-16 w-16 rounded-full" - }) + base: "h-16 w-16 rounded-full", + }), } diff --git a/app/contribute/intro.tsx b/app/contribute/intro.tsx index e59ab59..4f1f6e3 100644 --- a/app/contribute/intro.tsx +++ b/app/contribute/intro.tsx @@ -61,7 +61,7 @@ const FadeIn = ({ children }: { children: React.ReactNode }) => { gap={4} initial={{ opacity: 0 }} whileInView={{ - opacity: 1 + opacity: 1, }} viewport={{ margin: "0px 0px 200px 0px", amount: 0.5 }} > diff --git a/app/contribute/join/actions.ts b/app/contribute/join/actions.ts index c7997aa..96cf0af 100644 --- a/app/contribute/join/actions.ts +++ b/app/contribute/join/actions.ts @@ -7,7 +7,7 @@ import { formatDistanceToNow } from "date-fns" import { EmbedBuilder, WebhookClient } from "discord.js" const webhook = new WebhookClient({ - url: process.env.DISCORD_WEBHOOK_URL_ADMIN + url: process.env.DISCORD_WEBHOOK_URL_ADMIN, }) export async function joinAsContributor() { @@ -22,19 +22,19 @@ export async function joinAsContributor() { await prisma.user.update({ where: { id: user.id }, - data: { role: "pending" } + data: { role: "pending" }, }) const pendingUsers = await prisma.user.findMany({ where: { role: "pending" }, orderBy: { updatedAt: "asc" }, - take: 100 + take: 100, }) if (pendingUsers.length % 5 === 0) { const embed = new EmbedBuilder() .setTitle( - `[Reminder] ${pendingUsers.length === 100 ? "100+" : pendingUsers.length} pending users on waitlist` + `[Reminder] ${pendingUsers.length === 100 ? "100+" : pendingUsers.length} pending users on waitlist`, ) .setDescription("Longest-waiting users:") .setURL(new URL("/admin", siteUrl).toString()) @@ -42,13 +42,13 @@ export async function joinAsContributor() { const fields = pendingUsers.slice(0, 5).map(u => ({ name: u.handle, - value: `Waiting for ${formatDistanceToNow(u.updatedAt)}` + value: `Waiting for ${formatDistanceToNow(u.updatedAt)}`, })) embed.addFields(fields) await webhook.send({ - embeds: [embed] + embeds: [embed], }) } diff --git a/app/contribute/join/content.tsx b/app/contribute/join/content.tsx index 9275805..374fa70 100644 --- a/app/contribute/join/content.tsx +++ b/app/contribute/join/content.tsx @@ -35,14 +35,14 @@ export default function ContributorLoginPage() { tokens.colors.red[600] + "aa", tokens.colors.red[600] + "65 20%", "transparent 70%", - "transparent" + "transparent", ), gr.rRadial( "circle at 50% 50%", ...gr.stack( [colors.clear, `${25 + (1 - p) * 25}vw`], - [colors.outline.dimmest, `calc(${25 + (1 - p) * 25}vw + 2px)`] - ) + [colors.outline.dimmest, `calc(${25 + (1 - p) * 25}vw + 2px)`], + ), ), gr.linear( 90, @@ -51,8 +51,8 @@ export default function ContributorLoginPage() { [colors.outline.dimmest, `calc(50% - ${hw - 2}px)`], [colors.clear, `calc(50% + ${hw}px)`], [colors.outline.dimmest, `calc(50% + ${hw + 2}px)`], - [colors.clear, `calc(50% + ${hh + 2}px)`] - ) + [colors.clear, `calc(50% + ${hh + 2}px)`], + ), ), gr.linear( ...gr.stack( @@ -60,18 +60,18 @@ export default function ContributorLoginPage() { [colors.outline.dimmest, `calc(50% - ${hh - 2}px)`], [colors.clear, `calc(50% + ${hh}px)`], [colors.outline.dimmest, `calc(50% + ${hh + 2}px)`], - [colors.clear, `calc(50% + ${hh + 2}px)`] - ) + [colors.clear, `calc(50% + ${hh + 2}px)`], + ), ), - gr.linear(135, colors.root, "#292524") + gr.linear(135, colors.root, "#292524"), ) }, - [box] + [box], ) const initialBackground = useMotionValue(gradient(0)) const background = useSpring(initialBackground, { - damping: 25 + damping: 25, }) useEffect(() => { @@ -87,7 +87,7 @@ export default function ContributorLoginPage() { animate={{ opacity: 1 }} exit={{ opacity: 0 }} style={{ - background + background, }} > @@ -136,5 +136,5 @@ export default function ContributorLoginPage() { } const Content = styled(MotionDiv, { - base: "border-2 border-outline-dimmer bg-gradient-to-b from-higher to-root rounded-xl p-6 flex flex-col gap-3 shadow-lg shadow-black/50 max-w-sm" + base: "border-2 border-outline-dimmer bg-gradient-to-b from-higher to-root rounded-xl p-6 flex flex-col gap-3 shadow-lg shadow-black/50 max-w-sm", }) diff --git a/app/contribute/waitlist/content.tsx b/app/contribute/waitlist/content.tsx index ff59270..6ba9912 100644 --- a/app/contribute/waitlist/content.tsx +++ b/app/contribute/waitlist/content.tsx @@ -23,30 +23,30 @@ export default function WaitlistPage({ position }: { position: number }) { tokens.colors.red[600] + "88", tokens.colors.red[600] + "44 30%", "transparent 70%", - "transparent" + "transparent", ), gr.radial( `circle at ${p * 100 - 50}% ${100 - p * 50}%`, tokens.colors.red[600] + "44", tokens.colors.red[600] + "22 50%", "transparent 70%", - "transparent" + "transparent", ), gr.rRadial( "circle at 0% 100%", ...gr.stack( ["transparent", `calc(${60 - 40 * p}% - 2px)`], - [colors.outline.dimmest, `calc(${60 - 40 * p}%)`] - ) + [colors.outline.dimmest, `calc(${60 - 40 * p}%)`], + ), ), gr.radial( `circle at 50% ${50 * p}%`, ...gr.stack( [colors.clear, "calc(60% - 2px)"], [colors.outline.dimmest, "calc(60%)"], - [colors.clear, "100%"] - ) - ) + [colors.clear, "100%"], + ), + ), ) }, []) @@ -67,7 +67,7 @@ export default function WaitlistPage({ position }: { position: number }) { animate={{ opacity: 1 }} exit={{ opacity: 0 }} style={{ - background + background, }} > @@ -89,5 +89,5 @@ export default function WaitlistPage({ position }: { position: number }) { } const Content = styled(MotionDiv, { - base: "border-2 border-outline-dimmer bg-gradient-to-b from-higher to-root rounded-xl p-6 flex flex-col gap-3 shadow-lg shadow-black/50 max-w-sm" + base: "border-2 border-outline-dimmer bg-gradient-to-b from-higher to-root rounded-xl p-6 flex flex-col gap-3 shadow-lg shadow-black/50 max-w-sm", }) diff --git a/app/contribute/waitlist/page.tsx b/app/contribute/waitlist/page.tsx index 6d5632c..4b2340b 100644 --- a/app/contribute/waitlist/page.tsx +++ b/app/contribute/waitlist/page.tsx @@ -22,8 +22,8 @@ export default async function Login() { where: { role: "pending", id: { lte: user.id }, - createdAt: { lt: new Date() } - } + createdAt: { lt: new Date() }, + }, }) return diff --git a/app/home/selector.tsx b/app/home/selector.tsx index 9a89865..bcb9a30 100644 --- a/app/home/selector.tsx +++ b/app/home/selector.tsx @@ -8,7 +8,7 @@ import { ExternalLinkIcon, HexagonIcon, PlusIcon, - XIcon + XIcon, } from "lucide-react" import { useRouter } from "next/navigation" import { useEffect, useRef, useState } from "react" @@ -110,7 +110,7 @@ export default function Selector() { disabled={llms.length < 2} onClick={() => { router.push( - `/compare?llms=${llms.map(x => x.id).join(",")}&mode=edit` + `/compare?llms=${llms.map(x => x.id).join(",")}&mode=edit`, ) }} > @@ -166,18 +166,18 @@ export default function Selector() { const { LLMItem, IconContainer, SpecsContainer, CloseButton, PlaceholderLLM } = { LLMItem: styled("div", { - base: "flex flex-col rounded-xl border-2 border-outline-dimmer bg-default grow basis-0 max-w-[320px] min-w-[160px] shadow-md cursor-pointer hover:border-accent-dimmest transition-colors" + base: "flex flex-col rounded-xl border-2 border-outline-dimmer bg-default grow basis-0 max-w-[320px] min-w-[160px] shadow-md cursor-pointer hover:border-accent-dimmest transition-colors", }), IconContainer: styled("div", { - base: "flex items-center justify-center py-4 bg-root rounded-t-xl relative" + base: "flex items-center justify-center py-4 bg-root rounded-t-xl relative", }), SpecsContainer: styled("div", { - base: "flex flex-col gap-1 items-center w-full rounded-b-lg px-4 py-2" + base: "flex flex-col gap-1 items-center w-full rounded-b-lg px-4 py-2", }), CloseButton: styled("button", { - base: "absolute top-2 right-2 w-4 h-4" + base: "absolute top-2 right-2 w-4 h-4", }), PlaceholderLLM: styled("label", { - base: "flex flex-col gap-2 rounded-xl max-w-[320px] min-w-[160px] items-center align-self-stretch justify-center min-h-[124px] grow basis-0 border-2 border-dashed border-outline-dimmest text-outline-dimmer hover:text-foreground-dimmest hover:border-outline-dimmer transition-colors cursor-pointer" - }) + base: "flex flex-col gap-2 rounded-xl max-w-[320px] min-w-[160px] items-center align-self-stretch justify-center min-h-[124px] grow basis-0 border-2 border-dashed border-outline-dimmest text-outline-dimmer hover:text-foreground-dimmest hover:border-outline-dimmer transition-colors cursor-pointer", + }), } diff --git a/app/layout.tsx b/app/layout.tsx index 545bc8d..543b759 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,7 +12,7 @@ const inter = Inter({ subsets: ["latin"] }) export { metadata } from "./metadata" export default async function RootLayout({ - children + children, }: { children: React.ReactNode }) { diff --git a/app/llms/actions.ts b/app/llms/actions.ts index c58282f..63766ae 100644 --- a/app/llms/actions.ts +++ b/app/llms/actions.ts @@ -11,39 +11,37 @@ import { EmbedBuilder, WebhookClient } from "discord.js" import { z } from "zod" const webhook = new WebhookClient({ - url: process.env.DISCORD_WEBHOOK_URL_PUBLIC + url: process.env.DISCORD_WEBHOOK_URL_PUBLIC, }) const castVoteInput = z .object({ - llmId: z.string(), + llmId: z.number(), comment: z.string().nullish(), - action: z.nativeEnum(VoteStatus) + action: z.nativeEnum(VoteStatus), }) .superRefine((data, ctx) => { if (data.action === VoteStatus.reject && !data.comment) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "required when rejecting an LLM", - path: ["comment"] - }) - } - - if (isNaN(Number(data.llmId))) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Invalid ID", - path: ["llmId"] + path: ["comment"], }) } }) -type CastVoteReturn = { - success: boolean - message?: string -} | null +type CastVoteReturn = + | { + success: true + } + | { + success: false + message: string + } -export async function castVoteAction(_prevState: CastVoteReturn, e: FormData) { +export async function castVote( + input: z.infer, +): Promise { const res = await getSession() try { @@ -52,19 +50,15 @@ export async function castVoteAction(_prevState: CastVoteReturn, e: FormData) { const { user } = res requireContributorOrAdmin(user) - const { llmId, comment, action } = castVoteInput.parse( - Object.fromEntries(e.entries()) - ) - - const id = Number(llmId) + const { llmId, comment, action } = castVoteInput.parse(input) const llm = await prisma.lLM.findFirst({ where: { - id + id: llmId, }, include: { - votes: true - } + votes: true, + }, }) if (!llm) throw new Error("LLM not found") @@ -74,9 +68,9 @@ export async function castVoteAction(_prevState: CastVoteReturn, e: FormData) { const existingVote = await prisma.vote.findFirst({ where: { - llmId: id, - userId: user.id - } + llmId, + userId: user.id, + }, }) if (existingVote) throw new Error("You have already voted on this LLM") @@ -84,10 +78,10 @@ export async function castVoteAction(_prevState: CastVoteReturn, e: FormData) { const newVote = await prisma.vote.create({ data: { comment, - llmId: id, + llmId, userId: user.id, - status: action - } + status: action, + }, }) if (!newVote) throw new Error("Failed to cast vote") @@ -96,11 +90,11 @@ export async function castVoteAction(_prevState: CastVoteReturn, e: FormData) { await prisma.lLM.update({ where: { - id + id: llmId, }, data: { - status: consensus.status - } + status: consensus.status, + }, }) if (consensus.status !== LLMStatus.pending) { @@ -110,24 +104,24 @@ export async function castVoteAction(_prevState: CastVoteReturn, e: FormData) { .setTitle(`[LLM ${consensus.status}] ${llm.name}`) .setURL(new URL("/llms?llm=" + llm.id, siteUrl).toString()) .setDescription( - `${consensus.status} after ${allVotes.length} votes. ${allVotes.filter(v => v.status === VoteStatus.approve).length} approvals, ${allVotes.filter(v => v.status === VoteStatus.reject).length} rejections.` + `${consensus.status} after ${allVotes.filter(v => v.status === VoteStatus.approve).length} approvals, ${allVotes.filter(v => v.status === VoteStatus.reject).length} rejections.`, ) .setColor(consensus.status === "approved" ? 0x34d399 : 0xef4444) await webhook.send({ - embeds: [embed] + embeds: [embed], }) } return { - success: true + success: true, } } catch (e) { console.error(e) return { success: false, - message: formatError(e) + message: formatError(e), } } } diff --git a/app/llms/components/FieldTable.tsx b/app/llms/components/FieldTable.tsx index aadf3f3..7c269f8 100644 --- a/app/llms/components/FieldTable.tsx +++ b/app/llms/components/FieldTable.tsx @@ -5,7 +5,7 @@ import { styled } from "react-tailwind-variants" export default function FieldTable({ fields, - capLength + capLength, }: { fields: Array capLength?: number @@ -57,15 +57,15 @@ export default function FieldTable({ const Table = { Container: styled("div", { - base: "table border border-outline-dimmer w-full max-w-[640px]" + base: "table border border-outline-dimmer w-full max-w-[640px]", }), Row: styled("div", { - base: "table-row" + base: "table-row", }), Cell: styled("div", { - base: "table-cell min-h-10 first:max-w-[200px] last:w-full border border-outline-dimmer align-middle" + base: "table-cell min-h-10 first:max-w-[200px] last:w-full border border-outline-dimmer align-middle", }), CellContent: styled("div", { - base: "flex flex-col h-full min-h-10 px-2 justify-center" - }) + base: "flex flex-col h-full min-h-10 px-2 justify-center", + }), } diff --git a/app/llms/components/LLMItem.tsx b/app/llms/components/LLMItem.tsx index f3c4bd0..b739a41 100644 --- a/app/llms/components/LLMItem.tsx +++ b/app/llms/components/LLMItem.tsx @@ -10,7 +10,7 @@ import FieldTable from "./FieldTable" export default function LLMItem({ query, - llm + llm, }: { query: string llm: LLMWithRelations @@ -30,7 +30,7 @@ export default function LLMItem({ (acc, node, i) => { acc.push({ text: node, - highlighted: false + highlighted: false, }) if (i < splitText.length - 1 && matches) { @@ -39,7 +39,7 @@ export default function LLMItem({ return acc }, - [] as Array<{ text: string; highlighted: boolean }> + [] as Array<{ text: string; highlighted: boolean }>, ) } @@ -58,7 +58,7 @@ export default function LLMItem({ ) : ( {text} - ) + ), )} @@ -69,7 +69,7 @@ export default function LLMItem({ .sort( (a, b) => Number(b.status === VoteStatus.approve) - - Number(a.status === VoteStatus.approve) + Number(a.status === VoteStatus.approve), ) .map((vote, i) => ( @@ -106,7 +106,7 @@ export default function LLMItem({ ) : ( text - ) + ), )} @@ -122,16 +122,16 @@ const { ContentOverlay, StatusBadge, StatusBar, - StatusVote + StatusVote, } = { Container: styled("div", { - base: "flex flex-col gap-2 p-2 rounded-lg border-2 border-outline-dimmest bg-default hover:border-accent-dimmer cursor-pointer transition-colors" + base: "flex flex-col gap-2 p-2 rounded-lg border-2 border-outline-dimmest bg-default hover:border-accent-dimmer cursor-pointer transition-colors", }), Content: styled("div", { - base: "flex gap-2 items-start w-full overflow-hidden relative max-h-[200px]" + base: "flex gap-2 items-start w-full overflow-hidden relative max-h-[200px]", }), ContentOverlay: styled("div", { - base: `absolute inset-0 bg-gradient-to-t from-default via-transparent via-transparent to-transparent flex flex-col justify-end max-h-[200px]` + base: `absolute inset-0 bg-gradient-to-t from-default via-transparent via-transparent to-transparent flex flex-col justify-end max-h-[200px]`, }), StatusBadge: styled("div", { base: "rounded-md px-1.5 py-0.5 text-xs", @@ -139,20 +139,20 @@ const { status: { pending: "bg-amber-500/25 text-amber-300/75", approved: "bg-emerald-500/25 text-emerald-300/75", - rejected: "bg-rose-500/25 text-rose-300/75" - } - } + rejected: "bg-rose-500/25 text-rose-300/75", + }, + }, }), StatusBar: styled("div", { - base: "rounded-full h-1.5 w-[120px] bg-higher flex" + base: "rounded-full h-1.5 w-[120px] bg-higher flex", }), StatusVote: styled("div", { base: "grow basis-0 h-full first:rounded-l-full last:rounded-r-full shadow-[2px_0_0_2px,-2px_0_0_2px]", variants: { status: { approve: "bg-emerald-600 shadow-emerald-600/10", - reject: "bg-rose-600 shadow-rose-600/10" - } - } - }) + reject: "bg-rose-600 shadow-rose-600/10", + }, + }, + }), } diff --git a/app/llms/components/LLMOverlay/Fields.tsx b/app/llms/components/LLMOverlay/Fields.tsx new file mode 100644 index 0000000..bb8314d --- /dev/null +++ b/app/llms/components/LLMOverlay/Fields.tsx @@ -0,0 +1,142 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import Text from "@/components/ui/text" +import { abbrNumber } from "@/lib/numbers" +import { + ChangeRequestType, + Field, + MetaProperty, + MetaPropertyType, +} from "@prisma/client" +import { GitPullRequestIcon, MoreVerticalIcon, TrashIcon } from "lucide-react" +import { useState } from "react" +import { styled } from "react-tailwind-variants" +import { OverlayLLM } from "." +import RequestAddDialog from "./dialogs/RequestAdd" +import RequestChangeDialog from "./dialogs/RequestChange" +import RequestDeletionDialog from "./dialogs/RequestDeletion" + +export type FieldWithMeta = Field & { metaProperty: MetaProperty } + +export default function Fields({ + llm: { fields, ...llm }, + refetch, +}: { + llm: OverlayLLM + refetch: () => void +}) { + const [fieldToChange, setFieldToChange] = useState(null) + const [fieldToDelete, setFieldToDelete] = useState(null) + + return ( + + + {fields.map((field, i) => ( + + + + + {field.metaProperty.name} + + {field.note && ( + + {field.note} + + )} + + + + + + {field.metaProperty.type} + + + + + + + {field.metaProperty.type === MetaPropertyType.Number + ? abbrNumber(Number(field.value)) + : field.value} + + + + + + + + + setFieldToChange(field)} + > + + Request Changes + + setFieldToDelete(field)} + > + + Request Deletion + + + + + + + ))} + + + + + + ) +} + +const { Wrapper, OptionsButton, ...Table } = { + Wrapper: styled("div", { + base: "flex flex-col", + }), + Container: styled("div", { + base: "table border w-full max-w-[640px] border-outline-dimmer", + }), + Row: styled("div", { + base: "table-row group", + }), + Cell: styled("div", { + base: "table-cell min-h-10 first:max-w-[200px] last:w-full border align-middle", + variants: { + variant: { + [ChangeRequestType.delete]: "border-accent/30 bg-accent-dimmer/15", + [ChangeRequestType.add]: "bg-emerald-700/15 border-emerald-500/30", + default: "border-outline-dimmer", + }, + }, + defaultVariants: { + variant: "default", + }, + }), + CellContent: styled("div", { + base: "flex flex-col h-full min-h-10 px-2 justify-center", + }), + OptionsButton: styled("button", { + base: "absolute right-0 top-1/2 -translate-y-1/2 flex items-center justify-center px-1 opacity-0 group/item group-hover:opacity-100 transition-opacity", + }), +} + +export { Table } diff --git a/app/llms/components/LLMOverlay/Header.tsx b/app/llms/components/LLMOverlay/Header.tsx index 4518818..ba9c600 100644 --- a/app/llms/components/LLMOverlay/Header.tsx +++ b/app/llms/components/LLMOverlay/Header.tsx @@ -5,7 +5,7 @@ import { XIcon } from "lucide-react" export default function Header({ children = null, - className + className, }: { children?: React.ReactNode className?: string diff --git a/app/llms/components/LLMOverlay/VoteSection.tsx b/app/llms/components/LLMOverlay/VoteSection.tsx deleted file mode 100644 index a204cd2..0000000 --- a/app/llms/components/LLMOverlay/VoteSection.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { LLMWithRelations } from "@/app/api/search/types" -import { useCurrentUser } from "@/components/providers/CurrentUserProvider" -import { Button } from "@/components/ui/button" -import Flex from "@/components/ui/flex" -import Text from "@/components/ui/text" -import { Textarea } from "@/components/ui/textarea" -import { User, Vote, VoteStatus } from "@prisma/client" -import { InfoIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react" -import { Dispatch, SetStateAction, useEffect, useState } from "react" -import { useFormState } from "react-dom" -import { styled } from "react-tailwind-variants" -import { castVoteAction } from "../../actions" - -export default function VoteSection({ - llm, - setTab, - refetch -}: { - llm: LLMWithRelations - setTab: Dispatch> - refetch: () => void -}) { - const [state, castVote] = useFormState(castVoteAction, null) - const [comment, setComment] = useState("") - const [action, setAction] = useState(VoteStatus.approve) - - const user = useCurrentUser() - - const hasVoted = llm.votes.some(vote => vote.userId === user?.id) - const isOwner = llm.user.id === user?.id - const isConsensusAchieved = llm.status !== "pending" - - const isDisabled = hasVoted || isOwner - - useEffect(() => { - if (state?.success) { - setTab("votes") - refetch() - } - }, [state, setTab, refetch]) - - return ( - - - - -
- - -