Skip to content

Commit dd7415c

Browse files
authored
Merge branch 'main' into front/setting_page_theme
2 parents 1bc8316 + 7b7114b commit dd7415c

File tree

17 files changed

+118
-18
lines changed

17 files changed

+118
-18
lines changed

.env

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ SERPER_API_KEY=#your serper.dev api key here
1818
SERPAPI_KEY=#your serpapi key here
1919
SERPSTACK_API_KEY=#your serpstack api key here
2020
USE_LOCAL_WEBSEARCH=#set to true to parse google results yourself, overrides other API keys
21+
SEARXNG_QUERY_URL=# where '<query>' will be replaced with query keywords see https://docs.searxng.org/dev/search_api.html eg https://searxng.yourdomain.com/search?q=<query>&engines=duckduckgo,google&format=json
2122

2223
WEBSEARCH_ALLOWLIST=`[]` # if it's defined, allow websites from only this list.
2324
WEBSEARCH_BLOCKLIST=`[]` # if it's defined, block websites from this list.
@@ -135,4 +136,6 @@ ENABLE_ASSISTANTS=false #set to true to enable assistants feature
135136

136137
ALTERNATIVE_REDIRECT_URLS=`[]` #valide alternative redirect URL for OAuth
137138

138-
WEBHOOK_URL_REPORT_ASSISTANT=#provide webhook url to get notified when an assistant gets reported
139+
WEBHOOK_URL_REPORT_ASSISTANT=#provide webhook url to get notified when an assistant gets reported
140+
141+
ALLOWED_USER_EMAILS=`[]` # if it's defined, only these emails will be allowed to use the app

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ PUBLIC_APP_DISCLAIMER=
166166

