nsarrazin HF Staff commited on
Commit
2e4269d
·
unverified ·
1 Parent(s): 6041b51

feat(api): use api for tools & assistants (#1707)

Browse files

* feat: move reporting functionality to API for assistants and tools

* feat: add review API endpoints for assistants and tools

* feat(api): setup delete API endpoints for assistants and tools

* fix: show toast error if endpoint throws error

* feat(api): add subscribe/unsubscribe API endpoints for assistants

* fix: lint

* refactor: use API endpoints for assistant creation/edit

* feat: delete tool forms

* feat: use API endpoints for tool creation/edit

src/lib/components/AssistantSettings.svelte CHANGED
@@ -4,7 +4,6 @@
4
  import type { Assistant } from "$lib/types/Assistant";
5
 
6
  import { onMount } from "svelte";
7
- import { applyAction, enhance } from "$app/forms";
8
  import { page } from "$app/state";
9
  import { base } from "$app/paths";
10
  import CarbonPen from "~icons/carbon/pen";
@@ -20,24 +19,24 @@
20
  import HoverTooltip from "./HoverTooltip.svelte";
21
  import { findCurrentModel } from "$lib/utils/models";
22
  import AssistantToolPicker from "./AssistantToolPicker.svelte";
23
-
24
- type ActionData = {
25
- error: boolean;
26
- errors: {
27
- field: string | number;
28
- message: string;
29
- }[];
30
- } | null;
31
 
32
  type AssistantFront = Omit<Assistant, "_id" | "createdById"> & { _id: string };
33
 
34
  interface Props {
35
- form: ActionData;
36
  assistant?: AssistantFront | undefined;
37
  models?: Model[];
38
  }
39
 
40
- let { form = $bindable(), assistant = undefined, models = [] }: Props = $props();
 
 
 
 
 
 
 
41
 
42
  let files: FileList | null = $state(null);
43
  const settings = useSettingsStore();
@@ -61,10 +60,7 @@
61
  let inputMessage4 = $state(assistant?.exampleInputs[3] ?? "");
62
 
63
  function resetErrors() {
64
- if (form) {
65
- form.errors = [];
66
- form.error = false;
67
- }
68
  }
69
 
70
  function onFilesChange(e: Event) {
@@ -74,7 +70,7 @@
74
  inputEl.files = null;
75
  files = null;
76
 
77
- form = { error: true, errors: [{ field: "avatar", message: "Only images are allowed" }] };
78
  return;
79
  }
80
  files = inputEl.files;
@@ -83,8 +79,8 @@
83
  }
84
  }
85
 
86
- function getError(field: string, returnForm: ActionData) {
87
- return returnForm?.errors.find((error) => error.field === field)?.message ?? "";
88
  }
89
 
90
  let deleteExistingAvatar = $state(false);
@@ -109,10 +105,16 @@
109
  </script>
110
 
111
  <form
112
- method="POST"
113
  class="relative flex h-full flex-col overflow-y-auto p-4 md:p-8"
114
  enctype="multipart/form-data"
