victor HF staff coyotte508 HF staff coyotte508 HF staff julien-c HF staff commited on
Commit
82fcab7
1 Parent(s): 2804c18

Add settings (#134)

Browse files

Co-authored-by: Eliott C. <coyotte508@gmail.com>
Co-authored-by: coyotte508 <coyotte508@gmail.com>
Co-authored-by: Julien Chaumond <julien@huggingface.co>

.env CHANGED
@@ -15,7 +15,6 @@ PUBLIC_USER_MESSAGE_TOKEN=<|prompter|>
15
  PUBLIC_ASSISTANT_MESSAGE_TOKEN=<|assistant|>
16
  PUBLIC_SEP_TOKEN=</s>
17
  PUBLIC_PREPROMPT="Below are a series of dialogues between various people and an AI assistant. The AI tries to be helpful, polite, honest, sophisticated, emotionally aware, and humble-but-knowledgeable. The assistant is happy to help with almost anything, and will do its best to understand exactly what is needed. It also tries to avoid giving false or misleading information, and it caveats when it isn't entirely sure about the right answer. That said, the assistant is practical and really does its best, and doesn't let caution get too much in the way of being useful."
18
- PUBLIC_VERSION=0
19
  PUBLIC_GOOGLE_ANALYTICS_ID=#G-XXXXXXXX / Leave empty to disable
20
 
21
  # Copy this in .env.local with and replace "hf_<token>" your HF token from https://huggingface.co/settings/token
 
15
  PUBLIC_ASSISTANT_MESSAGE_TOKEN=<|assistant|>
16
  PUBLIC_SEP_TOKEN=</s>
17
  PUBLIC_PREPROMPT="Below are a series of dialogues between various people and an AI assistant. The AI tries to be helpful, polite, honest, sophisticated, emotionally aware, and humble-but-knowledgeable. The assistant is happy to help with almost anything, and will do its best to understand exactly what is needed. It also tries to avoid giving false or misleading information, and it caveats when it isn't entirely sure about the right answer. That said, the assistant is practical and really does its best, and doesn't let caution get too much in the way of being useful."
 
18
  PUBLIC_GOOGLE_ANALYTICS_ID=#G-XXXXXXXX / Leave empty to disable
19
 
20
  # Copy this in .env.local with and replace "hf_<token>" your HF token from https://huggingface.co/settings/token
PRIVACY.md CHANGED
@@ -1,10 +1,12 @@
1
  ## Privacy
2
 
3
- In this `v0` of HuggingChat, we only store messages to display them to the user, not for any other usage (including for research or model training purposes).
4
 
5
- Please note that in `v0`, users are not authenticated in any way, i.e. this app doesn't have access to your HF user account even if you're logged in to huggingface.co. The app is only using an anonymous session cookie. ❗️ Warning ❗️ this means if you switch browsers or clear cookies, you will currently lose your conversations.
6
 
7
- In a future version, we are considering exposing a setting for users to share their conversations with the model authors (here OpenAssistant) to improve their training data and their model over time. In other terms, model authors are the custodians of the data collected by their model, even if it's hosted on our platform.
 
 
8
 
9
  🗓 Please also consult huggingface.co's main privacy policy at https://huggingface.co/privacy. To exercise any of your legal privacy rights, please send an email to privacy@huggingface.co.
10
 
@@ -30,4 +32,4 @@ We welcome any feedback on this app: please participate to the public discussion
30
  ## Coming soon
31
 
32
  - LLM watermarking
33
- - User setting to share conversations with model authors
 
1
  ## Privacy
2
 
3
+ > Last updated: April 28, 2023
4
 
5
+ In this `v0.1` of HuggingChat, users are not authenticated in any way, i.e. this app doesn't have access to your HF user account even if you're logged in to huggingface.co. The app is only using an anonymous session cookie. ❗️ Warning ❗️ this means if you switch browsers or clear cookies, you will currently lose your conversations.
6
 
7
+ By default, your conversations are shared with the model's authors (for the `v0.1` model, to <a target="_blank" href="https://open-assistant.io/dashboard">Open Assistant</a>) to improve their training data and model over time. Model authors are the custodians of the data collected by their model, even if it's hosted on our platform.
8
+
9
+ If you disable data sharing in your settings, your conversations will not be used for any downstream usage (including for research or model training purposes), and they will only be stored to let you access past conversations. You can click on the Delete icon to delete any past conversation at any moment.
10
 
11
  🗓 Please also consult huggingface.co's main privacy policy at https://huggingface.co/privacy. To exercise any of your legal privacy rights, please send an email to privacy@huggingface.co.
12
 
 
32
  ## Coming soon
33
 
34
  - LLM watermarking
35
+ - User setting to share conversations with model authors (done ✅)
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "chat-ui",
3
- "version": "0.0.1",
4
  "private": true,
5
  "scripts": {
6
  "dev": "vite dev",
 
1
  {
2
  "name": "chat-ui",
3
+ "version": "0.1.0",
4
  "private": true,
5
  "scripts": {
6
  "dev": "vite dev",
src/lib/components/EthicsModal.svelte CHANGED
@@ -2,51 +2,43 @@
2
  import { PUBLIC_VERSION } from "$env/static/public";
3
  import Logo from "$lib/components/icons/Logo.svelte";
4
  import Modal from "$lib/components/Modal.svelte";
5
- import { onMount } from "svelte";
 
6
 
7
- let ethicsModal = false;
8
-
9
- const LOCAL_STORAGE_KEY = "has-seen-ethics-modal";
10
-
11
- onMount(() => {
12
- ethicsModal = localStorage.getItem(LOCAL_STORAGE_KEY) === null;
13
- });
14
-
15
- const handleClick = () => {
16
- ethicsModal = false;
17
- localStorage.setItem(LOCAL_STORAGE_KEY, "true");
18
- };
19
  </script>
20
 
21
- {#if ethicsModal}
22
- <Modal>
23
- <div
24
- class="flex w-full flex-col items-center gap-6 bg-gradient-to-t from-yellow-500/40 via-yellow-500/10 to-yellow-500/0 px-4 pb-10 pt-9 text-center"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  >
26
- <h2 class="flex items-center text-2xl font-semibold text-gray-800">
27
- <Logo classNames="text-3xl mr-1.5" />HuggingChat
28
- {#if typeof PUBLIC_VERSION !== "undefined"}
29
- <div
30
- class="ml-3 flex h-6 items-center rounded-lg border border-gray-100 bg-gray-50 px-2 text-base text-gray-400"
31
- >
32
- v{PUBLIC_VERSION}
33
- </div>
34
- {/if}
35
- </h2>
36
- <p class="px-4 text-lg font-semibold leading-snug text-gray-800 sm:px-12">
37
- This application is for demonstration purposes only.
38
- </p>
39
- <p class="text-gray-800">
40
- AI is an area of active research with known problems such as biased generation and
41
- misinformation. Do not use this application for high-stakes decisions or advice.
42
- </p>
43
- <button
44
- type="button"
45
- on:click={handleClick}
46
- class="mt-2 rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-yellow-500"
47
- >
48
- Start chatting
49
- </button>
50
- </div>
51
- </Modal>
52
- {/if}
 
2
  import { PUBLIC_VERSION } from "$env/static/public";
3
  import Logo from "$lib/components/icons/Logo.svelte";
4
  import Modal from "$lib/components/Modal.svelte";
5
+ import type { Settings } from "$lib/types/Settings";
6
+ import { updateSettings } from "$lib/updateSettings";
7
 
8
+ export let settings: Omit<Settings, "sessionId">;
 
 
 
 
 
 
 
 
 
 
 
9
  </script>
10
 
11
+ <Modal>
12
+ <div
13
+ class="flex w-full flex-col items-center gap-6 bg-gradient-to-t from-yellow-500/40 via-yellow-500/10 to-yellow-500/0 px-4 pb-10 pt-9 text-center"
14
+ >
15
+ <h2 class="flex items-center text-2xl font-semibold text-gray-800">
16
+ <Logo classNames="text-3xl mr-1.5" />HuggingChat
17
+ {#if typeof PUBLIC_VERSION !== "undefined"}
18
+ <div
19
+ class="ml-3 flex h-6 items-center rounded-lg border border-gray-100 bg-gray-50 px-2 text-base text-gray-400"
20
+ >
21
+ v{PUBLIC_VERSION}
22
+ </div>
23
+ {/if}
24
+ </h2>
25
+ <p class="px-4 text-lg font-semibold leading-snug text-gray-800 sm:px-12">
26
+ This application is for demonstration purposes only.
27
+ </p>
28
+ <p class="text-gray-800">
29
+ AI is an area of active research with known problems such as biased generation and
30
+ misinformation. Do not use this application for high-stakes decisions or advice.
31
+ </p>
32
+ <p class="px-2 text-sm text-gray-500">
33
+ Your conversations will be shared with model authors unless you disable it from your settings.
34
+ </p>
35
+ <!-- The updateSettings call will invalidate the settings, which will reload the page without the modal -->
36
+ <button
37
+ type="button"
38
+ on:click={() => updateSettings({ ...settings, ethicsModalAcceptedAt: new Date() })}
39
+ class="mt-2 rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-yellow-500"
40
  >
41
+ Start chatting
42
+ </button>
43
+ </div>
44
+ </Modal>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/NavMenu.svelte CHANGED
@@ -13,6 +13,7 @@
13
  const dispatch = createEventDispatcher<{
14
  shareConversation: { id: string; title: string };
15
  deleteConversation: string;
 
16
  editConversationTitle: { id: string; title: string };
17
  }>();
18
 
@@ -75,7 +76,7 @@
75
  {/each}
76
  </div>
77
  <div
78
- class="mt-0.5 flex flex-col gap-2 rounded-r-xl bg-gradient-to-l from-gray-50 p-3 text-sm dark:from-gray-800/30"
79
  >
80
  <button
81
  on:click={switchTheme}
@@ -84,6 +85,13 @@
84
  >
85
  Theme
86
  </button>
 
 
 
 
 
 
 
87
  <a
88
  href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions"
89
  target="_blank"
 
13
  const dispatch = createEventDispatcher<{
14
  shareConversation: { id: string; title: string };
15
  deleteConversation: string;
16
+ clickSettings: void;
17
  editConversationTitle: { id: string; title: string };
18
  }>();
19
 
 
76
  {/each}
77
  </div>
78
  <div
79
+ class="mt-0.5 flex flex-col gap-1 rounded-r-xl bg-gradient-to-l from-gray-50 p-3 text-sm dark:from-gray-800/30"
80
  >
81
  <button
82
  on:click={switchTheme}
 
85
  >
86
  Theme
87
  </button>
88
+ <button
89
+ on:click={() => dispatch("clickSettings")}
90
+ type="button"
91
+ class="group flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
92
+ >
93
+ Settings
94
+ </button>
95
  <a
96
  href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions"
97
  target="_blank"
src/lib/components/SettingsModal.svelte ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+
4
+ import Modal from "$lib/components/Modal.svelte";
5
+ import CarbonClose from "~icons/carbon/close";
6
+ import Switch from "$lib/components/Switch.svelte";
7
+ import type { Settings } from "$lib/types/Settings";
8
+ import { updateSettings } from "$lib/updateSettings";
9
+
10
+ export let settings: Pick<Settings, "shareConversationsWithModelAuthors">;
11
+
12
+ const dispatch = createEventDispatcher<{ close: void }>();
13
+ </script>
14
+
15
+ <Modal>
16
+ <div class="flex w-full flex-col gap-5 p-6">
17
+ <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
18
+ <h2>Settings</h2>
19
+ <button class="group" on:click={() => dispatch("close")}>
20
+ <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
21
+ </button>
22
+ </div>
23
+
24
+ <label class="flex cursor-pointer select-none items-center gap-2 text-gray-500" for="switch">
25
+ <Switch name="switch" bind:checked={settings.shareConversationsWithModelAuthors} />
26
+ Share conversations with model authors
27
+ </label>
28
+
29
+ <p class="text-gray-800">
30
+ Sharing your data will help improve the training data and make open models better over time.
31
+ </p>
32
+ <p class="text-gray-800">
33
+ Changing this setting will apply to all your conversations, past and future.
34
+ </p>
35
+ <p class="text-gray-800">
36
+ Read more about this model's authors,
37
+ <a
38
+ href="https://open-assistant.io/"
39
+ target="_blank"
40
+ rel="noreferrer"
41
+ class="underline decoration-gray-300 hover:decoration-gray-700">Open Assistant</a
42
+ >.
43
+ </p>
44
+ <button
45
+ type="button"
46
+ class="mt-2 rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 ring-gray-400 ring-offset-1 transition-colors hover:ring"
47
+ on:click={() =>
48
+ updateSettings({
49
+ shareConversationsWithModelAuthors: settings.shareConversationsWithModelAuthors,
50
+ }).then((res) => {
51
+ if (res) {
52
+ dispatch("close");
53
+ }
54
+ })}
55
+ >
56
+ Apply
57
+ </button>
58
+ </div>
59
+ </Modal>
src/lib/components/Switch.svelte ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let checked: boolean;
3
+ export let name: string;
4
+ </script>
5
+
6
+ <div
7
+ class="relative inline-flex h-5 w-9 items-center rounded-full p-1 shadow-inner transition-all {checked
8
+ ? 'bg-black'
9
+ : 'bg-gray-300 hover:bg-gray-400'}"
10
+ >
11
+ <input
12
+ bind:checked
13
+ type="checkbox"
14
+ {name}
15
+ id={name}
16
+ class="peer absolute inset-0 cursor-pointer opacity-0"
17
+ />
18
+ <div
19
+ class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-all peer-checked:translate-x-3.5"
20
+ />
21
+ </div>
src/lib/server/database.ts CHANGED
@@ -3,6 +3,7 @@ import { MongoClient } from "mongodb";
3
  import type { Conversation } from "$lib/types/Conversation";
4
  import type { SharedConversation } from "$lib/types/SharedConversation";
5
  import type { AbortedGeneration } from "$lib/types/AbortedGeneration";
 
6
 
7
  const client = new MongoClient(MONGODB_URL, {
8
  // directConnection: true
@@ -15,13 +16,16 @@ const db = client.db(MONGODB_DB_NAME);
15
  const conversations = db.collection<Conversation>("conversations");
16
  const sharedConversations = db.collection<SharedConversation>("sharedConversations");
17
  const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
 
18
 
19
  export { client, db };
20
- export const collections = { conversations, sharedConversations, abortedGenerations };
21
 
22
  client.on("open", () => {
23
  conversations.createIndex({ sessionId: 1, updatedAt: -1 });
24
  abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 });
25
  abortedGenerations.createIndex({ conversationId: 1 }, { unique: true });
26
  sharedConversations.createIndex({ hash: 1 }, { unique: true });
 
 
27
  });
 
3
  import type { Conversation } from "$lib/types/Conversation";
4
  import type { SharedConversation } from "$lib/types/SharedConversation";
5
  import type { AbortedGeneration } from "$lib/types/AbortedGeneration";
6
+ import type { Settings } from "$lib/types/Settings";
7
 
8
  const client = new MongoClient(MONGODB_URL, {
9
  // directConnection: true
 
16
  const conversations = db.collection<Conversation>("conversations");
17
  const sharedConversations = db.collection<SharedConversation>("sharedConversations");
18
  const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
19
+ const settings = db.collection<Settings>("settings");
20
 
21
  export { client, db };
22
+ export const collections = { conversations, sharedConversations, abortedGenerations, settings };
23
 
24
  client.on("open", () => {
25
  conversations.createIndex({ sessionId: 1, updatedAt: -1 });
26
  abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 });
27
  abortedGenerations.createIndex({ conversationId: 1 }, { unique: true });
28
  sharedConversations.createIndex({ hash: 1 }, { unique: true });
29
+ // Sparse so that we can have settings on userId later
30
+ settings.createIndex({ sessionId: 1 }, { unique: true, sparse: true });
31
  });
src/lib/types/AbortedGeneration.ts CHANGED
@@ -1,9 +1,8 @@
1
  // Ideally shouldn't be needed, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850
2
 
3
  import type { Conversation } from "./Conversation";
 
4
 
5
- export interface AbortedGeneration {
6
- createdAt: Date;
7
- updatedAt: Date;
8
  conversationId: Conversation["_id"];
9
  }
 
1
  // Ideally shouldn't be needed, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850
2
 
3
  import type { Conversation } from "./Conversation";
4
+ import type { Timestamps } from "./Timestamps";
5
 
6
+ export interface AbortedGeneration extends Timestamps {
 
 
7
  conversationId: Conversation["_id"];
8
  }
src/lib/types/Conversation.ts CHANGED
@@ -1,7 +1,8 @@
1
  import type { ObjectId } from "mongodb";
2
  import type { Message } from "./Message";
 
3
 
4
- export interface Conversation {
5
  _id: ObjectId;
6
 
7
  // Can be undefined for shared convo then deleted
@@ -10,9 +11,6 @@ export interface Conversation {
10
  title: string;
11
  messages: Message[];
12
 
13
- createdAt: Date;
14
- updatedAt: Date;
15
-
16
  meta?: {
17
  fromShareId?: string;
18
  };
 
1
  import type { ObjectId } from "mongodb";
2
  import type { Message } from "./Message";
3
+ import type { Timestamps } from "./Timestamps";
4
 
5
+ export interface Conversation extends Timestamps {
6
  _id: ObjectId;
7
 
8
  // Can be undefined for shared convo then deleted
 
11
  title: string;
12
  messages: Message[];
13
 
 
 
 
14
  meta?: {
15
  fromShareId?: string;
16
  };
src/lib/types/Settings.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Timestamps } from "./Timestamps";
2
+
3
+ export interface Settings extends Timestamps {
4
+ sessionId: string;
5
+
6
+ /**
7
+ * Note: Only conversations with this settings explictly set to true should be shared.
8
+ *
9
+ * This setting is explicitly set to true when users accept the ethics modal.
10
+ * */
11
+ shareConversationsWithModelAuthors: boolean;
12
+ ethicsModalAcceptedAt: Date | null;
13
+ }
src/lib/types/SharedConversation.ts CHANGED
@@ -1,13 +1,11 @@
1
  import type { Message } from "./Message";
 
2
 
3
- export interface SharedConversation {
4
  _id: string;
5
 
6
  hash: string;
7
 
8
  title: string;
9
  messages: Message[];
10
-
11
- createdAt: Date;
12
- updatedAt: Date;
13
  }
 
1
  import type { Message } from "./Message";
2
+ import type { Timestamps } from "./Timestamps";
3
 
4
+ export interface SharedConversation extends Timestamps {
5
  _id: string;
6
 
7
  hash: string;
8
 
9
  title: string;
10
  messages: Message[];
 
 
 
11
  }
src/lib/types/Timestamps.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export interface Timestamps {
2
+ createdAt: Date;
3
+ updatedAt: Date;
4
+ }
src/lib/types/UrlDependency.ts CHANGED
@@ -1,4 +1,5 @@
1
  /* eslint-disable no-shadow */
2
  export enum UrlDependency {
3
  ConversationList = "conversation:list",
 
4
  }
 
1
  /* eslint-disable no-shadow */
2
  export enum UrlDependency {
3
  ConversationList = "conversation:list",
4
+ Settings = "settings:list",
5
  }
src/lib/updateSettings.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { invalidate } from "$app/navigation";
2
+ import { base } from "$app/paths";
3
+ import { error } from "$lib/stores/errors";
4
+ import type { Settings } from "./types/Settings";
5
+ import { UrlDependency } from "./types/UrlDependency";
6
+
7
+ export async function updateSettings(
8
+ settings: Partial<Omit<Settings, "sessionId">>
9
+ ): Promise<boolean> {
10
+ try {
11
+ const res = await fetch(`${base}/settings`, {
12
+ method: "PATCH",
13
+ headers: { "Content-Type": "application/json" },
14
+ body: JSON.stringify(settings),
15
+ });
16
+ if (!res.ok) {
17
+ error.set("Error while updating settings, try again.");
18
+ return false;
19
+ }
20
+ await invalidate(UrlDependency.Settings);
21
+ return true;
22
+ } catch (err) {
23
+ console.error(err);
24
+ error.set(String(err));
25
+ return false;
26
+ }
27
+ }
src/routes/+layout.server.ts CHANGED
@@ -7,6 +7,9 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
7
  const { conversations } = collections;
8
 
9
  depends(UrlDependency.ConversationList);
 
 
 
10
 
11
  return {
12
  conversations: await conversations
@@ -22,5 +25,9 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
22
  })
23
  .map((conv) => ({ id: conv._id.toString(), title: conv.title }))
24
  .toArray(),
 
 
 
 
25
  };
26
  };
 
7
  const { conversations } = collections;
8
 
9
  depends(UrlDependency.ConversationList);
10
+ depends(UrlDependency.Settings);
11
+
12
+ const settings = await collections.settings.findOne({ sessionId: locals.sessionId });
13
 
14
  return {
15
  conversations: await conversations
 
25
  })
26
  .map((conv) => ({ id: conv._id.toString(), title: conv.title }))
27
  .toArray(),
28
+ settings: {
29
+ shareConversationsWithModelAuthors: settings?.shareConversationsWithModelAuthors ?? true,
30
+ ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null,
31
+ },
32
  };
33
  };
src/routes/+layout.svelte CHANGED
@@ -14,10 +14,12 @@
14
  import NavMenu from "$lib/components/NavMenu.svelte";
15
  import Toast from "$lib/components/Toast.svelte";
16
  import EthicsModal from "$lib/components/EthicsModal.svelte";
 
17
 
18
  export let data;
19
 
20
  let isNavOpen = false;
 
21
  let errorToastTimeout: NodeJS.Timeout;
22
  let currentError: string | null;
23
 
@@ -113,6 +115,7 @@
113
  conversations={data.conversations}
114
  on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
115
  on:deleteConversation={(ev) => deleteConversation(ev.detail)}
 
116
  on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
117
  />
118
  </MobileNav>
@@ -121,12 +124,18 @@
121
  conversations={data.conversations}
122
  on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
123
  on:deleteConversation={(ev) => deleteConversation(ev.detail)}
 
124
  on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
125
  />
126
  </nav>
127
  {#if currentError}
128
  <Toast message={currentError} />
129
  {/if}
130
- <EthicsModal />
 
 
 
 
 
131
  <slot />
132
  </div>
 
14
  import NavMenu from "$lib/components/NavMenu.svelte";
15
  import Toast from "$lib/components/Toast.svelte";
16
  import EthicsModal from "$lib/components/EthicsModal.svelte";
17
+ import SettingsModal from "$lib/components/SettingsModal.svelte";
18
 
19
  export let data;
20
 
21
  let isNavOpen = false;
22
+ let isSettingsOpen = false;
23
  let errorToastTimeout: NodeJS.Timeout;
24
  let currentError: string | null;
25
 
 
115
  conversations={data.conversations}
116
  on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
117
  on:deleteConversation={(ev) => deleteConversation(ev.detail)}
118
+ on:clickSettings={() => (isSettingsOpen = true)}
119
  on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
120
  />
121
  </MobileNav>
 
124
  conversations={data.conversations}
125
  on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
126
  on:deleteConversation={(ev) => deleteConversation(ev.detail)}
127
+ on:clickSettings={() => (isSettingsOpen = true)}
128
  on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
129
  />
130
  </nav>
131
  {#if currentError}
132
  <Toast message={currentError} />
133
  {/if}
134
+ {#if isSettingsOpen}
135
+ <SettingsModal on:close={() => (isSettingsOpen = false)} settings={data.settings} />
136
+ {/if}
137
+ {#if !data.settings.ethicsModalAcceptedAt}
138
+ <EthicsModal settings={data.settings} />
139
+ {/if}
140
  <slot />
141
  </div>
src/routes/settings/+server.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { collections } from "$lib/server/database.js";
2
+ import { subMinutes } from "date-fns";
3
+ import { z } from "zod";
4
+
5
+ export async function PATCH({ locals, request }) {
6
+ const json = await request.json();
7
+
8
+ const settings = z
9
+ .object({
10
+ shareConversationsWithModelAuthors: z.boolean().default(true),
11
+ ethicsModalAcceptedAt: z.optional(z.date({ coerce: true }).min(subMinutes(new Date(), 5))),
12
+ })
13
+ .parse(json);
14
+
15
+ await collections.settings.updateOne(
16
+ {
17
+ sessionId: locals.sessionId,
18
+ },
19
+ {
20
+ $set: {
21
+ ...settings,
22
+ updatedAt: new Date(),
23
+ },
24
+ $setOnInsert: {
25
+ createdAt: new Date(),
26
+ },
27
+ },
28
+ {
29
+ upsert: true,
30
+ }
31
+ );
32
+
33
+ return new Response();
34
+ }
svelte.config.js CHANGED
@@ -1,10 +1,13 @@
1
  import adapter from "@sveltejs/adapter-node";
2
  import { vitePreprocess } from "@sveltejs/kit/vite";
3
  import dotenv from "dotenv";
 
4
 
5
  dotenv.config({ path: "./.env.local" });
6
  dotenv.config({ path: "./.env" });
7
 
 
 
8
  /** @type {import('@sveltejs/kit').Config} */
9
  const config = {
10
  // Consult https://kit.svelte.dev/docs/integrations#preprocessors
 
1
  import adapter from "@sveltejs/adapter-node";
2
  import { vitePreprocess } from "@sveltejs/kit/vite";
3
  import dotenv from "dotenv";
4
+ import pkg from "./package.json" assert { type: "json" };
5
 
6
  dotenv.config({ path: "./.env.local" });
7
  dotenv.config({ path: "./.env" });
8
 
9
+ process.env.PUBLIC_VERSION = pkg.version.replace(/\.0\b/g, "");
10
+
11
  /** @type {import('@sveltejs/kit').Config} */
12
  const config = {
13
  // Consult https://kit.svelte.dev/docs/integrations#preprocessors