167167
You can enable the web search through an API by adding `YDC_API_KEY` ([docs.you.com](https://docs.you.com)) or `SERPER_API_KEY` ([serper.dev](https://serper.dev/)) or `SERPAPI_KEY` ([serpapi.com](https://serpapi.com/)) or `SERPSTACK_API_KEY` ([serpstack.com](https://serpstack.com/)) to your `.env.local`.
168168

169-
You can also simply enable the local websearch by setting `USE_LOCAL_WEBSEARCH=true` in your `.env.local`.
169+
You can also simply enable the local google websearch by setting `USE_LOCAL_WEBSEARCH=true` in your `.env.local` or specify a SearXNG instance by adding the query URL to `SEARXNG_QUERY_URL`.
170170

171171
### Custom models
172172

src/lib/server/database.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,11 @@ client.on("open", () => {
6767
sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(console.error);
6868
settings.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(console.error);
6969
settings.createIndex({ userId: 1 }, { unique: true, sparse: true }).catch(console.error);
70+
settings.createIndex({ assistants: 1 }).catch(console.error);
7071
users.createIndex({ hfUserId: 1 }, { unique: true }).catch(console.error);
7172
users.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(console.error);
73+
// No unicity because due to renames & outdated info from oauth provider, there may be the same username on different users
74+
users.createIndex({ username: 1 }).catch(console.error);
7275
messageEvents.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(console.error);
7376
sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(console.error);
7477
sessions.createIndex({ sessionId: 1 }, { unique: true }).catch(console.error);

src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export async function* openAIChatToTextGenerationStream(
2222
id: tokenId++,
2323
text: content ?? "",
2424
logprob: 0,
25-
special: false,
25+
special: last,
2626
},
2727
generated_text: last ? generatedText : null,
2828
details: null,

src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export async function* openAICompletionToTextGenerationStream(
2222
id: tokenId++,
2323
text,
2424
logprob: 0,
25-
special: false,
25+
special: last,
2626
},
2727
generated_text: last ? generatedText : null,
2828
details: null,

src/lib/server/models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const modelConfig = z.object({
2222
/** Used as an identifier in DB */
2323
id: z.string().optional(),
2424
/** Used to link to the model page, and for inference */
25-
name: z.string().min(1),
25+
name: z.string().default(""),
2626
displayName: z.string().min(1).optional(),
2727
description: z.string().min(1).optional(),
2828
websiteUrl: z.string().url().optional(),
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { SEARXNG_QUERY_URL } from "$env/static/private";
2+
3+
export async function searchSearxng(query: string) {
4+
const abortController = new AbortController();
5+
setTimeout(() => abortController.abort(), 10000);
6+
7+
// Insert the query into the URL template
8+
let url = SEARXNG_QUERY_URL.replace("<query>", query);
9+
10+
// Check if "&format=json" already exists in the URL
11+
if (!url.includes("&format=json")) {
12+
url += "&format=json";
13+
}
14+
15+
// Call the URL to return JSON data
16+
const jsonResponse = await fetch(url, {
17+
signal: abortController.signal,
18+
})
19+
.then((response) => response.json() as Promise<{ results: { url: string }[] }>)
20+
.catch((error) => {
21+
console.error("Failed to fetch or parse JSON", error);
22+
throw new Error("Failed to fetch or parse JSON");
23+
});
24+
25+
// Extract 'url' elements from the JSON response and trim to the top 5 URLs
26+
const urls = jsonResponse.results.slice(0, 5).map((item) => item.url);
27+
28+
if (!urls.length) {
29+
throw new Error(`Response doesn't contain any "url" elements`);
30+
}
31+
32+
// Map URLs to the correct object shape
33+
return { organic_results: urls.map((link) => ({ link })) };
34+
}

src/lib/server/websearch/searchWeb.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,33 @@ import {
55
SERPER_API_KEY,
66
SERPSTACK_API_KEY,
77
USE_LOCAL_WEBSEARCH,
8+
SEARXNG_QUERY_URL,
89
YDC_API_KEY,
910
} from "$env/static/private";
1011
import { getJson } from "serpapi";
1112
import type { GoogleParameters } from "serpapi";
1213
import { searchWebLocal } from "./searchWebLocal";
14+
import { searchSearxng } from "./searchSearxng";
1315

1416
// get which SERP api is providing web results
1517
export function getWebSearchProvider() {
16-
return YDC_API_KEY ? WebSearchProvider.YOU : WebSearchProvider.GOOGLE;
18+
if (YDC_API_KEY) {
19+
return WebSearchProvider.YOU;
20+
} else if (SEARXNG_QUERY_URL) {
21+
return WebSearchProvider.SEARXNG;
22+
} else {
23+
return WebSearchProvider.GOOGLE;
24+
}
1725
}
1826

1927
// Show result as JSON
2028
export async function searchWeb(query: string) {
2129
if (USE_LOCAL_WEBSEARCH) {
2230
return await searchWebLocal(query);
2331
}
32+
if (SEARXNG_QUERY_URL) {
33+
return await searchSearxng(query);
34+
}
2435
if (SERPER_API_KEY) {
2536
return await searchWebSerper(query);
2637
}

src/lib/types/WebSearch.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,5 @@ interface YouSearchHit {
4242
export enum WebSearchProvider {
4343
GOOGLE = "Google",
4444
YOU = "You.com",
45+
SEARXNG = "SearXNG",
4546
}

src/lib/utils/formatUserCount.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export function formatUserCount(userCount: number): string {
2+
const userCountRanges: { min: number; max: number; label: string }[] = [
3+
{ min: 0, max: 1, label: "1" },
4+
{ min: 2, max: 9, label: "1-10" },
5+
{ min: 10, max: 49, label: "10+" },
6+
{ min: 50, max: 99, label: "50+" },
7+
{ min: 100, max: 299, label: "100+" },
8+
{ min: 300, max: 499, label: "300+" },
9+
{ min: 500, max: 999, label: "500+" },
10+
{ min: 1_000, max: 2_999, label: "1k+" },
11+
{ min: 3_000, max: 4_999, label: "3k+" },
12+
{ min: 5_000, max: 9_999, label: "5k+" },
13+
{ min: 10_000, max: Infinity, label: "10k+" },
14+
];
15+
16+
const range = userCountRanges.find(({ min, max }) => userCount >= min && userCount <= max);
17+
return range?.label ?? "";
18+
}

src/routes/+layout.server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
MESSAGES_BEFORE_LOGIN,
1313
YDC_API_KEY,
1414
USE_LOCAL_WEBSEARCH,
15+
SEARXNG_QUERY_URL,
1516
ENABLE_ASSISTANTS,
1617
} from "$env/static/private";
1718
import { ObjectId } from "mongodb";
@@ -126,7 +127,8 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
126127
SERPER_API_KEY ||
127128
SERPSTACK_API_KEY ||
128129
YDC_API_KEY ||
129-
USE_LOCAL_WEBSEARCH
130+
USE_LOCAL_WEBSEARCH ||
131+
SEARXNG_QUERY_URL
130132
),
131133
ethicsModalAccepted: !!settings?.ethicsModalAcceptedAt,
132134
ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null,

src/routes/+layout.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@
157157
href="{PUBLIC_ORIGIN || $page.url.origin}{base}/{PUBLIC_APP_ASSETS}/manifest.json"
158158
/>
159159

160-
{#if PUBLIC_PLAUSIBLE_SCRIPT_URL}
160+
{#if PUBLIC_PLAUSIBLE_SCRIPT_URL && PUBLIC_ORIGIN}
161161
<script
162162
defer
163163
data-domain={new URL(PUBLIC_ORIGIN).hostname}
@@ -166,7 +166,7 @@
166166
{/if}
167167
</svelte:head>
168168

169-
{#if !$settings.ethicsModalAccepted}
169+
{#if !$settings.ethicsModalAccepted && $page.url.pathname !== "/privacy"}
170170
<DisclaimerModal />
171171
{/if}
172172

src/routes/assistant/[assistantId]/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
// `result` is an `ActionResult` object
9797
if (result.type === "success") {
9898
$settings.activeModel = data.assistant._id;
99-
goto(`${base}`);
99+
goto(`${base}` || "/");
100100
} else {
101101
await applyAction(result);
102102
}

src/routes/assistants/+page.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const load = async ({ url, locals }) => {
2626

2727
// fetch the top assistants sorted by user count from biggest to smallest, filter out all assistants with only 1 users. filter by model too if modelId is provided
2828
const filter: Filter<Assistant> = {
29-
modelId: modelId ?? { $exists: true },
29+
...(modelId && { modelId }),
3030
...(!createdByCurrentUser && { userCount: { $gt: 1 } }),
3131
...(createdByName ? { createdByName } : { featured: true }),
3232
};

src/routes/assistants/+page.svelte

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
import CarbonClose from "~icons/carbon/close";
1414
import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
1515
import CarbonEarthAmerica from "~icons/carbon/earth-americas-filled";
16-
// import CarbonViewOff from "~icons/carbon/view-off-filled";
16+
import CarbonUserMultiple from "~icons/carbon/user-multiple";
1717
import Pagination from "$lib/components/Pagination.svelte";
18+
import { formatUserCount } from "$lib/utils/formatUserCount";
1819
import { getHref } from "$lib/utils/getHref";
1920
2021
export let data: PageData;
@@ -144,13 +145,16 @@
144145
{#each data.assistants as assistant (assistant._id)}
145146
<a
146147
href="{base}/assistant/{assistant._id}"
147-
class="relative flex flex-col items-center justify-center overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 py-6 text-center shadow hover:bg-gray-50 hover:shadow-inner max-sm:px-4 sm:h-64 sm:pb-4 dark:border-gray-800/70 dark:bg-gray-950/20 dark:hover:bg-gray-950/40"
148+
class="relative flex flex-col items-center justify-center overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 py-6 text-center shadow hover:bg-gray-50 hover:shadow-inner max-sm:px-4 sm:h-64 sm:pb-4 xl:pt-8 dark:border-gray-800/70 dark:bg-gray-950/20 dark:hover:bg-gray-950/40"
148149
>
149-
<!-- {#if assistant.userCount && assistant.userCount > 1}
150-
<div class="absolute right-2 top-2" title="share with others to make it public">
151-
<CarbonViewOff class="opacity-70" />
150+
{#if assistant.userCount && assistant.userCount > 1}
151+
<div
152+
class="absolute right-3 top-3 flex items-center gap-1 text-xs text-gray-400"
153+
title="Number of users"
154+
>
155+
<CarbonUserMultiple class="text-xxs" />{formatUserCount(assistant.userCount)}
152156
</div>
153-
{/if} -->
157+
{/if}
154158
{#if assistant.avatar}
155159
<img
156160
src="{base}/settings/assistants/{assistant._id}/avatar.jpg"

src/routes/login/callback/+page.server.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import { getOIDCUserData, validateAndParseCsrfToken } from "$lib/server/auth";
33
import { z } from "zod";
44
import { base } from "$app/paths";
55
import { updateUser } from "./updateUser";
6+
import { ALLOWED_USER_EMAILS } from "$env/static/private";
7+
import JSON5 from "json5";
8+
9+
const allowedUserEmails = z
10+
.array(z.string().email())
11+
.optional()
12+
.default([])
13+
.parse(JSON5.parse(ALLOWED_USER_EMAILS));
614

715
export async function load({ url, locals, cookies, request, getClientAddress }) {
816
const { error: errorName, error_description: errorDescription } = z
@@ -33,6 +41,20 @@ export async function load({ url, locals, cookies, request, getClientAddress })
3341

3442
const { userData } = await getOIDCUserData({ redirectURI: validatedToken.redirectUrl }, code);
3543

44+
// Filter by allowed user emails
45+
if (allowedUserEmails.length > 0) {
46+
if (!userData.email) {
47+
throw error(403, "User not allowed: email not returned");
48+
}
49+
const emailVerified = userData.email_verified ?? true;
50+
if (!emailVerified) {
51+
throw error(403, "User not allowed: email not verified");
52+
}
53+
if (!allowedUserEmails.includes(userData.email)) {
54+
throw error(403, "User not allowed");
55+
}
56+
}
57+
3658
await updateUser({
3759
userData,
3860
locals,

src/routes/settings/assistants/[assistantId]/+page.server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ export const actions: Actions = {
3333

3434
// and remove it from all users settings
3535
await collections.settings.updateMany(
36-
{},
36+
{
37+
assistants: { $in: [assistant._id] },
38+
},
3739
{
3840
$pull: { assistants: assistant._id },
3941
}

0 commit comments

Comments
 (0)