115
- use:enhance={async ({ formData }) => {
 
 
 
 
 
 
 
116
  loading = true;
117
  if (files?.[0] && files[0].size > 0 && compress) {
118
  await compress(files[0], {
@@ -158,10 +160,40 @@
158
 
159
  formData.set("tools", tools.join(","));
160
 
161
- return async ({ result }) => {
162
- loading = false;
163
- await applyAction(result);
164
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  }}
166
  >
167
  {#if assistant}
@@ -240,7 +272,7 @@
240
  </label>
241
  </div>
242
  {/if}
243
- <p class="text-xs text-red-500">{getError("avatar", form)}</p>
244
  </div>
245
 
246
  <label>
@@ -251,7 +283,7 @@
251
  placeholder="Assistant Name"
252
  value={assistant?.name ?? ""}
253
  />
254
- <p class="text-xs text-red-500">{getError("name", form)}</p>
255
  </label>
256
 
257
  <label>
@@ -262,7 +294,7 @@
262
  placeholder="It knows everything about python"
263
  value={assistant?.description ?? ""}
264
  ></textarea>
265
- <p class="text-xs text-red-500">{getError("description", form)}</p>
266
  </label>
267
 
268
  <label>
@@ -277,7 +309,7 @@
277
  <option value={model.id}>{model.displayName}</option>
278
  {/each}
279
  </select>
280
- <p class="text-xs text-red-500">{getError("modelId", form)}</p>
281
  <button
282
  type="button"
283
  class="flex aspect-square items-center gap-2 whitespace-nowrap rounded-lg border px-3 {showModelSettings
@@ -291,7 +323,7 @@
291
  class="mt-2 rounded-lg border border-blue-500/20 bg-blue-500/5 px-2 py-0.5"
292
  class:hidden={!showModelSettings}
293
  >
294
- <p class="text-xs text-red-500">{getError("inputMessage1", form)}</p>
295
  <div class="my-2 grid grid-cols-1 gap-2.5 sm:grid-cols-2 sm:grid-rows-2">
296
  <label for="temperature" class="flex justify-between">
297
  <span class="m-1 ml-0 flex items-center gap-1.5 whitespace-nowrap text-sm">
@@ -415,7 +447,7 @@
415
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
416
  />
417
  </div>
418
- <p class="text-xs text-red-500">{getError("inputMessage1", form)}</p>
419
  </label>
420
  {#if selectedModel?.tools}
421
  <div>
@@ -504,7 +536,7 @@
504
  placeholder="wikipedia.org,bbc.com"
505
  value={assistant?.rag?.allowedDomains?.join(",") ?? ""}
506
  />
507
- <p class="text-xs text-red-500">{getError("ragDomainList", form)}</p>
508
  {/if}
509
 
510
  <label class="mt-1">
@@ -530,7 +562,7 @@
530
  placeholder="https://raw.githubusercontent.com/huggingface/chat-ui/main/README.md"
531
  value={assistant?.rag?.allowedLinks.join(",") ?? ""}
532
  />
533
- <p class="text-xs text-red-500">{getError("ragLinkList", form)}</p>
534
  {/if}
535
  </div>
536
  {/if}
@@ -597,7 +629,7 @@
597
  {/if}
598
  {/if}
599
 
600
- <p class="text-xs text-red-500">{getError("preprompt", form)}</p>
601
  </div>
602
  <div class="absolute bottom-6 flex w-full justify-end gap-2 md:right-0 md:w-fit">
603
  <a
 
4
  import type { Assistant } from "$lib/types/Assistant";
5
 
6
  import { onMount } from "svelte";
 
7
  import { page } from "$app/state";
8
  import { base } from "$app/paths";
9
  import CarbonPen from "~icons/carbon/pen";
 
19
  import HoverTooltip from "./HoverTooltip.svelte";
20
  import { findCurrentModel } from "$lib/utils/models";
21
  import AssistantToolPicker from "./AssistantToolPicker.svelte";
22
+ import { error } from "$lib/stores/errors";
23
+ import { goto } from "$app/navigation";
 
 
 
 
 
 
24
 
25
  type AssistantFront = Omit<Assistant, "_id" | "createdById"> & { _id: string };
26
 
27
  interface Props {
 
28
  assistant?: AssistantFront | undefined;
29
  models?: Model[];
30
  }
31
 
32
+ let errors = $state<
33
+ {
34
+ field: string;
35
+ message: string;
36
+ }[]
37
+ >([]);
38
+
39
+ let { assistant = undefined, models = [] }: Props = $props();
40
 
41
  let files: FileList | null = $state(null);
42
  const settings = useSettingsStore();
 
60
  let inputMessage4 = $state(assistant?.exampleInputs[3] ?? "");
61
 
62
  function resetErrors() {
63
+ errors = [];
 
 
 
64
  }
65
 
66
  function onFilesChange(e: Event) {
 
70
  inputEl.files = null;
71
  files = null;
72
 
73
+ errors = [{ field: "avatar", message: "Only images are allowed" }];
74
  return;
75
  }
76
  files = inputEl.files;
 
79
  }
80
  }
81
 
82
+ function getError(field: string) {
83
+ return errors.find((error) => error.field === field)?.message ?? "";
84
  }
85
 
86
  let deleteExistingAvatar = $state(false);
 
105
  </script>
106
 
107
  <form
 
108
  class="relative flex h-full flex-col overflow-y-auto p-4 md:p-8"
109
  enctype="multipart/form-data"
110
+ onsubmit={async (e) => {
111
+ e.preventDefault();
112
+ if (!e.target) {
113
+ return;
114
+ }
115
+ const formData = new FormData(e.target as HTMLFormElement, e.submitter);
116
+
117
+ console.log(formData);
118
  loading = true;
119
  if (files?.[0] && files[0].size > 0 && compress) {
120
  await compress(files[0], {
 
160
 
161
  formData.set("tools", tools.join(","));
162
 
163
+ let response: Response;
164
+ if (assistant?._id) {
165
+ response = await fetch(`${base}/api/assistant/${assistant._id}`, {
166
+ method: "PATCH",
167
+ body: formData,
168
+ });
169
+ if (response.ok) {
170
+ goto(`${base}/settings/assistants/${assistant?._id}`, { invalidateAll: true });
171
+ } else {
172
+ if (response.status === 400) {
173
+ const data = await response.json();
174
+ errors = data.errors;
175
+ } else {
176
+ $error = response.statusText;
177
+ }
178
+ }
179
+ } else {
180
+ response = await fetch(`${base}/api/assistant`, {
181
+ method: "POST",
182
+ body: formData,
183
+ });
184
+
185
+ if (response.ok) {
186
+ const { assistantId } = await response.json();
187
+ goto(`${base}/settings/assistants/${assistantId}`, { invalidateAll: true });
188
+ } else {
189
+ if (response.status === 400) {
190
+ const data = await response.json();
191
+ errors = data.errors;
192
+ } else {
193
+ $error = response.statusText;
194
+ }
195
+ }
196
+ }
197
  }}
198
  >
199
  {#if assistant}
 
272
  </label>
273
  </div>
274
  {/if}
275
+ <p class="text-xs text-red-500">{getError("avatar")}</p>
276
  </div>
277
 
278
  <label>
 
283
  placeholder="Assistant Name"
284
  value={assistant?.name ?? ""}
285
  />
286
+ <p class="text-xs text-red-500">{getError("name")}</p>
287
  </label>
288
 
289
  <label>
 
294
  placeholder="It knows everything about python"
295
  value={assistant?.description ?? ""}
296
  ></textarea>
297
+ <p class="text-xs text-red-500">{getError("description")}</p>
298
  </label>
299
 
300
  <label>
 
309
  <option value={model.id}>{model.displayName}</option>
310
  {/each}
311
  </select>
312
+ <p class="text-xs text-red-500">{getError("modelId")}</p>
313
  <button
314
  type="button"
315
  class="flex aspect-square items-center gap-2 whitespace-nowrap rounded-lg border px-3 {showModelSettings
 
323
  class="mt-2 rounded-lg border border-blue-500/20 bg-blue-500/5 px-2 py-0.5"
324
  class:hidden={!showModelSettings}
325
  >
326
+ <p class="text-xs text-red-500">{getError("inputMessage1")}</p>
327
  <div class="my-2 grid grid-cols-1 gap-2.5 sm:grid-cols-2 sm:grid-rows-2">
328
  <label for="temperature" class="flex justify-between">
329
  <span class="m-1 ml-0 flex items-center gap-1.5 whitespace-nowrap text-sm">
 
447
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
448
  />
449
  </div>
450
+ <p class="text-xs text-red-500">{getError("inputMessage1")}</p>
451
  </label>
452
  {#if selectedModel?.tools}
453
  <div>
 
536
  placeholder="wikipedia.org,bbc.com"
537
  value={assistant?.rag?.allowedDomains?.join(",") ?? ""}
538
  />
539
+ <p class="text-xs text-red-500">{getError("ragDomainList")}</p>
540
  {/if}
541
 
542
  <label class="mt-1">
 
562
  placeholder="https://raw.githubusercontent.com/huggingface/chat-ui/main/README.md"
563
  value={assistant?.rag?.allowedLinks.join(",") ?? ""}
564
  />
565
+ <p class="text-xs text-red-500">{getError("ragLinkList")}</p>
566
  {/if}
567
  </div>
568
  {/if}
 
629
  {/if}
630
  {/if}
631
 
632
+ <p class="text-xs text-red-500">{getError("preprompt")}</p>
633
  </div>
634
  <div class="absolute bottom-6 flex w-full justify-end gap-2 md:right-0 md:w-fit">
635
  <a
src/routes/api/assistant/+server.ts ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authCondition } from "$lib/server/auth.js";
2
+ import { requiresUser } from "$lib/server/auth.js";
3
+ import { asssistantSchema } from "./utils.js";
4
+ import { uploadAssistantAvatar } from "./utils.js";
5
+ import { collections } from "$lib/server/database.js";
6
+ import { ObjectId } from "mongodb";
7
+ import sharp from "sharp";
8
+ import { generateSearchTokens } from "$lib/utils/searchTokens";
9
+ import { usageLimits } from "$lib/server/usageLimits.js";
10
+ import { ReviewStatus } from "$lib/types/Review.js";
11
+
12
+ export async function POST({ request, locals }) {
13
+ const formData = await request.formData();
14
+ const parse = await asssistantSchema.safeParseAsync(Object.fromEntries(formData));
15
+
16
+ if (!parse.success) {
17
+ // Loop through the errors array and create a custom errors array
18
+ const errors = parse.error.errors.map((error) => {
19
+ return {
20
+ field: error.path[0],
21
+ message: error.message,
22
+ };
23
+ });
24
+
25
+ return new Response(JSON.stringify({ error: true, errors }), { status: 400 });
26
+ }
27
+
28
+ // can only create assistants when logged in, IF login is setup
29
+ if (!locals.user && requiresUser) {
30
+ const errors = [{ field: "preprompt", message: "Must be logged in. Unauthorized" }];
31
+ return new Response(JSON.stringify({ error: true, errors }), { status: 400 });
32
+ }
33
+
34
+ const createdById = locals.user?._id ?? locals.sessionId;
35
+
36
+ const assistantsCount = await collections.assistants.countDocuments({ createdById });
37
+
38
+ if (usageLimits?.assistants && assistantsCount > usageLimits.assistants) {
39
+ const errors = [
40
+ {
41
+ field: "preprompt",
42
+ message: "You have reached the maximum number of assistants. Delete some to continue.",
43
+ },
44
+ ];
45
+ return new Response(JSON.stringify({ error: true, errors }), { status: 400 });
46
+ }
47
+
48
+ const newAssistantId = new ObjectId();
49
+
50
+ const exampleInputs: string[] = [
51
+ parse?.data?.exampleInput1 ?? "",
52
+ parse?.data?.exampleInput2 ?? "",
53
+ parse?.data?.exampleInput3 ?? "",
54
+ parse?.data?.exampleInput4 ?? "",
55
+ ].filter((input) => !!input);
56
+
57
+ let hash;
58
+ if (parse.data.avatar && parse.data.avatar instanceof File && parse.data.avatar.size > 0) {
59
+ let image;
60
+ try {
61
+ image = await sharp(await parse.data.avatar.arrayBuffer())
62
+ .resize(512, 512, { fit: "inside" })
63
+ .jpeg({ quality: 80 })
64
+ .toBuffer();
65
+ } catch (e) {
66
+ const errors = [{ field: "avatar", message: (e as Error).message }];
67
+ return new Response(JSON.stringify({ error: true, errors }), { status: 400 });
68
+ }
69
+
70
+ hash = await uploadAssistantAvatar(new File([image], "avatar.jpg"), newAssistantId);
71
+ }
72
+
73
+ const { insertedId } = await collections.assistants.insertOne({
74
+ _id: newAssistantId,
75
+ createdById,
76
+ createdByName: locals.user?.username ?? locals.user?.name,
77
+ ...parse.data,
78
+ tools: parse.data.tools,
79
+ exampleInputs,
80
+ avatar: hash,
81
+ createdAt: new Date(),
82
+ updatedAt: new Date(),
83
+ userCount: 1,
84
+ review: ReviewStatus.PRIVATE,
85
+ rag: {
86
+ allowedLinks: parse.data.ragLinkList,
87
+ allowedDomains: parse.data.ragDomainList,
88
+ allowAllDomains: parse.data.ragAllowAll,
89
+ },
90
+ dynamicPrompt: parse.data.dynamicPrompt,
91
+ searchTokens: generateSearchTokens(parse.data.name),
92
+ last24HoursCount: 0,
93
+ generateSettings: {
94
+ temperature: parse.data.temperature,
95
+ top_p: parse.data.top_p,
96
+ repetition_penalty: parse.data.repetition_penalty,
97
+ top_k: parse.data.top_k,
98
+ },
99
+ });
100
+
101
+ // add insertedId to user settings
102
+
103
+ await collections.settings.updateOne(authCondition(locals), {
104
+ $addToSet: { assistants: insertedId },
105
+ });
106
+
107
+ return new Response(JSON.stringify({ success: true, assistantId: insertedId }), { status: 200 });
108
+ }
src/routes/api/assistant/[id]/+server.ts CHANGED
@@ -1,5 +1,10 @@
1
  import { collections } from "$lib/server/database";
 
2
  import { ObjectId } from "mongodb";
 
 
 
 
3
 
4
  export async function GET({ params }) {
5
  const id = params.id;
@@ -15,3 +20,160 @@ export async function GET({ params }) {
15
  return Response.json({ message: "Assistant not found" }, { status: 404 });
16
  }
17
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { collections } from "$lib/server/database";
2
+ import { error } from "@sveltejs/kit";
3
  import { ObjectId } from "mongodb";
4
+ import { asssistantSchema, uploadAssistantAvatar } from "../utils.js";
5
+ import { requiresUser } from "$lib/server/auth.js";
6
+ import sharp from "sharp";
7
+ import { generateSearchTokens } from "$lib/utils/searchTokens";
8
 
9
  export async function GET({ params }) {
10
  const id = params.id;
 
20
  return Response.json({ message: "Assistant not found" }, { status: 404 });
21
  }
22
  }
23
+
24
+ export async function PATCH({ request, locals, params }) {
25
+ const assistant = await collections.assistants.findOne({
26
+ _id: new ObjectId(params.id),
27
+ });
28
+
29
+ if (!assistant) {
30
+ throw Error("Assistant not found");
31
+ }
32
+
33
+ if (assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString()) {
34
+ throw Error("You are not the author of this assistant");
35
+ }
36
+
37
+ const formData = Object.fromEntries(await request.formData());
38
+
39
+ const parse = await asssistantSchema.safeParseAsync(formData);
40
+
41
+ if (!parse.success) {
42
+ // Loop through the errors array and create a custom errors array
43
+ const errors = parse.error.errors.map((error) => {
44
+ return {
45
+ field: error.path[0],
46
+ message: error.message,
47
+ };
48
+ });
49
+
50
+ return new Response(JSON.stringify({ error: true, errors }), { status: 400 });
51
+ }
52
+
53
+ // can only create assistants when logged in, IF login is setup
54
+ if (!locals.user && requiresUser) {
55
+ const errors = [{ field: "preprompt", message: "Must be logged in. Unauthorized" }];
56
+ return new Response(JSON.stringify({ error: true, errors }), { status: 400 });
57
+ }
58
+
59
+ const exampleInputs: string[] = [
60
+ parse?.data?.exampleInput1 ?? "",
61
+ parse?.data?.exampleInput2 ?? "",
62
+ parse?.data?.exampleInput3 ?? "",
63
+ parse?.data?.exampleInput4 ?? "",
64
+ ].filter((input) => !!input);
65
+
66
+ const deleteAvatar = parse.data.avatar === "null";
67
+
68
+ let hash;
69
+ if (parse.data.avatar && parse.data.avatar !== "null" && parse.data.avatar.size > 0) {
70
+ let image;
71
+ try {
72
+ image = await sharp(await parse.data.avatar.arrayBuffer())
73
+ .resize(512, 512, { fit: "inside" })
74
+ .jpeg({ quality: 80 })
75
+ .toBuffer();
76
+ } catch (e) {
77
+ const errors = [{ field: "avatar", message: (e as Error).message }];
78
+ return new Response(JSON.stringify({ error: true, errors }), { status: 400 });
79
+ }
80
+
81
+ const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
82
+
83
+ // Step 2: Delete the existing file if it exists
84
+ let fileId = await fileCursor.next();
85
+ while (fileId) {
86
+ await collections.bucket.delete(fileId._id);
87
+ fileId = await fileCursor.next();
88
+ }
89
+
90
+ hash = await uploadAssistantAvatar(new File([image], "avatar.jpg"), assistant._id);
91
+ } else if (deleteAvatar) {
92
+ // delete the avatar
93
+ const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
94
+
95
+ let fileId = await fileCursor.next();
96
+ while (fileId) {
97
+ await collections.bucket.delete(fileId._id);
98
+ fileId = await fileCursor.next();
99
+ }
100
+ }
101
+
102
+ const { acknowledged } = await collections.assistants.updateOne(
103
+ {
104
+ _id: assistant._id,
105
+ },
106
+ {
107
+ $set: {
108
+ name: parse.data.name,
109
+ description: parse.data.description,
110
+ modelId: parse.data.modelId,
111
+ preprompt: parse.data.preprompt,
112
+ exampleInputs,
113
+ avatar: deleteAvatar ? undefined : (hash ?? assistant.avatar),
114
+ updatedAt: new Date(),
115
+ rag: {
116
+ allowedLinks: parse.data.ragLinkList,
117
+ allowedDomains: parse.data.ragDomainList,
118
+ allowAllDomains: parse.data.ragAllowAll,
119
+ },
120
+ tools: parse.data.tools,
121
+ dynamicPrompt: parse.data.dynamicPrompt,
122
+ searchTokens: generateSearchTokens(parse.data.name),
123
+ generateSettings: {
124
+ temperature: parse.data.temperature,
125
+ top_p: parse.data.top_p,
126
+ repetition_penalty: parse.data.repetition_penalty,
127
+ top_k: parse.data.top_k,
128
+ },
129
+ },
130
+ }
131
+ );
132
+
133
+ if (acknowledged) {
134
+ return new Response(JSON.stringify({ success: true, assistantId: assistant._id }), {
135
+ status: 200,
136
+ });
137
+ } else {
138
+ return new Response(JSON.stringify({ error: true, message: "Update failed" }), { status: 500 });
139
+ }
140
+ }
141
+
142
+ export async function DELETE({ params, locals }) {
143
+ const assistant = await collections.assistants.findOne({ _id: new ObjectId(params.id) });
144
+
145
+ if (!assistant) {
146
+ return error(404, "Assistant not found");
147
+ }
148
+
149
+ if (
150
+ assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString() &&
151
+ !locals.user?.isAdmin
152
+ ) {
153
+ return error(403, "You are not the author of this assistant");
154
+ }
155
+
156
+ await collections.assistants.deleteOne({ _id: assistant._id });
157
+
158
+ // and remove it from all users settings
159
+ await collections.settings.updateMany(
160
+ {
161
+ assistants: { $in: [assistant._id] },
162
+ },
163
+ {
164
+ $pull: { assistants: assistant._id },
165
+ }
166
+ );
167
+
168
+ // and delete all avatars
169
+ const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
170
+
171
+ // Step 2: Delete the existing file if it exists
172
+ let fileId = await fileCursor.next();
173
+ while (fileId) {
174
+ await collections.bucket.delete(fileId._id);
175
+ fileId = await fileCursor.next();
176
+ }
177
+
178
+ return new Response("Assistant deleted", { status: 200 });
179
+ }
src/routes/api/assistant/[id]/report/+server.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+
3
+ import { collections } from "$lib/server/database";
4
+ import { error } from "@sveltejs/kit";
5
+ import { ObjectId } from "mongodb";
6
+ import { z } from "zod";
7
+
8
+ import { env } from "$env/dynamic/private";
9
+ import { env as envPublic } from "$env/dynamic/public";
10
+ import { sendSlack } from "$lib/server/sendSlack";
11
+ import type { Assistant } from "$lib/types/Assistant";
12
+
13
+ export async function POST({ params, request, locals, url }) {
14
+ // is there already a report from this user for this model ?
15
+ const report = await collections.reports.findOne({
16
+ createdBy: locals.user?._id ?? locals.sessionId,
17
+ object: "assistant",
18
+ contentId: new ObjectId(params.id),
19
+ });
20
+
21
+ if (report) {
22
+ return error(400, "Already reported");
23
+ }
24
+
25
+ const { reason } = z.object({ reason: z.string().min(1).max(128) }).parse(await request.json());
26
+
27
+ if (!reason) {
28
+ return error(400, "Invalid report reason");
29
+ }
30
+
31
+ const { acknowledged } = await collections.reports.insertOne({
32
+ _id: new ObjectId(),
33
+ contentId: new ObjectId(params.id),
34
+ object: "assistant",
35
+ createdBy: locals.user?._id ?? locals.sessionId,
36
+ createdAt: new Date(),
37
+ updatedAt: new Date(),
38
+ reason,
39
+ });
40
+
41
+ if (!acknowledged) {
42
+ return error(500, "Failed to report assistant");
43
+ }
44
+
45
+ if (env.WEBHOOK_URL_REPORT_ASSISTANT) {
46
+ const prefixUrl =
47
+ envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`;
48
+ const assistantUrl = `${prefixUrl}/assistant/${params.id}`;
49
+
50
+ const assistant = await collections.assistants.findOne<Pick<Assistant, "name">>(
51
+ { _id: new ObjectId(params.id) },
52
+ { projection: { name: 1 } }
53
+ );
54
+
55
+ const username = locals.user?.username;
56
+
57
+ await sendSlack(
58
+ `🔴 Assistant <${assistantUrl}|${assistant?.name}> reported by ${
59
+ username ? `<http://hf.co/${username}|${username}>` : "non-logged in user"
60
+ }.\n\n> ${reason}`
61
+ );
62
+ }
63
+
64
+ return new Response("Assistant reported", { status: 200 });
65
+ }
src/routes/api/assistant/[id]/review/+server.ts ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { collections } from "$lib/server/database";
2
+ import { error } from "@sveltejs/kit";
3
+ import { ObjectId } from "mongodb";
4
+ import { base } from "$app/paths";
5
+ import { env as envPublic } from "$env/dynamic/public";
6
+ import { ReviewStatus } from "$lib/types/Review";
7
+ import { sendSlack } from "$lib/server/sendSlack";
8
+ import { z } from "zod";
9
+
10
+ const schema = z.object({
11
+ status: z.nativeEnum(ReviewStatus),
12
+ });
13
+
14
+ export async function PATCH({ params, request, locals, url }) {
15
+ const assistantId = params.id;
16
+
17
+ const { status } = schema.parse(await request.json());
18
+
19
+ if (!assistantId) {
20
+ return error(400, "Assistant ID is required");
21
+ }
22
+
23
+ const assistant = await collections.assistants.findOne({
24
+ _id: new ObjectId(assistantId),
25
+ });
26
+
27
+ if (!assistant) {
28
+ return error(404, "Assistant not found");
29
+ }
30
+
31
+ if (
32
+ !locals.user ||
33
+ (!locals.user.isAdmin && assistant.createdById.toString() !== locals.user._id.toString())
34
+ ) {
35
+ return error(403, "Permission denied");
36
+ }
37
+
38
+ // only admins can set the status to APPROVED or DENIED
39
+ // if the status is already APPROVED or DENIED, only admins can change it
40
+
41
+ if (
42
+ (status === ReviewStatus.APPROVED ||
43
+ status === ReviewStatus.DENIED ||
44
+ assistant.review === ReviewStatus.APPROVED ||
45
+ assistant.review === ReviewStatus.DENIED) &&
46
+ !locals.user?.isAdmin
47
+ ) {
48
+ return error(403, "Permission denied");
49
+ }
50
+
51
+ const result = await collections.assistants.updateOne(
52
+ { _id: assistant._id },
53
+ { $set: { review: status } }
54
+ );
55
+
56
+ if (result.modifiedCount === 0) {
57
+ return error(500, "Failed to update review status");
58
+ }
59
+
60
+ if (status === ReviewStatus.PENDING) {
61
+ const prefixUrl =
62
+ envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`;
63
+ const assistantUrl = `${prefixUrl}/assistant/${assistantId}`;
64
+
65
+ const username = locals.user?.username;
66
+
67
+ await sendSlack(
68
+ `🟢 Assistant <${assistantUrl}|${assistant?.name}> requested to be featured by ${
69
+ username ? `<http://hf.co/${username}|${username}>` : "non-logged in user"
70
+ }.`
71
+ );
72
+ }
73
+
74
+ return new Response("Review status updated", { status: 200 });
75
+ }
src/routes/api/assistant/[id]/subscribe/+server.ts ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { authCondition } from "$lib/server/auth";
2
+
3
+ import { collections } from "$lib/server/database";
4
+ import { defaultModel } from "$lib/server/models.js";
5
+ import { error } from "@sveltejs/kit";
6
+ import { ObjectId } from "mongodb";
7
+
8
+ export async function POST({ params, locals }) {
9
+ const assistant = await collections.assistants.findOne({
10
+ _id: new ObjectId(params.id),
11
+ });
12
+
13
+ if (!assistant) {
14
+ return error(404, "Assistant not found");
15
+ }
16
+
17
+ // don't push if it's already there
18
+ const settings = await collections.settings.findOne(authCondition(locals));
19
+
20
+ if (settings?.assistants?.includes(assistant._id)) {
21
+ return error(400, "Already subscribed");
22
+ }
23
+
24
+ const result = await collections.settings.updateOne(authCondition(locals), {
25
+ $addToSet: { assistants: assistant._id },
26
+ });
27
+
28
+ // reduce count only if push succeeded
29
+ if (result.modifiedCount > 0) {
30
+ await collections.assistants.updateOne({ _id: assistant._id }, { $inc: { userCount: 1 } });
31
+ }
32
+
33
+ return new Response("Assistant subscribed", { status: 200 });
34
+ }
35
+
36
+ export async function DELETE({ params, locals }) {
37
+ const assistant = await collections.assistants.findOne({
38
+ _id: new ObjectId(params.id),
39
+ });
40
+
41
+ if (!assistant) {
42
+ return error(404, "Assistant not found");
43
+ }
44
+
45
+ const result = await collections.settings.updateOne(authCondition(locals), {
46
+ $pull: { assistants: assistant._id },
47
+ });
48
+
49
+ // reduce count only if pull succeeded
50
+ if (result.modifiedCount > 0) {
51
+ await collections.assistants.updateOne({ _id: assistant._id }, { $inc: { userCount: -1 } });
52
+ }
53
+
54
+ const settings = await collections.settings.findOne(authCondition(locals));
55
+
56
+ // if the assistant was the active model, set the default model as active
57
+ if (settings?.activeModel === assistant._id.toString()) {
58
+ await collections.settings.updateOne(authCondition(locals), {
59
+ $set: { activeModel: defaultModel.id },
60
+ });
61
+ }
62
+
63
+ return new Response("Assistant unsubscribed", { status: 200 });
64
+ }
src/routes/{settings/(nav)/assistants/new/+page.server.ts → api/assistant/utils.ts} RENAMED
@@ -1,19 +1,11 @@
1
- import { base } from "$app/paths";
2
- import { authCondition, requiresUser } from "$lib/server/auth";
 
3
  import { collections } from "$lib/server/database";
4
- import { fail, type Actions, redirect } from "@sveltejs/kit";
5
  import { ObjectId } from "mongodb";
6
-
7
- import { z } from "zod";
8
  import { sha256 } from "$lib/utils/sha256";
9
- import sharp from "sharp";
10
- 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
- import { ReviewStatus } from "$lib/types/Review";
15
 
16
- const newAsssistantSchema = z.object({
17
  name: z.string().min(1),
18
  modelId: z.string().min(1),
19
  preprompt: z.string().min(1),
@@ -22,7 +14,7 @@ const newAsssistantSchema = z.object({
22
  exampleInput2: z.string().optional(),
23
  exampleInput3: z.string().optional(),
24
  exampleInput4: z.string().optional(),
25
- avatar: z.instanceof(File).optional(),
26
  ragLinkList: z.preprocess(parseStringToList, z.string().url().array().max(10)),
27
  ragDomainList: z.preprocess(parseStringToList, z.string().array()),
28
  ragAllowAll: z.preprocess((v) => v === "true", z.boolean()),
@@ -58,7 +50,10 @@ const newAsssistantSchema = z.object({
58
  .optional(),
59
  });
60
 
61
- const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
 
 
 
62
  const hash = await sha256(await avatar.text());
63
  const upload = collections.bucket.openUploadStream(`${assistantId.toString()}`, {
64
  metadata: { type: avatar.type, hash },
@@ -74,104 +69,3 @@ const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string
74
  setTimeout(() => reject(new Error("Upload timed out")), 10000);
75
  });
76
  };
77
-
78
- export const actions: Actions = {
79
- default: async ({ request, locals }) => {
80
- const formData = Object.fromEntries(await request.formData());
81
-
82
- const parse = await newAsssistantSchema.safeParseAsync(formData);
83
-
84
- if (!parse.success) {
85
- // Loop through the errors array and create a custom errors array
86
- const errors = parse.error.errors.map((error) => {
87
- return {
88
- field: error.path[0],
89
- message: error.message,
90
- };
91
- });
92
-
93
- return fail(400, { error: true, errors });
94
- }
95
-
96
- // can only create assistants when logged in, IF login is setup
97
- if (!locals.user && requiresUser) {
98
- const errors = [{ field: "preprompt", message: "Must be logged in. Unauthorized" }];
99
- return fail(400, { error: true, errors });
100
- }
101
-
102
- const createdById = locals.user?._id ?? locals.sessionId;
103
-
104
- const assistantsCount = await collections.assistants.countDocuments({ createdById });
105
-
106
- if (usageLimits?.assistants && assistantsCount > usageLimits.assistants) {
107
- const errors = [
108
- {
109
- field: "preprompt",
110
- message: "You have reached the maximum number of assistants. Delete some to continue.",
111
- },
112
- ];
113
- return fail(400, { error: true, errors });
114
- }
115
-
116
- const newAssistantId = new ObjectId();
117
-
118
- const exampleInputs: string[] = [
119
- parse?.data?.exampleInput1 ?? "",
120
- parse?.data?.exampleInput2 ?? "",
121
- parse?.data?.exampleInput3 ?? "",
122
- parse?.data?.exampleInput4 ?? "",
123
- ].filter((input) => !!input);
124
-
125
- let hash;
126
- if (parse.data.avatar && parse.data.avatar.size > 0) {
127
- let image;
128
- try {
129
- image = await sharp(await parse.data.avatar.arrayBuffer())
130
- .resize(512, 512, { fit: "inside" })
131
- .jpeg({ quality: 80 })
132
- .toBuffer();
133
- } catch (e) {
134
- const errors = [{ field: "avatar", message: (e as Error).message }];
135
- return fail(400, { error: true, errors });
136
- }
137
-
138
- hash = await uploadAvatar(new File([image], "avatar.jpg"), newAssistantId);
139
- }
140
-
141
- const { insertedId } = await collections.assistants.insertOne({
142
- _id: newAssistantId,
143
- createdById,
144
- createdByName: locals.user?.username ?? locals.user?.name,
145
- ...parse.data,
146
- tools: parse.data.tools,
147
- exampleInputs,
148
- avatar: hash,
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,
156
- allowAllDomains: parse.data.ragAllowAll,
157
- },
158
- dynamicPrompt: parse.data.dynamicPrompt,
159
- searchTokens: generateSearchTokens(parse.data.name),
160
- last24HoursCount: 0,
161
- generateSettings: {
162
- temperature: parse.data.temperature,
163
- top_p: parse.data.top_p,
164
- repetition_penalty: parse.data.repetition_penalty,
165
- top_k: parse.data.top_k,
166
- },
167
- });
168
-
169
- // add insertedId to user settings
170
-
171
- await collections.settings.updateOne(authCondition(locals), {
172
- $addToSet: { assistants: insertedId },
173
- });
174
-
175
- redirect(302, `${base}/settings/assistants/${insertedId}`);
176
- },
177
- };
 
1
+ import { parseStringToList } from "$lib/utils/parseStringToList";
2
+ import { toolFromConfigs } from "$lib/server/tools";
3
+ import { z } from "zod";
4
  import { collections } from "$lib/server/database";
 
5
  import { ObjectId } from "mongodb";
 
 
6
  import { sha256 } from "$lib/utils/sha256";
 
 
 
 
 
 
7
 
8
+ export const asssistantSchema = z.object({
9
  name: z.string().min(1),
10
  modelId: z.string().min(1),
11
  preprompt: z.string().min(1),
 
14
  exampleInput2: z.string().optional(),
15
  exampleInput3: z.string().optional(),
16
  exampleInput4: z.string().optional(),
17
+ avatar: z.union([z.instanceof(File), z.literal("null")]).optional(),
18
  ragLinkList: z.preprocess(parseStringToList, z.string().url().array().max(10)),
19
  ragDomainList: z.preprocess(parseStringToList, z.string().array()),
20
  ragAllowAll: z.preprocess((v) => v === "true", z.boolean()),
 
50
  .optional(),
51
  });
52
 
53
+ export const uploadAssistantAvatar = async (
54
+ avatar: File,
55
+ assistantId: ObjectId
56
+ ): Promise<string> => {
57
  const hash = await sha256(await avatar.text());
58
  const upload = collections.bucket.openUploadStream(`${assistantId.toString()}`, {
59
  metadata: { type: avatar.type, hash },
 
69
  setTimeout(() => reject(new Error("Upload timed out")), 10000);
70
  });
71
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/api/tools/+server.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { env } from "$env/dynamic/private";
2
+ 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 { generateSearchTokens } from "$lib/utils/searchTokens.js";
6
+ import { ObjectId } from "mongodb";
7
+ import { ReviewStatus } from "$lib/types/Review.js";
8
+ import { error } from "@sveltejs/kit";
9
+ import { usageLimits } from "$lib/server/usageLimits.js";
10
+
11
+ export async function POST({ request, locals }) {
12
+ if (env.COMMUNITY_TOOLS !== "true") {
13
+ error(403, "Community tools are not enabled");
14
+ }
15
+ const body = await request.json();
16
+
17
+ const parse = editableToolSchema.safeParse(body);
18
+
19
+ if (!parse.success) {
20
+ // Loop through the errors array and create a custom errors array
21
+ const errors = parse.error.errors.map((error) => {
22
+ return {
23
+ field: error.path[0],
24
+ message: error.message,
25
+ };
26
+ });
27
+
28
+ return new Response(JSON.stringify({ error: true, errors }), { status: 400 });
29
+ }
30
+
31
+ // can only create tools when logged in, IF login is setup
32
+ if (!locals.user && requiresUser) {
33
+ const errors = [{ field: "description", message: "Must be logged in. Unauthorized" }];
34
+ return new Response(JSON.stringify({ error: true, errors }), { status: 400 });
35
+ }
36
+
37
+ const toolCounts = await collections.tools.countDocuments({ createdById: locals.user?._id });
38
+
39
+ if (usageLimits?.tools && toolCounts > usageLimits.tools) {
40
+ const errors = [
41
+ {
42
+ field: "description",
43
+ message: "You have reached the maximum number of tools. Delete some to continue.",
44
+ },
45
+ ];
46
+ return new Response(JSON.stringify({ error: true, errors }), { status: 400 });
47
+ }
48
+
49
+ if (!locals.user || !authCondition(locals)) {
50
+ error(401, "Unauthorized");
51
+ }
52
+
53
+ const { insertedId } = await collections.tools.insertOne({
54
+ ...parse.data,
55
+ type: "community" as const,
56
+ _id: new ObjectId(),
57
+ createdById: locals.user?._id,
58
+ createdByName: locals.user?.username,
59
+ createdAt: new Date(),
60
+ updatedAt: new Date(),
61
+ last24HoursUseCount: 0,
62
+ useCount: 0,
63
+ review: ReviewStatus.PRIVATE,
64
+ searchTokens: generateSearchTokens(parse.data.displayName),
65
+ });
66
+
67
+ return new Response(JSON.stringify({ toolId: insertedId.toString() }), { status: 200 });
68
+ }
src/routes/api/tools/[toolId]/+server.ts CHANGED
@@ -4,6 +4,10 @@ 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
 
8
  export async function GET({ params }) {
9
  if (env.COMMUNITY_TOOLS !== "true") {
@@ -49,3 +53,92 @@ export async function GET({ params }) {
49
  return new Response(`Tool "${toolId}" not found`, { status: 404 });
50
  }
51
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import { ReviewStatus } from "$lib/types/Review";
5
  import type { CommunityToolDB } from "$lib/types/Tool.js";
6
  import { ObjectId } from "mongodb";
7
+ import { editableToolSchema } from "$lib/server/tools/index.js";
8
+ import { generateSearchTokens } from "$lib/utils/searchTokens.js";
9
+ import { error } from "@sveltejs/kit";
10
+ import { requiresUser } from "$lib/server/auth";
11
 
12
  export async function GET({ params }) {
13
  if (env.COMMUNITY_TOOLS !== "true") {
 
53
  return new Response(`Tool "${toolId}" not found`, { status: 404 });
54
  }
55
  }
56
+
57
+ export async function PATCH({ request, params, locals }) {
58
+ const tool = await collections.tools.findOne({
59
+ _id: new ObjectId(params.toolId),
60
+ });
61
+
62
+ if (!tool) {
63
+ error(404, "Tool not found");
64
+ }
65
+
66
+ if (tool.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString()) {
67
+ error(403, "You are not the creator of this tool");
68
+ }
69
+
70
+ // can only create tools when logged in, IF login is setup
71
+ if (!locals.user && requiresUser) {
72
+ const errors = [{ field: "description", message: "Must be logged in. Unauthorized" }];
73
+ return new Response(JSON.stringify({ error: true, errors }), { status: 400 });
74
+ }
75
+
76
+ const body = await request.json();
77
+
78
+ const parse = editableToolSchema.safeParse(body);
79
+
80
+ if (!parse.success) {
81
+ // Loop through the errors array and create a custom errors array
82
+ const errors = parse.error.errors.map((error) => {
83
+ return {
84
+ field: error.path[0],
85
+ message: error.message,
86
+ };
87
+ });
88
+
89
+ return new Response(JSON.stringify({ error: true, errors }), { status: 400 });
90
+ }
91
+
92
+ // modify the tool
93
+ await collections.tools.updateOne(
94
+ { _id: tool._id },
95
+ {
96
+ $set: {
97
+ ...parse.data,
98
+ updatedAt: new Date(),
99
+ searchTokens: generateSearchTokens(parse.data.displayName),
100
+ },
101
+ }
102
+ );
103
+
104
+ return new Response(JSON.stringify({ toolId: tool._id.toString() }), { status: 200 });
105
+ }
106
+
107
+ export async function DELETE({ params, locals }) {
108
+ const tool = await collections.tools.findOne({ _id: new ObjectId(params.toolId) });
109
+
110
+ if (!tool) {
111
+ return new Response("Tool not found", { status: 404 });
112
+ }
113
+
114
+ if (
115
+ tool.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString() &&
116
+ !locals.user?.isAdmin
117
+ ) {
118
+ return new Response("You are not the creator of this tool", { status: 403 });
119
+ }
120
+
121
+ await collections.tools.deleteOne({ _id: tool._id });
122
+
123
+ // Remove the tool from all users' settings
124
+ await collections.settings.updateMany(
125
+ {
126
+ tools: { $in: [tool._id.toString()] },
127
+ },
128
+ {
129
+ $pull: { tools: tool._id.toString() },
130
+ }
131
+ );
132
+
133
+ // Remove the tool from all assistants
134
+ await collections.assistants.updateMany(
135
+ {
136
+ tools: { $in: [tool._id.toString()] },
137
+ },
138
+ {
139
+ $pull: { tools: tool._id.toString() },
140
+ }
141
+ );
142
+
143
+ return new Response("Tool deleted", { status: 200 });
144
+ }
src/routes/api/tools/[toolId]/report/+server.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+
3
+ import { collections } from "$lib/server/database";
4
+ import { error } from "@sveltejs/kit";
5
+ import { ObjectId } from "mongodb";
6
+ import { z } from "zod";
7
+
8
+ import { env } from "$env/dynamic/private";
9
+ import { env as envPublic } from "$env/dynamic/public";
10
+ import { sendSlack } from "$lib/server/sendSlack";
11
+ import type { Tool } from "$lib/types/Tool";
12
+
13
+ export async function POST({ params, request, locals, url }) {
14
+ // is there already a report from this user for this model ?
15
+ const report = await collections.reports.findOne({
16
+ createdBy: locals.user?._id ?? locals.sessionId,
17
+ object: "tool",
18
+ contentId: new ObjectId(params.toolId),
19
+ });
20
+
21
+ if (report) {
22
+ return error(400, "Already reported");
23
+ }
24
+
25
+ const { reason } = z.object({ reason: z.string().min(1).max(128) }).parse(await request.json());
26
+
27
+ if (!reason) {
28
+ return error(400, "Invalid report reason");
29
+ }
30
+
31
+ const { acknowledged } = await collections.reports.insertOne({
32
+ _id: new ObjectId(),
33
+ contentId: new ObjectId(params.toolId),
34
+ object: "tool",
35
+ createdBy: locals.user?._id ?? locals.sessionId,
36
+ createdAt: new Date(),
37
+ updatedAt: new Date(),
38
+ reason,
39
+ });
40
+
41
+ if (!acknowledged) {
42
+ return error(500, "Failed to report tool");
43
+ }
44
+
45
+ if (env.WEBHOOK_URL_REPORT_ASSISTANT) {
46
+ const prefixUrl =
47
+ envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`;
48
+ const toolUrl = `${prefixUrl}/tools/${params.toolId}`;
49
+
50
+ const tool = await collections.tools.findOne<Pick<Tool, "displayName" | "name">>(
51
+ { _id: new ObjectId(params.toolId) },
52
+ { projection: { displayName: 1, name: 1 } }
53
+ );
54
+
55
+ const username = locals.user?.username;
56
+
57
+ await sendSlack(
58
+ `🔴 Tool <${toolUrl}|${tool?.displayName ?? tool?.name}> reported by ${
59
+ username ? `<http://hf.co/${username}|${username}>` : "non-logged in user"
60
+ }.\n\n> ${reason}`
61
+ );
62
+ }
63
+
64
+ return new Response("Tool reported", { status: 200 });
65
+ }
src/routes/api/tools/[toolId]/review/+server.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { collections } from "$lib/server/database";
2
+ import { error } from "@sveltejs/kit";
3
+ import { ObjectId } from "mongodb";
4
+ import { base } from "$app/paths";
5
+ import { env as envPublic } from "$env/dynamic/public";
6
+ import { ReviewStatus } from "$lib/types/Review";
7
+ import { sendSlack } from "$lib/server/sendSlack";
8
+ import { z } from "zod";
9
+
10
+ const schema = z.object({
11
+ status: z.nativeEnum(ReviewStatus),
12
+ });
13
+
14
+ export async function PATCH({ params, request, locals, url }) {
15
+ const toolId = params.toolId;
16
+
17
+ const { status } = schema.parse(await request.json());
18
+
19
+ if (!toolId) {
20
+ return error(400, "Tool ID is required");
21
+ }
22
+
23
+ const tool = await collections.tools.findOne({
24
+ _id: new ObjectId(toolId),
25
+ });
26
+
27
+ if (!tool) {
28
+ return error(404, "Tool not found");
29
+ }
30
+
31
+ if (
32
+ !locals.user ||
33
+ (!locals.user.isAdmin && tool.createdById.toString() !== locals.user._id.toString())
34
+ ) {
35
+ return error(403, "Permission denied");
36
+ }
37
+
38
+ // only admins can set the status to APPROVED or DENIED
39
+ // if the status is already APPROVED or DENIED, only admins can change it
40
+
41
+ if (
42
+ (status === ReviewStatus.APPROVED ||
43
+ status === ReviewStatus.DENIED ||
44
+ tool.review === ReviewStatus.APPROVED ||
45
+ tool.review === ReviewStatus.DENIED) &&
46
+ !locals.user?.isAdmin
47
+ ) {
48
+ return error(403, "Permission denied");
49
+ }
50
+
51
+ const result = await collections.tools.updateOne({ _id: tool._id }, { $set: { review: status } });
52
+
53
+ if (result.modifiedCount === 0) {
54
+ return error(500, "Failed to update review status");
55
+ }
56
+
57
+ if (status === ReviewStatus.PENDING) {
58
+ const prefixUrl =
59
+ envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`;
60
+ const toolUrl = `${prefixUrl}/tools/${toolId}`;
61
+
62
+ const username = locals.user?.username;
63
+
64
+ await sendSlack(
65
+ `🟢🛠️ Tool <${toolUrl}|${tool?.displayName}> requested to be featured by ${
66
+ username ? `<http://hf.co/${username}|${username}>` : "non-logged in user"
67
+ }.`
68
+ );
69
+ }
70
+
71
+ return new Response("Review status updated", { status: 200 });
72
+ }
src/routes/settings/(nav)/+layout.svelte CHANGED
@@ -1,7 +1,7 @@
1
  <script lang="ts">
2
  import { onMount } from "svelte";
3
  import { base } from "$app/paths";
4
- import { afterNavigate, goto } from "$app/navigation";
5
  import { page } from "$app/state";
6
  import { useSettingsStore } from "$lib/stores/settings";
7
  import CarbonClose from "~icons/carbon/close";
@@ -11,6 +11,7 @@
11
 
12
  import UserIcon from "~icons/carbon/user";
13
  import type { LayoutData } from "../$types";
 
14
 
15
  interface Props {
16
  data: LayoutData;
@@ -159,7 +160,6 @@
159
  {/if}
160
  <button
161
  type="submit"
162
- form={`unsubscribe-${assistant._id}`}
163
  aria-label="Remove assistant from your list"
164
  class={[
165
  "rounded-full p-1 text-xs hover:bg-gray-500 hover:bg-opacity-20",
@@ -169,20 +169,25 @@
169
  assistant._id.toString() !== $settings.activeModel && "ml-auto",
170
  ]}
171
  onclick={(event) => {
172
- if (assistant._id.toString() === page.params.assistantId) {
173
- goto(`${base}/settings`);
174
- }
175
  event.stopPropagation();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  }}
177
  >
178
  <CarbonClose class="size-4 text-gray-500" />
179
  </button>
180
- <form
181
- id={`unsubscribe-${assistant._id}`}
182
- action="{base}/settings/assistants/{assistant._id.toString()}?/unsubscribe"
183
- method="POST"
184
- class="hidden"
185
- ></form>
186
  </a>
187
  {/each}
188
  <a
 
1
  <script lang="ts">
2
  import { onMount } from "svelte";
3
  import { base } from "$app/paths";
4
+ import { afterNavigate, goto, invalidateAll } from "$app/navigation";
5
  import { page } from "$app/state";
6
  import { useSettingsStore } from "$lib/stores/settings";
7
  import CarbonClose from "~icons/carbon/close";
 
11
 
12
  import UserIcon from "~icons/carbon/user";
13
  import type { LayoutData } from "../$types";
14
+ import { error } from "$lib/stores/errors";
15
 
16
  interface Props {
17
  data: LayoutData;
 
160
  {/if}
161
  <button
162
  type="submit"
 
163
  aria-label="Remove assistant from your list"
164
  class={[
165
  "rounded-full p-1 text-xs hover:bg-gray-500 hover:bg-opacity-20",
 
169
  assistant._id.toString() !== $settings.activeModel && "ml-auto",
170
  ]}
171
  onclick={(event) => {
 
 
 
172
  event.stopPropagation();
173
+ fetch(`${base}/api/assistant/${assistant._id}/subscribe`, {
174
+ method: "DELETE",
175
+ }).then((r) => {
176
+ if (r.ok) {
177
+ if (assistant._id.toString() === page.params.assistantId) {
178
+ goto(`${base}/settings`, { invalidateAll: true });
179
+ } else {
180
+ invalidateAll();
181
+ }
182
+ } else {
183
+ console.error(r);
184
+ $error = r.statusText;
185
+ }
186
+ });
187
  }}
188
  >
189
  <CarbonClose class="size-4 text-gray-500" />
190
  </button>
 
 
 
 
 
 
191
  </a>
192
  {/each}
193
  <a
src/routes/settings/(nav)/assistants/[assistantId]/+page.server.ts DELETED
@@ -1,267 +0,0 @@
1
- import { collections } from "$lib/server/database";
2
- import { type Actions, fail, redirect } from "@sveltejs/kit";
3
- import { ObjectId } from "mongodb";
4
- import { authCondition } from "$lib/server/auth";
5
- import { base } from "$app/paths";
6
- 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 { 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) });
15
-
16
- if (!assistant) {
17
- throw Error("Assistant not found");
18
- }
19
-
20
- if (
21
- assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString() &&
22
- !locals.user?.isAdmin
23
- ) {
24
- throw Error("You are not the author of this assistant");
25
- }
26
-
27
- return assistant;
28
- }
29
-
30
- export const actions: Actions = {
31
- delete: async ({ params, locals }) => {
32
- let assistant;
33
- try {
34
- assistant = await assistantOnlyIfAuthor(locals, params.assistantId);
35
- } catch (e) {
36
- return fail(400, { error: true, message: (e as Error).message });
37
- }
38
-
39
- await collections.assistants.deleteOne({ _id: assistant._id });
40
-
41
- // and remove it from all users settings
42
- await collections.settings.updateMany(
43
- {
44
- assistants: { $in: [assistant._id] },
45
- },
46
- {
47
- $pull: { assistants: assistant._id },
48
- }
49
- );
50
-
51
- // and delete all avatars
52
- const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
53
-
54
- // Step 2: Delete the existing file if it exists
55
- let fileId = await fileCursor.next();
56
- while (fileId) {
57
- await collections.bucket.delete(fileId._id);
58
- fileId = await fileCursor.next();
59
- }
60
-
61
- redirect(302, `${base}/settings`);
62
- },
63
- report: async ({ request, params, locals, url }) => {
64
- // is there already a report from this user for this model ?
65
- const report = await collections.reports.findOne({
66
- createdBy: locals.user?._id ?? locals.sessionId,
67
- object: "assistant",
68
- contentId: new ObjectId(params.assistantId),
69
- });
70
-
71
- if (report) {
72
- return fail(400, { error: true, message: "Already reported" });
73
- }
74
-
75
- const formData = await request.formData();
76
- const result = z.string().min(1).max(128).safeParse(formData?.get("reportReason"));
77
-
78
- if (!result.success) {
79
- return fail(400, { error: true, message: "Invalid report reason" });
80
- }
81
-
82
- const { acknowledged } = await collections.reports.insertOne({
83
- _id: new ObjectId(),
84
- contentId: new ObjectId(params.assistantId),
85
- object: "assistant",
86
- createdBy: locals.user?._id ?? locals.sessionId,
87
- createdAt: new Date(),
88
- updatedAt: new Date(),
89
- reason: result.data,
90
- });
91
-
92
- if (!acknowledged) {
93
- return fail(500, { error: true, message: "Failed to report assistant" });
94
- }
95
-
96
- if (env.WEBHOOK_URL_REPORT_ASSISTANT) {
97
- const prefixUrl =
98
- envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`;
99
- const assistantUrl = `${prefixUrl}/assistant/${params.assistantId}`;
100
-
101
- const assistant = await collections.assistants.findOne<Pick<Assistant, "name">>(
102
- { _id: new ObjectId(params.assistantId) },
103
- { projection: { name: 1 } }
104
- );
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" };
116
- },
117
-
118
- subscribe: async ({ params, locals }) => {
119
- const assistant = await collections.assistants.findOne({
120
- _id: new ObjectId(params.assistantId),
121
- });
122
-
123
- if (!assistant) {
124
- return fail(404, { error: true, message: "Assistant not found" });
125
- }
126
-
127
- // don't push if it's already there
128
- const settings = await collections.settings.findOne(authCondition(locals));
129
-
130
- if (settings?.assistants?.includes(assistant._id)) {
131
- return fail(400, { error: true, message: "Already subscribed" });
132
- }
133
-
134
- const result = await collections.settings.updateOne(authCondition(locals), {
135
- $addToSet: { assistants: assistant._id },
136
- });
137
-
138
- // reduce count only if push succeeded
139
- if (result.modifiedCount > 0) {
140
- await collections.assistants.updateOne({ _id: assistant._id }, { $inc: { userCount: 1 } });
141
- }
142
-
143
- return { from: "subscribe", ok: true, message: "Assistant added" };
144
- },
145
-
146
- unsubscribe: async ({ params, locals }) => {
147
- const assistant = await collections.assistants.findOne({
148
- _id: new ObjectId(params.assistantId),
149
- });
150
-
151
- if (!assistant) {
152
- return fail(404, { error: true, message: "Assistant not found" });
153
- }
154
-
155
- const result = await collections.settings.updateOne(authCondition(locals), {
156
- $pull: { assistants: assistant._id },
157
- });
158
-
159
- // reduce count only if pull succeeded
160
- if (result.modifiedCount > 0) {
161
- await collections.assistants.updateOne({ _id: assistant._id }, { $inc: { userCount: -1 } });
162
- }
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
@@ -2,7 +2,7 @@
2
  import { enhance } from "$app/forms";
3
  import { base } from "$app/paths";
4
  import { page } from "$app/state";
5
- import { goto } from "$app/navigation";
6
  import { env as envPublic } from "$env/dynamic/public";
7
  import { useSettingsStore } from "$lib/stores/settings";
8
  import type { PageData } from "./$types";
@@ -22,6 +22,7 @@
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
  interface Props {
27
  data: PageData;
@@ -50,10 +51,27 @@
50
  );
51
 
52
  let prepromptTags = $derived(assistant?.preprompt?.split(/(\{\{[^{}]*\}\})/) ?? []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  </script>
54
 
55
  {#if displayReportModal}
56
- <ReportModal on:close={() => (displayReportModal = false)} />
 
 
 
57
  {/if}
58
  <div class="flex h-full flex-col gap-2">
59
  <div class="flex flex-col sm:flex-row sm:gap-6">
@@ -128,7 +146,20 @@
128
  <a href="{base}/settings/assistants/{assistant?._id}/edit" class="underline"
129
  ><CarbonPen class="mr-1.5 inline text-xs" />Edit
130
  </a>
131
- <form method="POST" action="?/delete" use:enhance>
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  <button
133
  type="submit"
134
  class="flex items-center underline"
@@ -142,7 +173,20 @@
142
  </button>
143
  </form>
144
  {:else}
145
- <form method="POST" action="?/unsubscribe" use:enhance>
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  <button type="submit" class="underline">
147
  <CarbonTrash class="mr-1.5 inline text-xs" />Remove</button
148
  >
@@ -174,7 +218,20 @@
174
  >
175
 
176
  {#if !assistant?.createdByMe}
177
- <form method="POST" action="?/delete" use:enhance>
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  <button
179
  type="submit"
180
  class="flex items-center text-red-600 underline"
@@ -189,19 +246,19 @@
189
  </form>
190
  {/if}
191
  {#if assistant?.review === ReviewStatus.PRIVATE}
192
- <form method="POST" action="?/approve" use:enhance>
193
  <button type="submit" class="flex items-center text-green-600 underline">
194
  <CarbonStar class="mr-1.5 inline text-xs" />Force feature</button
195
  >
196
  </form>
197
  {/if}
198
  {#if assistant?.review === ReviewStatus.PENDING}
199
- <form method="POST" action="?/approve" use:enhance>
200
  <button type="submit" class="flex items-center text-green-600 underline">
201
  <CarbonStar class="mr-1.5 inline text-xs" />Approve</button
202
  >
203
  </form>
204
- <form method="POST" action="?/deny" use:enhance>
205
  <button type="submit" class="flex items-center text-red-600">
206
  <span class="mr-1.5 font-light no-underline">X</span>
207
  <span class="underline">Deny</span>
@@ -209,7 +266,7 @@
209
  </form>
210
  {/if}
211
  {#if assistant?.review === ReviewStatus.APPROVED || assistant?.review === ReviewStatus.DENIED}
212
- <form method="POST" action="?/unrequest" use:enhance>
213
  <button type="submit" class="flex items-center text-red-600 underline">
214
  <CarbonLock class="mr-1.5 inline text-xs " />Reset review</button
215
  >
@@ -218,15 +275,16 @@
218
  {/if}
219
  {#if assistant?.createdByMe && assistant?.review === ReviewStatus.PRIVATE}
220
  <form
221
- method="POST"
222
- action="?/request"
223
- use:enhance={async ({ cancel }) => {
224
  const confirmed = confirm(
225
  "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. "
226
  );
 
227
  if (!confirmed) {
228
- cancel();
229
  }
 
 
230
  }}
231
  >
232
  <button type="submit" class="flex items-center underline">
 
2
  import { enhance } from "$app/forms";
3
  import { base } from "$app/paths";
4
  import { page } from "$app/state";
5
+ import { goto, invalidateAll } from "$app/navigation";
6
  import { env as envPublic } from "$env/dynamic/public";
7
  import { useSettingsStore } from "$lib/stores/settings";
8
  import type { PageData } from "./$types";
 
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
+ import { error } from "$lib/stores/errors";
26
 
27
  interface Props {
28
  data: PageData;
 
51
  );
52
 
53
  let prepromptTags = $derived(assistant?.preprompt?.split(/(\{\{[^{}]*\}\})/) ?? []);
54
+
55
+ function setFeatured(status: ReviewStatus) {
56
+ fetch(`${base}/api/assistant/${assistant?._id}/review`, {
57
+ method: "PATCH",
58
+ body: JSON.stringify({ status }),
59
+ }).then((r) => {
60
+ if (r.ok) {
61
+ invalidateAll();
62
+ } else {
63
+ console.error(r);
64
+ $error = r.statusText;
65
+ }
66
+ });
67
+ }
68
  </script>
69
 
70
  {#if displayReportModal}
71
+ <ReportModal
72
+ on:close={() => (displayReportModal = false)}
73
+ reportUrl={`${base}/api/assistant/${assistant?._id}/report`}
74
+ />
75
  {/if}
76
  <div class="flex h-full flex-col gap-2">
77
  <div class="flex flex-col sm:flex-row sm:gap-6">
 
146
  <a href="{base}/settings/assistants/{assistant?._id}/edit" class="underline"
147
  ><CarbonPen class="mr-1.5 inline text-xs" />Edit
148
  </a>
149
+ <form
150
+ onsubmit={() => {
151
+ fetch(`${base}/api/assistant/${assistant?._id}`, {
152
+ method: "DELETE",
153
+ }).then((r) => {
154
+ if (r.ok) {
155
+ goto(`${base}/settings/assistants`, { invalidateAll: true });
156
+ } else {
157
+ console.error(r);
158
+ $error = r.statusText;
159
+ }
160
+ });
161
+ }}
162
+ >
163
  <button
164
  type="submit"
165
  class="flex items-center underline"
 
173
  </button>
174
  </form>
175
  {:else}
176
+ <form
177
+ onsubmit={() => {
178
+ fetch(`${base}/api/assistant/${assistant?._id}/subscribe`, {
179
+ method: "DELETE",
180
+ }).then((r) => {
181
+ if (r.ok) {
182
+ goto(`${base}/settings/assistants`, { invalidateAll: true });
183
+ } else {
184
+ console.error(r);
185
+ $error = r.statusText;
186
+ }
187
+ });
188
+ }}
189
+ >
190
  <button type="submit" class="underline">
191
  <CarbonTrash class="mr-1.5 inline text-xs" />Remove</button
192
  >
 
218
  >
219
 
220
  {#if !assistant?.createdByMe}
221
+ <form
222
+ onsubmit={() => {
223
+ fetch(`${base}/api/assistant/${assistant?._id}`, {
224
+ method: "DELETE",
225
+ }).then((r) => {
226
+ if (r.ok) {
227
+ goto(`${base}/settings/assistants`, { invalidateAll: true });
228
+ } else {
229
+ console.error(r);
230
+ $error = r.statusText;
231
+ }
232
+ });
233
+ }}
234
+ >
235
  <button
236
  type="submit"
237
  class="flex items-center text-red-600 underline"
 
246
  </form>
247
  {/if}
248
  {#if assistant?.review === ReviewStatus.PRIVATE}
249
+ <form onsubmit={() => setFeatured(ReviewStatus.APPROVED)}>
250
  <button type="submit" class="flex items-center text-green-600 underline">
251
  <CarbonStar class="mr-1.5 inline text-xs" />Force feature</button
252
  >
253
  </form>
254
  {/if}
255
  {#if assistant?.review === ReviewStatus.PENDING}
256
+ <form onsubmit={() => setFeatured(ReviewStatus.APPROVED)}>
257
  <button type="submit" class="flex items-center text-green-600 underline">
258
  <CarbonStar class="mr-1.5 inline text-xs" />Approve</button
259
  >
260
  </form>
261
+ <form onsubmit={() => setFeatured(ReviewStatus.DENIED)}>
262
  <button type="submit" class="flex items-center text-red-600">
263
  <span class="mr-1.5 font-light no-underline">X</span>
264
  <span class="underline">Deny</span>
 
266
  </form>
267
  {/if}
268
  {#if assistant?.review === ReviewStatus.APPROVED || assistant?.review === ReviewStatus.DENIED}
269
+ <form onsubmit={() => setFeatured(ReviewStatus.PRIVATE)}>
270
  <button type="submit" class="flex items-center text-red-600 underline">
271
  <CarbonLock class="mr-1.5 inline text-xs " />Reset review</button
272
  >
 
275
  {/if}
276
  {#if assistant?.createdByMe && assistant?.review === ReviewStatus.PRIVATE}
277
  <form
278
+ onsubmit={() => {
 
 
279
  const confirmed = confirm(
280
  "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. "
281
  );
282
+
283
  if (!confirmed) {
284
+ return;
285
  }
286
+
287
+ setFeatured(ReviewStatus.PENDING);
288
  }}
289
  >
290
  <button type="submit" class="flex items-center underline">
src/routes/settings/(nav)/assistants/[assistantId]/ReportModal.svelte CHANGED
@@ -1,5 +1,4 @@
1
  <script lang="ts">
2
- import { applyAction, enhance } from "$app/forms";
3
  import { invalidateAll } from "$app/navigation";
4
  import Modal from "$lib/components/Modal.svelte";
5
  import { createEventDispatcher } from "svelte";
@@ -7,18 +6,24 @@
7
  const dispatch = createEventDispatcher<{ close: void }>();
8
 
9
  let reason = $state("");
 
 
 
 
 
 
10
  </script>
11
 
12
  <Modal on:close>
13
  <form
14
- method="POST"
15
- action="?/report"
16
- use:enhance={() => {
17
- return async ({ result }) => {
18
- await applyAction(result);
19
  dispatch("close");
20
  invalidateAll();
21
- };
22
  }}
23
  class="w-full min-w-64 p-4"
24
  >
 
1
  <script lang="ts">
 
2
  import { invalidateAll } from "$app/navigation";
3
  import Modal from "$lib/components/Modal.svelte";
4
  import { createEventDispatcher } from "svelte";
 
6
  const dispatch = createEventDispatcher<{ close: void }>();
7
 
8
  let reason = $state("");
9
+
10
+ interface Props {
11
+ reportUrl: string;
12
+ }
13
+
14
+ let { reportUrl }: Props = $props();
15
  </script>
16
 
17
  <Modal on:close>
18
  <form
19
+ onsubmit={() => {
20
+ fetch(`${reportUrl}`, {
21
+ method: "POST",
22
+ body: JSON.stringify({ reason }),
23
+ }).then(() => {
24
  dispatch("close");
25
  invalidateAll();
26
+ });
27
  }}
28
  class="w-full min-w-64 p-4"
29
  >
src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.server.ts DELETED
@@ -1,193 +0,0 @@
1
- import { base } from "$app/paths";
2
- import { requiresUser } from "$lib/server/auth";
3
- import { collections } from "$lib/server/database";
4
- import { fail, type Actions, redirect } from "@sveltejs/kit";
5
- import { ObjectId } from "mongodb";
6
-
7
- import { z } from "zod";
8
- import { sha256 } from "$lib/utils/sha256";
9
-
10
- import sharp from "sharp";
11
- import { parseStringToList } from "$lib/utils/parseStringToList";
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),
17
- modelId: z.string().min(1),
18
- preprompt: z.string().min(1),
19
- description: z.string().optional(),
20
- exampleInput1: z.string().optional(),
21
- exampleInput2: z.string().optional(),
22
- exampleInput3: z.string().optional(),
23
- exampleInput4: z.string().optional(),
24
- avatar: z.union([z.instanceof(File), z.literal("null")]).optional(),
25
- ragLinkList: z.preprocess(parseStringToList, z.string().url().array().max(10)),
26
- ragDomainList: z.preprocess(parseStringToList, z.string().array()),
27
- ragAllowAll: z.preprocess((v) => v === "true", z.boolean()),
28
- dynamicPrompt: z.preprocess((v) => v === "on", z.boolean()),
29
- temperature: z
30
- .union([z.literal(""), z.coerce.number().min(0.1).max(2)])
31
- .transform((v) => (v === "" ? undefined : v)),
32
- top_p: z
33
- .union([z.literal(""), z.coerce.number().min(0.05).max(1)])
34
- .transform((v) => (v === "" ? undefined : v)),
35
-
36
- repetition_penalty: z
37
- .union([z.literal(""), z.coerce.number().min(0.1).max(2)])
38
- .transform((v) => (v === "" ? undefined : v)),
39
-
40
- top_k: z
41
- .union([z.literal(""), z.coerce.number().min(5).max(100)])
42
- .transform((v) => (v === "" ? undefined : v)),
43
- tools: z
44
- .string()
45
- .optional()
46
- .transform((v) => (v ? v.split(",") : []))
47
- .transform(async (v) => [
48
- ...(await collections.tools
49
- .find({ _id: { $in: v.map((toolId) => new ObjectId(toolId)) } })
50
- .project({ _id: 1 })
51
- .toArray()
52
- .then((tools) => tools.map((tool) => tool._id.toString()))),
53
- ...toolFromConfigs
54
- .filter((el) => (v ?? []).includes(el._id.toString()))
55
- .map((el) => el._id.toString()),
56
- ])
57
- .optional(),
58
- });
59
-
60
- const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise<string> => {
61
- const hash = await sha256(await avatar.text());
62
- const upload = collections.bucket.openUploadStream(`${assistantId.toString()}`, {
63
- metadata: { type: avatar.type, hash },
64
- });
65
-
66
- upload.write((await avatar.arrayBuffer()) as unknown as Buffer);
67
- upload.end();
68
-
69
- // only return the filename when upload throws a finish event or a 10s time out occurs
70
- return new Promise((resolve, reject) => {
71
- upload.once("finish", () => resolve(hash));
72
- upload.once("error", reject);
73
- setTimeout(() => reject(new Error("Upload timed out")), 10000);
74
- });
75
- };
76
-
77
- export const actions: Actions = {
78
- default: async ({ request, locals, params }) => {
79
- const assistant = await collections.assistants.findOne({
80
- _id: new ObjectId(params.assistantId),
81
- });
82
-
83
- if (!assistant) {
84
- throw Error("Assistant not found");
85
- }
86
-
87
- if (assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString()) {
88
- throw Error("You are not the author of this assistant");
89
- }
90
-
91
- const formData = Object.fromEntries(await request.formData());
92
-
93
- const parse = await newAsssistantSchema.safeParseAsync(formData);
94
-
95
- if (!parse.success) {
96
- // Loop through the errors array and create a custom errors array
97
- const errors = parse.error.errors.map((error) => {
98
- return {
99
- field: error.path[0],
100
- message: error.message,
101
- };
102
- });
103
-
104
- return fail(400, { error: true, errors });
105
- }
106
-
107
- // can only create assistants when logged in, IF login is setup
108
- if (!locals.user && requiresUser) {
109
- const errors = [{ field: "preprompt", message: "Must be logged in. Unauthorized" }];
110
- return fail(400, { error: true, errors });
111
- }
112
-
113
- const exampleInputs: string[] = [
114
- parse?.data?.exampleInput1 ?? "",
115
- parse?.data?.exampleInput2 ?? "",
116
- parse?.data?.exampleInput3 ?? "",
117
- parse?.data?.exampleInput4 ?? "",
118
- ].filter((input) => !!input);
119
-
120
- const deleteAvatar = parse.data.avatar === "null";
121
-
122
- let hash;
123
- if (parse.data.avatar && parse.data.avatar !== "null" && parse.data.avatar.size > 0) {
124
- let image;
125
- try {
126
- image = await sharp(await parse.data.avatar.arrayBuffer())
127
- .resize(512, 512, { fit: "inside" })
128
- .jpeg({ quality: 80 })
129
- .toBuffer();
130
- } catch (e) {
131
- const errors = [{ field: "avatar", message: (e as Error).message }];
132
- return fail(400, { error: true, errors });
133
- }
134
-
135
- const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
136
-
137
- // Step 2: Delete the existing file if it exists
138
- let fileId = await fileCursor.next();
139
- while (fileId) {
140
- await collections.bucket.delete(fileId._id);
141
- fileId = await fileCursor.next();
142
- }
143
-
144
- hash = await uploadAvatar(new File([image], "avatar.jpg"), assistant._id);
145
- } else if (deleteAvatar) {
146
- // delete the avatar
147
- const fileCursor = collections.bucket.find({ filename: assistant._id.toString() });
148
-
149
- let fileId = await fileCursor.next();
150
- while (fileId) {
151
- await collections.bucket.delete(fileId._id);
152
- fileId = await fileCursor.next();
153
- }
154
- }
155
-
156
- const { acknowledged } = await collections.assistants.updateOne(
157
- {
158
- _id: assistant._id,
159
- },
160
- {
161
- $set: {
162
- name: parse.data.name,
163
- description: parse.data.description,
164
- modelId: parse.data.modelId,
165
- preprompt: parse.data.preprompt,
166
- exampleInputs,
167
- avatar: deleteAvatar ? undefined : (hash ?? assistant.avatar),
168
- updatedAt: new Date(),
169
- rag: {
170
- allowedLinks: parse.data.ragLinkList,
171
- allowedDomains: parse.data.ragDomainList,
172
- allowAllDomains: parse.data.ragAllowAll,
173
- },
174
- tools: parse.data.tools,
175
- dynamicPrompt: parse.data.dynamicPrompt,
176
- searchTokens: generateSearchTokens(parse.data.name),
177
- generateSettings: {
178
- temperature: parse.data.temperature,
179
- top_p: parse.data.top_p,
180
- repetition_penalty: parse.data.repetition_penalty,
181
- top_k: parse.data.top_k,
182
- },
183
- },
184
- }
185
- );
186
-
187
- if (acknowledged) {
188
- redirect(302, `${base}/settings/assistants/${assistant._id}`);
189
- } else {
190
- throw Error("Update failed");
191
- }
192
- },
193
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/settings/(nav)/assistants/[assistantId]/edit/[email protected] CHANGED
@@ -1,16 +1,15 @@
1
  <script lang="ts">
2
- import type { PageData, ActionData } from "./$types";
3
  import { page } from "$app/state";
4
  import AssistantSettings from "$lib/components/AssistantSettings.svelte";
5
 
6
  interface Props {
7
  data: PageData;
8
- form: ActionData;
9
  }
10
 
11
- let { data, form = $bindable() }: Props = $props();
12
 
13
  let assistant = data.assistants.find((el) => el._id.toString() === page.params.assistantId);
14
  </script>
15
 
16
- <AssistantSettings bind:form {assistant} models={data.models} />
 
1
  <script lang="ts">
2
+ import type { PageData } from "./$types";
3
  import { page } from "$app/state";
4
  import AssistantSettings from "$lib/components/AssistantSettings.svelte";
5
 
6
  interface Props {
7
  data: PageData;
 
8
  }
9
 
10
+ let { data }: Props = $props();
11
 
12
  let assistant = data.assistants.find((el) => el._id.toString() === page.params.assistantId);
13
  </script>
14
 
15
+ <AssistantSettings {assistant} models={data.models} />
src/routes/settings/(nav)/assistants/new/[email protected] CHANGED
@@ -1,13 +1,12 @@
1
  <script lang="ts">
2
- import type { ActionData, PageData } from "./$types";
3
  import AssistantSettings from "$lib/components/AssistantSettings.svelte";
 
4
 
5
  interface Props {
6
  data: PageData;
7
- form: ActionData;
8
  }
9
 
10
- let { data, form = $bindable() }: Props = $props();
11
  </script>
12
 
13
- <AssistantSettings bind:form models={data.models} />
 
1
  <script lang="ts">
 
2
  import AssistantSettings from "$lib/components/AssistantSettings.svelte";
3
+ import type { PageData } from "./$types";
4
 
5
  interface Props {
6
  data: PageData;
 
7
  }
8
 
9
+ let { data }: Props = $props();
10
  </script>
11
 
12
+ <AssistantSettings models={data.models} />
src/routes/tools/ToolEdit.svelte CHANGED
@@ -8,33 +8,25 @@
8
  import { browser } from "$app/environment";
9
  import ToolLogo from "$lib/components/ToolLogo.svelte";
10
  import { colors, icons } from "$lib/utils/tools";
11
- import { applyAction, enhance } from "$app/forms";
12
  import { getGradioApi } from "$lib/utils/getGradioApi";
13
- import { useSettingsStore } from "$lib/stores/settings";
14
  import { goto } from "$app/navigation";
15
  import { base } from "$app/paths";
16
  import ToolInputComponent from "./ToolInputComponent.svelte";
 
17
 
18
  import CarbonInformation from "~icons/carbon/information";
19
-
20
- type ActionData = {
21
- error?: boolean;
22
- errors?: {
23
- field: string | number;
24
- message: string;
25
- }[];
26
- } | null;
27
 
28
  interface Props {
29
  tool?: CommunityToolEditable | undefined;
30
  readonly?: boolean;
31
- form: ActionData;
32
  }
33
 
34
- let { tool = undefined, readonly = false, form = $bindable() }: Props = $props();
 
35
 
36
- function getError(field: string, returnForm: ActionData) {
37
- return returnForm?.errors?.find((error) => error.field === field)?.message ?? "";
38
  }
39
 
40
  let APIloading = $state(false);
@@ -73,7 +65,6 @@
73
  return;
74
  }
75
 
76
- form = { error: false, errors: [] };
77
  APIloading = true;
78
 
79
  const api = await getGradioApi(editableTool.baseUrl);
@@ -118,17 +109,14 @@
118
  if (parsedOutputComponent.success) {
119
  editableTool.outputComponent = "0;" + parsedOutputComponent.data;
120
  } else {
121
- form = {
122
- error: true,
123
- errors: [
124
- {
125
- field: "outputComponent",
126
- message: `Invalid output component. Type ${
127
- api.named_endpoints[editableTool.endpoint].returns?.[0]?.component
128
- } is not yet supported. Feel free to report this issue so we can add support for it.`,
129
- },
130
- ],
131
- };
132
  editableTool.outputComponent = null;
133
  }
134
 
@@ -157,33 +145,50 @@
157
  }
158
  }
159
 
160
- const settings = useSettingsStore();
161
-
162
  let formSubmittable = $derived(
163
  editableTool.name && editableTool.baseUrl && editableTool.outputComponent
164
  );
165
  </script>
166
 
167
  <form
168
- method="POST"
169
  class="relative flex h-full flex-col overflow-y-auto p-4 md:p-8"
170
- use:enhance={async ({ formData }) => {
 
171
  formLoading = true;
 
172
 
173
- formData.append("tool", JSON.stringify(editableTool));
 
 
174
 
175
- return async ({ result }) => {
176
- if (result.type === "success" && result.data && typeof result.data.toolId === "string") {
177
- $settings.tools = [...($settings.tools ?? []), result.data.toolId];
178
- await goto(`${base}/tools/${result.data.toolId}`).then(() => {
179
- formLoading = false;
180
  });
181
  } else {
182
- await applyAction(result).then(() => {
183
- formLoading = false;
 
 
184
  });
185
  }
186
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  }}
188
  >
189
  {#if tool}
@@ -217,7 +222,7 @@
217
  placeholder="Image generator"
218
  bind:value={editableTool.displayName}
219
  />
220
- <p class="text-xs text-red-500">{getError("displayName", form)}</p>
221
  </label>
222
 
223
  <div class="flex flex-row gap-4">
@@ -240,7 +245,7 @@
240
  <option value={icon}>{icon}</option>
241
  {/each}
242
  </select>
243
- <p class="text-xs text-red-500">{getError("icon", form)}</p>
244
  </label>
245
 
246
  <label class="flex-grow">
@@ -255,7 +260,7 @@
255
  <option value={color}>{color}</option>
256
  {/each}
257
  </select>
258
- <p class="text-xs text-red-500">{getError("color", form)}</p>
259
  </label>
260
  </div>
261
 
@@ -272,7 +277,7 @@
272
  placeholder="This tool lets you generate images using SDXL."
273
  bind:value={editableTool.description}
274
  ></textarea>
275
- <p class="text-xs text-red-500">{getError("description", form)}</p>
276
  </label>
277
 
278
  <label>
@@ -292,7 +297,7 @@
292
  placeholder="ByteDance/Hyper-SDXL-1Step-T2I"
293
  bind:value={editableTool.baseUrl}
294
  />
295
- <p class="text-xs text-red-500">{getError("spaceUrl", form)}</p>
296
  </label>
297
  <p class="text-justify text-gray-800">
298
  Tools allows models that support them to use external application directly via function
@@ -378,7 +383,7 @@
378
  </div>
379
 
380
  <p class="text-xs text-red-500">
381
- {getError(`inputs`, form)}
382
  </p>
383
 
384
  {#each editableTool.inputs as input, inputIdx}
@@ -559,7 +564,7 @@
559
  {/if}
560
  {/if}
561
  <p class="text-xs text-red-500">
562
- {getError("outputComponent", form)}
563
  </p>
564
  </label>
565
 
@@ -578,7 +583,7 @@
578
  class="peer rounded-lg border-2 border-gray-200 bg-gray-100 p-1"
579
  />
580
  <p class="text-xs text-red-500">
581
- {getError("showOutput", form)}
582
  </p>
583
  </label>
584
  </div>
 
8
  import { browser } from "$app/environment";
9
  import ToolLogo from "$lib/components/ToolLogo.svelte";
10
  import { colors, icons } from "$lib/utils/tools";
 
11
  import { getGradioApi } from "$lib/utils/getGradioApi";
 
12
  import { goto } from "$app/navigation";
13
  import { base } from "$app/paths";
14
  import ToolInputComponent from "./ToolInputComponent.svelte";
15
+ import { error as errorStore } from "$lib/stores/errors";
16
 
17
  import CarbonInformation from "~icons/carbon/information";
18
+ import { page } from "$app/state";
 
 
 
 
 
 
 
19
 
20
  interface Props {
21
  tool?: CommunityToolEditable | undefined;
22
  readonly?: boolean;
 
23
  }
24
 
25
+ let errors = $state<{ field: string; message: string }[]>([]);
26
+ let { tool = undefined, readonly = false }: Props = $props();
27
 
28
+ function getError(field: string) {
29
+ return errors.find((error) => error.field === field)?.message ?? "";
30
  }
31
 
32
  let APIloading = $state(false);
 
65
  return;
66
  }
67
 
 
68
  APIloading = true;
69
 
70
  const api = await getGradioApi(editableTool.baseUrl);
 
109
  if (parsedOutputComponent.success) {
110
  editableTool.outputComponent = "0;" + parsedOutputComponent.data;
111
  } else {
112
+ errors = [
113
+ {
114
+ field: "outputComponent",
115
+ message: `Invalid output component. Type ${
116
+ api.named_endpoints[editableTool.endpoint].returns?.[0]?.component
117
+ } is not yet supported. Feel free to report this issue so we can add support for it.`,
118
+ },
119
+ ];
 
 
 
120
  editableTool.outputComponent = null;
121
  }
122
 
 
145
  }
146
  }
147
 
 
 
148
  let formSubmittable = $derived(
149
  editableTool.name && editableTool.baseUrl && editableTool.outputComponent
150
  );
151
  </script>
152
 
153
  <form
 
154
  class="relative flex h-full flex-col overflow-y-auto p-4 md:p-8"
155
+ onsubmit={async (e) => {
156
+ e.preventDefault();
157
  formLoading = true;
158
+ errors = [];
159
 
160
+ try {
161
+ const body = JSON.stringify(editableTool);
162
+ let response: Response;
163
 
164
+ if (page.params.toolId) {
165
+ response = await fetch(`${base}/api/tools/${page.params.toolId}`, {
166
+ method: "PATCH",
167
+ headers: { "Content-Type": "application/json" },
168
+ body,
169
  });
170
  } else {
171
+ response = await fetch(`${base}/api/tools`, {
172
+ method: "POST",
173
+ headers: { "Content-Type": "application/json" },
174
+ body,
175
  });
176
  }
177
+
178
+ if (response.ok) {
179
+ const { toolId } = await response.json();
180
+ goto(`${base}/tools/${toolId}`, { invalidateAll: true });
181
+ } else if (response.status === 400) {
182
+ const data = await response.json();
183
+ errors = data.errors;
184
+ } else {
185
+ $errorStore = response.statusText;
186
+ }
187
+ } catch (e) {
188
+ $errorStore = (e as Error).message;
189
+ } finally {
190
+ formLoading = false;
191
+ }
192
  }}
193
  >
194
  {#if tool}
 
222
  placeholder="Image generator"
223
  bind:value={editableTool.displayName}
224
  />
225
+ <p class="text-xs text-red-500">{getError("displayName")}</p>
226
  </label>
227
 
228
  <div class="flex flex-row gap-4">
 
245
  <option value={icon}>{icon}</option>
246
  {/each}
247
  </select>
248
+ <p class="text-xs text-red-500">{getError("icon")}</p>
249
  </label>
250
 
251
  <label class="flex-grow">
 
260
  <option value={color}>{color}</option>
261
  {/each}
262
  </select>
263
+ <p class="text-xs text-red-500">{getError("color")}</p>
264
  </label>
265
  </div>
266
 
 
277
  placeholder="This tool lets you generate images using SDXL."
278
  bind:value={editableTool.description}
279
  ></textarea>
280
+ <p class="text-xs text-red-500">{getError("description")}</p>
281
  </label>
282
 
283
  <label>
 
297
  placeholder="ByteDance/Hyper-SDXL-1Step-T2I"
298
  bind:value={editableTool.baseUrl}
299
  />
300
+ <p class="text-xs text-red-500">{getError("spaceUrl")}</p>
301
  </label>
302
  <p class="text-justify text-gray-800">
303
  Tools allows models that support them to use external application directly via function
 
383
  </div>
384
 
385
  <p class="text-xs text-red-500">
386
+ {getError("inputs")}
387
  </p>
388
 
389
  {#each editableTool.inputs as input, inputIdx}
 
564
  {/if}
565
  {/if}
566
  <p class="text-xs text-red-500">
567
+ {getError("outputComponent")}
568
  </p>
569
  </label>
570
 
 
583
  class="peer rounded-lg border-2 border-gray-200 bg-gray-100 p-1"
584
  />
585
  <p class="text-xs text-red-500">
586
+ {getError("showOutput")}
587
  </p>
588
  </label>
589
  </div>
src/routes/tools/[toolId]/+page.server.ts DELETED
@@ -1,214 +0,0 @@
1
- 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 { 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";
10
- import { z } from "zod";
11
-
12
- async function toolOnlyIfAuthor(locals: App.Locals, toolId?: string) {
13
- const tool = await collections.tools.findOne({ _id: new ObjectId(toolId) });
14
-
15
- if (!tool) {
16
- throw Error("Tool not found");
17
- }
18
-
19
- if (
20
- tool.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString() &&
21
- !locals.user?.isAdmin
22
- ) {
23
- throw Error("You are not the creator of this tool");
24
- }
25
-
26
- return tool;
27
- }
28
-
29
- export const actions: Actions = {
30
- delete: async ({ params, locals }) => {
31
- let tool;
32
- try {
33
- tool = await toolOnlyIfAuthor(locals, params.toolId);
34
- } catch (e) {
35
- return fail(400, { error: true, message: (e as Error).message });
36
- }
37
-
38
- await collections.tools.deleteOne({ _id: tool._id });
39
-
40
- // Remove the tool from all users' settings
41
- await collections.settings.updateMany(
42
- {
43
- tools: { $in: [tool._id.toString()] },
44
- },
45
- {
46
- $pull: { tools: tool._id.toString() },
47
- }
48
- );
49
-
50
- // Remove the tool from all assistants
51
- await collections.assistants.updateMany(
52
- {
53
- tools: { $in: [tool._id.toString()] },
54
- },
55
- {
56
- $pull: { tools: tool._id.toString() },
57
- }
58
- );
59
-
60
- redirect(302, `${base}/tools`);
61
- },
62
- report: async ({ request, params, locals, url }) => {
63
- // is there already a report from this user for this model ?
64
- const report = await collections.reports.findOne({
65
- createdBy: locals.user?._id ?? locals.sessionId,
66
- object: "tool",
67
- contentId: new ObjectId(params.toolId),
68
- });
69
-
70
- if (report) {
71
- return fail(400, { error: true, message: "Already reported" });
72
- }
73
-
74
- const formData = await request.formData();
75
- const result = z.string().min(1).max(128).safeParse(formData?.get("reportReason"));
76
-
77
- if (!result.success) {
78
- return fail(400, { error: true, message: "Invalid report reason" });
79
- }
80
-
81
- const { acknowledged } = await collections.reports.insertOne({
82
- _id: new ObjectId(),
83
- contentId: new ObjectId(params.toolId),
84
- object: "tool",
85
- createdBy: locals.user?._id ?? locals.sessionId,
86
- createdAt: new Date(),
87
- updatedAt: new Date(),
88
- reason: result.data,
89
- });
90
-
91
- if (!acknowledged) {
92
- return fail(500, { error: true, message: "Failed to report tool" });
93
- }
94
-
95
- if (env.WEBHOOK_URL_REPORT_ASSISTANT) {
96
- const prefixUrl =
97
- envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`;
98
- const toolUrl = `${prefixUrl}/tools/${params.toolId}`;
99
-
100
- const tool = await collections.tools.findOne<Pick<Tool, "displayName" | "name">>(
101
- { _id: new ObjectId(params.toolId) },
102
- { projection: { displayName: 1, name: 1 } }
103
- );
104
-
105
- const username = locals.user?.username;
106
-
107
- await sendSlack(
108
- `🔴 Tool <${toolUrl}|${tool?.displayName ?? tool?.name}> 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
@@ -1,5 +1,5 @@
1
  <script lang="ts">
2
- import { afterNavigate, goto } from "$app/navigation";
3
  import { base } from "$app/paths";
4
  import { page } from "$app/state";
5
  import Modal from "$lib/components/Modal.svelte";
@@ -9,7 +9,7 @@
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";
13
  import CopyToClipBoardBtn from "$lib/components/CopyToClipBoardBtn.svelte";
14
 
15
  import CarbonPen from "~icons/carbon/pen";
@@ -19,6 +19,7 @@
19
  import CarbonLink from "~icons/carbon/link";
20
  import CarbonStar from "~icons/carbon/star";
21
  import CarbonLock from "~icons/carbon/locked";
 
22
 
23
  let { data } = $props();
24
 
@@ -43,10 +44,27 @@
43
  let currentModelSupportTools = $derived(
44
  data.models.find((m) => m.id === $settings.activeModel)?.tools ?? false
45
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  </script>
47
 
48
  {#if displayReportModal}
49
- <ReportModal on:close={() => (displayReportModal = false)} />
 
 
 
50
  {/if}
51
 
52
  <Modal on:close={() => goto(previousPage)} width="min-w-xl">
@@ -139,17 +157,17 @@
139
  ><CarbonPen class="mr-1.5 inline text-xs" />Edit
140
  </a>
141
  <form
142
- method="POST"
143
- action="?/delete"
144
- use:enhance={async () => {
145
- return async ({ result }) => {
146
- if (result.type === "success") {
147
- $settings.tools = ($settings?.tools ?? []).filter((t) => t !== data.tool._id);
148
  goto(`${base}/tools`, { invalidateAll: true });
149
  } else {
150
- await applyAction(result);
 
151
  }
152
- };
153
  }}
154
  >
155
  <button
@@ -195,7 +213,20 @@
195
  >
196
 
197
  {#if !data.tool?.createdByMe}
198
- <form method="POST" action="?/delete" use:enhance>
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  <button
200
  type="submit"
201
  class="flex items-center text-red-600 underline"
@@ -210,19 +241,19 @@
210
  </form>
211
  {/if}
212
  {#if data.tool?.review === ReviewStatus.PRIVATE}
213
- <form method="POST" action="?/approve" use:enhance>
214
  <button type="submit" class="flex items-center text-green-600 underline">
215
  <CarbonStar class="mr-1.5 inline text-xs" />Force feature</button
216
  >
217
  </form>
218
  {/if}
219
  {#if data.tool?.review === ReviewStatus.PENDING}
220
- <form method="POST" action="?/approve" use:enhance>
221
  <button type="submit" class="flex items-center text-green-600 underline">
222
  <CarbonStar class="mr-1.5 inline text-xs" />Approve</button
223
  >
224
  </form>
225
- <form method="POST" action="?/deny" use:enhance>
226
  <button type="submit" class="flex items-center text-red-600">
227
  <span class="mr-1.5 font-light no-underline">X</span>
228
  <span class="underline">Deny</span>
@@ -230,7 +261,7 @@
230
  </form>
231
  {/if}
232
  {#if data.tool?.review === ReviewStatus.APPROVED || data.tool?.review === ReviewStatus.DENIED}
233
- <form method="POST" action="?/unrequest" use:enhance>
234
  <button type="submit" class="flex items-center text-red-600 underline">
235
  <CarbonLock class="mr-1.5 inline text-xs " />Reset review</button
236
  >
@@ -239,15 +270,16 @@
239
  {/if}
240
  {#if data.tool?.createdByMe && data.tool?.review === ReviewStatus.PRIVATE}
241
  <form
242
- method="POST"
243
- action="?/request"
244
- use:enhance={async ({ cancel }) => {
245
  const confirmed = confirm(
246
  "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."
247
  );
 
248
  if (!confirmed) {
249
- cancel();
250
  }
 
 
251
  }}
252
  >
253
  <button type="submit" class="flex items-center underline">
 
1
  <script lang="ts">
2
+ import { afterNavigate, goto, invalidateAll } from "$app/navigation";
3
  import { base } from "$app/paths";
4
  import { page } from "$app/state";
5
  import Modal from "$lib/components/Modal.svelte";
 
9
  import { ReviewStatus } from "$lib/types/Review";
10
 
11
  import ReportModal from "../../settings/(nav)/assistants/[assistantId]/ReportModal.svelte";
12
+ import { enhance } from "$app/forms";
13
  import CopyToClipBoardBtn from "$lib/components/CopyToClipBoardBtn.svelte";
14
 
15
  import CarbonPen from "~icons/carbon/pen";
 
19
  import CarbonLink from "~icons/carbon/link";
20
  import CarbonStar from "~icons/carbon/star";
21
  import CarbonLock from "~icons/carbon/locked";
22
+ import { error } from "$lib/stores/errors";
23
 
24
  let { data } = $props();
25
 
 
44
  let currentModelSupportTools = $derived(
45
  data.models.find((m) => m.id === $settings.activeModel)?.tools ?? false
46
  );
47
+
48
+ function setFeatured(status: ReviewStatus) {
49
+ fetch(`${base}/api/tools/${data.tool?._id}/review`, {
50
+ method: "PATCH",
51
+ body: JSON.stringify({ status }),
52
+ }).then((r) => {
53
+ if (r.ok) {
54
+ invalidateAll();
55
+ } else {
56
+ console.error(r);
57
+ $error = r.statusText;
58
+ }
59
+ });
60
+ }
61
  </script>
62
 
63
  {#if displayReportModal}
64
+ <ReportModal
65
+ on:close={() => (displayReportModal = false)}
66
+ reportUrl={`${base}/api/tools/${data.tool?._id}/report`}
67
+ />
68
  {/if}
69
 
70
  <Modal on:close={() => goto(previousPage)} width="min-w-xl">
 
157
  ><CarbonPen class="mr-1.5 inline text-xs" />Edit
158
  </a>
159
  <form
160
+ onsubmit={() => {
161
+ fetch(`${base}/api/tools/${data.tool?._id}`, {
162
+ method: "DELETE",
163
+ }).then((r) => {
164
+ if (r.ok) {
 
165
  goto(`${base}/tools`, { invalidateAll: true });
166
  } else {
167
+ console.error(r);
168
+ $error = r.statusText;
169
  }
170
+ });
171
  }}
172
  >
173
  <button
 
213
  >
214
 
215
  {#if !data.tool?.createdByMe}
216
+ <form
217
+ onsubmit={() => {
218
+ fetch(`${base}/api/tools/${data.tool?._id}`, {
219
+ method: "DELETE",
220
+ }).then((r) => {
221
+ if (r.ok) {
222
+ goto(`${base}/tools`, { invalidateAll: true });
223
+ } else {
224
+ console.error(r);
225
+ $error = r.statusText;
226
+ }
227
+ });
228
+ }}
229
+ >
230
  <button
231
  type="submit"
232
  class="flex items-center text-red-600 underline"
 
241
  </form>
242
  {/if}
243
  {#if data.tool?.review === ReviewStatus.PRIVATE}
244
+ <form onsubmit={() => setFeatured(ReviewStatus.APPROVED)}>
245
  <button type="submit" class="flex items-center text-green-600 underline">
246
  <CarbonStar class="mr-1.5 inline text-xs" />Force feature</button
247
  >
248
  </form>
249
  {/if}
250
  {#if data.tool?.review === ReviewStatus.PENDING}
251
+ <form onsubmit={() => setFeatured(ReviewStatus.APPROVED)}>
252
  <button type="submit" class="flex items-center text-green-600 underline">
253
  <CarbonStar class="mr-1.5 inline text-xs" />Approve</button
254
  >
255
  </form>
256
+ <form onsubmit={() => setFeatured(ReviewStatus.DENIED)}>
257
  <button type="submit" class="flex items-center text-red-600">
258
  <span class="mr-1.5 font-light no-underline">X</span>
259
  <span class="underline">Deny</span>
 
261
  </form>
262
  {/if}
263
  {#if data.tool?.review === ReviewStatus.APPROVED || data.tool?.review === ReviewStatus.DENIED}
264
+ <form onsubmit={() => setFeatured(ReviewStatus.PRIVATE)}>
265
  <button type="submit" class="flex items-center text-red-600 underline">
266
  <CarbonLock class="mr-1.5 inline text-xs " />Reset review</button
267
  >
 
270
  {/if}
271
  {#if data.tool?.createdByMe && data.tool?.review === ReviewStatus.PRIVATE}
272
  <form
273
+ onsubmit={() => {
 
 
274
  const confirmed = confirm(
275
  "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."
276
  );
277
+
278
  if (!confirmed) {
279
+ return;
280
  }
281
+
282
+ setFeatured(ReviewStatus.PENDING);
283
  }}
284
  >
285
  <button type="submit" class="flex items-center underline">
src/routes/tools/[toolId]/edit/+page.server.ts DELETED
@@ -1,64 +0,0 @@
1
- import { base } from "$app/paths";
2
- import { 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 { generateSearchTokens } from "$lib/utils/searchTokens.js";
6
- import { error, fail, redirect } from "@sveltejs/kit";
7
- import { ObjectId } from "mongodb";
8
-
9
- export const actions = {
10
- default: async ({ request, params, locals }) => {
11
- const tool = await collections.tools.findOne({
12
- _id: new ObjectId(params.toolId),
13
- });
14
-
15
- if (!tool) {
16
- throw Error("Tool not found");
17
- }
18
-
19
- if (tool.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString()) {
20
- throw Error("You are not the creator of this tool");
21
- }
22
-
23
- // can only create tools when logged in, IF login is setup
24
- if (!locals.user && requiresUser) {
25
- const errors = [{ field: "description", message: "Must be logged in. Unauthorized" }];
26
- return fail(400, { error: true, errors });
27
- }
28
-
29
- const body = await request.formData();
30
- const toolStringified = body.get("tool");
31
-
32
- if (!toolStringified || typeof toolStringified !== "string") {
33
- error(400, "Tool is required");
34
- }
35
-
36
- const parse = editableToolSchema.safeParse(JSON.parse(toolStringified));
37
-
38
- if (!parse.success) {
39
- // Loop through the errors array and create a custom errors array
40
- const errors = parse.error.errors.map((error) => {
41
- return {
42
- field: error.path[0],
43
- message: error.message,
44
- };
45
- });
46
-
47
- return fail(400, { error: true, errors });
48
- }
49
-
50
- // modify the tool
51
- await collections.tools.updateOne(
52
- { _id: tool._id },
53
- {
54
- $set: {
55
- ...parse.data,
56
- updatedAt: new Date(),
57
- searchTokens: generateSearchTokens(parse.data.displayName),
58
- },
59
- }
60
- );
61
-
62
- redirect(302, `${base}/tools/${tool._id.toString()}`);
63
- },
64
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/tools/[toolId]/edit/+page.svelte CHANGED
@@ -2,7 +2,7 @@
2
  import Modal from "$lib/components/Modal.svelte";
3
  import ToolEdit from "../../ToolEdit.svelte";
4
 
5
- let { data, form = $bindable() } = $props();
6
  </script>
7
 
8
  <Modal
@@ -10,7 +10,6 @@
10
  width="h-[95dvh] w-[90dvw] overflow-hidden rounded-2xl bg-white shadow-2xl outline-none sm:h-[85dvh] xl:w-[1200px] 2xl:h-[75dvh]"
11
  >
12
  <ToolEdit
13
- bind:form
14
  tool={data.tool}
15
  readonly={!data.tool.createdByMe}
16
  on:close={() => {
 
2
  import Modal from "$lib/components/Modal.svelte";
3
  import ToolEdit from "../../ToolEdit.svelte";
4
 
5
+ let { data } = $props();
6
  </script>
7
 
8
  <Modal
 
10
  width="h-[95dvh] w-[90dvw] overflow-hidden rounded-2xl bg-white shadow-2xl outline-none sm:h-[85dvh] xl:w-[1200px] 2xl:h-[75dvh]"
11
  >
12
  <ToolEdit
 
13
  tool={data.tool}
14
  readonly={!data.tool.createdByMe}
15
  on:close={() => {
src/routes/tools/new/+page.server.ts DELETED
@@ -1,76 +0,0 @@
1
- import { env } from "$env/dynamic/private";
2
- 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 { 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";
10
-
11
- export const actions = {
12
- default: async ({ request, locals }) => {
13
- if (env.COMMUNITY_TOOLS !== "true") {
14
- error(403, "Community tools are not enabled");
15
- }
16
-
17
- const body = await request.formData();
18
- const toolStringified = body.get("tool");
19
-
20
- if (!toolStringified || typeof toolStringified !== "string") {
21
- error(400, "Tool is required");
22
- }
23
-
24
- const parse = editableToolSchema.safeParse(JSON.parse(toolStringified));
25
-
26
- if (!parse.success) {
27
- // Loop through the errors array and create a custom errors array
28
- const errors = parse.error.errors.map((error) => {
29
- return {
30
- field: error.path[0],
31
- message: error.message,
32
- };
33
- });
34
-
35
- return fail(400, { error: true, errors });
36
- }
37
-
38
- // can only create tools when logged in, IF login is setup
39
- if (!locals.user && requiresUser) {
40
- const errors = [{ field: "description", message: "Must be logged in. Unauthorized" }];
41
- return fail(400, { error: true, errors });
42
- }
43
-
44
- const toolCounts = await collections.tools.countDocuments({ createdById: locals.user?._id });
45
-
46
- if (usageLimits?.tools && toolCounts > usageLimits.tools) {
47
- const errors = [
48
- {
49
- field: "description",
50
- message: "You have reached the maximum number of tools. Delete some to continue.",
51
- },
52
- ];
53
- return fail(400, { error: true, errors });
54
- }
55
-
56
- if (!locals.user || !authCondition(locals)) {
57
- error(401, "Unauthorized");
58
- }
59
-
60
- const { insertedId } = await collections.tools.insertOne({
61
- ...parse.data,
62
- type: "community" as const,
63
- _id: new ObjectId(),
64
- createdById: locals.user?._id,
65
- createdByName: locals.user?.username,
66
- createdAt: new Date(),
67
- updatedAt: new Date(),
68
- last24HoursUseCount: 0,
69
- useCount: 0,
70
- review: ReviewStatus.PRIVATE,
71
- searchTokens: generateSearchTokens(parse.data.displayName),
72
- });
73
-
74
- return { toolId: insertedId.toString() };
75
- },
76
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/tools/new/+page.svelte CHANGED
@@ -1,13 +1,11 @@
1
  <script lang="ts">
2
  import Modal from "$lib/components/Modal.svelte";
3
  import ToolEdit from "../ToolEdit.svelte";
4
-
5
- let { form = $bindable() } = $props();
6
  </script>
7
 
8
  <Modal
9
  on:close={() => window.history.back()}
10
  width="h-[95dvh] w-[90dvw] overflow-hidden rounded-2xl bg-white shadow-2xl outline-none sm:h-[85dvh] xl:w-[1200px] 2xl:h-[75dvh]"
11
  >
12
- <ToolEdit bind:form on:close={() => window.history.back()} />
13
  </Modal>
 
1
  <script lang="ts">
2
  import Modal from "$lib/components/Modal.svelte";
3
  import ToolEdit from "../ToolEdit.svelte";
 
 
4
  </script>
5
 
6
  <Modal
7
  on:close={() => window.history.back()}
8
  width="h-[95dvh] w-[90dvw] overflow-hidden rounded-2xl bg-white shadow-2xl outline-none sm:h-[85dvh] xl:w-[1200px] 2xl:h-[75dvh]"
9
  >
10
+ <ToolEdit on:close={() => window.history.back()} />
11
  </Modal>