nsarrazin HF staff Galén coyotte508 HF staff victor HF staff commited on
Commit
2cb745f
1 Parent(s): 3cbea34

Reworked settings menu (#591)

Browse files

* initial work on new settings page

* new settings - fully working

* tweak to settings layout

* Added model links

* lint

* redirect on bad model

* delete error page

* fix links & reset button

* add saving indicator

* Make new settings work on mobile

* small tweak mobile

* Fix preprompt so it gets read correctly from the model.preprompt (#595)

* 🔒️ Harden session ID generator (#599)

* bump svelte & related to latest (#600)

* Update dockerfile to node 20 (#601)

* Only refresh cookie on post (#606)

* Session management improvements: Multi sessions, renew on login/logout (#603)

* wip: update sessionId on every login

* comment out object.freeze

* only refresh cookies on post

* Add support for multiple sessions per user

* fix tests

* 🛂 Hash sessionId in DB

* 🐛 do not forget about event.locals.sessionId

* Update src/lib/server/auth.ts

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

* Add `expiresAt` field

* remove index causing errors

* Fix bug where sessions were not properly being deleted on logout

* Moved session refresh outside of form content check

---------

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

* fix settings custom prompt bug

* simplify

WIP

* mobile

* misc

* mobile

* remove debug log

* Fix switches working only on label

* Revert "Fix switches working only on label"

This reverts commit b46d852316603da660de978d8338261adc6dc07b.

* Switch fix

---------

Co-authored-by: Galén <105213101+galen-ft@users.noreply.github.com>
Co-authored-by: Eliott C <coyotte508@gmail.com>
Co-authored-by: Victor Mustar <victor.mustar@gmail.com>

package-lock.json CHANGED
@@ -10,6 +10,7 @@
10
  "dependencies": {
11
  "@huggingface/hub": "^0.5.1",
12
  "@huggingface/inference": "^2.6.3",
 
13
  "@xenova/transformers": "^2.6.0",
14
  "autoprefixer": "^10.4.14",
15
  "browser-image-resizer": "^2.4.1",
@@ -587,6 +588,14 @@
587
  "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
588
  "dev": true
589
  },
 
 
 
 
 
 
 
 
590
  "node_modules/@iconify-json/carbon": {
591
  "version": "1.1.16",
592
  "resolved": "https://registry.npmjs.org/@iconify-json/carbon/-/carbon-1.1.16.tgz",
@@ -608,8 +617,7 @@
608
  "node_modules/@iconify/types": {
609
  "version": "2.0.0",
610
  "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
611
- "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
612
- "dev": true
613
  },
614
  "node_modules/@iconify/utils": {
615
  "version": "2.1.5",
 
10
  "dependencies": {
11
  "@huggingface/hub": "^0.5.1",
12
  "@huggingface/inference": "^2.6.3",
13
+ "@iconify-json/bi": "^1.1.21",
14
  "@xenova/transformers": "^2.6.0",
15
  "autoprefixer": "^10.4.14",
16
  "browser-image-resizer": "^2.4.1",
 
588
  "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
589
  "dev": true
590
  },
591
+ "node_modules/@iconify-json/bi": {
592
+ "version": "1.1.21",
593
+ "resolved": "https://registry.npmjs.org/@iconify-json/bi/-/bi-1.1.21.tgz",
594
+ "integrity": "sha512-6TaRGfIoelS9GBxU4SHkj59pbKliI0WQK4jq2hjuDFE49wrtvREyktOXfyKD11UjMGqx3EpSQKQVEZqaTzmrxA==",
595
+ "dependencies": {
596
+ "@iconify/types": "*"
597
+ }
598
+ },
599
  "node_modules/@iconify-json/carbon": {
600
  "version": "1.1.16",
601
  "resolved": "https://registry.npmjs.org/@iconify-json/carbon/-/carbon-1.1.16.tgz",
 
617
  "node_modules/@iconify/types": {
618
  "version": "2.0.0",
619
  "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
620
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
 
621
  },
622
  "node_modules/@iconify/utils": {
623
  "version": "2.1.5",
package.json CHANGED
@@ -46,6 +46,7 @@
46
  "dependencies": {
47
  "@huggingface/hub": "^0.5.1",
48
  "@huggingface/inference": "^2.6.3",
 
49
  "@xenova/transformers": "^2.6.0",
50
  "autoprefixer": "^10.4.14",
51
  "browser-image-resizer": "^2.4.1",
 
46
  "dependencies": {
47
  "@huggingface/hub": "^0.5.1",
48
  "@huggingface/inference": "^2.6.3",
49
+ "@iconify-json/bi": "^1.1.21",
50
  "@xenova/transformers": "^2.6.0",
51
  "autoprefixer": "^10.4.14",
52
  "browser-image-resizer": "^2.4.1",
src/lib/actions/clickOutside.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function clickOutside(element: HTMLDialogElement, callbackFunction: () => void) {
2
+ function onClick(event: MouseEvent) {
3
+ if (!element.contains(event.target as Node)) {
4
+ callbackFunction();
5
+ }
6
+ }
7
+
8
+ document.body.addEventListener("click", onClick);
9
+
10
+ return {
11
+ update(newCallbackFunction: () => void) {
12
+ callbackFunction = newCallbackFunction;
13
+ },
14
+ destroy() {
15
+ document.body.removeEventListener("click", onClick);
16
+ },
17
+ };
18
+ }
src/lib/components/DisclaimerModal.svelte CHANGED
@@ -4,11 +4,11 @@
4
  import { PUBLIC_APP_DESCRIPTION, PUBLIC_APP_NAME } from "$env/static/public";
5
  import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
6
  import Modal from "$lib/components/Modal.svelte";
 
7
  import { cookiesAreEnabled } from "$lib/utils/cookiesAreEnabled";
8
- import type { LayoutData } from "../../routes/$types";
9
  import Logo from "./icons/Logo.svelte";
10
 
11
- export let settings: LayoutData["settings"];
12
  </script>
13
 
14
  <Modal>
@@ -31,36 +31,26 @@
31
 
32
  <div class="flex w-full flex-col items-center gap-2">
33
  {#if $page.data.guestMode || !$page.data.loginEnabled}
34
- <form action="{base}/settings" method="POST" class="w-full">
35
- <input type="hidden" name="ethicsModalAccepted" value={true} />
36
- {#each Object.entries(settings).filter(([k]) => !(k === "customPrompts")) as [key, val]}
37
- <input type="hidden" name={key} value={val} />
38
- {/each}
39
- <input
40
- type="hidden"
41
- name="customPrompts"
42
- value={JSON.stringify(settings.customPrompts)}
43
- />
44
- <button
45
- type="submit"
46
- class="w-full justify-center rounded-full border-2 border-gray-300 bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
47
- class:bg-white={$page.data.loginEnabled}
48
- class:text-gray-800={$page.data.loginEnabled}
49
- class:hover:bg-slate-100={$page.data.loginEnabled}
50
- on:click={(e) => {
51
- if (!cookiesAreEnabled()) {
52
- e.preventDefault();
53
- window.open(window.location.href, "_blank");
54
- }
55
- }}
56
- >
57
- {#if $page.data.loginEnabled}
58
- Try as guest
59
- {:else}
60
- Start chatting
61
- {/if}
62
- </button>
63
- </form>
64
  {/if}
65
  {#if $page.data.loginEnabled}
66
  <form action="{base}/login" target="_parent" method="POST" class="w-full">
 
4
  import { PUBLIC_APP_DESCRIPTION, PUBLIC_APP_NAME } from "$env/static/public";
5
  import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
6
  import Modal from "$lib/components/Modal.svelte";
7
+ import { useSettingsStore } from "$lib/stores/settings";
8
  import { cookiesAreEnabled } from "$lib/utils/cookiesAreEnabled";
 
9
  import Logo from "./icons/Logo.svelte";
10
 
11
+ const settings = useSettingsStore();
12
  </script>
13
 
14
  <Modal>
 
31
 
32
  <div class="flex w-full flex-col items-center gap-2">
33
  {#if $page.data.guestMode || !$page.data.loginEnabled}
34
+ <button
35
+ class="w-full justify-center rounded-full border-2 border-gray-300 bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
36
+ class:bg-white={$page.data.loginEnabled}
37
+ class:text-gray-800={$page.data.loginEnabled}
38
+ class:hover:bg-slate-100={$page.data.loginEnabled}
39
+ on:click={(e) => {
40
+ if (!cookiesAreEnabled()) {
41
+ e.preventDefault();
42
+ window.open(window.location.href, "_blank");
43
+ }
44
+
45
+ $settings.ethicsModalAccepted = true;
46
+ }}
47
+ >
48
+ {#if $page.data.loginEnabled}
49
+ Try as guest
50
+ {:else}
51
+ Start chatting
52
+ {/if}
53
+ </button>
 
 
 
 
 
 
 
 
 
 
54
  {/if}
55
  {#if $page.data.loginEnabled}
56
  <form action="{base}/login" target="_parent" method="POST" class="w-full">
src/lib/components/LoginModal.svelte CHANGED
@@ -4,9 +4,11 @@
4
  import { PUBLIC_APP_DESCRIPTION, PUBLIC_APP_NAME } from "$env/static/public";
5
  import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
6
  import Modal from "$lib/components/Modal.svelte";
7
- import type { LayoutData } from "../../routes/$types";
 
8
  import Logo from "./icons/Logo.svelte";
9
- export let settings: LayoutData["settings"];
 
10
  </script>
11
 
12
  <Modal on:close>
@@ -42,13 +44,16 @@
42
  {/if}
43
  </button>
44
  {:else}
45
- <input type="hidden" name="ethicsModalAccepted" value={true} />
46
- {#each Object.entries(settings) as [key, val]}
47
- <input type="hidden" name={key} value={val} />
48
- {/each}
49
  <button
50
- type="submit"
51
  class="flex w-full items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
 
 
 
 
 
 
 
 
52
  >
53
  Start chatting
54
  </button>
 
4
  import { PUBLIC_APP_DESCRIPTION, PUBLIC_APP_NAME } from "$env/static/public";
5
  import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
6
  import Modal from "$lib/components/Modal.svelte";
7
+ import { useSettingsStore } from "$lib/stores/settings";
8
+ import { cookiesAreEnabled } from "$lib/utils/cookiesAreEnabled";
9
  import Logo from "./icons/Logo.svelte";
10
+
11
+ const settings = useSettingsStore();
12
  </script>
13
 
14
  <Modal on:close>
 
44
  {/if}
45
  </button>
46
  {:else}
 
 
 
 
47
  <button
 
48
  class="flex w-full items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
49
+ on:click={(e) => {
50
+ if (!cookiesAreEnabled()) {
51
+ e.preventDefault();
52
+ window.open(window.location.href, "_blank");
53
+ }
54
+
55
+ $settings.ethicsModalAccepted = true;
56
+ }}
57
  >
58
  Start chatting
59
  </button>
src/lib/components/ModelsModal.svelte DELETED
@@ -1,153 +0,0 @@
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 CarbonCheckmark from "~icons/carbon/checkmark-filled";
7
- import ModelCardMetadata from "./ModelCardMetadata.svelte";
8
- import type { Model } from "$lib/types/Model";
9
- import type { LayoutData } from "../../routes/$types";
10
- import { enhance } from "$app/forms";
11
- import { base } from "$app/paths";
12
-
13
- import CarbonEdit from "~icons/carbon/edit";
14
- import CarbonSave from "~icons/carbon/save";
15
- import CarbonRestart from "~icons/carbon/restart";
16
-
17
- export let settings: LayoutData["settings"];
18
- export let models: Array<Model>;
19
-
20
- let selectedModelId = settings.activeModel;
21
-
22
- const dispatch = createEventDispatcher<{ close: void }>();
23
-
24
- let expanded = false;
25
-
26
- function onToggle() {
27
- if (expanded) {
28
- settings.customPrompts[selectedModelId] = value;
29
- }
30
- expanded = !expanded;
31
- }
32
-
33
- let value = "";
34
-
35
- function onModelChange() {
36
- value =
37
- settings.customPrompts[selectedModelId] ??
38
- models.filter((el) => el.id === selectedModelId)[0].preprompt ??
39
- "";
40
- }
41
-
42
- $: selectedModelId, onModelChange();
43
- </script>
44
-
45
- <Modal width="max-w-lg" on:close>
46
- <form
47
- action="{base}/settings"
48
- method="post"
49
- on:submit={() => {
50
- if (expanded) {
51
- onToggle();
52
- }
53
- }}
54
- use:enhance={() => {
55
- dispatch("close");
56
- }}
57
- class="flex w-full flex-col gap-5 p-6"
58
- >
59
- {#each Object.entries(settings).filter(([k]) => !(k == "activeModel" || k === "customPrompts")) as [key, val]}
60
- <input type="hidden" name={key} value={val} />
61
- {/each}
62
- <input type="hidden" name="customPrompts" value={JSON.stringify(settings.customPrompts)} />
63
- <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
64
- <h2>Models</h2>
65
- <button type="button" class="group" on:click={() => dispatch("close")}>
66
- <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
67
- </button>
68
- </div>
69
-
70
- <div class="space-y-4">
71
- {#each models as model}
72
- {@const active = model.id === selectedModelId}
73
- <div
74
- class="relative rounded-xl border border-gray-100 {active
75
- ? 'bg-gradient-to-r from-primary-200/40 via-primary-500/10'
76
- : ''}"
77
- >
78
- <label
79
- class="group flex cursor-pointer flex-col p-3"
80
- on:change
81
- aria-label={model.displayName}
82
- >
83
- <input
84
- type="radio"
85
- class="sr-only"
86
- name="activeModel"
87
- value={model.id}
88
- bind:group={selectedModelId}
89
- />
90
- <div
91
- class="mb-1.5 block pr-8 text-sm font-semibold leading-tight text-gray-800 sm:text-base"
92
- >
93
- {model.displayName}
94
- </div>
95
- {#if model.description}
96
- <div class="text-xs text-gray-500 sm:text-sm">{model.description}</div>
97
- {/if}
98
- <CarbonCheckmark
99
- class="absolute right-2 top-2 text-xl {active
100
- ? 'text-primary-400'
101
- : 'text-transparent group-hover:text-gray-200'}"
102
- />
103
- </label>
104
- {#if active}
105
- <div class=" overflow-hidden rounded-xl px-3 pb-2">
106
- <div class="flex flex-row flex-nowrap gap-2 pb-1">
107
- <div class="text-xs font-semibold text-gray-500">System Prompt</div>
108
- {#if expanded}
109
- <button
110
- class="text-gray-500 hover:text-gray-900"
111
- on:click|preventDefault={onToggle}
112
- >
113
- <CarbonSave class="text-sm" />
114
- </button>
115
- <button
116
- class="text-gray-500 hover:text-gray-900"
117
- on:click|preventDefault={() => {
118
- value = model.preprompt ?? "";
119
- }}
120
- >
121
- <CarbonRestart class="text-sm" />
122
- </button>
123
- {:else}
124
- <button
125
- class=" text-gray-500 hover:text-gray-900"
126
- on:click|preventDefault={onToggle}
127
- >
128
- <CarbonEdit class="text-sm" />
129
- </button>
130
- {/if}
131
- </div>
132
- <textarea
133
- enterkeyhint="send"
134
- tabindex="0"
135
- rows="1"
136
- class="h-20 w-full resize-none scroll-p-3 overflow-x-hidden overflow-y-scroll rounded-md border border-gray-300 bg-transparent p-1 text-xs outline-none focus:ring-0 focus-visible:ring-0"
137
- bind:value
138
- hidden={!expanded}
139
- />
140
- </div>
141
- {/if}
142
- <ModelCardMetadata {model} />
143
- </div>
144
- {/each}
145
- </div>
146
- <button
147
- type="submit"
148
- class="sticky bottom-6 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"
149
- >
150
- Apply
151
- </button>
152
- </form>
153
- </Modal>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/NavMenu.svelte CHANGED
@@ -1,6 +1,5 @@
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
- import { createEventDispatcher } from "svelte";
4
 
5
  import Logo from "$lib/components/icons/Logo.svelte";
6
  import { switchTheme } from "$lib/switchTheme";
@@ -9,12 +8,6 @@
9
  import NavConversationItem from "./NavConversationItem.svelte";
10
  import type { LayoutData } from "../../routes/$types";
11
 
12
- const dispatch = createEventDispatcher<{
13
- shareConversation: { id: string; title: string };
14
- clickSettings: void;
15
- clickLogout: void;
16
- }>();
17
-
18
  interface Conv {
19
  id: string;
20
  title: string;
@@ -119,13 +112,12 @@
119
  >
120
  Theme
121
  </button>
122
- <button
123
- on:click={() => dispatch("clickSettings")}
124
- type="button"
125
  class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
126
  >
127
  Settings
128
- </button>
129
  {#if PUBLIC_APP_NAME === "HuggingChat"}
130
  <a
131
  href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions"
 
1
  <script lang="ts">
2
  import { base } from "$app/paths";
 
3
 
4
  import Logo from "$lib/components/icons/Logo.svelte";
5
  import { switchTheme } from "$lib/switchTheme";
 
8
  import NavConversationItem from "./NavConversationItem.svelte";
9
  import type { LayoutData } from "../../routes/$types";
10
 
 
 
 
 
 
 
11
  interface Conv {
12
  id: string;
13
  title: string;
 
112
  >
113
  Theme
114
  </button>
115
+ <a
116
+ href="{base}/settings"
 
117
  class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
118
  >
119
  Settings
120
+ </a>
121
  {#if PUBLIC_APP_NAME === "HuggingChat"}
122
  <a
123
  href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions"
src/lib/components/SettingsModal.svelte DELETED
@@ -1,132 +0,0 @@
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 { enhance } from "$app/forms";
8
- import { base } from "$app/paths";
9
- import { PUBLIC_APP_DATA_SHARING } from "$env/static/public";
10
- import type { Model } from "$lib/types/Model";
11
- import type { LayoutData } from "../../routes/$types";
12
-
13
- export let settings: LayoutData["settings"];
14
- export let models: Array<Model>;
15
-
16
- let shareConversationsWithModelAuthors = settings.shareConversationsWithModelAuthors;
17
- let isConfirmingDeletion = false;
18
-
19
- const dispatch = createEventDispatcher<{ close: void }>();
20
- </script>
21
-
22
- <Modal on:close>
23
- <div class="flex w-full flex-col gap-5 p-6">
24
- <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
25
- <h2>Settings</h2>
26
- <button type="button" class="group" on:click={() => dispatch("close")}>
27
- <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
28
- </button>
29
- </div>
30
- <form
31
- class="flex flex-col gap-5"
32
- use:enhance={() => {
33
- dispatch("close");
34
- }}
35
- method="post"
36
- action="{base}/settings"
37
- >
38
- {#if PUBLIC_APP_DATA_SHARING}
39
- <label class="flex cursor-pointer select-none items-center gap-2 text-gray-500">
40
- {#each Object.entries(settings).filter(([k]) => !(k === "shareConversationsWithModelAuthors" || k === "customPrompts")) as [key, val]}
41
- <input type="hidden" name={key} value={val} />
42
- {/each}
43
- <input
44
- type="hidden"
45
- name="customPrompts"
46
- value={JSON.stringify(settings.customPrompts)}
47
- />
48
- <Switch
49
- name="shareConversationsWithModelAuthors"
50
- bind:checked={shareConversationsWithModelAuthors}
51
- />
52
- Share conversations with model authors
53
- </label>
54
-
55
- <p class="text-gray-800">
56
- Sharing your data will help improve the training data and make open models better over
57
- time.
58
- </p>
59
- <p class="text-gray-800">
60
- You can change this setting at any time, it applies to all your conversations.
61
- </p>
62
- <div>
63
- <p class="text-gray-800">Read more about model authors:</p>
64
- <ul class="list-inside list-disc">
65
- {#each models as model}
66
- <li class="list-item">
67
- <a
68
- href={model["websiteUrl"]}
69
- target="_blank"
70
- rel="noreferrer"
71
- class="underline decoration-gray-300 hover:decoration-gray-700">{model["name"]}</a
72
- >
73
- </li>
74
- {/each}
75
- </ul>
76
- </div>
77
- {/if}
78
- <label class="flex cursor-pointer select-none items-center gap-2 text-sm text-gray-500">
79
- <input
80
- type="checkbox"
81
- name="hideEmojiOnSidebar"
82
- bind:checked={settings.hideEmojiOnSidebar}
83
- />
84
- Hide emoticons in conversation topics
85
- </label>
86
- <form
87
- method="post"
88
- action="{base}/conversations?/delete"
89
- on:submit|preventDefault={() => (isConfirmingDeletion = true)}
90
- >
91
- <button type="submit" class="underline decoration-gray-300 hover:decoration-gray-700">
92
- Delete all conversations
93
- </button>
94
- </form>
95
- <button
96
- type="submit"
97
- 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-all focus-visible:outline-none focus-visible:ring hover:ring"
98
- >
99
- Apply
100
- </button>
101
- </form>
102
-
103
- {#if isConfirmingDeletion}
104
- <Modal on:close={() => (isConfirmingDeletion = false)}>
105
- <form
106
- use:enhance={() => {
107
- dispatch("close");
108
- }}
109
- method="post"
110
- action="{base}/conversations?/delete"
111
- class="flex w-full flex-col gap-5 p-6"
112
- >
113
- <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
114
- <h2>Are you sure?</h2>
115
- <button type="button" class="group" on:click={() => (isConfirmingDeletion = false)}>
116
- <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
117
- </button>
118
- </div>
119
- <p class="text-gray-800">
120
- This action will delete all your conversations. This cannot be undone.
121
- </p>
122
- <button
123
- type="submit"
124
- class="mt-2 rounded-full bg-red-700 px-5 py-2 text-lg font-semibold text-gray-100 ring-gray-400 ring-offset-1 transition-all focus-visible:outline-none focus-visible:ring hover:ring"
125
- >
126
- Confirm deletion
127
- </button>
128
- </form>
129
- </Modal>
130
- {/if}
131
- </div>
132
- </Modal>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/Switch.svelte CHANGED
@@ -10,9 +10,7 @@
10
  aria-label="switch"
11
  role="switch"
12
  tabindex="0"
13
- on:click
14
- on:keypress
15
- class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full bg-gray-300 p-1 shadow-inner ring-gray-400 transition-all peer-checked:bg-blue-600 peer-focus-visible:ring peer-focus-visible:ring-offset-1 hover:bg-gray-400 dark:bg-gray-600 peer-checked:[&>div]:translate-x-3.5"
16
  >
17
  <div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-all" />
18
  </div>
 
10
  aria-label="switch"
11
  role="switch"
12
  tabindex="0"
13
+ class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-gray-300 p-1 shadow-inner ring-gray-400 transition-all peer-checked:bg-blue-600 peer-focus-visible:ring peer-focus-visible:ring-offset-1 hover:bg-gray-400 dark:bg-gray-600 peer-checked:[&>div]:translate-x-3.5"
 
 
14
  >
15
  <div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-all" />
16
  </div>
src/lib/components/chat/ChatIntroduction.svelte CHANGED
@@ -4,21 +4,19 @@
4
  import { PUBLIC_APP_DESCRIPTION } from "$env/static/public";
5
  import Logo from "$lib/components/icons/Logo.svelte";
6
  import { createEventDispatcher } from "svelte";
7
- import IconChevron from "$lib/components/icons/IconChevron.svelte";
8
  import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
9
  import AnnouncementBanner from "../AnnouncementBanner.svelte";
10
- import ModelsModal from "../ModelsModal.svelte";
11
  import type { Model } from "$lib/types/Model";
12
  import ModelCardMetadata from "../ModelCardMetadata.svelte";
13
  import type { LayoutData } from "../../../routes/$types";
14
  import { findCurrentModel } from "$lib/utils/models";
 
15
 
16
  export let currentModel: Model;
17
  export let settings: LayoutData["settings"];
18
  export let models: Model[];
19
 
20
- let isModelsModalOpen = false;
21
-
22
  $: currentModelMetadata = findCurrentModel(models, settings.activeModel);
23
 
24
  const announcementBanners = PUBLIC_ANNOUNCEMENT_BANNERS
@@ -57,21 +55,16 @@
57
  >
58
  </AnnouncementBanner>
59
  {/each}
60
-
61
- {#if isModelsModalOpen}
62
- <ModelsModal {settings} {models} on:close={() => (isModelsModalOpen = false)} />
63
- {/if}
64
  <div class="overflow-hidden rounded-xl border dark:border-gray-800">
65
  <div class="flex p-3">
66
  <div>
67
  <div class="text-sm text-gray-600 dark:text-gray-400">Current Model</div>
68
  <div class="font-semibold">{currentModel.displayName}</div>
69
  </div>
70
- <button
71
- type="button"
72
- on:click={() => (isModelsModalOpen = true)}
73
  class="btn ml-auto flex h-7 w-7 self-start rounded-full bg-gray-100 p-1 text-xs hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-600"
74
- ><IconChevron /></button
75
  >
76
  </div>
77
  <ModelCardMetadata variant="dark" model={currentModel} />
 
4
  import { PUBLIC_APP_DESCRIPTION } from "$env/static/public";
5
  import Logo from "$lib/components/icons/Logo.svelte";
6
  import { createEventDispatcher } from "svelte";
7
+ import IconGear from "~icons/bi/gear-fill";
8
  import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
9
  import AnnouncementBanner from "../AnnouncementBanner.svelte";
 
10
  import type { Model } from "$lib/types/Model";
11
  import ModelCardMetadata from "../ModelCardMetadata.svelte";
12
  import type { LayoutData } from "../../../routes/$types";
13
  import { findCurrentModel } from "$lib/utils/models";
14
+ import { base } from "$app/paths";
15
 
16
  export let currentModel: Model;
17
  export let settings: LayoutData["settings"];
18
  export let models: Model[];
19
 
 
 
20
  $: currentModelMetadata = findCurrentModel(models, settings.activeModel);
21
 
22
  const announcementBanners = PUBLIC_ANNOUNCEMENT_BANNERS
 
55
  >
56
  </AnnouncementBanner>
57
  {/each}
 
 
 
 
58
  <div class="overflow-hidden rounded-xl border dark:border-gray-800">
59
  <div class="flex p-3">
60
  <div>
61
  <div class="text-sm text-gray-600 dark:text-gray-400">Current Model</div>
62
  <div class="font-semibold">{currentModel.displayName}</div>
63
  </div>
64
+ <a
65
+ href="{base}/settings/{currentModel.id}"
 
66
  class="btn ml-auto flex h-7 w-7 self-start rounded-full bg-gray-100 p-1 text-xs hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-600"
67
+ ><IconGear /></a
68
  >
69
  </div>
70
  <ModelCardMetadata variant="dark" model={currentModel} />
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -13,7 +13,6 @@
13
  import ChatInput from "./ChatInput.svelte";
14
  import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
15
  import type { Model } from "$lib/types/Model";
16
- import type { LayoutData } from "../../../routes/$types";
17
  import WebSearchToggle from "../WebSearchToggle.svelte";
18
  import LoginModal from "../LoginModal.svelte";
19
  import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
@@ -23,6 +22,7 @@
23
  import RetryBtn from "../RetryBtn.svelte";
24
  import UploadBtn from "../UploadBtn.svelte";
25
  import file2base64 from "$lib/utils/file2base64";
 
26
 
27
  export let messages: Message[] = [];
28
  export let loading = false;
@@ -30,7 +30,6 @@
30
  export let shared = false;
31
  export let currentModel: Model;
32
  export let models: Model[];
33
- export let settings: LayoutData["settings"];
34
  export let webSearchMessages: WebSearchUpdate[] = [];
35
  export let preprompt: string | undefined = undefined;
36
  export let files: File[] = [];
@@ -72,14 +71,15 @@
72
  $: lastIsError = messages[messages.length - 1]?.from === "user" && !loading;
73
 
74
  $: sources = files.map((file) => file2base64(file));
 
 
75
  </script>
76
 
77
  <div class="relative min-h-0 min-w-0">
78
- {#if !settings.ethicsModalAcceptedAt}
79
- <DisclaimerModal {settings} />
80
  {:else if loginModalOpen}
81
  <LoginModal
82
- {settings}
83
  on:close={() => {
84
  loginModalOpen = false;
85
  }}
@@ -88,7 +88,7 @@
88
  <ChatMessages
89
  {loading}
90
  {pending}
91
- {settings}
92
  {currentModel}
93
  {models}
94
  {messages}
@@ -139,7 +139,7 @@
139
  class="dark:via-gray-80 w-full bg-gradient-to-t from-white via-white/80 to-white/0 dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:border-t max-md:bg-white max-md:px-4 max-md:dark:bg-gray-900"
140
  >
141
  <div class="flex w-full pb-3 max-md:pt-3">
142
- {#if settings?.searchEnabled}
143
  <WebSearchToggle />
144
  {/if}
145
  {#if loading}
 
13
  import ChatInput from "./ChatInput.svelte";
14
  import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
15
  import type { Model } from "$lib/types/Model";
 
16
  import WebSearchToggle from "../WebSearchToggle.svelte";
17
  import LoginModal from "../LoginModal.svelte";
18
  import type { WebSearchUpdate } from "$lib/types/MessageUpdate";
 
22
  import RetryBtn from "../RetryBtn.svelte";
23
  import UploadBtn from "../UploadBtn.svelte";
24
  import file2base64 from "$lib/utils/file2base64";
25
+ import { useSettingsStore } from "$lib/stores/settings";
26
 
27
  export let messages: Message[] = [];
28
  export let loading = false;
 
30
  export let shared = false;
31
  export let currentModel: Model;
32
  export let models: Model[];
 
33
  export let webSearchMessages: WebSearchUpdate[] = [];
34
  export let preprompt: string | undefined = undefined;
35
  export let files: File[] = [];
 
71
  $: lastIsError = messages[messages.length - 1]?.from === "user" && !loading;
72
 
73
  $: sources = files.map((file) => file2base64(file));
74
+
75
+ const settings = useSettingsStore();
76
  </script>
77
 
78
  <div class="relative min-h-0 min-w-0">
79
+ {#if !$settings.ethicsModalAccepted}
80
+ <DisclaimerModal />
81
  {:else if loginModalOpen}
82
  <LoginModal
 
83
  on:close={() => {
84
  loginModalOpen = false;
85
  }}
 
88
  <ChatMessages
89
  {loading}
90
  {pending}
91
+ settings={$page.data.settings}
92
  {currentModel}
93
  {models}
94
  {messages}
 
139
  class="dark:via-gray-80 w-full bg-gradient-to-t from-white via-white/80 to-white/0 dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:border-t max-md:bg-white max-md:px-4 max-md:dark:bg-gray-900"
140
  >
141
  <div class="flex w-full pb-3 max-md:pt-3">
142
+ {#if $page.data.settings?.searchEnabled}
143
  <WebSearchToggle />
144
  {/if}
145
  {#if loading}
src/lib/stores/settings.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { browser } from "$app/environment";
2
+ import { base } from "$app/paths";
3
+ import { getContext, setContext } from "svelte";
4
+ import { type Writable, writable, get } from "svelte/store";
5
+
6
+ type SettingsStore = {
7
+ shareConversationsWithModelAuthors: boolean;
8
+ hideEmojiOnSidebar: boolean;
9
+ ethicsModalAccepted: boolean;
10
+ ethicsModalAcceptedAt: Date | null;
11
+ activeModel: string;
12
+ customPrompts: Record<string, string>;
13
+ recentlySaved: boolean;
14
+ };
15
+ export function useSettingsStore() {
16
+ return getContext<Writable<SettingsStore>>("settings");
17
+ }
18
+
19
+ export function createSettingsStore(initialValue: Omit<SettingsStore, "recentlySaved">) {
20
+ const baseStore = writable({ ...initialValue, recentlySaved: false });
21
+
22
+ let timeoutId: NodeJS.Timeout;
23
+
24
+ async function setSettings(settings: Partial<SettingsStore>) {
25
+ baseStore.update((s) => ({
26
+ ...s,
27
+ ...settings,
28
+ }));
29
+
30
+ clearTimeout(timeoutId);
31
+
32
+ if (browser) {
33
+ timeoutId = setTimeout(async () => {
34
+ await fetch(`${base}/settings`, {
35
+ method: "POST",
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ },
39
+ body: JSON.stringify({
40
+ ...get(baseStore),
41
+ ...settings,
42
+ }),
43
+ });
44
+
45
+ // set savedRecently to true for 3s
46
+ baseStore.update((s) => ({
47
+ ...s,
48
+ recentlySaved: true,
49
+ }));
50
+ setTimeout(() => {
51
+ baseStore.update((s) => ({
52
+ ...s,
53
+ recentlySaved: false,
54
+ }));
55
+ }, 3000);
56
+ }, 300);
57
+ // debounce server calls by 300ms
58
+ }
59
+ }
60
+
61
+ const newStore = {
62
+ subscribe: baseStore.subscribe,
63
+ set: setSettings,
64
+ update: (fn: (s: SettingsStore) => SettingsStore) => {
65
+ setSettings(fn(get(baseStore)));
66
+ },
67
+ } satisfies Writable<SettingsStore>;
68
+
69
+ setContext("settings", newStore);
70
+
71
+ return newStore;
72
+ }
src/lib/types/Settings.ts CHANGED
@@ -24,4 +24,5 @@ export interface Settings extends Timestamps {
24
  export const DEFAULT_SETTINGS = {
25
  shareConversationsWithModelAuthors: true,
26
  activeModel: defaultModel.id,
 
27
  };
 
24
  export const DEFAULT_SETTINGS = {
25
  shareConversationsWithModelAuthors: true,
26
  activeModel: defaultModel.id,
27
+ hideEmojiOnSidebar: false,
28
  };
src/routes/+layout.server.ts CHANGED
@@ -83,13 +83,14 @@ export const load: LayoutServerLoad = async ({ locals, depends, url }) => {
83
  }))
84
  .toArray(),
85
  settings: {
86
- shareConversationsWithModelAuthors:
87
- settings?.shareConversationsWithModelAuthors ??
88
- DEFAULT_SETTINGS.shareConversationsWithModelAuthors,
89
  ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null,
90
  activeModel: settings?.activeModel ?? DEFAULT_SETTINGS.activeModel,
91
  hideEmojiOnSidebar: settings?.hideEmojiOnSidebar ?? false,
92
- searchEnabled: !!(SERPAPI_KEY || SERPER_API_KEY || YDC_API_KEY || USE_LOCAL_WEBSEARCH),
 
 
93
  customPrompts: settings?.customPrompts ?? {},
94
  },
95
  models: models.map((model) => ({
 
83
  }))
84
  .toArray(),
85
  settings: {
86
+ searchEnabled: !!(SERPAPI_KEY || SERPER_API_KEY || YDC_API_KEY || USE_LOCAL_WEBSEARCH),
87
+ ethicsModalAccepted: !!settings?.ethicsModalAcceptedAt,
 
88
  ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null,
89
  activeModel: settings?.activeModel ?? DEFAULT_SETTINGS.activeModel,
90
  hideEmojiOnSidebar: settings?.hideEmojiOnSidebar ?? false,
91
+ shareConversationsWithModelAuthors:
92
+ settings?.shareConversationsWithModelAuthors ??
93
+ DEFAULT_SETTINGS.shareConversationsWithModelAuthors,
94
  customPrompts: settings?.customPrompts ?? {},
95
  },
96
  models: models.map((model) => ({
src/routes/+layout.svelte CHANGED
@@ -13,14 +13,13 @@
13
  import MobileNav from "$lib/components/MobileNav.svelte";
14
  import NavMenu from "$lib/components/NavMenu.svelte";
15
  import Toast from "$lib/components/Toast.svelte";
16
- import SettingsModal from "$lib/components/SettingsModal.svelte";
17
  import { PUBLIC_APP_ASSETS, PUBLIC_APP_NAME } from "$env/static/public";
18
  import titleUpdate from "$lib/stores/titleUpdate";
 
19
 
20
  export let data;
21
 
22
  let isNavOpen = false;
23
- let isSettingsOpen = false;
24
  let errorToastTimeout: ReturnType<typeof setTimeout>;
25
  let currentError: string | null;
26
 
@@ -104,6 +103,8 @@
104
 
105
  $titleUpdate = null;
106
  }
 
 
107
  </script>
108
 
109
  <svelte:head>
@@ -152,7 +153,6 @@
152
  canLogin={data.user === undefined && data.loginEnabled}
153
  on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
154
  on:deleteConversation={(ev) => deleteConversation(ev.detail)}
155
- on:clickSettings={() => (isSettingsOpen = true)}
156
  on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
157
  />
158
  </MobileNav>
@@ -163,19 +163,11 @@
163
  canLogin={data.user === undefined && data.loginEnabled}
164
  on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
165
  on:deleteConversation={(ev) => deleteConversation(ev.detail)}
166
- on:clickSettings={() => (isSettingsOpen = true)}
167
  on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
168
  />
169
  </nav>
170
  {#if currentError}
171
  <Toast message={currentError} />
172
  {/if}
173
- {#if isSettingsOpen}
174
- <SettingsModal
175
- on:close={() => (isSettingsOpen = false)}
176
- settings={data.settings}
177
- models={data.models}
178
- />
179
- {/if}
180
  <slot />
181
  </div>
 
13
  import MobileNav from "$lib/components/MobileNav.svelte";
14
  import NavMenu from "$lib/components/NavMenu.svelte";
15
  import Toast from "$lib/components/Toast.svelte";
 
16
  import { PUBLIC_APP_ASSETS, PUBLIC_APP_NAME } from "$env/static/public";
17
  import titleUpdate from "$lib/stores/titleUpdate";
18
+ import { createSettingsStore } from "$lib/stores/settings";
19
 
20
  export let data;
21
 
22
  let isNavOpen = false;
 
23
  let errorToastTimeout: ReturnType<typeof setTimeout>;
24
  let currentError: string | null;
25
 
 
103
 
104
  $titleUpdate = null;
105
  }
106
+
107
+ createSettingsStore(data.settings);
108
  </script>
109
 
110
  <svelte:head>
 
153
  canLogin={data.user === undefined && data.loginEnabled}
154
  on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
155
  on:deleteConversation={(ev) => deleteConversation(ev.detail)}
 
156
  on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
157
  />
158
  </MobileNav>
 
163
  canLogin={data.user === undefined && data.loginEnabled}
164
  on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
165
  on:deleteConversation={(ev) => deleteConversation(ev.detail)}
 
166
  on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
167
  />
168
  </nav>
169
  {#if currentError}
170
  <Toast message={currentError} />
171
  {/if}
 
 
 
 
 
 
 
172
  <slot />
173
  </div>
src/routes/+page.svelte CHANGED
@@ -59,6 +59,5 @@
59
  {loading}
60
  currentModel={findCurrentModel([...data.models, ...data.oldModels], data.settings.activeModel)}
61
  models={data.models}
62
- settings={data.settings}
63
  bind:files
64
  />
 
59
  {loading}
60
  currentModel={findCurrentModel([...data.models, ...data.oldModels], data.settings.activeModel)}
61
  models={data.models}
 
62
  bind:files
63
  />
src/routes/conversation/[id]/+page.svelte CHANGED
@@ -15,7 +15,7 @@
15
  import type { Message } from "$lib/types/Message";
16
  import type { MessageUpdate, WebSearchUpdate } from "$lib/types/MessageUpdate";
17
  import titleUpdate from "$lib/stores/titleUpdate";
18
- import file2base64 from "$lib/utils/file2base64.js";
19
  export let data;
20
 
21
  let messages = data.messages;
@@ -336,5 +336,4 @@
336
  on:stop={() => (($isAborted = true), (loading = false))}
337
  models={data.models}
338
  currentModel={findCurrentModel([...data.models, ...data.oldModels], data.model)}
339
- settings={data.settings}
340
  />
 
15
  import type { Message } from "$lib/types/Message";
16
  import type { MessageUpdate, WebSearchUpdate } from "$lib/types/MessageUpdate";
17
  import titleUpdate from "$lib/stores/titleUpdate";
18
+ import file2base64 from "$lib/utils/file2base64";
19
  export let data;
20
 
21
  let messages = data.messages;
 
336
  on:stop={() => (($isAborted = true), (loading = false))}
337
  models={data.models}
338
  currentModel={findCurrentModel([...data.models, ...data.oldModels], data.model)}
 
339
  />
src/routes/conversation/[id]/+server.ts CHANGED
@@ -12,7 +12,7 @@ import { runWebSearch } from "$lib/server/websearch/runWebSearch";
12
  import type { WebSearch } from "$lib/types/WebSearch";
13
  import { abortedGenerations } from "$lib/server/abortedGenerations";
14
  import { summarize } from "$lib/server/summarize";
15
- import { uploadFile } from "$lib/server/files/uploadFile.js";
16
  import sizeof from "image-size";
17
 
18
  export async function POST({ request, locals, params, getClientAddress }) {
@@ -302,7 +302,6 @@ export async function POST({ request, locals, params, getClientAddress }) {
302
  }
303
  }
304
  } catch (e) {
305
- console.error(e);
306
  update({ type: "status", status: "error", message: (e as Error).message });
307
  }
308
  await collections.conversations.updateOne(
 
12
  import type { WebSearch } from "$lib/types/WebSearch";
13
  import { abortedGenerations } from "$lib/server/abortedGenerations";
14
  import { summarize } from "$lib/server/summarize";
15
+ import { uploadFile } from "$lib/server/files/uploadFile";
16
  import sizeof from "image-size";
17
 
18
  export async function POST({ request, locals, params, getClientAddress }) {
 
302
  }
303
  }
304
  } catch (e) {
 
305
  update({ type: "status", status: "error", message: (e as Error).message });
306
  }
307
  await collections.conversations.updateOne(
src/routes/settings/+layout.svelte ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { base } from "$app/paths";
3
+ import { clickOutside } from "$lib/actions/clickOutside";
4
+ import { browser } from "$app/environment";
5
+ import { afterNavigate, goto } from "$app/navigation";
6
+ import { page } from "$app/stores";
7
+ import { useSettingsStore } from "$lib/stores/settings";
8
+ import CarbonClose from "~icons/carbon/close";
9
+ import CarbonCheckmark from "~icons/carbon/checkmark";
10
+
11
+ import UserIcon from "~icons/carbon/user";
12
+ export let data;
13
+
14
+ let previousPage: string = base;
15
+
16
+ afterNavigate(({ from }) => {
17
+ if (!from?.url.pathname.includes("settings")) {
18
+ previousPage = from?.url.pathname || previousPage;
19
+ }
20
+ });
21
+
22
+ const settings = useSettingsStore();
23
+ </script>
24
+
25
+ <div
26
+ class="fixed inset-0 flex items-center justify-center bg-black/80 backdrop-blur-sm dark:bg-black/50"
27
+ >
28
+ <dialog
29
+ open
30
+ use:clickOutside={() => {
31
+ if (browser) window;
32
+ goto(previousPage);
33
+ }}
34
+ class="z-10 grid h-[95dvh] w-[90dvw] grid-cols-1 content-start gap-x-10 gap-y-6 overflow-hidden rounded-2xl bg-white p-4 shadow-2xl outline-none sm:h-[80dvh] md:grid-cols-3 md:grid-rows-[auto,1fr] md:p-8 xl:w-[1100px]"
35
+ >
36
+ <div class="col-span-1 flex items-center justify-between md:col-span-3">
37
+ <h2 class="text-xl font-bold">Settings</h2>
38
+ <button
39
+ class="btn rounded-lg"
40
+ on:click={() => {
41
+ if (browser) window;
42
+ goto(previousPage);
43
+ }}
44
+ >
45
+ <CarbonClose class="text-xl text-gray-900 hover:text-black" />
46
+ </button>
47
+ </div>
48
+ <div
49
+ class="col-span-1 flex flex-col overflow-y-auto whitespace-nowrap max-md:-mx-4 max-md:h-[160px] max-md:border md:pr-6"
50
+ >
51
+ {#each data.models as model}
52
+ <a
53
+ href="{base}/settings/{model.id}"
54
+ class="group flex h-11 flex-none items-center gap-3 pl-3 pr-2 text-gray-500 hover:bg-gray-100 md:rounded-xl {model.id ===
55
+ $page.params.model
56
+ ? '!bg-gray-100 !text-gray-800'
57
+ : ''}"
58
+ >
59
+ <div class="truncate">{model.displayName}</div>
60
+ {#if model.id === $settings.activeModel}
61
+ <div
62
+ class="rounded-lg bg-black px-2 py-1.5 text-xs font-semibold leading-none text-white"
63
+ >
64
+ Active
65
+ </div>
66
+ {/if}
67
+ </a>
68
+ {/each}
69
+ <a
70
+ href="{base}/settings"
71
+ class="group mt-auto flex h-11 flex-none items-center gap-3 pl-3 pr-2 text-gray-500 hover:bg-gray-100 max-md:order-first md:rounded-xl {$page
72
+ .params.model === undefined
73
+ ? '!bg-gray-100 !text-gray-800'
74
+ : ''}"
75
+ >
76
+ <UserIcon class="pr-1 text-lg" />
77
+ Application Settings
78
+ </a>
79
+ </div>
80
+ <div class="col-span-1 overflow-y-auto md:col-span-2">
81
+ <slot />
82
+ </div>
83
+
84
+ {#if $settings.recentlySaved}
85
+ <div class="absolute bottom-0 right-0 m-2 inline p-2 text-gray-400">
86
+ <CarbonCheckmark class="inline text-lg" />
87
+ Saved
88
+ </div>
89
+ {/if}
90
+ </dialog>
91
+ </div>
src/routes/settings/+page.server.ts DELETED
@@ -1,53 +0,0 @@
1
- import { base } from "$app/paths";
2
- import { collections } from "$lib/server/database";
3
- import { redirect } from "@sveltejs/kit";
4
- import { z } from "zod";
5
- import { models, validateModel } from "$lib/server/models";
6
- import { authCondition } from "$lib/server/auth";
7
- import { DEFAULT_SETTINGS } from "$lib/types/Settings";
8
-
9
- const booleanFormObject = z
10
- .union([z.literal("true"), z.literal("on"), z.literal("false"), z.null()])
11
- .transform((value) => {
12
- return value === "true" || value === "on";
13
- });
14
-
15
- export const actions = {
16
- default: async function ({ request, locals }) {
17
- const formData = await request.formData();
18
-
19
- const { ethicsModalAccepted, ...settings } = z
20
- .object({
21
- shareConversationsWithModelAuthors: booleanFormObject,
22
- hideEmojiOnSidebar: booleanFormObject,
23
- ethicsModalAccepted: z.boolean({ coerce: true }).optional(),
24
- activeModel: validateModel(models),
25
- customPrompts: z.record(z.string()).default({}),
26
- })
27
- .parse({
28
- hideEmojiOnSidebar: formData.get("hideEmojiOnSidebar"),
29
- shareConversationsWithModelAuthors: formData.get("shareConversationsWithModelAuthors"),
30
- ethicsModalAccepted: formData.get("ethicsModalAccepted"),
31
- activeModel: formData.get("activeModel") ?? DEFAULT_SETTINGS.activeModel,
32
- customPrompts: JSON.parse(formData.get("customPrompts")?.toString() ?? "{}"),
33
- });
34
-
35
- await collections.settings.updateOne(
36
- authCondition(locals),
37
- {
38
- $set: {
39
- ...settings,
40
- ...(ethicsModalAccepted && { ethicsModalAcceptedAt: new Date() }),
41
- updatedAt: new Date(),
42
- },
43
- $setOnInsert: {
44
- createdAt: new Date(),
45
- },
46
- },
47
- {
48
- upsert: true,
49
- }
50
- );
51
- throw redirect(303, request.headers.get("referer") || `${base}/`);
52
- },
53
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/settings/+page.svelte ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 CarbonTrashCan from "~icons/carbon/trash-can";
7
+
8
+ import { enhance } from "$app/forms";
9
+ import { base } from "$app/paths";
10
+
11
+ import { useSettingsStore } from "$lib/stores/settings";
12
+ import Switch from "$lib/components/Switch.svelte";
13
+
14
+ let isConfirmingDeletion = false;
15
+
16
+ const dispatch = createEventDispatcher<{ close: void }>();
17
+
18
+ let settings = useSettingsStore();
19
+ </script>
20
+
21
+ <div class="flex w-full flex-col gap-5">
22
+ <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
23
+ <h2>Application Settings</h2>
24
+ </div>
25
+
26
+ <div class="flex h-full flex-col gap-4 pt-4 max-sm:pt-0">
27
+ <!-- svelte-ignore a11y-label-has-associated-control -->
28
+ <label class="flex items-center">
29
+ <Switch
30
+ name="shareConversationsWithModelAuthors"
31
+ bind:checked={$settings.shareConversationsWithModelAuthors}
32
+ />
33
+ <div class="inline cursor-pointer select-none items-center gap-2 pl-2">
34
+ Share conversations with model authors
35
+ </div>
36
+ </label>
37
+
38
+ <p class="text-sm text-gray-500">
39
+ Sharing your data will help improve the training data and make open models better over time.
40
+ </p>
41
+
42
+ <!-- svelte-ignore a11y-label-has-associated-control -->
43
+ <label class="mt-6 flex items-center">
44
+ <Switch name="hideEmojiOnSidebar" bind:checked={$settings.hideEmojiOnSidebar} />
45
+ <div class="inline cursor-pointer select-none items-center gap-2 pl-2">
46
+ Hide emoticons in conversation topics
47
+ </div>
48
+ </label>
49
+
50
+ <button
51
+ on:click|preventDefault={() => (isConfirmingDeletion = true)}
52
+ type="submit"
53
+ class="mt-6 flex items-center underline decoration-gray-300 underline-offset-2 hover:decoration-gray-700"
54
+ ><CarbonTrashCan class="mr-2 inline text-sm text-red-500" />Delete all conversations</button
55
+ >
56
+ </div>
57
+
58
+ {#if isConfirmingDeletion}
59
+ <Modal on:close={() => (isConfirmingDeletion = false)}>
60
+ <form
61
+ use:enhance={() => {
62
+ dispatch("close");
63
+ }}
64
+ method="post"
65
+ action="{base}/conversations?/delete"
66
+ class="flex w-full flex-col gap-5 p-6"
67
+ >
68
+ <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
69
+ <h2>Are you sure?</h2>
70
+ <button
71
+ type="button"
72
+ class="group"
73
+ on:click|stopPropagation={() => (isConfirmingDeletion = false)}
74
+ >
75
+ <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
76
+ </button>
77
+ </div>
78
+ <p class="text-gray-800">
79
+ This action will delete all your conversations. This cannot be undone.
80
+ </p>
81
+ <button
82
+ type="submit"
83
+ class="mt-2 rounded-full bg-red-700 px-5 py-2 text-lg font-semibold text-gray-100 ring-gray-400 ring-offset-1 transition-all focus-visible:outline-none focus-visible:ring hover:ring"
84
+ >
85
+ Confirm deletion
86
+ </button>
87
+ </form>
88
+ </Modal>
89
+ {/if}
90
+ </div>
src/routes/settings/+server.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { collections } from "$lib/server/database";
2
+ import { z } from "zod";
3
+ import { models, validateModel } from "$lib/server/models";
4
+ import { authCondition } from "$lib/server/auth";
5
+ import { DEFAULT_SETTINGS } from "$lib/types/Settings";
6
+
7
+ export async function POST({ request, locals }) {
8
+ const body = await request.json();
9
+
10
+ const { ethicsModalAccepted, ...settings } = z
11
+ .object({
12
+ shareConversationsWithModelAuthors: z
13
+ .boolean()
14
+ .default(DEFAULT_SETTINGS.shareConversationsWithModelAuthors),
15
+ hideEmojiOnSidebar: z.boolean().default(DEFAULT_SETTINGS.hideEmojiOnSidebar),
16
+ ethicsModalAccepted: z.boolean().optional(),
17
+ activeModel: validateModel(models).default(DEFAULT_SETTINGS.activeModel),
18
+ customPrompts: z.record(z.string()).default({}),
19
+ })
20
+ .parse(body);
21
+
22
+ await collections.settings.updateOne(
23
+ authCondition(locals),
24
+ {
25
+ $set: {
26
+ ...settings,
27
+ ...(ethicsModalAccepted && { ethicsModalAcceptedAt: new Date() }),
28
+ updatedAt: new Date(),
29
+ },
30
+ $setOnInsert: {
31
+ createdAt: new Date(),
32
+ },
33
+ },
34
+ {
35
+ upsert: true,
36
+ }
37
+ );
38
+ // return ok response
39
+ return new Response();
40
+ }
src/routes/settings/[...model]/+page.svelte ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { page } from "$app/stores";
3
+ import type { BackendModel } from "$lib/server/models";
4
+ import { useSettingsStore } from "$lib/stores/settings";
5
+ import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
6
+
7
+ const settings = useSettingsStore();
8
+
9
+ $: if ($settings.customPrompts[$page.params.model] === undefined) {
10
+ $settings.customPrompts = {
11
+ ...$settings.customPrompts,
12
+ [$page.params.model]:
13
+ $page.data.models.find((el: BackendModel) => el.id === $page.params.model)?.preprompt || "",
14
+ };
15
+ }
16
+
17
+ $: hasCustomPreprompt =
18
+ $settings.customPrompts[$page.params.model] !==
19
+ $page.data.models.find((el: BackendModel) => el.id === $page.params.model)?.preprompt;
20
+
21
+ $: isActive = $settings.activeModel === $page.params.model;
22
+
23
+ $: model = $page.data.models.find((el: BackendModel) => el.id === $page.params.model);
24
+ </script>
25
+
26
+ <div class="flex flex-col items-start">
27
+ <h2 class="mb-2.5 text-xl font-semibold">
28
+ {$page.params.model}
29
+ </h2>
30
+
31
+ <div class="flex items-center gap-4">
32
+ <a
33
+ href={model.modelUrl || "https://huggingface.co/" + model.name}
34
+ target="_blank"
35
+ rel="noreferrer"
36
+ class="flex items-center truncate underline underline-offset-2"
37
+ >
38
+ <CarbonArrowUpRight class="mr-1.5 shrink-0 text-xs " />
39
+ Model page
40
+ </a>
41
+
42
+ {#if model.datasetName || model.datasetUrl}
43
+ <a
44
+ href={model.datasetUrl || "https://huggingface.co/datasets/" + model.datasetName}
45
+ target="_blank"
46
+ rel="noreferrer"
47
+ class="flex items-center truncate underline underline-offset-2"
48
+ >
49
+ <CarbonArrowUpRight class="mr-1.5 shrink-0 text-xs " />
50
+ Dataset page
51
+ </a>
52
+ {/if}
53
+
54
+ {#if model.websiteUrl}
55
+ <a
56
+ href={model.websiteUrl}
57
+ target="_blank"
58
+ class="flex items-center truncate underline underline-offset-2"
59
+ rel="noreferrer"
60
+ >
61
+ <CarbonArrowUpRight class="mr-1.5 shrink-0 text-xs " />
62
+ Model website
63
+ </a>
64
+ {/if}
65
+ </div>
66
+
67
+ <button
68
+ class="{isActive
69
+ ? 'bg-gray-100'
70
+ : 'bg-black text-white'} my-8 flex items-center rounded-full px-3 py-1"
71
+ disabled={isActive}
72
+ name="Activate model"
73
+ on:click|stopPropagation={() => {
74
+ $settings.activeModel = $page.params.model;
75
+ }}
76
+ >
77
+ {isActive ? "Active model" : "Activate"}
78
+ </button>
79
+
80
+ <div class="flex w-full flex-col gap-2">
81
+ <div class="flex w-full flex-row content-between">
82
+ <h3 class="mb-1.5 text-lg font-semibold text-gray-800">System Prompt</h3>
83
+ {#if hasCustomPreprompt}
84
+ <button
85
+ class="ml-auto underline decoration-gray-300 hover:decoration-gray-700"
86
+ on:click|stopPropagation={() =>
87
+ ($settings.customPrompts[$page.params.model] = model.preprompt)}
88
+ >
89
+ Reset
90
+ </button>
91
+ {/if}
92
+ </div>
93
+ <textarea
94
+ rows="10"
95
+ class="w-full resize-none rounded-md border-2 bg-gray-100 p-2"
96
+ bind:value={$settings.customPrompts[$page.params.model]}
97
+ />
98
+ </div>
99
+ </div>
src/routes/settings/[...model]/+page.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+ import { redirect } from "@sveltejs/kit";
3
+
4
+ export async function load({ parent, params }) {
5
+ const data = await parent();
6
+
7
+ if (!data.models.map(({ id }) => id).includes(params.model)) {
8
+ throw redirect(302, `${base}/settings`);
9
+ }
10
+
11
+ return data;
12
+ }