nsarrazin HF Staff commited on
Commit
69c6804
·
unverified ·
1 Parent(s): 287184e

feat: Update featured workflow to allow user to submit featured requests (#1510)

Browse files

* feat: Update featured workflow to allow user to submit featured requests

* fix: merge conflict

* fix: styling of buttons

scripts/populate.ts CHANGED
@@ -20,6 +20,7 @@ import { Message } from "../src/lib/types/Message.ts";
20
 
21
  import { addChildren } from "../src/lib/utils/tree/addChildren.ts";
22
  import { generateSearchTokens } from "../src/lib/utils/searchTokens.ts";
 
23
 
24
  const rl = readline.createInterface({
25
  input: process.stdin,
@@ -151,6 +152,7 @@ async function seed() {
151
  disableStream: faker.datatype.boolean(0.25),
152
  customPrompts: {},
153
  assistants: [],
 
154
  };
155
  await collections.settings.updateOne(
156
  { userId: user._id },
@@ -175,7 +177,7 @@ async function seed() {
175
  createdAt: faker.date.recent({ days: 30 }),
176
  updatedAt: faker.date.recent({ days: 30 }),
177
  userCount: faker.number.int({ min: 1, max: 100000 }),
178
- featured: faker.datatype.boolean(0.25),
179
  modelId: faker.helpers.arrayElement(modelIds),
180
  description: faker.lorem.sentence(),
181
  preprompt: faker.hacker.phrase(),
@@ -307,7 +309,7 @@ async function seed() {
307
  createdAt: faker.date.recent({ days: 30 }),
308
  updatedAt: faker.date.recent({ days: 30 }),
309
  searchTokens: generateSearchTokens(displayName),
310
- featured: faker.datatype.boolean(),
311
  outputComponent: null,
312
  outputComponentIdx: null,
313
  };
 
20
 
21
  import { addChildren } from "../src/lib/utils/tree/addChildren.ts";
22
  import { generateSearchTokens } from "../src/lib/utils/searchTokens.ts";
23
+ import { ReviewStatus } from "../src/lib/types/Review.ts";
24
 
25
  const rl = readline.createInterface({
26
  input: process.stdin,
 
152
  disableStream: faker.datatype.boolean(0.25),
153
  customPrompts: {},
154
  assistants: [],
155
+ disableStream: faker.datatype.boolean(0.25),
156
  };
157
  await collections.settings.updateOne(
158
  { userId: user._id },
 
177
  createdAt: faker.date.recent({ days: 30 }),
178
  updatedAt: faker.date.recent({ days: 30 }),
179
  userCount: faker.number.int({ min: 1, max: 100000 }),
180
+ review: faker.helpers.enumValue(ReviewStatus),
181
  modelId: faker.helpers.arrayElement(modelIds),
182
  description: faker.lorem.sentence(),
183
  preprompt: faker.hacker.phrase(),
 
309
  createdAt: faker.date.recent({ days: 30 }),
310
  updatedAt: faker.date.recent({ days: 30 }),
311
  searchTokens: generateSearchTokens(displayName),
312
+ review: faker.helpers.enumValue(ReviewStatus),
313
  outputComponent: null,
314
  outputComponentIdx: null,
315
  };
src/lib/migrations/routines/06-trim-message-updates.ts CHANGED
@@ -41,7 +41,7 @@ function convertMessageUpdate(message: Message, update: MessageUpdate): MessageU
41
  }
42
 
43
  const trimMessageUpdates: Migration = {
44
- _id: new ObjectId("000000000006"),
45
  name: "Trim message updates to reduce stored size",
46
  up: async () => {
47
  const allConversations = collections.conversations.find({});
 
41
  }
42
 
43
  const trimMessageUpdates: Migration = {
44
+ _id: new ObjectId("000000000000000000000006"),
45
  name: "Trim message updates to reduce stored size",
46
  up: async () => {
47
  const allConversations = collections.conversations.find({});
src/lib/migrations/routines/07-reset-tools-in-settings.ts CHANGED
@@ -3,7 +3,7 @@ import { collections } from "$lib/server/database";
3
  import { ObjectId } from "mongodb";
4
 
5
  const resetTools: Migration = {
6
- _id: new ObjectId("000000000007"),
7
  name: "Reset tools to empty",
8
  up: async () => {
9
  const { settings } = collections;
 
3
  import { ObjectId } from "mongodb";
4
 
5
  const resetTools: Migration = {
6
+ _id: new ObjectId("000000000000000000000007"),
7
  name: "Reset tools to empty",
8
  up: async () => {
9
  const { settings } = collections;
src/lib/migrations/routines/08-update-featured-to-review.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Migration } from ".";
2
+ import { collections } from "$lib/server/database";
3
+ import { ObjectId } from "mongodb";
4
+ import { ReviewStatus } from "$lib/types/Review";
5
+
6
+ const updateFeaturedToReview: Migration = {
7
+ _id: new ObjectId("000000000000000000000008"),
8
+ name: "Update featured to review",
9
+ up: async () => {
10
+ const { assistants, tools } = collections;
11
+
12
+ // Update assistants
13
+ await assistants.updateMany({ featured: true }, { $set: { review: ReviewStatus.APPROVED } });
14
+ await assistants.updateMany(
15
+ { featured: { $ne: true } },
16
+ { $set: { review: ReviewStatus.PRIVATE } }
17
+ );
18
+
19
+ await assistants.updateMany({}, { $unset: { featured: "" } });
20
+
21
+ // Update tools
22
+ await tools.updateMany({ featured: true }, { $set: { review: ReviewStatus.APPROVED } });
23
+ await tools.updateMany({ featured: { $ne: true } }, { $set: { review: ReviewStatus.PRIVATE } });
24
+
25
+ await tools.updateMany({}, { $unset: { featured: "" } });
26
+
27
+ return true;
28
+ },
29
+ runEveryTime: false,
30
+ };
31
+
32
+ export default updateFeaturedToReview;
src/lib/migrations/routines/index.ts CHANGED
@@ -8,6 +8,7 @@ import updateMessageUpdates from "./04-update-message-updates";
8
  import updateMessageFiles from "./05-update-message-files";
9
  import trimMessageUpdates from "./06-trim-message-updates";
10
  import resetTools from "./07-reset-tools-in-settings";
 
11
 
12
  export interface Migration {
13
  _id: ObjectId;
@@ -27,4 +28,5 @@ export const migrations: Migration[] = [
27
  updateMessageFiles,
28
  trimMessageUpdates,
29
  resetTools,
 
30
  ];
 
8
  import updateMessageFiles from "./05-update-message-files";
9
  import trimMessageUpdates from "./06-trim-message-updates";
10
  import resetTools from "./07-reset-tools-in-settings";
11
+ import updateFeaturedToReview from "./08-update-featured-to-review";
12
 
13
  export interface Migration {
14
  _id: ObjectId;
 
28
  updateMessageFiles,
29
  trimMessageUpdates,
30
  resetTools,
31
+ updateFeaturedToReview,
32
  ];
src/lib/server/database.ts CHANGED
@@ -202,7 +202,7 @@ export class Database {
202
  sessions.createIndex({ sessionId: 1 }, { unique: true }).catch((e) => logger.error(e));
203
  assistants.createIndex({ createdById: 1, userCount: -1 }).catch((e) => logger.error(e));
204
  assistants.createIndex({ userCount: 1 }).catch((e) => logger.error(e));
205
- assistants.createIndex({ featured: 1, userCount: -1 }).catch((e) => logger.error(e));
206
  assistants.createIndex({ modelId: 1, userCount: -1 }).catch((e) => logger.error(e));
207
  assistants.createIndex({ searchTokens: 1 }).catch((e) => logger.error(e));
208
  assistants.createIndex({ last24HoursCount: 1 }).catch((e) => logger.error(e));
 
202
  sessions.createIndex({ sessionId: 1 }, { unique: true }).catch((e) => logger.error(e));
203
  assistants.createIndex({ createdById: 1, userCount: -1 }).catch((e) => logger.error(e));
204
  assistants.createIndex({ userCount: 1 }).catch((e) => logger.error(e));
205
+ assistants.createIndex({ review: 1, userCount: -1 }).catch((e) => logger.error(e));
206
  assistants.createIndex({ modelId: 1, userCount: -1 }).catch((e) => logger.error(e));
207
  assistants.createIndex({ searchTokens: 1 }).catch((e) => logger.error(e));
208
  assistants.createIndex({ last24HoursCount: 1 }).catch((e) => logger.error(e));
src/lib/server/sendSlack.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { env } from "$env/dynamic/private";
2
+ import { logger } from "$lib/server/logger";
3
+
4
+ export async function sendSlack(text: string) {
5
+ if (!env.WEBHOOK_URL_REPORT_ASSISTANT) {
6
+ logger.warn("WEBHOOK_URL_REPORT_ASSISTANT is not set, tried to send a slack message.");
7
+ return;
8
+ }
9
+
10
+ const res = await fetch(env.WEBHOOK_URL_REPORT_ASSISTANT, {
11
+ method: "POST",
12
+ headers: {
13
+ "Content-type": "application/json",
14
+ },
15
+ body: JSON.stringify({
16
+ text,
17
+ }),
18
+ });
19
+
20
+ if (!res.ok) {
21
+ logger.error(`Webhook message failed. ${res.statusText} ${res.text}`);
22
+ }
23
+ }
src/lib/types/Assistant.ts CHANGED
@@ -1,6 +1,7 @@
1
  import type { ObjectId } from "mongodb";
2
  import type { User } from "./User";
3
  import type { Timestamps } from "./Timestamps";
 
4
 
5
  export interface Assistant extends Timestamps {
6
  _id: ObjectId;
@@ -13,7 +14,7 @@ export interface Assistant extends Timestamps {
13
  exampleInputs: string[];
14
  preprompt: string;
15
  userCount?: number;
16
- featured?: boolean;
17
  rag?: {
18
  allowAllDomains: boolean;
19
  allowedDomains: string[];
 
1
  import type { ObjectId } from "mongodb";
2
  import type { User } from "./User";
3
  import type { Timestamps } from "./Timestamps";
4
+ import type { ReviewStatus } from "./Review";
5
 
6
  export interface Assistant extends Timestamps {
7
  _id: ObjectId;
 
14
  exampleInputs: string[];
15
  preprompt: string;
16
  userCount?: number;
17
+ review: ReviewStatus;
18
  rag?: {
19
  allowAllDomains: boolean;
20
  allowedDomains: string[];
src/lib/types/Review.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export enum ReviewStatus {
2
+ PRIVATE = "PRIVATE",
3
+ PENDING = "PENDING",
4
+ APPROVED = "APPROVED",
5
+ DENIED = "DENIED",
6
+ }
src/lib/types/Tool.ts CHANGED
@@ -4,6 +4,7 @@ import type { Timestamps } from "./Timestamps";
4
  import type { BackendToolContext } from "$lib/server/tools";
5
  import type { MessageUpdate } from "./MessageUpdate";
6
  import { z } from "zod";
 
7
 
8
  export const ToolColor = z.union([
9
  z.literal("purple"),
@@ -122,7 +123,7 @@ export interface CommunityTool extends BaseTool, Timestamps {
122
  useCount: number;
123
  last24HoursUseCount: number;
124
 
125
- featured: boolean;
126
  searchTokens: string[];
127
  }
128
 
@@ -136,7 +137,7 @@ export type CommunityToolEditable = Omit<
136
  | "last24HoursUseCount"
137
  | "createdById"
138
  | "createdByName"
139
- | "featured"
140
  | "searchTokens"
141
  | "type"
142
  | "createdAt"
 
4
  import type { BackendToolContext } from "$lib/server/tools";
5
  import type { MessageUpdate } from "./MessageUpdate";
6
  import { z } from "zod";
7
+ import type { ReviewStatus } from "./Review";
8
 
9
  export const ToolColor = z.union([
10
  z.literal("purple"),
 
123
  useCount: number;
124
  last24HoursUseCount: number;
125
 
126
+ review: ReviewStatus;
127
  searchTokens: string[];
128
  }
129
 
 
137
  | "last24HoursUseCount"
138
  | "createdById"
139
  | "createdByName"
140
+ | "review"
141
  | "searchTokens"
142
  | "type"
143
  | "createdAt"
src/routes/+layout.server.ts CHANGED
@@ -11,6 +11,7 @@ import type { ConvSidebar } from "$lib/types/ConvSidebar";
11
  import { toolFromConfigs } from "$lib/server/tools";
12
  import { MetricsServer } from "$lib/server/metrics";
13
  import type { ToolFront, ToolInputFile } from "$lib/types/Tool";
 
14
 
15
  export const load: LayoutServerLoad = async ({ locals, depends, request }) => {
16
  depends(UrlDependency.ConversationList);
@@ -221,7 +222,7 @@ export const load: LayoutServerLoad = async ({ locals, depends, request }) => {
221
  ),
222
  communityToolCount: await collections.tools.countDocuments({
223
  type: "community",
224
- featured: true,
225
  }),
226
  assistants: assistants
227
  .filter((el) => userAssistantsSet.has(el._id.toString()))
 
11
  import { toolFromConfigs } from "$lib/server/tools";
12
  import { MetricsServer } from "$lib/server/metrics";
13
  import type { ToolFront, ToolInputFile } from "$lib/types/Tool";
14
+ import { ReviewStatus } from "$lib/types/Review";
15
 
16
  export const load: LayoutServerLoad = async ({ locals, depends, request }) => {
17
  depends(UrlDependency.ConversationList);
 
222
  ),
223
  communityToolCount: await collections.tools.countDocuments({
224
  type: "community",
225
+ review: ReviewStatus.APPROVED,
226
  }),
227
  assistants: assistants
228
  .filter((el) => userAssistantsSet.has(el._id.toString()))
src/routes/api/assistants/+server.ts CHANGED
@@ -4,6 +4,7 @@ import type { User } from "$lib/types/User";
4
  import { generateQueryTokens } from "$lib/utils/searchTokens.js";
5
  import type { Filter } from "mongodb";
6
  import { env } from "$env/dynamic/private";
 
7
 
8
  const NUM_PER_PAGE = 24;
9
 
@@ -27,7 +28,7 @@ export async function GET({ url, locals }) {
27
 
28
  // if there is no user, we show community assistants, so only show featured assistants
29
  const shouldBeFeatured =
30
- env.REQUIRE_FEATURED_ASSISTANTS === "true" && !user ? { featured: true } : {};
31
 
32
  // if the user queried is not the current user, only show "public" assistants that have been shared before
33
  const shouldHaveBeenShared =
 
4
  import { generateQueryTokens } from "$lib/utils/searchTokens.js";
5
  import type { Filter } from "mongodb";
6
  import { env } from "$env/dynamic/private";
7
+ import { ReviewStatus } from "$lib/types/Review";
8
 
9
  const NUM_PER_PAGE = 24;
10
 
 
28
 
29
  // if there is no user, we show community assistants, so only show featured assistants
30
  const shouldBeFeatured =
31
+ env.REQUIRE_FEATURED_ASSISTANTS === "true" && !user ? { review: ReviewStatus.APPROVED } : {};
32
 
33
  // if the user queried is not the current user, only show "public" assistants that have been shared before
34
  const shouldHaveBeenShared =
src/routes/api/tools/[toolId]/+server.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { env } from "$env/dynamic/private";
2
  import { collections } from "$lib/server/database.js";
3
  import { toolFromConfigs } from "$lib/server/tools/index.js";
 
4
  import type { CommunityToolDB } from "$lib/types/Tool.js";
5
  import { ObjectId } from "mongodb";
6
 
@@ -33,12 +34,12 @@ export async function GET({ params }) {
33
  color: tool.color,
34
  icon: tool.icon,
35
  createdByName: tool.createdByName,
36
- featured: tool.featured,
37
  }
38
  : undefined
39
  );
40
 
41
- if (!tool || !tool.featured) {
42
  return new Response(`Tool "${toolId}" not found`, { status: 404 });
43
  }
44
 
 
1
  import { env } from "$env/dynamic/private";
2
  import { collections } from "$lib/server/database.js";
3
  import { toolFromConfigs } from "$lib/server/tools/index.js";
4
+ import { ReviewStatus } from "$lib/types/Review";
5
  import type { CommunityToolDB } from "$lib/types/Tool.js";
6
  import { ObjectId } from "mongodb";
7
 
 
34
  color: tool.color,
35
  icon: tool.icon,
36
  createdByName: tool.createdByName,
37
+ review: tool.review,
38
  }
39
  : undefined
40
  );
41
 
42
+ if (!tool || tool.review !== ReviewStatus.APPROVED) {
43
  return new Response(`Tool "${toolId}" not found`, { status: 404 });
44
  }
45
 
src/routes/api/tools/search/+server.ts CHANGED
@@ -4,7 +4,7 @@ import { toolFromConfigs } from "$lib/server/tools/index.js";
4
  import type { BaseTool, CommunityToolDB } from "$lib/types/Tool.js";
5
  import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens.js";
6
  import type { Filter } from "mongodb";
7
-
8
  export async function GET({ url }) {
9
  if (env.COMMUNITY_TOOLS !== "true") {
10
  return new Response("Community tools are not enabled", { status: 403 });
@@ -15,7 +15,7 @@ export async function GET({ url }) {
15
 
16
  const filter: Filter<CommunityToolDB> = {
17
  ...(queryTokens && { searchTokens: { $all: queryTokens } }),
18
- featured: true,
19
  };
20
 
21
  const matchingCommunityTools = await collections.tools
 
4
  import type { BaseTool, CommunityToolDB } from "$lib/types/Tool.js";
5
  import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens.js";
6
  import type { Filter } from "mongodb";
7
+ import { ReviewStatus } from "$lib/types/Review";
8
  export async function GET({ url }) {
9
  if (env.COMMUNITY_TOOLS !== "true") {
10
  return new Response("Community tools are not enabled", { status: 403 });
 
15
 
16
  const filter: Filter<CommunityToolDB> = {
17
  ...(queryTokens && { searchTokens: { $all: queryTokens } }),
18
+ review: ReviewStatus.APPROVED,
19
  };
20
 
21
  const matchingCommunityTools = await collections.tools
src/routes/assistants/+page.server.ts CHANGED
@@ -6,7 +6,7 @@ import type { User } from "$lib/types/User";
6
  import { generateQueryTokens } from "$lib/utils/searchTokens.js";
7
  import { error, redirect } from "@sveltejs/kit";
8
  import type { Filter } from "mongodb";
9
-
10
  const NUM_PER_PAGE = 24;
11
 
12
  export const load = async ({ url, locals }) => {
@@ -36,7 +36,7 @@ export const load = async ({ url, locals }) => {
36
  // if there is no user, we show community assistants, so only show featured assistants
37
  const shouldBeFeatured =
38
  env.REQUIRE_FEATURED_ASSISTANTS === "true" && !user && !(locals.user?.isAdmin && showUnfeatured)
39
- ? { featured: true }
40
  : {};
41
 
42
  // if the user queried is not the current user, only show "public" assistants that have been shared before
 
6
  import { generateQueryTokens } from "$lib/utils/searchTokens.js";
7
  import { error, redirect } from "@sveltejs/kit";
8
  import type { Filter } from "mongodb";
9
+ import { ReviewStatus } from "$lib/types/Review";
10
  const NUM_PER_PAGE = 24;
11
 
12
  export const load = async ({ url, locals }) => {
 
36
  // if there is no user, we show community assistants, so only show featured assistants
37
  const shouldBeFeatured =
38
  env.REQUIRE_FEATURED_ASSISTANTS === "true" && !user && !(locals.user?.isAdmin && showUnfeatured)
39
+ ? { review: ReviewStatus.APPROVED }
40
  : {};
41
 
42
  // if the user queried is not the current user, only show "public" assistants that have been shared before
src/routes/assistants/+page.svelte CHANGED
@@ -25,6 +25,7 @@
25
  import IconInternet from "$lib/components/icons/IconInternet.svelte";
26
  import { isDesktop } from "$lib/utils/isDesktop";
27
  import { SortKey } from "$lib/types/Assistant";
 
28
 
29
  export let data: PageData;
30
 
@@ -245,7 +246,7 @@
245
 
246
  <button
247
  class="relative flex flex-col items-center justify-center overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 py-6 text-center shadow hover:bg-gray-50 hover:shadow-inner dark:border-gray-800/70 dark:bg-gray-950/20 dark:hover:bg-gray-950/40 max-sm:px-4 sm:h-64 sm:pb-4 xl:pt-8
248
- {!assistant.featured && !createdByMe ? 'border !border-red-500/30' : ''}"
249
  on:click={() => {
250
  if (data.settings.assistants.includes(assistant._id.toString())) {
251
  settings.instantSet({ activeModel: assistant._id.toString() });
 
25
  import IconInternet from "$lib/components/icons/IconInternet.svelte";
26
  import { isDesktop } from "$lib/utils/isDesktop";
27
  import { SortKey } from "$lib/types/Assistant";
28
+ import { ReviewStatus } from "$lib/types/Review";
29
 
30
  export let data: PageData;
31
 
 
246
 
247
  <button
248
  class="relative flex flex-col items-center justify-center overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 py-6 text-center shadow hover:bg-gray-50 hover:shadow-inner dark:border-gray-800/70 dark:bg-gray-950/20 dark:hover:bg-gray-950/40 max-sm:px-4 sm:h-64 sm:pb-4 xl:pt-8
249
+ {!(assistant.review === ReviewStatus.APPROVED) && !createdByMe ? 'border !border-red-500/30' : ''}"
250
  on:click={() => {
251
  if (data.settings.assistants.includes(assistant._id.toString())) {
252
  settings.instantSet({ activeModel: assistant._id.toString() });
src/routes/settings/(nav)/assistants/[assistantId]/+page.server.ts CHANGED
@@ -7,7 +7,8 @@ import { env as envPublic } from "$env/dynamic/public";
7
  import { env } from "$env/dynamic/private";
8
  import { z } from "zod";
9
  import type { Assistant } from "$lib/types/Assistant";
10
- import { logger } from "$lib/server/logger";
 
11
 
12
  async function assistantOnlyIfAuthor(locals: App.Locals, assistantId?: string) {
13
  const assistant = await collections.assistants.findOne({ _id: new ObjectId(assistantId) });
@@ -104,21 +105,11 @@ export const actions: Actions = {
104
 
105
  const username = locals.user?.username;
106
 
107
- const res = await fetch(env.WEBHOOK_URL_REPORT_ASSISTANT, {
108
- method: "POST",
109
- headers: {
110
- "Content-type": "application/json",
111
- },
112
- body: JSON.stringify({
113
- text: `Assistant <${assistantUrl}|${assistant?.name}> reported by ${
114
- username ? `<http://hf.co/${username}|${username}>` : "non-logged in user"
115
- }.\n\n> ${result.data}`,
116
- }),
117
- });
118
-
119
- if (!res.ok) {
120
- logger.error(`Webhook assistant report failed. ${res.statusText} ${res.text}`);
121
- }
122
  }
123
 
124
  return { from: "report", ok: true, message: "Assistant reported" };
@@ -172,54 +163,105 @@ export const actions: Actions = {
172
 
173
  redirect(302, `${base}/settings`);
174
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
- unfeature: async ({ params, locals }) => {
177
- if (!locals.user?.isAdmin) {
178
- return fail(403, { error: true, message: "Permission denied" });
179
- }
 
 
 
 
 
 
 
 
 
 
180
 
181
- const assistant = await collections.assistants.findOne({
182
- _id: new ObjectId(params.assistantId),
183
- });
184
 
185
- if (!assistant) {
186
- return fail(404, { error: true, message: "Assistant not found" });
187
- }
188
 
189
- const result = await collections.assistants.updateOne(
190
- { _id: assistant._id },
191
- { $set: { featured: false } }
192
- );
 
 
193
 
194
- if (result.modifiedCount === 0) {
195
- return fail(500, { error: true, message: "Failed to unfeature assistant" });
196
- }
197
 
198
- return { from: "unfeature", ok: true, message: "Assistant unfeatured" };
199
- },
 
 
 
 
 
 
 
200
 
201
- feature: async ({ params, locals }) => {
202
- if (!locals.user?.isAdmin) {
203
- return fail(403, { error: true, message: "Permission denied" });
204
- }
205
 
206
- const assistant = await collections.assistants.findOne({
207
- _id: new ObjectId(params.assistantId),
208
- });
209
 
210
- if (!assistant) {
211
- return fail(404, { error: true, message: "Assistant not found" });
212
- }
 
213
 
214
- const result = await collections.assistants.updateOne(
215
- { _id: assistant._id },
216
- { $set: { featured: true } }
217
- );
218
 
219
- if (result.modifiedCount === 0) {
220
- return fail(500, { error: true, message: "Failed to feature assistant" });
221
- }
 
 
 
222
 
223
- return { from: "feature", ok: true, message: "Assistant featured" };
224
- },
225
- };
 
7
  import { env } from "$env/dynamic/private";
8
  import { z } from "zod";
9
  import type { Assistant } from "$lib/types/Assistant";
10
+ import { ReviewStatus } from "$lib/types/Review";
11
+ import { sendSlack } from "$lib/server/sendSlack";
12
 
13
  async function assistantOnlyIfAuthor(locals: App.Locals, assistantId?: string) {
14
  const assistant = await collections.assistants.findOne({ _id: new ObjectId(assistantId) });
 
105
 
106
  const username = locals.user?.username;
107
 
108
+ await sendSlack(
109
+ `🔴 Assistant <${assistantUrl}|${assistant?.name}> reported by ${
110
+ username ? `<http://hf.co/${username}|${username}>` : "non-logged in user"
111
+ }.\n\n> ${result.data}`
112
+ );
 
 
 
 
 
 
 
 
 
 
113
  }
114
 
115
  return { from: "report", ok: true, message: "Assistant reported" };
 
163
 
164
  redirect(302, `${base}/settings`);
165
  },
166
+ deny: async ({ params, locals, url }) => {
167
+ return await setReviewStatus({
168
+ assistantId: params.assistantId,
169
+ locals,
170
+ status: ReviewStatus.DENIED,
171
+ url,
172
+ });
173
+ },
174
+ approve: async ({ params, locals, url }) => {
175
+ return await setReviewStatus({
176
+ assistantId: params.assistantId,
177
+ locals,
178
+ status: ReviewStatus.APPROVED,
179
+ url,
180
+ });
181
+ },
182
+ request: async ({ params, locals, url }) => {
183
+ return await setReviewStatus({
184
+ assistantId: params.assistantId,
185
+ locals,
186
+ status: ReviewStatus.PENDING,
187
+ url,
188
+ });
189
+ },
190
+ unrequest: async ({ params, locals, url }) => {
191
+ return await setReviewStatus({
192
+ assistantId: params.assistantId,
193
+ locals,
194
+ status: ReviewStatus.PRIVATE,
195
+ url,
196
+ });
197
+ },
198
+ };
199
 
200
+ async function setReviewStatus({
201
+ locals,
202
+ assistantId,
203
+ status,
204
+ url,
205
+ }: {
206
+ locals: App.Locals;
207
+ assistantId?: string;
208
+ status: ReviewStatus;
209
+ url: URL;
210
+ }) {
211
+ if (!assistantId) {
212
+ return fail(400, { error: true, message: "Assistant ID is required" });
213
+ }
214
 
215
+ const assistant = await collections.assistants.findOne({
216
+ _id: new ObjectId(assistantId),
217
+ });
218
 
219
+ if (!assistant) {
220
+ return fail(404, { error: true, message: "Assistant not found" });
221
+ }
222
 
223
+ if (
224
+ !locals.user ||
225
+ (!locals.user.isAdmin && assistant.createdById.toString() !== locals.user._id.toString())
226
+ ) {
227
+ return fail(403, { error: true, message: "Permission denied" });
228
+ }
229
 
230
+ // only admins can set the status to APPROVED or DENIED
231
+ // if the status is already APPROVED or DENIED, only admins can change it
 
232
 
233
+ if (
234
+ (status === ReviewStatus.APPROVED ||
235
+ status === ReviewStatus.DENIED ||
236
+ assistant.review === ReviewStatus.APPROVED ||
237
+ assistant.review === ReviewStatus.DENIED) &&
238
+ !locals.user?.isAdmin
239
+ ) {
240
+ return fail(403, { error: true, message: "Permission denied" });
241
+ }
242
 
243
+ const result = await collections.assistants.updateOne(
244
+ { _id: assistant._id },
245
+ { $set: { review: status } }
246
+ );
247
 
248
+ if (result.modifiedCount === 0) {
249
+ return fail(500, { error: true, message: "Failed to update review status" });
250
+ }
251
 
252
+ if (status === ReviewStatus.PENDING) {
253
+ const prefixUrl =
254
+ envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`;
255
+ const assistantUrl = `${prefixUrl}/assistant/${assistantId}`;
256
 
257
+ const username = locals.user?.username;
 
 
 
258
 
259
+ await sendSlack(
260
+ `🟢 Assistant <${assistantUrl}|${assistant?.name}> requested to be featured by ${
261
+ username ? `<http://hf.co/${username}|${username}>` : "non-logged in user"
262
+ }.`
263
+ );
264
+ }
265
 
266
+ return { from: "setReviewStatus", ok: true, message: "Review status updated" };
267
+ }
 
src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte CHANGED
@@ -15,11 +15,13 @@
15
  import CarbonChat from "~icons/carbon/chat";
16
  import CarbonStar from "~icons/carbon/star";
17
  import CarbonTools from "~icons/carbon/tools";
 
18
 
19
  import CopyToClipBoardBtn from "$lib/components/CopyToClipBoardBtn.svelte";
20
  import ReportModal from "./ReportModal.svelte";
21
  import IconInternet from "$lib/components/icons/IconInternet.svelte";
22
  import ToolBadge from "$lib/components/ToolBadge.svelte";
 
23
 
24
  export let data: PageData;
25
 
@@ -151,24 +153,51 @@
151
  {/if}
152
  {/if}
153
  {#if data?.user?.isAdmin}
154
- <form method="POST" action="?/delete" use:enhance>
155
- <button type="submit" class="flex items-center text-red-600 underline">
156
- <CarbonTrash class="mr-1.5 inline text-xs" />Delete</button
157
- >
158
- </form>
159
- {#if assistant?.featured}
160
- <form method="POST" action="?/unfeature" use:enhance>
161
  <button type="submit" class="flex items-center text-red-600 underline">
162
- <CarbonTrash class="mr-1.5 inline text-xs" />Un-feature</button
163
  >
164
  </form>
165
- {:else}
166
- <form method="POST" action="?/feature" use:enhance>
 
167
  <button type="submit" class="flex items-center text-green-600 underline">
168
- <CarbonStar class="mr-1.5 inline text-xs" />Feature</button
169
  >
170
  </form>
 
 
 
 
 
 
171
  {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  {/if}
173
  </div>
174
  </div>
 
15
  import CarbonChat from "~icons/carbon/chat";
16
  import CarbonStar from "~icons/carbon/star";
17
  import CarbonTools from "~icons/carbon/tools";
18
+ import CarbonLock from "~icons/carbon/locked";
19
 
20
  import CopyToClipBoardBtn from "$lib/components/CopyToClipBoardBtn.svelte";
21
  import ReportModal from "./ReportModal.svelte";
22
  import IconInternet from "$lib/components/icons/IconInternet.svelte";
23
  import ToolBadge from "$lib/components/ToolBadge.svelte";
24
+ import { ReviewStatus } from "$lib/types/Review";
25
 
26
  export let data: PageData;
27
 
 
153
  {/if}
154
  {/if}
155
  {#if data?.user?.isAdmin}
156
+ {#if !assistant?.createdByMe}
157
+ <form method="POST" action="?/delete" use:enhance>
 
 
 
 
 
158
  <button type="submit" class="flex items-center text-red-600 underline">
159
+ <CarbonTrash class="mr-1.5 inline text-xs" />Delete</button
160
  >
161
  </form>
162
+ {/if}
163
+ {#if assistant?.review === ReviewStatus.PENDING}
164
+ <form method="POST" action="?/approve" use:enhance>
165
  <button type="submit" class="flex items-center text-green-600 underline">
166
+ <CarbonStar class="mr-1.5 inline text-xs" />Approve</button
167
  >
168
  </form>
169
+ <form method="POST" action="?/deny" use:enhance>
170
+ <button type="submit" class="flex items-center text-red-600">
171
+ <span class="mr-1.5 font-light no-underline">X</span>
172
+ <span class="underline">Deny</span>
173
+ </button>
174
+ </form>
175
  {/if}
176
+ {#if assistant?.review === ReviewStatus.APPROVED || assistant?.review === ReviewStatus.DENIED}
177
+ <form method="POST" action="?/unrequest" use:enhance>
178
+ <button type="submit" class="flex items-center text-red-600 underline">
179
+ <CarbonLock class="mr-1.5 inline text-xs " />Make private</button
180
+ >
181
+ </form>
182
+ {/if}
183
+ {/if}
184
+ {#if assistant?.createdByMe && assistant?.review === ReviewStatus.PRIVATE}
185
+ <form
186
+ method="POST"
187
+ action="?/request"
188
+ use:enhance={async ({ cancel }) => {
189
+ const confirmed = confirm(
190
+ "Are you sure you want to request this assistant to be featured? Make sure you have tried the assistant and that it works as expected. "
191
+ );
192
+ if (!confirmed) {
193
+ cancel();
194
+ }
195
+ }}
196
+ >
197
+ <button type="submit" class="flex items-center underline">
198
+ <CarbonStar class="mr-1.5 inline text-xs" />Request to be featured</button
199
+ >
200
+ </form>
201
  {/if}
202
  </div>
203
  </div>
src/routes/settings/(nav)/assistants/new/+page.server.ts CHANGED
@@ -11,6 +11,7 @@ import { parseStringToList } from "$lib/utils/parseStringToList";
11
  import { usageLimits } from "$lib/server/usageLimits";
12
  import { generateSearchTokens } from "$lib/utils/searchTokens";
13
  import { toolFromConfigs } from "$lib/server/tools";
 
14
 
15
  const newAsssistantSchema = z.object({
16
  name: z.string().min(1),
@@ -148,7 +149,7 @@ export const actions: Actions = {
148
  createdAt: new Date(),
149
  updatedAt: new Date(),
150
  userCount: 1,
151
- featured: false,
152
  rag: {
153
  allowedLinks: parse.data.ragLinkList,
154
  allowedDomains: parse.data.ragDomainList,
 
11
  import { usageLimits } from "$lib/server/usageLimits";
12
  import { generateSearchTokens } from "$lib/utils/searchTokens";
13
  import { toolFromConfigs } from "$lib/server/tools";
14
+ import { ReviewStatus } from "$lib/types/Review";
15
 
16
  const newAsssistantSchema = z.object({
17
  name: z.string().min(1),
 
149
  createdAt: new Date(),
150
  updatedAt: new Date(),
151
  userCount: 1,
152
+ review: ReviewStatus.PRIVATE,
153
  rag: {
154
  allowedLinks: parse.data.ragLinkList,
155
  allowedDomains: parse.data.ragDomainList,
src/routes/tools/+page.server.ts CHANGED
@@ -3,6 +3,7 @@ import { authCondition } from "$lib/server/auth.js";
3
  import { Database, collections } from "$lib/server/database.js";
4
  import { toolFromConfigs } from "$lib/server/tools/index.js";
5
  import { SortKey } from "$lib/types/Assistant.js";
 
6
  import type { CommunityToolDB } from "$lib/types/Tool.js";
7
  import type { User } from "$lib/types/User.js";
8
  import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens.js";
@@ -47,7 +48,7 @@ export const load = async ({ url, locals }) => {
47
  const filter: Filter<CommunityToolDB> = {
48
  ...(!createdByCurrentUser &&
49
  !activeOnly &&
50
- !(locals.user?.isAdmin && showUnfeatured) && { featured: true }),
51
  ...(user && { createdById: user._id }),
52
  ...(queryTokens && { searchTokens: { $all: queryTokens } }),
53
  ...(activeOnly && {
 
3
  import { Database, collections } from "$lib/server/database.js";
4
  import { toolFromConfigs } from "$lib/server/tools/index.js";
5
  import { SortKey } from "$lib/types/Assistant.js";
6
+ import { ReviewStatus } from "$lib/types/Review";
7
  import type { CommunityToolDB } from "$lib/types/Tool.js";
8
  import type { User } from "$lib/types/User.js";
9
  import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens.js";
 
48
  const filter: Filter<CommunityToolDB> = {
49
  ...(!createdByCurrentUser &&
50
  !activeOnly &&
51
+ !(locals.user?.isAdmin && showUnfeatured) && { review: ReviewStatus.APPROVED }),
52
  ...(user && { createdById: user._id }),
53
  ...(queryTokens && { searchTokens: { $all: queryTokens } }),
54
  ...(activeOnly && {
src/routes/tools/+page.svelte CHANGED
@@ -19,6 +19,7 @@
19
  import { isDesktop } from "$lib/utils/isDesktop";
20
  import { SortKey } from "$lib/types/Assistant";
21
  import ToolLogo from "$lib/components/ToolLogo.svelte";
 
22
 
23
  export let data: PageData;
24
 
@@ -233,8 +234,9 @@
233
  {@const isOfficial = !tool.createdByName}
234
  <a
235
  href="{base}/tools/{tool._id.toString()}"
236
- class="relative flex flex-row items-center gap-4 overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 text-center shadow hover:bg-gray-50 hover:shadow-inner dark:bg-gray-950/20 dark:hover:bg-gray-950/40 max-sm:px-4 sm:h-24 {!tool.featured &&
237
- !isOfficial
 
238
  ? ' border-red-500/30'
239
  : 'dark:border-gray-800/70'}"
240
  class:!border-blue-600={isActive}
 
19
  import { isDesktop } from "$lib/utils/isDesktop";
20
  import { SortKey } from "$lib/types/Assistant";
21
  import ToolLogo from "$lib/components/ToolLogo.svelte";
22
+ import { ReviewStatus } from "$lib/types/Review";
23
 
24
  export let data: PageData;
25
 
 
234
  {@const isOfficial = !tool.createdByName}
235
  <a
236
  href="{base}/tools/{tool._id.toString()}"
237
+ class="relative flex flex-row items-center gap-4 overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 text-center shadow hover:bg-gray-50 hover:shadow-inner dark:bg-gray-950/20 dark:hover:bg-gray-950/40 max-sm:px-4 sm:h-24 {!(
238
+ tool.review === ReviewStatus.APPROVED
239
+ ) && !isOfficial
240
  ? ' border-red-500/30'
241
  : 'dark:border-gray-800/70'}"
242
  class:!border-blue-600={isActive}
src/routes/tools/[toolId]/+layout.server.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { base } from "$app/paths";
2
  import { collections } from "$lib/server/database.js";
3
  import { toolFromConfigs } from "$lib/server/tools/index.js";
 
4
  import { redirect } from "@sveltejs/kit";
5
  import { ObjectId } from "mongodb";
6
 
@@ -21,7 +22,7 @@ export const load = async ({ params, locals }) => {
21
  createdByName: null,
22
  createdByMe: false,
23
  reported: false,
24
- featured: false,
25
  },
26
  };
27
  }
 
1
  import { base } from "$app/paths";
2
  import { collections } from "$lib/server/database.js";
3
  import { toolFromConfigs } from "$lib/server/tools/index.js";
4
+ import { ReviewStatus } from "$lib/types/Review.js";
5
  import { redirect } from "@sveltejs/kit";
6
  import { ObjectId } from "mongodb";
7
 
 
22
  createdByName: null,
23
  createdByMe: false,
24
  reported: false,
25
+ review: ReviewStatus.APPROVED,
26
  },
27
  };
28
  }
src/routes/tools/[toolId]/+page.server.ts CHANGED
@@ -2,7 +2,8 @@ import { base } from "$app/paths";
2
  import { env } from "$env/dynamic/private";
3
  import { env as envPublic } from "$env/dynamic/public";
4
  import { collections } from "$lib/server/database";
5
- import { logger } from "$lib/server/logger";
 
6
  import type { Tool } from "$lib/types/Tool";
7
  import { fail, redirect, type Actions } from "@sveltejs/kit";
8
  import { ObjectId } from "mongodb";
@@ -103,64 +104,111 @@ export const actions: Actions = {
103
 
104
  const username = locals.user?.username;
105
 
106
- const res = await fetch(env.WEBHOOK_URL_REPORT_ASSISTANT, {
107
- method: "POST",
108
- headers: {
109
- "Content-type": "application/json",
110
- },
111
- body: JSON.stringify({
112
- text: `Tool <${toolUrl}|${tool?.displayName}> reported by ${
113
- username ? `<http://hf.co/${username}|${username}>` : "non-logged in user"
114
- }.\n\n> ${result.data}`,
115
- }),
116
- });
117
-
118
- if (!res.ok) {
119
- logger.error(`Webhook tool report failed. ${res.statusText} ${res.text}`);
120
- }
121
  }
122
 
123
  return { from: "report", ok: true, message: "Tool reported" };
124
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- unfeature: async ({ params, locals }) => {
127
- if (!locals.user?.isAdmin) {
128
- return fail(403, { error: true, message: "Permission denied" });
129
- }
 
 
 
 
 
 
 
 
 
 
130
 
131
- const tool = await collections.tools.findOne({
132
- _id: new ObjectId(params.toolId),
133
- });
134
 
135
- if (!tool) {
136
- return fail(404, { error: true, message: "Tool not found" });
137
- }
138
 
139
- const result = await collections.tools.updateOne(
140
- { _id: tool._id },
141
- { $set: { featured: false } }
142
- );
 
 
143
 
144
- if (result.modifiedCount === 0) {
145
- return fail(500, { error: true, message: "Failed to unfeature tool" });
146
- }
147
 
148
- return { from: "unfeature", ok: true, message: "Tool unfeatured" };
149
- },
150
- feature: async ({ params, locals }) => {
151
- if (!locals.user?.isAdmin) {
152
- return fail(403, { error: true, message: "Permission denied" });
153
- }
 
 
 
154
 
155
- const result = await collections.tools.updateOne(
156
- { _id: new ObjectId(params.toolId) },
157
- { $set: { featured: true } }
158
- );
159
 
160
- if (result.modifiedCount === 0) {
161
- return fail(500, { error: true, message: "Failed to feature tool" });
162
- }
163
 
164
- return { from: "feature", ok: true, message: "Tool featured" };
165
- },
166
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import { env } from "$env/dynamic/private";
3
  import { env as envPublic } from "$env/dynamic/public";
4
  import { collections } from "$lib/server/database";
5
+ import { sendSlack } from "$lib/server/sendSlack";
6
+ import { ReviewStatus } from "$lib/types/Review";
7
  import type { Tool } from "$lib/types/Tool";
8
  import { fail, redirect, type Actions } from "@sveltejs/kit";
9
  import { ObjectId } from "mongodb";
 
104
 
105
  const username = locals.user?.username;
106
 
107
+ await sendSlack(
108
+ `🔴 Tool <${toolUrl}|${tool?.displayName}> reported by ${
109
+ username ? `<http://hf.co/${username}|${username}>` : "non-logged in user"
110
+ }.\n\n> ${result.data}`
111
+ );
 
 
 
 
 
 
 
 
 
 
112
  }
113
 
114
  return { from: "report", ok: true, message: "Tool reported" };
115
  },
116
+ deny: async ({ params, locals, url }) => {
117
+ return await setReviewStatus({
118
+ toolId: params.toolId,
119
+ locals,
120
+ status: ReviewStatus.DENIED,
121
+ url,
122
+ });
123
+ },
124
+ approve: async ({ params, locals, url }) => {
125
+ return await setReviewStatus({
126
+ toolId: params.toolId,
127
+ locals,
128
+ status: ReviewStatus.APPROVED,
129
+ url,
130
+ });
131
+ },
132
+ request: async ({ params, locals, url }) => {
133
+ return await setReviewStatus({
134
+ toolId: params.toolId,
135
+ locals,
136
+ status: ReviewStatus.PENDING,
137
+ url,
138
+ });
139
+ },
140
+ unrequest: async ({ params, locals, url }) => {
141
+ return await setReviewStatus({
142
+ toolId: params.toolId,
143
+ locals,
144
+ status: ReviewStatus.PRIVATE,
145
+ url,
146
+ });
147
+ },
148
+ };
149
 
150
+ async function setReviewStatus({
151
+ locals,
152
+ toolId,
153
+ status,
154
+ url,
155
+ }: {
156
+ locals: App.Locals;
157
+ toolId?: string;
158
+ status: ReviewStatus;
159
+ url: URL;
160
+ }) {
161
+ if (!toolId) {
162
+ return fail(400, { error: true, message: "Tool ID is required" });
163
+ }
164
 
165
+ const tool = await collections.tools.findOne({
166
+ _id: new ObjectId(toolId),
167
+ });
168
 
169
+ if (!tool) {
170
+ return fail(404, { error: true, message: "Tool not found" });
171
+ }
172
 
173
+ if (
174
+ !locals.user ||
175
+ (!locals.user.isAdmin && tool.createdById.toString() !== locals.user._id.toString())
176
+ ) {
177
+ return fail(403, { error: true, message: "Permission denied" });
178
+ }
179
 
180
+ // only admins can set the status to APPROVED or DENIED
181
+ // if the status is already APPROVED or DENIED, only admins can change it
 
182
 
183
+ if (
184
+ (status === ReviewStatus.APPROVED ||
185
+ status === ReviewStatus.DENIED ||
186
+ tool.review === ReviewStatus.APPROVED ||
187
+ tool.review === ReviewStatus.DENIED) &&
188
+ !locals.user?.isAdmin
189
+ ) {
190
+ return fail(403, { error: true, message: "Permission denied" });
191
+ }
192
 
193
+ const result = await collections.tools.updateOne({ _id: tool._id }, { $set: { review: status } });
 
 
 
194
 
195
+ if (result.modifiedCount === 0) {
196
+ return fail(500, { error: true, message: "Failed to update review status" });
197
+ }
198
 
199
+ if (status === ReviewStatus.PENDING) {
200
+ const prefixUrl =
201
+ envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`;
202
+ const toolUrl = `${prefixUrl}/tools/${toolId}`;
203
+
204
+ const username = locals.user?.username;
205
+
206
+ await sendSlack(
207
+ `🟢 Tool <${toolUrl}|${tool?.displayName}> requested to be featured by ${
208
+ username ? `<http://hf.co/${username}|${username}>` : "non-logged in user"
209
+ }.`
210
+ );
211
+ }
212
+
213
+ return { from: "setReviewStatus", ok: true, message: "Review status updated" };
214
+ }
src/routes/tools/[toolId]/+page.svelte CHANGED
@@ -6,6 +6,7 @@
6
  import ToolLogo from "$lib/components/ToolLogo.svelte";
7
  import { env as envPublic } from "$env/dynamic/public";
8
  import { useSettingsStore } from "$lib/stores/settings";
 
9
 
10
  import ReportModal from "../../settings/(nav)/assistants/[assistantId]/ReportModal.svelte";
11
  import { applyAction, enhance } from "$app/forms";
@@ -17,6 +18,7 @@
17
  import CarbonFlag from "~icons/carbon/flag";
18
  import CarbonLink from "~icons/carbon/link";
19
  import CarbonStar from "~icons/carbon/star";
 
20
 
21
  export let data;
22
 
@@ -99,7 +101,7 @@
99
  <button
100
  class="{isActive
101
  ? 'bg-gray-100 text-gray-800'
102
- : 'bg-black !text-white'} mx-auto my-2 flex w-full w-min items-center justify-center rounded-full px-3 py-1 text-base"
103
  name="Activate model"
104
  on:click|stopPropagation={() => {
105
  if (isActive) {
@@ -164,25 +166,52 @@
164
  {/if}
165
  {/if}
166
  {#if data?.user?.isAdmin}
167
- <form method="POST" action="?/delete" use:enhance>
168
- <button type="submit" class="flex items-center text-red-600 underline">
169
- <CarbonTrash class="mr-1.5 inline text-xs" />Delete</button
170
- >
171
- </form>
172
- {#if data.tool?.featured}
173
- <form method="POST" action="?/unfeature" use:enhance>
174
  <button type="submit" class="flex items-center text-red-600 underline">
175
- <CarbonTrash class="mr-1.5 inline text-xs" />Un-feature</button
176
  >
177
  </form>
178
- {:else}
179
- <form method="POST" action="?/feature" use:enhance>
 
180
  <button type="submit" class="flex items-center text-green-600 underline">
181
- <CarbonStar class="mr-1.5 inline text-xs" />Feature</button
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  >
183
  </form>
184
  {/if}
185
  {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  </div>
187
  </div>
188
  </div>
 
6
  import ToolLogo from "$lib/components/ToolLogo.svelte";
7
  import { env as envPublic } from "$env/dynamic/public";
8
  import { useSettingsStore } from "$lib/stores/settings";
9
+ import { ReviewStatus } from "$lib/types/Review";
10
 
11
  import ReportModal from "../../settings/(nav)/assistants/[assistantId]/ReportModal.svelte";
12
  import { applyAction, enhance } from "$app/forms";
 
18
  import CarbonFlag from "~icons/carbon/flag";
19
  import CarbonLink from "~icons/carbon/link";
20
  import CarbonStar from "~icons/carbon/star";
21
+ import CarbonLock from "~icons/carbon/locked";
22
 
23
  export let data;
24
 
 
101
  <button
102
  class="{isActive
103
  ? 'bg-gray-100 text-gray-800'
104
+ : 'bg-black !text-white'} mx-auto my-2 flex w-min items-center justify-center rounded-full px-3 py-1 text-base"
105
  name="Activate model"
106
  on:click|stopPropagation={() => {
107
  if (isActive) {
 
166
  {/if}
167
  {/if}
168
  {#if data?.user?.isAdmin}
169
+ {#if !data.tool?.createdByMe}
170
+ <form method="POST" action="?/delete" use:enhance>
 
 
 
 
 
171
  <button type="submit" class="flex items-center text-red-600 underline">
172
+ <CarbonTrash class="mr-1.5 inline text-xs" />Delete</button
173
  >
174
  </form>
175
+ {/if}
176
+ {#if data.tool?.review === ReviewStatus.PENDING}
177
+ <form method="POST" action="?/approve" use:enhance>
178
  <button type="submit" class="flex items-center text-green-600 underline">
179
+ <CarbonStar class="mr-1.5 inline text-xs" />Approve</button
180
+ >
181
+ </form>
182
+ <form method="POST" action="?/deny" use:enhance>
183
+ <button type="submit" class="flex items-center text-red-600">
184
+ <span class="mr-1.5 font-light no-underline">X</span>
185
+ <span class="underline">Deny</span>
186
+ </button>
187
+ </form>
188
+ {/if}
189
+ {#if data.tool?.review === ReviewStatus.APPROVED || data.tool?.review === ReviewStatus.DENIED}
190
+ <form method="POST" action="?/unrequest" use:enhance>
191
+ <button type="submit" class="flex items-center text-red-600 underline">
192
+ <CarbonLock class="mr-1.5 inline text-xs " />Make private</button
193
  >
194
  </form>
195
  {/if}
196
  {/if}
197
+ {#if data.tool?.createdByMe && data.tool?.review === ReviewStatus.PRIVATE}
198
+ <form
199
+ method="POST"
200
+ action="?/request"
201
+ use:enhance={async ({ cancel }) => {
202
+ const confirmed = confirm(
203
+ "Are you sure you want to request this tool to be featured? Make sure you have tried the tool and that it works as expected. We will review your request once submitted."
204
+ );
205
+ if (!confirmed) {
206
+ cancel();
207
+ }
208
+ }}
209
+ >
210
+ <button type="submit" class="flex items-center underline">
211
+ <CarbonStar class="mr-1.5 inline text-xs" />Request to be featured</button
212
+ >
213
+ </form>
214
+ {/if}
215
  </div>
216
  </div>
217
  </div>
src/routes/tools/new/+page.server.ts CHANGED
@@ -3,6 +3,7 @@ import { authCondition, requiresUser } from "$lib/server/auth.js";
3
  import { collections } from "$lib/server/database.js";
4
  import { editableToolSchema } from "$lib/server/tools/index.js";
5
  import { usageLimits } from "$lib/server/usageLimits.js";
 
6
  import { generateSearchTokens } from "$lib/utils/searchTokens.js";
7
  import { error, fail } from "@sveltejs/kit";
8
  import { ObjectId } from "mongodb";
@@ -66,7 +67,7 @@ export const actions = {
66
  updatedAt: new Date(),
67
  last24HoursUseCount: 0,
68
  useCount: 0,
69
- featured: locals.user?.isAdmin ?? false, // admin tools are featured by default
70
  searchTokens: generateSearchTokens(parse.data.displayName),
71
  });
72
 
 
3
  import { collections } from "$lib/server/database.js";
4
  import { editableToolSchema } from "$lib/server/tools/index.js";
5
  import { usageLimits } from "$lib/server/usageLimits.js";
6
+ import { ReviewStatus } from "$lib/types/Review";
7
  import { generateSearchTokens } from "$lib/utils/searchTokens.js";
8
  import { error, fail } from "@sveltejs/kit";
9
  import { ObjectId } from "mongodb";
 
67
  updatedAt: new Date(),
68
  last24HoursUseCount: 0,
69
  useCount: 0,
70
+ review: ReviewStatus.PRIVATE,
71
  searchTokens: generateSearchTokens(parse.data.displayName),
72
  });
73