Adrien Denat victor HF staff julien-c HF staff coyotte508 HF staff commited on
Commit
4dae10f
1 Parent(s): 229d4b4

add mobile menu (#64)

Browse files

* add mobile menu

* fix

* Auto-naming convos by summarizing them (#58)

* Auto-naming convos by summarizing them

* Ok let's do it only on first message then

* revamp

* location.reload for now

* avoid huge titles

* fix messages width

* add community feedback to nav

* fix tokens keeping coming even when changing conversation (#62)

* favicon

* 🐛 Fix generating bug (#68)

* 🐛 Fix redirect after delete

* ✨ Remove endoftext (#70)

* 🐛 Remove sanitized < (#71)

* 🩹 Change 301 to 302 in case we want to use the route for something else

* 🩹 Use passed fetch cc

@julien-c


When using @huggingface/infernece in the backend we'll need to make it support custom fetch as well

* refactor mobile menu + improve accessibility

* fix missing menu on md size + regression share button on hover

* fix chat title truncate on mobile

* fix layout max-width on mobile

* use a single event dispatcher instead of 2

* add missing type="button"

* remove duplicated wrapper after merge conflict

---------

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

src/lib/components/MobileNav.svelte ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { navigating } from "$app/stores";
3
+ import { createEventDispatcher } from "svelte";
4
+ import { browser } from "$app/environment";
5
+ import { base } from "$app/paths";
6
+
7
+ import CarbonClose from "~icons/carbon/close";
8
+ import CarbonAdd from "~icons/carbon/add";
9
+ import CarbonTextAlignJustify from "~icons/carbon/text-align-justify";
10
+
11
+ export let isOpen = false;
12
+ export let title: string;
13
+
14
+ $: title = title || "New Chat";
15
+
16
+ let closeEl: HTMLButtonElement;
17
+ let openEl: HTMLButtonElement;
18
+
19
+ const dispatch = createEventDispatcher();
20
+
21
+ $: if ($navigating) {
22
+ dispatch("toggle", false);
23
+ }
24
+
25
+ $: if (isOpen && closeEl) {
26
+ closeEl.focus();
27
+ } else if (!isOpen && browser && document.activeElement === closeEl) {
28
+ openEl.focus();
29
+ }
30
+ </script>
31
+
32
+ <nav class="md:hidden flex items-center h-12 border-b px-4 justify-between dark:border-gray-800">
33
+ <button
34
+ type="button"
35
+ class="flex items-center justify-center w-9 h-9 -ml-3 shrink-0"
36
+ on:click={() => dispatch("toggle", true)}
37
+ aria-label="Open menu"
38
+ bind:this={openEl}><CarbonTextAlignJustify /></button
39
+ >
40
+ <span class="px-4 truncate">{title}</span>
41
+ <a href={base || "/"} class="flex items-center justify-center w-9 h-9 -mr-3 shrink-0"
42
+ ><CarbonAdd /></a
43
+ >
44
+ </nav>
45
+ <nav
46
+ class="fixed inset-0 z-50 grid grid-rows-[auto,auto,1fr,auto] grid-cols-1 max-h-screen bg-white dark:bg-gray-900 bg-gradient-to-l from-gray-50 dark:from-gray-800/30 {isOpen
47
+ ? 'block'
48
+ : 'hidden'}"
49
+ >
50
+ <div class="flex items-center px-4 h-12">
51
+ <button
52
+ type="button"
53
+ class="flex items-center justify-center ml-auto w-9 h-9 -mr-3"
54
+ on:click={() => dispatch("toggle", false)}
55
+ aria-label="Close menu"
56
+ bind:this={closeEl}><CarbonClose /></button
57
+ >
58
+ </div>
59
+ <slot />
60
+ </nav>
src/lib/components/NavMenu.svelte ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { base } from "$app/paths";
3
+ import { page } from "$app/stores";
4
+ import { createEventDispatcher } from "svelte";
5
+
6
+ import CarbonTrashCan from "~icons/carbon/trash-can";
7
+ import CarbonExport from "~icons/carbon/export";
8
+ import { switchTheme } from "$lib/switchTheme";
9
+
10
+ const dispatch = createEventDispatcher<{
11
+ shareConversation: { id: string; title: string };
12
+ deleteConversation: string;
13
+ }>();
14
+
15
+ export let conversations: Array<{
16
+ id: string;
17
+ title: string;
18
+ }> = [];
19
+ </script>
20
+
21
+ <div class="flex-none sticky top-0 p-3 flex flex-col">
22
+ <a
23
+ href={base || "/"}
24
+ class="border px-12 py-2.5 rounded-lg shadow bg-white dark:bg-gray-700 dark:border-gray-600 text-center"
25
+ >
26
+ New Chat
27
+ </a>
28
+ </div>
29
+ <div class="flex flex-col overflow-y-auto p-3 -mt-3 gap-1">
30
+ {#each conversations as conv}
31
+ <a
32
+ data-sveltekit-noscroll
33
+ href="{base}/conversation/{conv.id}"
34
+ class="group pl-3 pr-2 h-11 rounded-lg flex-none text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-1.5 {conv.id ===
35
+ $page.params.id
36
+ ? 'bg-gray-100 dark:bg-gray-700'
37
+ : ''}"
38
+ >
39
+ <div class="flex-1 truncate">{conv.title}</div>
40
+
41
+ <button
42
+ type="button"
43
+ class="flex md:hidden md:group-hover:flex w-5 h-5 items-center justify-center rounded"
44
+ title="Share conversation"
45
+ on:click|preventDefault={() =>
46
+ dispatch("shareConversation", { id: conv.id, title: conv.title })}
47
+ >
48
+ <CarbonExport class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 text-xs" />
49
+ </button>
50
+
51
+ <button
52
+ type="button"
53
+ class="flex md:hidden md:group-hover:flex w-5 h-5 items-center justify-center rounded"
54
+ title="Delete conversation"
55
+ on:click|preventDefault={() => dispatch("deleteConversation", conv.id)}
56
+ >
57
+ <CarbonTrashCan
58
+ class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 text-xs"
59
+ />
60
+ </button>
61
+ </a>
62
+ {/each}
63
+ </div>
64
+ <div class="flex flex-col p-3 gap-2">
65
+ <button
66
+ on:click={switchTheme}
67
+ type="button"
68
+ class="text-left flex items-center first-letter:capitalize truncate py-3 px-3 rounded-lg flex-none text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
69
+ >
70
+ Theme
71
+ </button>
72
+ <a
73
+ href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions"
74
+ class="text-left flex items-center first-letter:capitalize truncate py-3 px-3 rounded-lg flex-none text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
75
+ >
76
+ Community feedback
77
+ </a>
78
+ <a
79
+ href={base}
80
+ class="truncate py-3 px-3 rounded-lg flex-none text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
81
+ >
82
+ Settings
83
+ </a>
84
+ </div>
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -20,12 +20,7 @@
20
  const dispatch = createEventDispatcher<{ message: string; share: void }>();
21
  </script>
22
 
23
- <div class="relative h-screen">
24
- <nav class="sm:hidden flex items-center h-12 border-b px-4 justify-between dark:border-gray-800">
25
- <button><CarbonTextAlignJustify /></button>
26
- <button>New Chat</button>
27
- <button><CarbonAdd /></button>
28
- </nav>
29
  <ChatMessages {loading} {pending} {messages} on:message />
30
  <div
31
  class="flex flex-col max-md:border-t dark:border-gray-800 items-center max-md:dark:bg-gray-900 max-md:bg-white bg-gradient-to-t from-white to-white/0 dark:from-gray-900 dark:to-gray-900/0 justify-center absolute inset-x-0 max-w-3xl xl:max-w-4xl mx-auto px-5 bottom-0 py-4 md:py-8 w-full"
 
20
  const dispatch = createEventDispatcher<{ message: string; share: void }>();
21
  </script>
22
 
23
+ <div class="relative min-h-0">
 
 
 
 
 
24
  <ChatMessages {loading} {pending} {messages} on:message />
25
  <div
26
  class="flex flex-col max-md:border-t dark:border-gray-800 items-center max-md:dark:bg-gray-900 max-md:bg-white bg-gradient-to-t from-white to-white/0 dark:from-gray-900 dark:to-gray-900/0 justify-center absolute inset-x-0 max-w-3xl xl:max-w-4xl mx-auto px-5 bottom-0 py-4 md:py-8 w-full"
src/lib/switchTheme.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ export function switchTheme() {
2
+ const { classList } = document.querySelector("html") as HTMLElement;
3
+ if (classList.contains("dark")) {
4
+ classList.remove("dark");
5
+ localStorage.theme = "light";
6
+ } else {
7
+ classList.add("dark");
8
+ localStorage.theme = "dark";
9
+ }
10
+ }
src/routes/+layout.svelte CHANGED
@@ -3,25 +3,16 @@
3
  import { page } from "$app/stores";
4
  import "../styles/main.css";
5
  import type { LayoutData } from "./$types";
6
-
7
- import CarbonTrashCan from "~icons/carbon/trash-can";
8
- import CarbonExport from "~icons/carbon/export";
9
  import { base } from "$app/paths";
10
  import { shareConversation } from "$lib/shareConversation";
11
  import { UrlDependency } from "$lib/types/UrlDependency";
12
 
 
 
 
13
  export let data: LayoutData;
14
 
15
- function switchTheme() {
16
- const { classList } = document.querySelector("html") as HTMLElement;
17
- if (classList.contains("dark")) {
18
- classList.remove("dark");
19
- localStorage.theme = "light";
20
- } else {
21
- classList.add("dark");
22
- localStorage.theme = "dark";
23
- }
24
- }
25
 
26
  async function deleteConversation(id: string) {
27
  try {
@@ -50,76 +41,27 @@
50
  </script>
51
 
52
  <div
53
- class="grid h-screen w-screen md:grid-cols-[280px,1fr] overflow-hidden text-smd dark:text-gray-300"
54
  >
 
 
 
 
 
 
 
 
 
 
 
55
  <nav
56
  class="max-md:hidden grid grid-rows-[auto,1fr,auto] grid-cols-1 max-h-screen bg-gradient-to-l from-gray-50 dark:from-gray-800/30 rounded-r-xl"
57
  >
58
- <div class="flex-none sticky top-0 p-3 flex flex-col">
59
- <a
60
- href={base || "/"}
61
- class="border px-12 py-2.5 rounded-lg shadow bg-white dark:bg-gray-700 dark:border-gray-600 text-center"
62
- >
63
- New Chat
64
- </a>
65
- </div>
66
- <div class="flex flex-col overflow-y-auto p-3 -mt-3 gap-1">
67
- {#each data.conversations as conv}
68
- <a
69
- data-sveltekit-noscroll
70
- href="{base}/conversation/{conv.id}"
71
- class="pl-3 pr-2 h-11 group rounded-lg flex-none text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-1.5 {conv.id ===
72
- $page.params.id
73
- ? 'bg-gray-100 dark:bg-gray-700'
74
- : ''}"
75
- >
76
- <div class="flex-1 truncate">{conv.title}</div>
77
-
78
- <button
79
- type="button"
80
- class="w-5 h-5 items-center justify-center hidden group-hover:flex rounded"
81
- title="Share conversation"
82
- on:click|preventDefault={() => shareConversation(conv.id, conv.title)}
83
- >
84
- <CarbonExport
85
- class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 text-xs"
86
- />
87
- </button>
88
-
89
- <button
90
- type="button"
91
- class="w-5 h-5 items-center justify-center hidden group-hover:flex rounded"
92
- title="Delete conversation"
93
- on:click|preventDefault={() => deleteConversation(conv.id)}
94
- >
95
- <CarbonTrashCan
96
- class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 text-xs"
97
- />
98
- </button>
99
- </a>
100
- {/each}
101
- </div>
102
- <div class="flex flex-col p-3 gap-2">
103
- <button
104
- on:click={switchTheme}
105
- type="button"
106
- class="text-left flex items-center first-letter:capitalize truncate py-3 px-3 rounded-lg flex-none text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
107
- >
108
- Theme
109
- </button>
110
- <a
111
- href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions"
112
- class="text-left flex items-center first-letter:capitalize truncate py-3 px-3 rounded-lg flex-none text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
113
- >
114
- Community feedback
115
- </a>
116
- <a
117
- href={base}
118
- class="truncate py-3 px-3 rounded-lg flex-none text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
119
- >
120
- Settings
121
- </a>
122
- </div>
123
  </nav>
124
  <slot />
125
  </div>
 
3
  import { page } from "$app/stores";
4
  import "../styles/main.css";
5
  import type { LayoutData } from "./$types";
 
 
 
6
  import { base } from "$app/paths";
7
  import { shareConversation } from "$lib/shareConversation";
8
  import { UrlDependency } from "$lib/types/UrlDependency";
9
 
10
+ import MobileNav from "$lib/components/MobileNav.svelte";
11
+ import NavMenu from "$lib/components/NavMenu.svelte";
12
+
13
  export let data: LayoutData;
14
 
15
+ let isNavOpen = false;
 
 
 
 
 
 
 
 
 
16
 
17
  async function deleteConversation(id: string) {
18
  try {
 
41
  </script>
42
 
43
  <div
44
+ class="grid h-screen w-screen grid-cols-1 grid-rows-[auto,1fr] md:grid-rows-[1fr] md:grid-cols-[280px,1fr] overflow-hidden text-smd dark:text-gray-300"
45
  >
46
+ <MobileNav
47
+ isOpen={isNavOpen}
48
+ on:toggle={(ev) => (isNavOpen = ev.detail)}
49
+ title={data.conversations.find((conv) => conv.id === $page.params.id)?.title}
50
+ >
51
+ <NavMenu
52
+ conversations={data.conversations}
53
+ on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
54
+ on:deleteConversation={(ev) => deleteConversation(ev.detail)}
55
+ />
56
+ </MobileNav>
57
  <nav
58
  class="max-md:hidden grid grid-rows-[auto,1fr,auto] grid-cols-1 max-h-screen bg-gradient-to-l from-gray-50 dark:from-gray-800/30 rounded-r-xl"
59
  >
60
+ <NavMenu
61
+ conversations={data.conversations}
62
+ on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
63
+ on:deleteConversation={(ev) => deleteConversation(ev.detail)}
64
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  </nav>
66
  <slot />
67
  </div>