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 +5 -0
- src/lib/assistantStats/refresh-assistants-counts.ts +84 -0
- src/lib/migrations/lock.ts +17 -6
- src/lib/migrations/migrations.spec.ts +14 -9
- src/lib/migrations/migrations.ts +8 -5
- src/lib/server/database.ts +9 -0
- src/lib/types/Assistant.ts +7 -0
- src/lib/types/AssistantStats.ts +11 -0
- src/routes/assistants/+page.server.ts +7 -2
- src/routes/assistants/+page.svelte +19 -1
- src/routes/conversation/[id]/+server.ts +9 -0
- src/routes/settings/(nav)/assistants/new/+page.server.ts +4 -3
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 |
-
|
|
|
|
|
|
|
4 |
try {
|
|
|
|
|
5 |
const insert = await collections.semaphores.insertOne({
|
|
|
6 |
key,
|
7 |
createdAt: new Date(),
|
8 |
updatedAt: new Date(),
|
9 |
});
|
10 |
|
11 |
-
return
|
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
|
19 |
await collections.semaphores.deleteOne({
|
|
|
20 |
key,
|
21 |
});
|
22 |
}
|
23 |
|
24 |
-
export async function isDBLocked(key
|
25 |
const res = await collections.semaphores.countDocuments({
|
26 |
key,
|
27 |
});
|
28 |
return res > 0;
|
29 |
}
|
30 |
|
31 |
-
export async function refreshLock(key
|
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 |
-
|
27 |
-
|
28 |
-
expect(await
|
29 |
-
await
|
30 |
-
|
|
|
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
|
29 |
|
30 |
-
if (!
|
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
|
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({
|
|
|
|
|
|
|
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
|
|
|
|
|
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,
|