From e85d1e1f95d45e681a4838642cc9eeb68c5b480e Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Fri, 15 Dec 2023 12:31:32 +0100 Subject: [PATCH 01/65] First push on assistants --- .env | 4 +- src/lib/components/NavConversationItem.svelte | 5 +- src/lib/components/NavMenu.svelte | 1 + .../chat/AssistantIntroduction.svelte | 98 ++++++++++++ src/lib/components/chat/ChatMessages.svelte | 34 +++- src/lib/components/chat/ChatWindow.svelte | 3 + src/lib/server/database.ts | 8 + src/lib/stores/settings.ts | 6 + src/lib/types/Assistant.ts | 15 ++ src/lib/types/Conversation.ts | 2 + src/lib/types/Report.ts | 10 ++ src/lib/types/Settings.ts | 5 + src/lib/types/SharedConversation.ts | 1 + src/routes/+layout.server.ts | 26 +++- src/routes/+page.svelte | 21 ++- .../assistant/[assistantId]/+page.server.ts | 11 ++ .../assistant/[assistantId]/+page.svelte | 82 ++++++++++ src/routes/conversation/+server.ts | 20 ++- src/routes/conversation/[id]/+page.server.ts | 10 ++ src/routes/settings/+layout.server.ts | 31 ++++ src/routes/settings/+layout.svelte | 57 ++++++- src/routes/settings/+server.ts | 3 +- .../assistants/[assistantId]/+page.server.ts | 118 ++++++++++++++ .../assistants/[assistantId]/+page.svelte | 145 ++++++++++++++++++ .../[assistantId]/avatar/+server.ts | 47 ++++++ .../settings/assistants/new/+page.server.ts | 91 +++++++++++ .../settings/assistants/new/+page.svelte | 111 ++++++++++++++ 27 files changed, 945 insertions(+), 20 deletions(-) create mode 100644 src/lib/components/chat/AssistantIntroduction.svelte create mode 100644 src/lib/types/Assistant.ts create mode 100644 src/lib/types/Report.ts create mode 100644 src/routes/assistant/[assistantId]/+page.server.ts create mode 100644 src/routes/assistant/[assistantId]/+page.svelte create mode 100644 src/routes/settings/+layout.server.ts create mode 100644 src/routes/settings/assistants/[assistantId]/+page.server.ts create mode 100644 src/routes/settings/assistants/[assistantId]/+page.svelte create mode 100644 src/routes/settings/assistants/[assistantId]/avatar/+server.ts create mode 100644 src/routes/settings/assistants/new/+page.server.ts create mode 100644 src/routes/settings/assistants/new/+page.svelte diff --git a/.env b/.env index c4d23840998..3362e6cd9fc 100644 --- a/.env +++ b/.env @@ -109,4 +109,6 @@ LLM_SUMMERIZATION=true # PUBLIC_APP_COLOR=yellow # PUBLIC_APP_DESCRIPTION="Making the community's best AI chat models available to everyone." # PUBLIC_APP_DATA_SHARING=1 -# PUBLIC_APP_DISCLAIMER=1 \ No newline at end of file +# PUBLIC_APP_DISCLAIMER=1 + +DISABLE_ASSISTANTS=false # set to true to disable assistants feature \ No newline at end of file diff --git a/src/lib/components/NavConversationItem.svelte b/src/lib/components/NavConversationItem.svelte index 88117b9f18e..c4c0ba96d41 100644 --- a/src/lib/components/NavConversationItem.svelte +++ b/src/lib/components/NavConversationItem.svelte @@ -8,7 +8,7 @@ import CarbonClose from "~icons/carbon/close"; import CarbonEdit from "~icons/carbon/edit"; - export let conv: { id: string; title: string }; + export let conv: { id: string; title: string; avatarId: string }; let confirmDelete = false; @@ -33,6 +33,9 @@ {#if confirmDelete} Delete {/if} + {#if conv.avatarId} + {conv.avatarId} + {/if} {conv.title} diff --git a/src/lib/components/NavMenu.svelte b/src/lib/components/NavMenu.svelte index 56460f71aa6..6d4ddb3d830 100644 --- a/src/lib/components/NavMenu.svelte +++ b/src/lib/components/NavMenu.svelte @@ -12,6 +12,7 @@ id: string; title: string; updatedAt: Date; + // avatarId: string; } export let conversations: Array = []; diff --git a/src/lib/components/chat/AssistantIntroduction.svelte b/src/lib/components/chat/AssistantIntroduction.svelte new file mode 100644 index 00000000000..9c1762e6b3f --- /dev/null +++ b/src/lib/components/chat/AssistantIntroduction.svelte @@ -0,0 +1,98 @@ + + +
+
+
+ {#if assistant.avatar} + avatar + {:else} +
+ {assistant?.name[0].toLocaleUpperCase()} +
+ {/if} + +
+

+ {assistant.modelId} +

+

{assistant.name}

+

+ {assistant.description} +

+ +

+ Created by {assistant.createdByName} +

+
+
+ + + +
+ {#if assistant.exampleInputs} +
+
+
+ {#each assistant.exampleInputs as example} + + + + + {/each} +
+
+
+ {/if} +
diff --git a/src/lib/components/chat/ChatMessages.svelte b/src/lib/components/chat/ChatMessages.svelte index 9ce0b115a3b..e1a70276be8 100644 --- a/src/lib/components/chat/ChatMessages.svelte +++ b/src/lib/components/chat/ChatMessages.svelte @@ -10,12 +10,17 @@ import type { WebSearchUpdate } from "$lib/types/MessageUpdate"; import { browser } from "$app/environment"; import SystemPromptModal from "../SystemPromptModal.svelte"; + import type { Assistant } from "$lib/types/Assistant"; + import AssistantIntroduction from "./AssistantIntroduction.svelte"; + import { page } from "$app/stores"; + import { base } from "$app/paths"; export let messages: Message[]; export let loading: boolean; export let pending: boolean; export let isAuthor: boolean; export let currentModel: Model; + export let assistant: Assistant | undefined; export let models: Model[]; export let preprompt: string | undefined; export let readOnly: boolean; @@ -42,7 +47,28 @@ >
{#each messages as message, i} - {#if i === 0 && preprompt && preprompt != currentModel.preprompt} + {#if i === 0 && $page.data?.assistant} + + {#if $page.data?.assistant.avatar} + Avatar + {:else} +
+ {$page.data?.assistant.name[0].toLocaleUpperCase()} +
+ {/if} + + {$page.data.assistant.name} +
+ {:else if i === 0 && preprompt && preprompt != currentModel.preprompt} {/if} {:else} - + {#if !assistant} + + {:else} + + {/if} {/each} {#if pending} ("conversations"); +const assistants = db.collection("assistants"); +const reports = db.collection("reports"); const sharedConversations = db.collection("sharedConversations"); const abortedGenerations = db.collection("abortedGenerations"); const settings = db.collection("settings"); @@ -34,6 +38,8 @@ const bucket = new GridFSBucket(db, { bucketName: "files" }); export { client, db }; export const collections = { conversations, + assistants, + reports, sharedConversations, abortedGenerations, settings, @@ -66,4 +72,6 @@ client.on("open", () => { messageEvents.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(console.error); sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(console.error); sessions.createIndex({ sessionId: 1 }, { unique: true }).catch(console.error); + assistants.createIndex({ createdBy: 1 }).catch(console.error); + reports.createIndex({ assistantId: 1 }).catch(console.error); }); diff --git a/src/lib/stores/settings.ts b/src/lib/stores/settings.ts index 51dab38dda2..b64036609cf 100644 --- a/src/lib/stores/settings.ts +++ b/src/lib/stores/settings.ts @@ -1,5 +1,8 @@ import { browser } from "$app/environment"; +import { invalidate } from "$app/navigation"; import { base } from "$app/paths"; +import { UrlDependency } from "$lib/types/UrlDependency"; +import type { ObjectId } from "mongodb"; import { getContext, setContext } from "svelte"; import { type Writable, writable, get } from "svelte/store"; @@ -11,7 +14,9 @@ type SettingsStore = { activeModel: string; customPrompts: Record; recentlySaved: boolean; + assistants: ObjectId[]; }; + export function useSettingsStore() { return getContext>("settings"); } @@ -42,6 +47,7 @@ export function createSettingsStore(initialValue: Omit ({ ...s, diff --git a/src/lib/types/Assistant.ts b/src/lib/types/Assistant.ts new file mode 100644 index 00000000000..8772b4b0a79 --- /dev/null +++ b/src/lib/types/Assistant.ts @@ -0,0 +1,15 @@ +import type { ObjectId } from "mongodb"; +import type { User } from "./User"; +import type { Timestamps } from "./Timestamps"; + +export interface Assistant extends Timestamps { + _id: ObjectId; + createdById: User["_id"] | string; // user id or session + createdByName?: User["username"]; + avatar?: boolean; + name: string; + description?: string; + modelId: string; + exampleInputs: string[]; + preprompt: string; +} diff --git a/src/lib/types/Conversation.ts b/src/lib/types/Conversation.ts index 5788ce63fd8..e036ce05a0d 100644 --- a/src/lib/types/Conversation.ts +++ b/src/lib/types/Conversation.ts @@ -2,6 +2,7 @@ import type { ObjectId } from "mongodb"; import type { Message } from "./Message"; import type { Timestamps } from "./Timestamps"; import type { User } from "./User"; +import type { Assistant } from "./Assistant"; export interface Conversation extends Timestamps { _id: ObjectId; @@ -19,4 +20,5 @@ export interface Conversation extends Timestamps { }; preprompt?: string; + assistantId?: Assistant["_id"]; } diff --git a/src/lib/types/Report.ts b/src/lib/types/Report.ts new file mode 100644 index 00000000000..f4f450f5167 --- /dev/null +++ b/src/lib/types/Report.ts @@ -0,0 +1,10 @@ +import type { ObjectId } from "mongodb"; +import type { User } from "./User"; +import type { Assistant } from "./Assistant"; +import type { Timestamps } from "./Timestamps"; + +export interface Report extends Timestamps { + _id: ObjectId; + createdBy: User["_id"] | string; + assistantId: Assistant["_id"]; +} diff --git a/src/lib/types/Settings.ts b/src/lib/types/Settings.ts index 1dc5e764eb3..5a6804e05a8 100644 --- a/src/lib/types/Settings.ts +++ b/src/lib/types/Settings.ts @@ -1,4 +1,5 @@ import { defaultModel } from "$lib/server/models"; +import type { Assistant } from "./Assistant"; import type { Timestamps } from "./Timestamps"; import type { User } from "./User"; @@ -18,6 +19,8 @@ export interface Settings extends Timestamps { // model name and system prompts customPrompts?: Record; + + assistants?: Assistant["_id"][]; } // TODO: move this to a constant file along with other constants @@ -25,4 +28,6 @@ export const DEFAULT_SETTINGS = { shareConversationsWithModelAuthors: true, activeModel: defaultModel.id, hideEmojiOnSidebar: false, + customPrompts: {}, + assistants: [], }; diff --git a/src/lib/types/SharedConversation.ts b/src/lib/types/SharedConversation.ts index 8571f2c3f3a..d36c26643ec 100644 --- a/src/lib/types/SharedConversation.ts +++ b/src/lib/types/SharedConversation.ts @@ -10,4 +10,5 @@ export interface SharedConversation extends Timestamps { title: string; messages: Message[]; preprompt?: string; + assistantId?: string; } diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 7c0958c1b5b..39c350dea7d 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -11,7 +11,9 @@ import { MESSAGES_BEFORE_LOGIN, YDC_API_KEY, USE_LOCAL_WEBSEARCH, + DISABLE_ASSISTANTS, } from "$env/static/private"; +import { ObjectId } from "mongodb"; export const load: LayoutServerLoad = async ({ locals, depends }) => { const { conversations } = collections; @@ -20,7 +22,11 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { const settings = await collections.settings.findOne(authCondition(locals)); // If the active model in settings is not valid, set it to the default model. This can happen if model was disabled. - if (settings && !validateModel(models).safeParse(settings?.activeModel).success) { + if ( + settings && + !validateModel(models).safeParse(settings?.activeModel).success && + !settings.assistants?.map((el) => el.toString())?.includes(settings?.activeModel) + ) { settings.activeModel = defaultModel.id; await collections.settings.updateOne(authCondition(locals), { $set: { activeModel: defaultModel.id }, @@ -58,6 +64,20 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { const loginRequired = requiresUser && !locals.user && userHasExceededMessages; + const disableAssistants = DISABLE_ASSISTANTS === "true"; + + const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? ""); + + const assistant = assistantActive + ? JSON.parse( + JSON.stringify( + await collections.assistants.findOne({ + _id: new ObjectId(settings?.activeModel), + }) + ) + ) + : null; + return { conversations: await conversations .find(authCondition(locals)) @@ -86,6 +106,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { settings?.shareConversationsWithModelAuthors ?? DEFAULT_SETTINGS.shareConversationsWithModelAuthors, customPrompts: settings?.customPrompts ?? {}, + assistants: settings?.assistants?.map((el) => el.toString()) ?? [], }, models: models.map((model) => ({ id: model.id, @@ -104,10 +125,13 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => { })), oldModels, user: locals.user && { + id: locals.user._id.toString(), username: locals.user.username, avatarUrl: locals.user.avatarUrl, email: locals.user.email, }, + assistant, + disableAssistants, loginRequired, loginEnabled: requiresUser, guestMode: requiresUser && messagesBeforeLogin > 0, diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index fa458cfe80b..78d93c4a3f3 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -17,14 +17,32 @@ async function createConversation(message: string) { try { loading = true; + + // check if $settings.activeModel is a valid model + // else check if it's an assistant, and use that model + // else use the first model + + const validModels = data.models.map((model) => model.id); + + let model; + if (validModels.includes($settings.activeModel)) { + model = $settings.activeModel; + } else { + if (validModels.includes(data.assistant?.modelId)) { + model = data.assistant?.modelId; + } else { + model = data.models[0].id; + } + } const res = await fetch(`${base}/conversation`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ - model: $settings.activeModel, + model, preprompt: $settings.customPrompts[$settings.activeModel], + assistantId: data.assistant?._id, }), }); @@ -60,6 +78,7 @@ createConversation(ev.detail)} {loading} + assistant={data.assistant} currentModel={findCurrentModel([...data.models, ...data.oldModels], $settings.activeModel)} models={data.models} bind:files diff --git a/src/routes/assistant/[assistantId]/+page.server.ts b/src/routes/assistant/[assistantId]/+page.server.ts new file mode 100644 index 00000000000..afd375fd309 --- /dev/null +++ b/src/routes/assistant/[assistantId]/+page.server.ts @@ -0,0 +1,11 @@ +import { collections } from "$lib/server/database.js"; +import { ObjectId } from "mongodb"; + +export const load = async ({ params }) => { + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(params.assistantId), + }); + + return { assistant: JSON.parse(JSON.stringify(assistant)) }; + // throw redirect(302, `${base}/settings/assistants/` + params.assistantId); +}; diff --git a/src/routes/assistant/[assistantId]/+page.svelte b/src/routes/assistant/[assistantId]/+page.svelte new file mode 100644 index 00000000000..f8b44293270 --- /dev/null +++ b/src/routes/assistant/[assistantId]/+page.svelte @@ -0,0 +1,82 @@ + + +
+ { + if (browser) window; + goto(previousPage); + }} + class="z-10 flex flex-col content-center items-center gap-x-10 gap-y-2 overflow-hidden rounded-2xl bg-white p-4 shadow-2xl outline-none md:grid-cols-3 md:grid-rows-[auto,1fr] md:p-8" + > + {#if data.assistant.avatar} + avatar + {:else} +
+ {data.assistant.name[0].toLocaleUpperCase()} +
+ {/if} +

+ {data.assistant.name} +

+

+ {data.assistant.description} +

+

+ Created by + {data.assistant.createdByName} + +

+ +
{ + return async ({ result }) => { + // `result` is an `ActionResult` object + if (result.type === "success") { + $settings.activeModel = data.assistant._id; + goto(`${base}`); + } else { + await applyAction(result); + } + }; + }} + > + +
+
+
diff --git a/src/routes/conversation/+server.ts b/src/routes/conversation/+server.ts index 6452e985d67..9e4ec1ca7cd 100644 --- a/src/routes/conversation/+server.ts +++ b/src/routes/conversation/+server.ts @@ -17,12 +17,11 @@ export const POST: RequestHandler = async ({ locals, request }) => { .object({ fromShare: z.string().optional(), model: validateModel(models), + assistantId: z.string().optional(), preprompt: z.string().optional(), }) .parse(JSON.parse(body)); - let preprompt = values.preprompt; - if (values.fromShare) { const conversation = await collections.sharedConversations.findOne({ _id: values.fromShare, @@ -35,7 +34,8 @@ export const POST: RequestHandler = async ({ locals, request }) => { title = conversation.title; messages = conversation.messages; values.model = conversation.model; - preprompt = conversation.preprompt; + values.preprompt = conversation.preprompt; + values.assistantId = conversation.assistantId; } const model = models.find((m) => m.name === values.model); @@ -49,7 +49,18 @@ export const POST: RequestHandler = async ({ locals, request }) => { } // Use the model preprompt if there is no conversation/preprompt in the request body - preprompt = preprompt === undefined ? model?.preprompt : preprompt; + const preprompt = await (async () => { + if (values.assistantId) { + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(values.assistantId), + }); + return assistant?.preprompt; + } else if (values.preprompt) { + return values.preprompt; + } else { + return model?.preprompt; + } + })(); const res = await collections.conversations.insertOne({ _id: new ObjectId(), @@ -57,6 +68,7 @@ export const POST: RequestHandler = async ({ locals, request }) => { messages, model: values.model, preprompt: preprompt === model?.preprompt ? model?.preprompt : preprompt, + assistantId: values.assistantId ? new ObjectId(values.assistantId) : undefined, createdAt: new Date(), updatedAt: new Date(), ...(locals.user ? { userId: locals.user._id } : { sessionId: locals.sessionId }), diff --git a/src/routes/conversation/[id]/+page.server.ts b/src/routes/conversation/[id]/+page.server.ts index ee25b61c05a..cbc80ffea12 100644 --- a/src/routes/conversation/[id]/+page.server.ts +++ b/src/routes/conversation/[id]/+page.server.ts @@ -44,11 +44,21 @@ export const load = async ({ params, depends, locals }) => { throw error(404, "Conversation not found."); } } + return { messages: conversation.messages, title: conversation.title, model: conversation.model, preprompt: conversation.preprompt, + assistant: conversation.assistantId + ? JSON.parse( + JSON.stringify( + await collections.assistants.findOne({ + _id: new ObjectId(conversation.assistantId), + }) + ) + ) + : null, shared, }; }; diff --git a/src/routes/settings/+layout.server.ts b/src/routes/settings/+layout.server.ts new file mode 100644 index 00000000000..ab46b8798db --- /dev/null +++ b/src/routes/settings/+layout.server.ts @@ -0,0 +1,31 @@ +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import type { LayoutServerLoad } from "./$types"; + +export const load = (async ({ locals, parent }) => { + const { settings } = await parent(); + + // find assistants matching the settings assistants + const assistants = await collections.assistants + .find({ + _id: { $in: settings.assistants.map((el) => new ObjectId(el)) }, + }) + .toArray(); + + return { + assistants: await Promise.all( + assistants.map(async (el) => ({ + ...el, + _id: el._id.toString(), + createdById: undefined, + createdByMe: + el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), + reported: + (await collections.reports.countDocuments({ + assistantId: el._id, + createdBy: locals.user?._id ?? locals.sessionId, + })) > 0, + })) + ), + }; +}) satisfies LayoutServerLoad; diff --git a/src/routes/settings/+layout.svelte b/src/routes/settings/+layout.svelte index 4d07caa33a0..c681867e373 100644 --- a/src/routes/settings/+layout.svelte +++ b/src/routes/settings/+layout.svelte @@ -7,6 +7,7 @@ import { useSettingsStore } from "$lib/stores/settings"; import CarbonClose from "~icons/carbon/close"; import CarbonCheckmark from "~icons/carbon/checkmark"; + import CarbonAdd from "~icons/carbon/add"; import UserIcon from "~icons/carbon/user"; export let data; @@ -48,13 +49,13 @@
+

Models

+ {#each data.models.filter((el) => !el.unlisted) as model}
{model.displayName}
{#if model.id === $settings.activeModel} @@ -66,12 +67,52 @@ {/if}
{/each} + {#if !data.disableAssistants} +

Assistants

+ {#each data.assistants as assistant} + + {#if assistant.avatar} + Avatar + {:else} +
+ {assistant.name[0].toLocaleUpperCase()} +
+ {/if} +
{assistant.name}
+ {#if assistant._id.toString() === $settings.activeModel} +
+ Active +
+ {/if} +
+ {/each} + + + +
Create new assistant
+
+ {/if} + Application Settings diff --git a/src/routes/settings/+server.ts b/src/routes/settings/+server.ts index 5455edeb2ac..81289bacba2 100644 --- a/src/routes/settings/+server.ts +++ b/src/routes/settings/+server.ts @@ -1,6 +1,5 @@ import { collections } from "$lib/server/database"; import { z } from "zod"; -import { models, validateModel } from "$lib/server/models"; import { authCondition } from "$lib/server/auth"; import { DEFAULT_SETTINGS } from "$lib/types/Settings"; @@ -14,7 +13,7 @@ export async function POST({ request, locals }) { .default(DEFAULT_SETTINGS.shareConversationsWithModelAuthors), hideEmojiOnSidebar: z.boolean().default(DEFAULT_SETTINGS.hideEmojiOnSidebar), ethicsModalAccepted: z.boolean().optional(), - activeModel: validateModel(models).default(DEFAULT_SETTINGS.activeModel), + activeModel: z.string().default(DEFAULT_SETTINGS.activeModel), customPrompts: z.record(z.string()).default({}), }) .parse(body); diff --git a/src/routes/settings/assistants/[assistantId]/+page.server.ts b/src/routes/settings/assistants/[assistantId]/+page.server.ts new file mode 100644 index 00000000000..391331eaa45 --- /dev/null +++ b/src/routes/settings/assistants/[assistantId]/+page.server.ts @@ -0,0 +1,118 @@ +import { collections } from "$lib/server/database"; +import { type Actions, fail, redirect } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; +import { authCondition } from "$lib/server/auth"; +import { base } from "$app/paths"; + +async function assistantOnlyIfAuthor(locals: App.Locals, assistantId?: string) { + const assistant = await collections.assistants.findOne({ _id: new ObjectId(assistantId) }); + + if (!assistant) { + throw Error("Assistant not found"); + } + + if (assistant.createdById.toString() !== (locals.user?._id ?? locals.sessionId).toString()) { + throw Error("You are not the author of this assistant"); + } + + return assistant; +} + +export const actions: Actions = { + delete: async ({ params, locals }) => { + let assistant; + try { + assistant = await assistantOnlyIfAuthor(locals, params.assistantId); + } catch (e) { + return fail(400, { error: true, message: (e as Error).message }); + } + + await collections.assistants.deleteOne({ _id: assistant._id }); + + // and remove it from all users settings + await collections.settings.updateMany( + {}, + { + $pull: { assistants: assistant._id }, + } + ); + + throw redirect(302, `${base}/settings`); + }, + + edit: async ({ params, locals }) => { + let assistant; + + try { + assistant = await assistantOnlyIfAuthor(locals, params.assistantId); + } catch (e) { + return fail(400, { error: true, message: (e as Error).message }); + } + + throw redirect(303, `${base}/settings/assistant/new?from=${assistant._id}`); + }, + + report: async ({ params, locals }) => { + // is there already a report from this user for this model ? + const report = await collections.reports.findOne({ + assistantId: new ObjectId(params.assistantId), + createdBy: locals.user?._id ?? locals.sessionId, + }); + + if (report) { + return fail(400, { error: true, message: "Already reported" }); + } + + const { acknowledged } = await collections.reports.insertOne({ + _id: new ObjectId(), + assistantId: new ObjectId(params.assistantId), + createdBy: locals.user?._id ?? locals.sessionId, + createdAt: new Date(), + updatedAt: new Date(), + }); + + if (!acknowledged) { + return fail(500, { error: true, message: "Failed to report assistant" }); + } + return { from: "report", ok: true, message: "Assistant reported" }; + }, + + subscribe: async ({ params, locals }) => { + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(params.assistantId), + }); + + if (!assistant) { + return fail(404, { error: true, message: "Assistant not found" }); + } + + // don't push if it's already there + const settings = await collections.settings.findOne(authCondition(locals)); + + if (settings?.assistants?.includes(assistant._id)) { + return fail(400, { error: true, message: "Already subscribed" }); + } + + await collections.settings.updateOne(authCondition(locals), { + $push: { assistants: assistant._id }, + }); + + return { from: "subscribe", ok: true, message: "Assistant added" }; + }, + + unsubscribe: async ({ params, locals }) => { + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(params.assistantId), + }); + + if (!assistant) { + return fail(404, { error: true, message: "Assistant not found" }); + } + + await collections.settings.updateOne(authCondition(locals), { + $pull: { assistants: assistant._id }, + }); + + throw redirect(302, `${base}/settings`); + }, +}; diff --git a/src/routes/settings/assistants/[assistantId]/+page.svelte b/src/routes/settings/assistants/[assistantId]/+page.svelte new file mode 100644 index 00000000000..e2d6692f703 --- /dev/null +++ b/src/routes/settings/assistants/[assistantId]/+page.svelte @@ -0,0 +1,145 @@ + + +
+
+ {#if assistant?.avatar} + + Avatar + {:else} +
+ {assistant?.name[0].toLocaleUpperCase()} +
+ {/if} + +
+

+ {assistant?.name} +

+ +

+ {assistant?.description} +

+ +

+ Model: {assistant?.modelId} +

+ +
+
+ +
+ +

System Instructions

+ + + +
+ {#if assistant?.createdByMe} +
+ +
+
+ +
+ {:else} +
+ +
+
+ +
+ {#if !assistant?.reported} +
+ +
+ {:else} + + {/if} + {/if} +
+
diff --git a/src/routes/settings/assistants/[assistantId]/avatar/+server.ts b/src/routes/settings/assistants/[assistantId]/avatar/+server.ts new file mode 100644 index 00000000000..c063301b9e7 --- /dev/null +++ b/src/routes/settings/assistants/[assistantId]/avatar/+server.ts @@ -0,0 +1,47 @@ +import { collections } from "$lib/server/database"; +import { error, type RequestHandler } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; + +export const GET: RequestHandler = async ({ params }) => { + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(params.assistantId), + }); + + // check user + if (!assistant) { + throw error(404, "No assistant found"); + } + + if (!assistant.avatar) { + throw error(404, "No avatar found"); + } + + const fileId = collections.bucket.find({ filename: assistant._id.toString() }); + + let mime = ""; + + const content = await fileId.next().then(async (file) => { + mime = file?.metadata?.mime; + + if (!file?._id) { + throw error(404, "Avatar not found"); + } + + const fileStream = collections.bucket.openDownloadStream(file?._id); + + const fileBuffer = await new Promise((resolve, reject) => { + const chunks: Uint8Array[] = []; + fileStream.on("data", (chunk) => chunks.push(chunk)); + fileStream.on("error", reject); + fileStream.on("end", () => resolve(Buffer.concat(chunks))); + }); + + return fileBuffer; + }); + + return new Response(content, { + headers: { + "Content-Type": mime ?? "application/octet-stream", + }, + }); +}; diff --git a/src/routes/settings/assistants/new/+page.server.ts b/src/routes/settings/assistants/new/+page.server.ts new file mode 100644 index 00000000000..bd4f1234351 --- /dev/null +++ b/src/routes/settings/assistants/new/+page.server.ts @@ -0,0 +1,91 @@ +import { base } from "$app/paths"; +import { authCondition, requiresUser } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { fail, type Actions, redirect } from "@sveltejs/kit"; +import { ObjectId } from "mongodb"; + +import { z } from "zod"; +import sizeof from "image-size"; + +const newAsssistantSchema = z.object({ + name: z.string().min(1), + modelId: z.string().min(1), + preprompt: z.string().min(1), + description: z.string().optional(), + exampleInputs: z.string().optional(), + avatar: z.instanceof(File).optional(), +}); + +const uploadAvatar = async (avatar: File, assistantId: ObjectId): Promise => { + const upload = collections.bucket.openUploadStream(`${assistantId.toString()}`, { + metadata: { type: avatar.type }, + }); + + upload.write((await avatar.arrayBuffer()) as unknown as Buffer); + upload.end(); + + // only return the filename when upload throws a finish event or a 10s time out occurs + return new Promise((resolve, reject) => { + upload.once("finish", () => resolve(assistantId.toString())); + upload.once("error", reject); + setTimeout(() => reject(new Error("Upload timed out")), 10000); + }); +}; + +export const actions: Actions = { + default: async ({ request, locals }) => { + const formData = Object.fromEntries(await request.formData()); + + const parse = newAsssistantSchema.safeParse(formData); + + if (!parse.success) { + // Loop through the errors array and create a custom errors array + const errors = parse.error.errors.map((error) => { + return { + field: error.path[0], + message: error.message, + }; + }); + + return fail(400, { error: true, errors }); + } + + // can only create assistants when logged in, IF login is setup + if (!locals.user && requiresUser) { + return fail(401, { error: true, message: "Unauthorized" }); + } + + const createdById = locals.user?._id ?? locals.sessionId; + + const newAssistantId = new ObjectId(); + + if (parse.data.avatar && parse.data.avatar.size > 0) { + const dims = sizeof(Buffer.from(await parse.data.avatar.arrayBuffer())); + + if ((dims.height ?? 1000) > 512 || (dims.width ?? 1000) > 512) { + return fail(400, { error: true, message: "Avatar too big" }); + } + + await uploadAvatar(parse.data.avatar, newAssistantId); + } + + const { insertedId } = await collections.assistants.insertOne({ + _id: newAssistantId, + createdById, + createdByName: locals.user?.username, + ...parse.data, + avatar: (parse?.data?.avatar?.size ?? 0) > 0, + exampleInputs: [parse.data.exampleInputs ?? ""], + createdAt: new Date(), + updatedAt: new Date(), + }); + + // add insertedId to user settings + + await collections.settings.updateOne(authCondition(locals), { + $push: { assistants: insertedId }, + }); + + throw redirect(302, `${base}/settings/assistants/${insertedId}`); + }, +}; diff --git a/src/routes/settings/assistants/new/+page.svelte b/src/routes/settings/assistants/new/+page.svelte new file mode 100644 index 00000000000..1c0a8c70bd2 --- /dev/null +++ b/src/routes/settings/assistants/new/+page.svelte @@ -0,0 +1,111 @@ + + +
{ + const avatar = formData.get("avatar"); + + if (avatar && typeof avatar !== "string" && avatar.size > 0 && compress) { + await compress(avatar, { + maxWidth: 500, + maxHeight: 500, + quality: 1, + }).then((resizedImage) => { + formData.set("avatar", resizedImage); + }); + } + }} +> +

Create new assistant

+
+
+ + + + +