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 +60 -0
- src/lib/components/NavMenu.svelte +84 -0
- src/lib/components/chat/ChatWindow.svelte +1 -6
- src/lib/switchTheme.ts +10 -0
- src/routes/+layout.svelte +21 -79
@@ -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>
|
@@ -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>
|
@@ -20,12 +20,7 @@
|
|
20 |
const dispatch = createEventDispatcher<{ message: string; share: void }>();
|
21 |
</script>
|
22 |
|
23 |
-
<div class="relative h-
|
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"
|
@@ -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 |
+
}
|
@@ -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 |
-
|
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 |
-
<
|
59 |
-
|
60 |
-
|
61 |
-
|
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>
|