Mishig coyotte508 HF staff commited on
Commit
a4c3fca
1 Parent(s): 3bfcc01

[Assistants] trending feature (#938)

Browse files

* [Assistants] trending feature

* cleaner sort query

* create & use `AssistantStats` collection

* fix grouping

* rm usage of migration

* ♻️ Refacto lock usage / AssistantStat

* 🩹 Fix last24HoursCount.count => last24HoursCount

* 🐛 Fix collection name

* 🐛 Fix DB query

* 🐛 Fix rate limit on assitant creation

* 🐛 Fix aggregation query

* ✨ Only run refreshAssistants if assistants are enabled

* hide UI until we have enough stats collection

---------

Co-authored-by: coyotte508 <coyotte508@gmail.com>

src/hooks.server.ts CHANGED
@@ -1,6 +1,7 @@
1
  import {
2
  ADMIN_API_SECRET,
3
  COOKIE_NAME,
 
4
  EXPOSE_API,
5
  MESSAGES_BEFORE_LOGIN,
6
  PARQUET_EXPORT_SECRET,
@@ -19,9 +20,13 @@ import { sha256 } from "$lib/utils/sha256";
19
  import { addWeeks } from "date-fns";
20
  import { checkAndRunMigrations } from "$lib/migrations/migrations";
21
  import { building } from "$app/environment";
 
22
 
23
  if (!building) {
24
  await checkAndRunMigrations();
 
 
 
25
  }
26
 
27
  export const handle: Handle = async ({ event, resolve }) => {
 
1
  import {
2
  ADMIN_API_SECRET,
3
  COOKIE_NAME,
4
+ ENABLE_ASSISTANTS,
5
  EXPOSE_API,
6
  MESSAGES_BEFORE_LOGIN,
7
  PARQUET_EXPORT_SECRET,
 
20
  import { addWeeks } from "date-fns";
21
  import { checkAndRunMigrations } from "$lib/migrations/migrations";
22
  import { building } from "$app/environment";
23
+ import { refreshAssistantsCounts } from "$lib/assistantStats/refresh-assistants-counts";
24
 
25
  if (!building) {
26
  await checkAndRunMigrations();
27
+ if (ENABLE_ASSISTANTS) {
28
+ refreshAssistantsCounts();
29
+ }
30
  }
31
 
32
  export const handle: Handle = async ({ event, resolve }) => {
src/lib/assistantStats/refresh-assistants-counts.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { client, collections } from "$lib/server/database";
2
+ import { acquireLock, refreshLock } from "$lib/migrations/lock";
3
+ import type { ObjectId } from "mongodb";
4
+ import { subDays } from "date-fns";
5
+
6
+ const LOCK_KEY = "assistants.count";
7
+
8
+ let hasLock = false;
9
+ let lockId: ObjectId | null = null;
10
+
11
+ async function refreshAssistantsCountsHelper() {
12
+ if (!hasLock) {
13
+ return;
14
+ }
15
+
16
+ try {
17
+ await client.withSession((session) =>
18
+ session.withTransaction(async () => {
19
+ await collections.assistants
20
+ .aggregate([
21
+ { $project: { _id: 1 } },
22
+ { $set: { last24HoursCount: 0 } },
23
+ {
24
+ $unionWith: {
25
+ coll: "assistants.stats",
26
+ pipeline: [
27
+ { $match: { "date.at": { $gte: subDays(new Date(), 1) }, "date.span": "hour" } },
28
+ {
29
+ $group: {
30
+ _id: "$assistantId",
31
+ last24HoursCount: { $sum: "$count" },
32
+ },
33
+ },
34
+ ],
35
+ },
36
+ },
37
+ {
38
+ $group: {
39
+ _id: "$_id",
40
+ last24HoursCount: { $sum: "$last24HoursCount" },
41
+ },
42
+ },
43
+ {
44
+ $merge: {
45
+ into: "assistants",
46
+ on: "_id",
47
+ whenMatched: "merge",
48
+ whenNotMatched: "discard",
49
+ },
50
+ },
51
+ ])
52
+ .next();
53
+ })
54
+ );
55
+ } catch (e) {
56
+ console.log("Refresh assistants counter failed!");
57
+ console.error(e);
58
+ }
59
+ }
60
+
61
+ async function maintainLock() {
62
+ if (hasLock && lockId) {
63
+ hasLock = await refreshLock(LOCK_KEY, lockId);
64
+
65
+ if (!hasLock) {
66
+ lockId = null;
67
+ }
68
+ } else if (!hasLock) {
69
+ lockId = (await acquireLock(LOCK_KEY)) || null;
70
+ hasLock = !!lockId;
71
+ }
72
+
73
+ setTimeout(maintainLock, 10_000);
74
+ }
75
+
76
+ export function refreshAssistantsCounts() {
77
+ const ONE_HOUR_MS = 3_600_000;
78
+
79
+ maintainLock().then(() => {
80
+ refreshAssistantsCountsHelper();
81
+
82
+ setInterval(refreshAssistantsCountsHelper, ONE_HOUR_MS);
83
+ });
84
+ }
src/lib/migrations/lock.ts CHANGED
@@ -1,36 +1,45 @@
1
  import { collections } from "$lib/server/database";
 
2
 
3
- export async function acquireLock(key = "migrations") {
 
 
 
4
  try {
 
 
5
  const insert = await collections.semaphores.insertOne({
 
6
  key,
7
  createdAt: new Date(),
8
  updatedAt: new Date(),
9
  });
10
 
11
- return !!insert.acknowledged; // true if the document was inserted
12
  } catch (e) {
13
  // unique index violation, so there must already be a lock
14
  return false;
15
  }
16
  }
17
 
18
- export async function releaseLock(key = "migrations") {
19
  await collections.semaphores.deleteOne({
 
20
  key,
21
  });
22
  }
23
 
24
- export async function isDBLocked(key = "migrations"): Promise<boolean> {
25
  const res = await collections.semaphores.countDocuments({
26
  key,
27
  });
28
  return res > 0;
29
  }
30
 
31
- export async function refreshLock(key = "migrations") {
32
- await collections.semaphores.updateOne(
33
  {
 
34
  key,
35
  },
36
  {
@@ -39,4 +48,6 @@ export async function refreshLock(key = "migrations") {
39
  },
40
  }
41
  );
 
 
42
  }
 
1
  import { collections } from "$lib/server/database";
2
+ import { ObjectId } from "mongodb";
3
 
4
+ /**
5
+ * Returns the lock id if the lock was acquired, false otherwise
6
+ */
7
+ export async function acquireLock(key: string): Promise<ObjectId | false> {
8
  try {
9
+ const id = new ObjectId();
10
+
11
  const insert = await collections.semaphores.insertOne({
12
+ _id: id,
13
  key,
14
  createdAt: new Date(),
15
  updatedAt: new Date(),
16
  });
17
 
18
+ return insert.acknowledged ? id : false; // true if the document was inserted
19
  } catch (e) {
20
  // unique index violation, so there must already be a lock
21
  return false;
22
  }
23
  }
24
 
25
+ export async function releaseLock(key: string, lockId: ObjectId) {
26
  await collections.semaphores.deleteOne({
27
+ _id: lockId,
28
  key,
29
  });
30
  }
31
 
32
+ export async function isDBLocked(key: string): Promise<boolean> {
33
  const res = await collections.semaphores.countDocuments({
34
  key,
35
  });
36
  return res > 0;
37
  }
38
 
39
+ export async function refreshLock(key: string, lockId: ObjectId): Promise<boolean> {
40
+ const result = await collections.semaphores.updateOne(
41
  {
42
+ _id: lockId,
43
  key,
44
  },
45
  {
 
48
  },
49
  }
50
  );
51
+
52
+ return result.matchedCount > 0;
53
  }
src/lib/migrations/migrations.spec.ts CHANGED
@@ -1,8 +1,10 @@
1
- import { afterEach, describe, expect, it } from "vitest";
2
  import { migrations } from "./routines";
3
  import { acquireLock, isDBLocked, refreshLock, releaseLock } from "./lock";
4
  import { collections } from "$lib/server/database";
5
 
 
 
6
  describe("migrations", () => {
7
  it("should not have duplicates guid", async () => {
8
  const guids = migrations.map((m) => m._id.toString());
@@ -11,7 +13,7 @@ describe("migrations", () => {
11
  });
12
 
13
  it("should acquire only one lock on DB", async () => {
14
- const results = await Promise.all(new Array(1000).fill(0).map(() => acquireLock()));
15
  const locks = results.filter((r) => r);
16
 
17
  const semaphores = await collections.semaphores.find({}).toArray();
@@ -23,21 +25,24 @@ describe("migrations", () => {
23
  });
24
 
25
  it("should read the lock correctly", async () => {
26
- expect(await acquireLock()).toBe(true);
27
- expect(await isDBLocked()).toBe(true);
28
- expect(await acquireLock()).toBe(false);
29
- await releaseLock();
30
- expect(await isDBLocked()).toBe(false);
 
31
  });
32
 
33
  it("should refresh the lock", async () => {
34
- await acquireLock();
 
 
35
 
36
  // get the updatedAt time
37
 
38
  const updatedAtInitially = (await collections.semaphores.findOne({}))?.updatedAt;
39
 
40
- await refreshLock();
41
 
42
  const updatedAtAfterRefresh = (await collections.semaphores.findOne({}))?.updatedAt;
43
 
 
1
+ import { afterEach, assert, describe, expect, it } from "vitest";
2
  import { migrations } from "./routines";
3
  import { acquireLock, isDBLocked, refreshLock, releaseLock } from "./lock";
4
  import { collections } from "$lib/server/database";
5
 
6
+ const LOCK_KEY = "migrations";
7
+
8
  describe("migrations", () => {
9
  it("should not have duplicates guid", async () => {
10
  const guids = migrations.map((m) => m._id.toString());
 
13
  });
14
 
15
  it("should acquire only one lock on DB", async () => {
16
+ const results = await Promise.all(new Array(1000).fill(0).map(() => acquireLock(LOCK_KEY)));
17
  const locks = results.filter((r) => r);
18
 
19
  const semaphores = await collections.semaphores.find({}).toArray();
 
25
  });
26
 
27
  it("should read the lock correctly", async () => {
28
+ const lockId = await acquireLock(LOCK_KEY);
29
+ assert(lockId);
30
+ expect(await isDBLocked(LOCK_KEY)).toBe(true);
31
+ expect(!!(await acquireLock(LOCK_KEY))).toBe(false);
32
+ await releaseLock(LOCK_KEY, lockId);
33
+ expect(await isDBLocked(LOCK_KEY)).toBe(false);
34
  });
35
 
36
  it("should refresh the lock", async () => {
37
+ const lockId = await acquireLock(LOCK_KEY);
38
+
39
+ assert(lockId);
40
 
41
  // get the updatedAt time
42
 
43
  const updatedAtInitially = (await collections.semaphores.findOne({}))?.updatedAt;
44
 
45
+ await refreshLock(LOCK_KEY, lockId);
46
 
47
  const updatedAtAfterRefresh = (await collections.semaphores.findOne({}))?.updatedAt;
48
 
src/lib/migrations/migrations.ts CHANGED
@@ -3,6 +3,8 @@ import { migrations } from "./routines";
3
  import { acquireLock, releaseLock, isDBLocked, refreshLock } from "./lock";
4
  import { isHuggingChat } from "$lib/utils/isHuggingChat";
5
 
 
 
6
  export async function checkAndRunMigrations() {
7
  // make sure all GUIDs are unique
8
  if (new Set(migrations.map((m) => m._id.toString())).size !== migrations.length) {
@@ -25,16 +27,17 @@ export async function checkAndRunMigrations() {
25
  // connect to the database
26
  const connectedClient = await client.connect();
27
 
28
- const hasLock = await acquireLock();
29
 
30
- if (!hasLock) {
31
  // another instance already has the lock, so we exit early
32
  console.log(
33
  "[MIGRATIONS] Another instance already has the lock. Waiting for DB to be unlocked."
34
  );
35
 
 
36
  // block until the lock is released
37
- while (await isDBLocked()) {
38
  await new Promise((resolve) => setTimeout(resolve, 1000));
39
  }
40
  return;
@@ -43,7 +46,7 @@ export async function checkAndRunMigrations() {
43
  // once here, we have the lock
44
  // make sure to refresh it regularly while it's running
45
  const refreshInterval = setInterval(async () => {
46
- await refreshLock();
47
  }, 1000 * 10);
48
 
49
  // iterate over all migrations
@@ -112,5 +115,5 @@ export async function checkAndRunMigrations() {
112
  console.log("[MIGRATIONS] All migrations applied. Releasing lock");
113
 
114
  clearInterval(refreshInterval);
115
- await releaseLock();
116
  }
 
3
  import { acquireLock, releaseLock, isDBLocked, refreshLock } from "./lock";
4
  import { isHuggingChat } from "$lib/utils/isHuggingChat";
5
 
6
+ const LOCK_KEY = "migrations";
7
+
8
  export async function checkAndRunMigrations() {
9
  // make sure all GUIDs are unique
10
  if (new Set(migrations.map((m) => m._id.toString())).size !== migrations.length) {
 
27
  // connect to the database
28
  const connectedClient = await client.connect();
29
 
30
+ const lockId = await acquireLock(LOCK_KEY);
31
 
32
+ if (!lockId) {
33
  // another instance already has the lock, so we exit early
34
  console.log(
35
  "[MIGRATIONS] Another instance already has the lock. Waiting for DB to be unlocked."
36
  );
37
 
38
+ // Todo: is this necessary? Can we just return?
39
  // block until the lock is released
40
+ while (await isDBLocked(LOCK_KEY)) {
41
  await new Promise((resolve) => setTimeout(resolve, 1000));
42
  }
43
  return;
 
46
  // once here, we have the lock
47
  // make sure to refresh it regularly while it's running
48
  const refreshInterval = setInterval(async () => {
49
+ await refreshLock(LOCK_KEY, lockId);
50
  }, 1000 * 10);
51
 
52
  // iterate over all migrations
 
115
  console.log("[MIGRATIONS] All migrations applied. Releasing lock");
116
 
117
  clearInterval(refreshInterval);
118
+ await releaseLock(LOCK_KEY, lockId);
119
  }
src/lib/server/database.ts CHANGED
@@ -12,6 +12,7 @@ import type { Report } from "$lib/types/Report";
12
  import type { ConversationStats } from "$lib/types/ConversationStats";
13
  import type { MigrationResult } from "$lib/types/MigrationResult";
14
  import type { Semaphore } from "$lib/types/Semaphore";
 
15
 
16
  if (!MONGODB_URL) {
17
  throw new Error(
@@ -32,6 +33,7 @@ export function getCollections(mongoClient: MongoClient) {
32
  const conversations = db.collection<Conversation>("conversations");
33
  const conversationStats = db.collection<ConversationStats>(CONVERSATION_STATS_COLLECTION);
34
  const assistants = db.collection<Assistant>("assistants");
 
35
  const reports = db.collection<Report>("reports");
36
  const sharedConversations = db.collection<SharedConversation>("sharedConversations");
37
  const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
@@ -47,6 +49,7 @@ export function getCollections(mongoClient: MongoClient) {
47
  conversations,
48
  conversationStats,
49
  assistants,
 
50
  reports,
51
  sharedConversations,
52
  abortedGenerations,
@@ -67,6 +70,7 @@ const {
67
  conversations,
68
  conversationStats,
69
  assistants,
 
70
  reports,
71
  sharedConversations,
72
  abortedGenerations,
@@ -143,6 +147,11 @@ client.on("open", () => {
143
  assistants.createIndex({ featured: 1, userCount: -1 }).catch(console.error);
144
  assistants.createIndex({ modelId: 1, userCount: -1 }).catch(console.error);
145
  assistants.createIndex({ searchTokens: 1 }).catch(console.error);
 
 
 
 
 
146
  reports.createIndex({ assistantId: 1 }).catch(console.error);
147
  reports.createIndex({ createdBy: 1, assistantId: 1 }).catch(console.error);
148
 
 
12
  import type { ConversationStats } from "$lib/types/ConversationStats";
13
  import type { MigrationResult } from "$lib/types/MigrationResult";
14
  import type { Semaphore } from "$lib/types/Semaphore";
15
+ import type { AssistantStats } from "$lib/types/AssistantStats";
16
 
17
  if (!MONGODB_URL) {
18
  throw new Error(
 
33
  const conversations = db.collection<Conversation>("conversations");
34
  const conversationStats = db.collection<ConversationStats>(CONVERSATION_STATS_COLLECTION);
35
  const assistants = db.collection<Assistant>("assistants");
36
+ const assistantStats = db.collection<AssistantStats>("assistants.stats");
37
  const reports = db.collection<Report>("reports");
38
  const sharedConversations = db.collection<SharedConversation>("sharedConversations");
39
  const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
 
49
  conversations,
50
  conversationStats,
51
  assistants,
52
+ assistantStats,
53
  reports,
54
  sharedConversations,
55
  abortedGenerations,
 
70
  conversations,
71
  conversationStats,
72
  assistants,
73
+ assistantStats,
74
  reports,
75
  sharedConversations,
76
  abortedGenerations,
 
147
  assistants.createIndex({ featured: 1, userCount: -1 }).catch(console.error);
148
  assistants.createIndex({ modelId: 1, userCount: -1 }).catch(console.error);
149
  assistants.createIndex({ searchTokens: 1 }).catch(console.error);
150
+ assistants.createIndex({ last24HoursCount: 1 }).catch(console.error);
151
+ assistantStats
152
+ // Order of keys is important for the queries
153
+ .createIndex({ "date.span": 1, "date.at": 1, assistantId: 1 }, { unique: true })
154
+ .catch(console.error);
155
  reports.createIndex({ assistantId: 1 }).catch(console.error);
156
  reports.createIndex({ createdBy: 1, assistantId: 1 }).catch(console.error);
157
 
src/lib/types/Assistant.ts CHANGED
@@ -27,4 +27,11 @@ export interface Assistant extends Timestamps {
27
  };
28
  dynamicPrompt?: boolean;
29
  searchTokens: string[];
 
 
 
 
 
 
 
30
  }
 
27
  };
28
  dynamicPrompt?: boolean;
29
  searchTokens: string[];
30
+ last24HoursCount: number;
31
+ }
32
+
33
+ // eslint-disable-next-line no-shadow
34
+ export enum SortKey {
35
+ POPULAR = "popular",
36
+ TRENDING = "trending",
37
  }
src/lib/types/AssistantStats.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Timestamps } from "./Timestamps";
2
+ import type { Assistant } from "./Assistant";
3
+
4
+ export interface AssistantStats extends Timestamps {
5
+ assistantId: Assistant["_id"];
6
+ date: {
7
+ at: Date;
8
+ span: "hour";
9
+ };
10
+ count: number;
11
+ }
src/routes/assistants/+page.server.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { base } from "$app/paths";
2
  import { ENABLE_ASSISTANTS } from "$env/static/private";
3
  import { collections } from "$lib/server/database.js";
4
- import type { Assistant } from "$lib/types/Assistant";
5
  import type { User } from "$lib/types/User";
6
  import { generateQueryTokens } from "$lib/utils/searchTokens.js";
7
  import { error, redirect } from "@sveltejs/kit";
@@ -18,6 +18,7 @@ export const load = async ({ url, locals }) => {
18
  const pageIndex = parseInt(url.searchParams.get("p") ?? "0");
19
  const username = url.searchParams.get("user");
20
  const query = url.searchParams.get("q")?.trim() ?? null;
 
21
  const createdByCurrentUser = locals.user?.username && locals.user.username === username;
22
 
23
  let user: Pick<User, "_id"> | null = null;
@@ -41,7 +42,10 @@ export const load = async ({ url, locals }) => {
41
  const assistants = await collections.assistants
42
  .find(filter)
43
  .skip(NUM_PER_PAGE * pageIndex)
44
- .sort({ userCount: -1 })
 
 
 
45
  .limit(NUM_PER_PAGE)
46
  .toArray();
47
 
@@ -53,5 +57,6 @@ export const load = async ({ url, locals }) => {
53
  numTotalItems,
54
  numItemsPerPage: NUM_PER_PAGE,
55
  query,
 
56
  };
57
  };
 
1
  import { base } from "$app/paths";
2
  import { ENABLE_ASSISTANTS } from "$env/static/private";
3
  import { collections } from "$lib/server/database.js";
4
+ import { SortKey, type Assistant } from "$lib/types/Assistant";
5
  import type { User } from "$lib/types/User";
6
  import { generateQueryTokens } from "$lib/utils/searchTokens.js";
7
  import { error, redirect } from "@sveltejs/kit";
 
18
  const pageIndex = parseInt(url.searchParams.get("p") ?? "0");
19
  const username = url.searchParams.get("user");
20
  const query = url.searchParams.get("q")?.trim() ?? null;
21
+ const sort = url.searchParams.get("sort")?.trim() ?? SortKey.POPULAR;
22
  const createdByCurrentUser = locals.user?.username && locals.user.username === username;
23
 
24
  let user: Pick<User, "_id"> | null = null;
 
42
  const assistants = await collections.assistants
43
  .find(filter)
44
  .skip(NUM_PER_PAGE * pageIndex)
45
+ .sort({
46
+ ...(sort === SortKey.TRENDING && { last24HoursCount: -1 }),
47
+ userCount: -1,
48
+ })
49
  .limit(NUM_PER_PAGE)
50
  .toArray();
51
 
 
57
  numTotalItems,
58
  numItemsPerPage: NUM_PER_PAGE,
59
  query,
60
+ sort,
61
  };
62
  };
src/routes/assistants/+page.svelte CHANGED
@@ -22,6 +22,7 @@
22
  import { useSettingsStore } from "$lib/stores/settings";
23
  import IconInternet from "$lib/components/icons/IconInternet.svelte";
24
  import { isDesktop } from "$lib/utils/isDesktop";
 
25
 
26
  export let data: PageData;
27
 
@@ -32,6 +33,7 @@
32
  let filterInputEl: HTMLInputElement;
33
  let filterValue = data.query;
34
  let isFilterInPorgress = false;
 
35
 
36
  const onModelChange = (e: Event) => {
37
  const newUrl = getHref($page.url, {
@@ -71,6 +73,14 @@
71
  }
72
  }, SEARCH_DEBOUNCE_DELAY);
73
 
 
 
 
 
 
 
 
 
74
  const settings = useSettingsStore();
75
  </script>
76
 
@@ -130,7 +140,7 @@
130
  </a>
131
  </div>
132
 
133
- <div class="mt-7 flex items-center gap-x-2 text-sm">
134
  {#if assistantsCreator && !createdByMe}
135
  <div
136
  class="flex items-center gap-1.5 rounded-full border border-gray-300 bg-gray-50 px-3 py-1 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
@@ -198,6 +208,14 @@
198
  type="search"
199
  />
200
  </div>
 
 
 
 
 
 
 
 
201
  </div>
202
 
203
  <div class="mt-8 grid grid-cols-2 gap-3 sm:gap-5 md:grid-cols-3 lg:grid-cols-4">
 
22
  import { useSettingsStore } from "$lib/stores/settings";
23
  import IconInternet from "$lib/components/icons/IconInternet.svelte";
24
  import { isDesktop } from "$lib/utils/isDesktop";
25
+ import { SortKey } from "$lib/types/Assistant";
26
 
27
  export let data: PageData;
28
 
 
33
  let filterInputEl: HTMLInputElement;
34
  let filterValue = data.query;
35
  let isFilterInPorgress = false;
36
+ let sortValue = data.sort as SortKey;
37
 
38
  const onModelChange = (e: Event) => {
39
  const newUrl = getHref($page.url, {
 
73
  }
74
  }, SEARCH_DEBOUNCE_DELAY);
75
 
76
+ const sortAssistants = () => {
77
+ const newUrl = getHref($page.url, {
78
+ newKeys: { sort: sortValue },
79
+ existingKeys: { behaviour: "delete", keys: ["p"] },
80
+ });
81
+ goto(newUrl);
82
+ };
83
+
84
  const settings = useSettingsStore();
85
  </script>
86
 
 
140
  </a>
141
  </div>
142
 
143
+ <div class="mt-7 flex flex-wrap items-center gap-x-2 gap-y-3 text-sm">
144
  {#if assistantsCreator && !createdByMe}
145
  <div
146
  class="flex items-center gap-1.5 rounded-full border border-gray-300 bg-gray-50 px-3 py-1 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
 
208
  type="search"
209
  />
210
  </div>
211
+ <select
212
+ bind:value={sortValue}
213
+ on:change={sortAssistants}
214
+ class="hidden rounded-lg border border-gray-300 bg-gray-50 px-2 py-1 text-sm text-gray-900 focus:border-blue-700 focus:ring-blue-700 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"
215
+ >
216
+ <option value={SortKey.POPULAR}>{SortKey.POPULAR}</option>
217
+ <option value={SortKey.TRENDING}>{SortKey.TRENDING}</option>
218
+ </select>
219
  </div>
220
 
221
  <div class="mt-8 grid grid-cols-2 gap-3 sm:gap-5 md:grid-cols-3 lg:grid-cols-4">
src/routes/conversation/[id]/+server.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { MESSAGES_BEFORE_LOGIN, ENABLE_ASSISTANTS_RAG } from "$env/static/private";
 
2
  import { authCondition, requiresUser } from "$lib/server/auth";
3
  import { collections } from "$lib/server/database";
4
  import { models } from "$lib/server/models";
@@ -510,6 +511,14 @@ export async function POST({ request, locals, params, getClientAddress }) {
510
  },
511
  });
512
 
 
 
 
 
 
 
 
 
513
  // Todo: maybe we should wait for the message to be saved before ending the response - in case of errors
514
  return new Response(stream, {
515
  headers: {
 
1
  import { MESSAGES_BEFORE_LOGIN, ENABLE_ASSISTANTS_RAG } from "$env/static/private";
2
+ import { startOfHour } from "date-fns";
3
  import { authCondition, requiresUser } from "$lib/server/auth";
4
  import { collections } from "$lib/server/database";
5
  import { models } from "$lib/server/models";
 
511
  },
512
  });
513
 
514
+ if (conv.assistantId) {
515
+ await collections.assistantStats.updateOne(
516
+ { assistantId: conv.assistantId, "date.at": startOfHour(new Date()), "date.span": "hour" },
517
+ { $inc: { count: 1 } },
518
+ { upsert: true }
519
+ );
520
+ }
521
+
522
  // Todo: maybe we should wait for the message to be saved before ending the response - in case of errors
523
  return new Response(stream, {
524
  headers: {
src/routes/settings/(nav)/assistants/new/+page.server.ts CHANGED
@@ -82,7 +82,9 @@ export const actions: Actions = {
82
  return fail(400, { error: true, errors });
83
  }
84
 
85
- const assistantsCount = await collections.assistants.countDocuments(authCondition(locals));
 
 
86
 
87
  if (usageLimits?.assistants && assistantsCount > usageLimits.assistants) {
88
  const errors = [
@@ -94,8 +96,6 @@ export const actions: Actions = {
94
  return fail(400, { error: true, errors });
95
  }
96
 
97
- const createdById = locals.user?._id ?? locals.sessionId;
98
-
99
  const newAssistantId = new ObjectId();
100
 
101
  const exampleInputs: string[] = [
@@ -139,6 +139,7 @@ export const actions: Actions = {
139
  },
140
  dynamicPrompt: parse.data.dynamicPrompt,
141
  searchTokens: generateSearchTokens(parse.data.name),
 
142
  generateSettings: {
143
  temperature: parse.data.temperature,
144
  top_p: parse.data.top_p,
 
82
  return fail(400, { error: true, errors });
83
  }
84
 
85
+ const createdById = locals.user?._id ?? locals.sessionId;
86
+
87
+ const assistantsCount = await collections.assistants.countDocuments({ createdById });
88
 
89
  if (usageLimits?.assistants && assistantsCount > usageLimits.assistants) {
90
  const errors = [
 
96
  return fail(400, { error: true, errors });
97
  }
98
 
 
 
99
  const newAssistantId = new ObjectId();
100
 
101
  const exampleInputs: string[] = [
 
139
  },
140
  dynamicPrompt: parse.data.dynamicPrompt,
141
  searchTokens: generateSearchTokens(parse.data.name),
142
+ last24HoursCount: 0,
143
  generateSettings: {
144
  temperature: parse.data.temperature,
145
  top_p: parse.data.top_p,