Spaces:
Running
Running
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 +65 -33
- src/routes/api/assistant/+server.ts +108 -0
- src/routes/api/assistant/[id]/+server.ts +162 -0
- src/routes/api/assistant/[id]/report/+server.ts +65 -0
- src/routes/api/assistant/[id]/review/+server.ts +75 -0
- src/routes/api/assistant/[id]/subscribe/+server.ts +64 -0
- src/routes/{settings/(nav)/assistants/new/+page.server.ts → api/assistant/utils.ts} +9 -115
- src/routes/api/tools/+server.ts +68 -0
- src/routes/api/tools/[toolId]/+server.ts +93 -0
- src/routes/api/tools/[toolId]/report/+server.ts +65 -0
- src/routes/api/tools/[toolId]/review/+server.ts +72 -0
- src/routes/settings/(nav)/+layout.svelte +16 -11
- src/routes/settings/(nav)/assistants/[assistantId]/+page.server.ts +0 -267
- src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte +71 -13
- src/routes/settings/(nav)/assistants/[assistantId]/ReportModal.svelte +12 -7
- src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.server.ts +0 -193
- src/routes/settings/(nav)/assistants/[assistantId]/edit/[email protected] +3 -4
- src/routes/settings/(nav)/assistants/new/[email protected] +3 -4
- src/routes/tools/ToolEdit.svelte +52 -47
- src/routes/tools/[toolId]/+page.server.ts +0 -214
- src/routes/tools/[toolId]/+page.svelte +52 -20
- src/routes/tools/[toolId]/edit/+page.server.ts +0 -64
- src/routes/tools/[toolId]/edit/+page.svelte +1 -2
- src/routes/tools/new/+page.server.ts +0 -76
- src/routes/tools/new/+page.svelte +1 -3
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 |
-
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
78 |
return;
|
79 |
}
|
80 |
files = inputEl.files;
|
@@ -83,8 +79,8 @@
|
|
83 |
}
|
84 |
}
|
85 |
|
86 |
-
function getError(field: string
|
87 |
-
return
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
162 |
-
|
163 |
-
await
|
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"
|
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"
|
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"
|
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"
|
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"
|
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"
|
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"
|
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"
|
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"
|
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 {
|
2 |
-
import {
|
|
|
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
|
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
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
132 |
<button
|
133 |
type="submit"
|
134 |
class="flex items-center underline"
|
@@ -142,7 +173,20 @@
|
|
142 |
</button>
|
143 |
</form>
|
144 |
{:else}
|
145 |
-
<form
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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
|
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
|
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
|
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 |
-
|
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 |
-
|
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 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
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
|
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
|
12 |
|
13 |
let assistant = data.assistants.find((el) => el._id.toString() === page.params.assistantId);
|
14 |
</script>
|
15 |
|
16 |
-
<AssistantSettings
|
|
|
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
|
11 |
</script>
|
12 |
|
13 |
-
<AssistantSettings
|
|
|
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
|
|
|
35 |
|
36 |
-
function getError(field: string
|
37 |
-
return
|
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 |
-
|
122 |
-
|
123 |
-
|
124 |
-
{
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
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 |
-
|
|
|
171 |
formLoading = true;
|
|
|
172 |
|
173 |
-
|
|
|
|
|
174 |
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
});
|
181 |
} else {
|
182 |
-
await
|
183 |
-
|
|
|
|
|
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"
|
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"
|
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"
|
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"
|
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"
|
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(
|
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"
|
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"
|
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 {
|
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
|
|
|
|
|
|
|
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 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
if (
|
147 |
-
$settings.tools = ($settings?.tools ?? []).filter((t) => t !== data.tool._id);
|
148 |
goto(`${base}/tools`, { invalidateAll: true });
|
149 |
} else {
|
150 |
-
|
|
|
151 |
}
|
152 |
-
};
|
153 |
}}
|
154 |
>
|
155 |
<button
|
@@ -195,7 +213,20 @@
|
|
195 |
>
|
196 |
|
197 |
{#if !data.tool?.createdByMe}
|
198 |
-
<form
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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
|
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
|
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
|
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 |
-
|
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 |
-
|
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
|
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
|
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>
|