coyotte508 HF staff commited on
Commit
1b66f8d
1 Parent(s): 067b52a

Split app structure & create convos (#20)

Browse files
src/hooks.server.ts CHANGED
@@ -6,14 +6,17 @@ export const handle: Handle = async ({ event, resolve }) => {
6
 
7
  event.locals.sessionId = token || crypto.randomUUID();
8
 
9
- // Refresh cookie expiration date
10
- event.cookies.set('session', event.locals.sessionId, {
11
- path: '/',
12
- sameSite: 'lax',
13
- secure: true,
14
- httpOnly: true,
15
- expires: addYears(new Date(), 1)
16
- });
 
 
 
17
 
18
  const response = await resolve(event);
19
 
 
6
 
7
  event.locals.sessionId = token || crypto.randomUUID();
8
 
9
+ // Setting a cookie breaks /api/conversation, maybe due to the proxy
10
+ if (!event.url.pathname.startsWith('/api')) {
11
+ // Refresh cookie expiration date
12
+ event.cookies.set('session', event.locals.sessionId, {
13
+ path: '/',
14
+ sameSite: 'lax',
15
+ secure: true,
16
+ httpOnly: true,
17
+ expires: addYears(new Date(), 1)
18
+ });
19
+ }
20
 
21
  const response = await resolve(event);
22
 
src/lib/Types.ts DELETED
@@ -1,28 +0,0 @@
1
- export type Message =
2
- | {
3
- from: 'user';
4
- content: string;
5
- }
6
- | {
7
- from: 'bot';
8
- content: string;
9
- };
10
-
11
- export interface Token {
12
- id: number;
13
- text: string;
14
- logprob: number;
15
- special: boolean;
16
- }
17
-
18
- export interface StreamResponse {
19
- /**
20
- * Generated token
21
- */
22
- token: Token;
23
- /**
24
- * Complete generated text
25
- * Only available when the generation is finished
26
- */
27
- generated_text?: string;
28
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/chat/ChatInput.svelte CHANGED
@@ -1,24 +1,24 @@
1
  <script lang="ts">
2
- import { createEventDispatcher } from 'svelte';
3
-
4
  export let value = '';
5
  export let minRows = 1;
6
  export let maxRows: null | number = null;
7
  export let placeholder = '';
 
8
  export let autofocus = false;
9
 
10
- const dispatch = createEventDispatcher();
11
-
12
  $: minHeight = `${1 + minRows * 1.5}em`;
13
  $: maxHeight = maxRows ? `${1 + maxRows * 1.5}em` : `auto`;
14
 
15
  function handleKeydown(event: KeyboardEvent) {
16
  // submit on enter
17
  if (event.key === 'Enter' && !event.shiftKey) {
18
- dispatch('submit');
19
  event.preventDefault();
 
 
20
  }
21
  }
 
 
22
  </script>
23
 
24
  <div class="relative flex-1 min-w-0">
@@ -32,6 +32,8 @@
32
  rows="1"
33
  class="absolute m-0 w-full h-full top-0 resize-none border-0 bg-transparent p-3 focus:ring-0 focus-visible:ring-0 dark:bg-transparent outline-none scrollbar-custom overflow-x-hidden overflow-y-scroll"
34
  bind:value
 
 
35
  on:keydown={handleKeydown}
36
  {placeholder}
37
  {autofocus}
 
1
  <script lang="ts">
 
 
2
  export let value = '';
3
  export let minRows = 1;
4
  export let maxRows: null | number = null;
5
  export let placeholder = '';
6
+ export let disabled = false;
7
  export let autofocus = false;
8
 
 
 
9
  $: minHeight = `${1 + minRows * 1.5}em`;
10
  $: maxHeight = maxRows ? `${1 + maxRows * 1.5}em` : `auto`;
11
 
12
  function handleKeydown(event: KeyboardEvent) {
13
  // submit on enter
14
  if (event.key === 'Enter' && !event.shiftKey) {
 
15
  event.preventDefault();
16
+
17
+ textareaElement.closest('form')?.requestSubmit();
18
  }
19
  }
20
+
21
+ let textareaElement: HTMLTextAreaElement;
22
  </script>
23
 
24
  <div class="relative flex-1 min-w-0">
 
32
  rows="1"
33
  class="absolute m-0 w-full h-full top-0 resize-none border-0 bg-transparent p-3 focus:ring-0 focus-visible:ring-0 dark:bg-transparent outline-none scrollbar-custom overflow-x-hidden overflow-y-scroll"
34
  bind:value
35
+ bind:this={textareaElement}
36
+ {disabled}
37
  on:keydown={handleKeydown}
38
  {placeholder}
39
  {autofocus}
src/lib/components/chat/ChatMessage.svelte CHANGED
@@ -1,10 +1,10 @@
1
  <script lang="ts">
2
- import type { Message } from '$lib/Types';
3
 
4
  export let message: Message;
5
  </script>
6
 
7
- {#if message.from === 'bot'}
8
  <div class="flex items-start justify-start gap-4 leading-relaxed">
9
  <img
10
  alt=""
 
1
  <script lang="ts">
2
+ import type { Message } from '$lib/types/Message';
3
 
4
  export let message: Message;
5
  </script>
6
 
7
+ {#if message.from === 'assistant'}
8
  <div class="flex items-start justify-start gap-4 leading-relaxed">
9
  <img
10
  alt=""
src/lib/components/chat/ChatMessages.svelte ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { Message } from '$lib/types/Message';
3
+ import { snapScrollToBottom } from '$lib/actions/snapScrollToBottom';
4
+ import ScrollToBottomBtn from '$lib/components/ScrollToBottomBtn.svelte';
5
+ import ChatIntroduction from './ChatIntroduction.svelte';
6
+ import ChatMessage from './ChatMessage.svelte';
7
+
8
+ export let messages: Message[];
9
+
10
+ let chatContainer: HTMLElement;
11
+ </script>
12
+
13
+ <div class="overflow-y-auto h-full" use:snapScrollToBottom={messages} bind:this={chatContainer}>
14
+ <div class="max-w-3xl xl:max-w-4xl mx-auto px-5 pt-6 flex flex-col gap-8 h-full">
15
+ {#each messages as message}
16
+ <ChatMessage {message} />
17
+ {:else}
18
+ <ChatIntroduction />
19
+ {/each}
20
+ <div class="h-32 flex-none" />
21
+ </div>
22
+ <ScrollToBottomBtn class="bottom-10 right-12" scrollNode={chatContainer} />
23
+ </div>
src/lib/components/chat/ChatWindow.svelte ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { Message } from '$lib/types/Message';
3
+ import { createEventDispatcher } from 'svelte';
4
+ import ChatMessages from './ChatMessages.svelte';
5
+ import ChatInput from './ChatInput.svelte';
6
+
7
+ export let messages: Message[] = [];
8
+ export let disabled: boolean;
9
+
10
+ let message: string;
11
+
12
+ const dispatch = createEventDispatcher<{ message: string }>();
13
+ </script>
14
+
15
+ <div class="relative h-screen">
16
+ <nav class="sm:hidden flex items-center h-12 border-b px-4 justify-between dark:border-gray-800">
17
+ <button>[ ]</button>
18
+ <button>New Chat</button>
19
+ <button>+</button>
20
+ </nav>
21
+ <ChatMessages {messages} />
22
+ <div
23
+ class="flex 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 dark:from-gray-900 to-transparent 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"
24
+ >
25
+ <form
26
+ on:submit|preventDefault={() => {
27
+ dispatch('message', message);
28
+ message = '';
29
+ }}
30
+ class="w-full relative flex items-center rounded-xl flex-1 max-w-4xl border bg-gray-100 focus-within:border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:focus-within:border-gray-500 transition-all"
31
+ >
32
+ <div class="w-full flex flex-1 border-none bg-transparent">
33
+ <ChatInput
34
+ placeholder="Ask anything"
35
+ bind:value={message}
36
+ {disabled}
37
+ autofocus
38
+ maxRows={10}
39
+ />
40
+ <button
41
+ class="p-1 px-[0.7rem] self-end my-1 h-[2.4rem] rounded-lg hover:bg-gray-100 enabled:dark:hover:text-gray-400 dark:hover:bg-gray-900 disabled:hover:bg-transparent dark:disabled:hover:bg-transparent disabled:opacity-60 dark:disabled:opacity-40 flex-shrink-0 transition-all mx-1"
42
+ disabled={!message || disabled}
43
+ type="submit"
44
+ >
45
+ <svg
46
+ class="text-gray-500 dark:text-gray-300 pointer-events-none"
47
+ xmlns="http://www.w3.org/2000/svg"
48
+ xmlns:xlink="http://www.w3.org/1999/xlink"
49
+ aria-hidden="true"
50
+ focusable="false"
51
+ role="img"
52
+ width="1em"
53
+ height="1em"
54
+ preserveAspectRatio="xMidYMid meet"
55
+ viewBox="0 0 32 32"
56
+ ><path
57
+ d="M30 28.59L22.45 21A11 11 0 1 0 21 22.45L28.59 30zM5 14a9 9 0 1 1 9 9a9 9 0 0 1-9-9z"
58
+ fill="currentColor"
59
+ /></svg
60
+ >
61
+ </button>
62
+ </div>
63
+ </form>
64
+ </div>
65
+ </div>
src/lib/stores/pendingMessage.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import { writable } from 'svelte/store';
2
+
3
+ export const pendingMessage = writable<string>('');
src/lib/types/Message.ts CHANGED
@@ -1,4 +1,4 @@
1
  export interface Message {
2
  from: 'user' | 'assistant';
3
- content: 'string';
4
  }
 
1
  export interface Message {
2
  from: 'user' | 'assistant';
3
+ content: string;
4
  }
src/routes/{+page.server.ts → +layout.server.ts} RENAMED
@@ -1,8 +1,8 @@
1
- import type { PageServerLoad } from './$types';
2
  import { collections } from '$lib/server/database';
3
  import type { Conversation } from '$lib/types/Conversation';
4
 
5
- export const load: PageServerLoad = async (event) => {
6
  const { conversations } = collections;
7
 
8
  return {
 
1
+ import type { LayoutServerLoad } from './$types';
2
  import { collections } from '$lib/server/database';
3
  import type { Conversation } from '$lib/types/Conversation';
4
 
5
+ export const load: LayoutServerLoad = async (event) => {
6
  const { conversations } = collections;
7
 
8
  return {
src/routes/+layout.svelte CHANGED
@@ -1,5 +1,58 @@
1
  <script lang="ts">
2
  import '../styles.css';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  </script>
4
 
5
- <slot />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <script lang="ts">
2
  import '../styles.css';
3
+ import type { LayoutData } from './$types';
4
+
5
+ export let data: LayoutData;
6
+
7
+ function switchTheme() {
8
+ const { classList } = document.querySelector('html') as HTMLElement;
9
+ if (classList.contains('dark')) {
10
+ classList.remove('dark');
11
+ localStorage.theme = 'light';
12
+ } else {
13
+ classList.add('dark');
14
+ localStorage.theme = 'dark';
15
+ }
16
+ }
17
  </script>
18
 
19
+ <div
20
+ class="grid h-screen w-screen md:grid-cols-[280px,1fr] overflow-hidden text-smd dark:text-gray-300"
21
+ >
22
+ <nav
23
+ 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"
24
+ >
25
+ <div class="flex-none sticky top-0 p-3 flex flex-col">
26
+ <button
27
+ on:click={() => location.reload()}
28
+ class="border px-12 py-2.5 rounded-lg shadow bg-white dark:bg-gray-700 dark:border-gray-600"
29
+ >New Chat</button
30
+ >
31
+ </div>
32
+ <div class="flex flex-col overflow-y-auto p-3 -mt-3 gap-2">
33
+ {#each data.conversations as conv}
34
+ <a
35
+ href="/conversation/{conv.id}"
36
+ 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"
37
+ >
38
+ {conv.title}
39
+ </a>
40
+ {/each}
41
+ </div>
42
+ <div class="flex flex-col p-3 gap-2">
43
+ <button
44
+ on:click={switchTheme}
45
+ 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"
46
+ >
47
+ Theme
48
+ </button>
49
+ <a
50
+ href="/"
51
+ 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"
52
+ >
53
+ Settings
54
+ </a>
55
+ </div>
56
+ </nav>
57
+ <slot />
58
+ </div>
src/routes/+page.svelte CHANGED
@@ -1,207 +1,36 @@
1
  <script lang="ts">
2
- import type { Message } from '$lib/Types';
3
-
4
- import { HfInference } from '@huggingface/inference';
5
-
6
- import ChatMessage from '$lib/components/chat/ChatMessage.svelte';
7
- import ChatIntroduction from '$lib/components/chat/ChatIntroduction.svelte';
8
- import ChatInput from '$lib/components/chat/ChatInput.svelte';
9
-
10
- import {
11
- PUBLIC_ASSISTANT_MESSAGE_TOKEN,
12
- PUBLIC_SEP_TOKEN,
13
- PUBLIC_USER_MESSAGE_TOKEN
14
- } from '$env/static/public';
15
- import { page } from '$app/stores';
16
- import type { PageData } from './$types';
17
-
18
- export let data: PageData;
19
-
20
- const userToken = PUBLIC_USER_MESSAGE_TOKEN || '<|prompter|>';
21
- const assistantToken = PUBLIC_ASSISTANT_MESSAGE_TOKEN || '<|assistant|>';
22
- const sepToken = PUBLIC_SEP_TOKEN || '<|endoftext|>';
23
- import { snapScrollToBottom } from '$lib/actions/snapScrollToBottom';
24
- import ScrollToBottomBtn from '$lib/components/ScrollToBottomBtn.svelte';
25
-
26
- const hf = new HfInference();
27
- const model = hf.endpoint(`${$page.url.origin}/api/conversation`);
28
-
29
- let messages: Message[] = [];
30
- let message = '';
31
- let chatContainer: HTMLElement;
32
-
33
- function switchTheme() {
34
- const { classList } = document.querySelector('html') as HTMLElement;
35
- if (classList.contains('dark')) {
36
- classList.remove('dark');
37
- localStorage.theme = 'light';
38
- } else {
39
- classList.add('dark');
40
- localStorage.theme = 'dark';
41
- }
42
- }
43
-
44
- async function getTextGenerationStream(inputs: string) {
45
- const response = model.textGenerationStream(
46
- {
47
- inputs,
48
- parameters: {
49
- // Taken from https://huggingface.co/spaces/huggingface/open-assistant-private-testing/blob/main/app.py#L54
50
- // @ts-ignore
51
- stop: ['<|endoftext|>'],
52
- max_new_tokens: 1024,
53
- truncate: 1024,
54
- typical_p: 0.2
55
  }
56
- },
57
- {
58
- use_cache: false
59
- }
60
- );
61
-
62
- // Regex to check if the text finishes by "<" but is not a piece of code like "`<img>`"
63
- const endOfTextRegex = /(?<!`)<(?!`)/;
64
-
65
- for await (const data of response) {
66
- if (!data) break;
67
-
68
- try {
69
- if (!data.token.special) {
70
- if (messages.at(-1)?.from !== 'bot') {
71
- // First token has a space at the beginning, trim it
72
- messages = [...messages, { from: 'bot', content: data.token.text.trimStart() }];
73
- } else {
74
- const isEndOfText = endOfTextRegex.test(data.token.text);
75
 
76
- messages.at(-1)!.content += isEndOfText
77
- ? data.token.text.replace('<', '')
78
- : data.token.text;
79
- messages = messages;
80
-
81
- if (isEndOfText) break;
82
- }
83
- }
84
- } catch (error) {
85
- console.error(error);
86
- break;
87
  }
88
- }
89
- }
90
 
91
- function onWrite() {
92
- if (!message) return;
93
 
94
- messages = [...messages, { from: 'user', content: message }];
95
- message = '';
96
- const inputs =
97
- messages
98
- .map(
99
- (m) =>
100
- (m.from === 'user' ? userToken + m.content : assistantToken + m.content) +
101
- (m.content.endsWith(sepToken) ? '' : sepToken)
102
- )
103
- .join('') + assistantToken;
104
 
105
- getTextGenerationStream(inputs);
 
 
 
 
106
  }
107
  </script>
108
 
109
- <div
110
- class="grid h-screen w-screen md:grid-cols-[280px,1fr] overflow-hidden text-smd dark:text-gray-300"
111
- >
112
- <nav
113
- 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"
114
- >
115
- <div class="flex-none sticky top-0 p-3 flex flex-col">
116
- <button
117
- on:click={() => location.reload()}
118
- class="border px-12 py-2.5 rounded-lg shadow bg-white dark:bg-gray-700 dark:border-gray-600"
119
- >New Chat</button
120
- >
121
- </div>
122
- <div class="flex flex-col overflow-y-auto p-3 -mt-3 gap-2">
123
- {#each data.conversations as conv}
124
- <a
125
- href="/conversation/{conv.id}"
126
- 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"
127
- >
128
- {conv.title}
129
- </a>
130
- {/each}
131
- </div>
132
- <div class="flex flex-col p-3 gap-2">
133
- <button
134
- on:click={switchTheme}
135
- 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"
136
- >
137
- Theme
138
- </button>
139
- <a
140
- href="/"
141
- 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"
142
- >
143
- Settings
144
- </a>
145
- </div>
146
- </nav>
147
- <div class="relative h-screen">
148
- <nav
149
- class="sm:hidden flex items-center h-12 border-b px-4 justify-between dark:border-gray-800"
150
- >
151
- <button>[ ]</button>
152
- <button>New Chat</button>
153
- <button>+</button>
154
- </nav>
155
- <div class="overflow-y-auto h-full" use:snapScrollToBottom={messages} bind:this={chatContainer}>
156
- <div class="max-w-3xl xl:max-w-4xl mx-auto px-5 pt-6 flex flex-col gap-8 h-full">
157
- {#each messages as message}
158
- <ChatMessage {message} />
159
- {:else}
160
- <ChatIntroduction />
161
- {/each}
162
- <div class="h-32 flex-none" />
163
- </div>
164
- <ScrollToBottomBtn class="bottom-10 right-12" scrollNode={chatContainer} />
165
- </div>
166
- <div
167
- class="flex 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 dark:from-gray-900 to-transparent 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"
168
- >
169
- <form
170
- on:submit={onWrite}
171
- class="w-full relative flex items-center rounded-xl flex-1 max-w-4xl border bg-gray-100 focus-within:border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:focus-within:border-gray-500 transition-all"
172
- >
173
- <div class="w-full flex flex-1 border-none bg-transparent">
174
- <ChatInput
175
- placeholder="Ask anything"
176
- bind:value={message}
177
- on:submit={onWrite}
178
- autofocus
179
- maxRows={10}
180
- />
181
- <button
182
- class="p-1 px-[0.7rem] self-end my-1 h-[2.4rem] rounded-lg hover:bg-gray-100 enabled:dark:hover:text-gray-400 dark:hover:bg-gray-900 disabled:hover:bg-transparent dark:disabled:hover:bg-transparent disabled:opacity-60 dark:disabled:opacity-40 flex-shrink-0 transition-all mx-1"
183
- disabled={!message}
184
- type="submit"
185
- >
186
- <svg
187
- class="text-gray-500 dark:text-gray-300 pointer-events-none"
188
- xmlns="http://www.w3.org/2000/svg"
189
- xmlns:xlink="http://www.w3.org/1999/xlink"
190
- aria-hidden="true"
191
- focusable="false"
192
- role="img"
193
- width="1em"
194
- height="1em"
195
- preserveAspectRatio="xMidYMid meet"
196
- viewBox="0 0 32 32"
197
- ><path
198
- d="M30 28.59L22.45 21A11 11 0 1 0 21 22.45L28.59 30zM5 14a9 9 0 1 1 9 9a9 9 0 0 1-9-9z"
199
- fill="currentColor"
200
- /></svg
201
- ></button
202
- >
203
- </div>
204
- </form>
205
- </div>
206
- </div>
207
- </div>
 
1
  <script lang="ts">
2
+ import { goto, invalidate, invalidateAll } from '$app/navigation';
3
+ import ChatWindow from '$lib/components/chat/ChatWindow.svelte';
4
+ import { pendingMessage } from '$lib/stores/pendingMessage';
5
+
6
+ let loading = false;
7
+
8
+ async function createConversation(message: string) {
9
+ try {
10
+ loading = true;
11
+ const res = await fetch('/conversation', {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Content-Type': 'application/json'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  }
16
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ if (!res.ok) {
19
+ alert('Error while creating conversation: ' + (await res.text()));
20
+ return;
 
 
 
 
 
 
 
 
21
  }
 
 
22
 
23
+ const { conversationId } = await res.json();
 
24
 
25
+ // Ugly hack to use a store as temp storage, feel free to improve ^^
26
+ pendingMessage.set(message);
 
 
 
 
 
 
 
 
27
 
28
+ // invalidateAll to update list of conversations
29
+ await goto(`/conversation/${conversationId}`, { invalidateAll: true });
30
+ } finally {
31
+ loading = false;
32
+ }
33
  }
34
  </script>
35
 
36
+ <ChatWindow on:message={(ev) => createConversation(ev.detail)} disabled={loading} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/api/conversation/+server.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { HF_TOKEN } from '$env/static/private';
2
  import { PUBLIC_MODEL_ENDPOINT } from '$env/static/public';
3
 
4
- export async function POST({ request }) {
5
  return await fetch(PUBLIC_MODEL_ENDPOINT, {
6
  headers: {
7
  ...request.headers,
 
1
  import { HF_TOKEN } from '$env/static/private';
2
  import { PUBLIC_MODEL_ENDPOINT } from '$env/static/public';
3
 
4
+ export async function POST({ request, fetch }) {
5
  return await fetch(PUBLIC_MODEL_ENDPOINT, {
6
  headers: {
7
  ...request.headers,
src/routes/conversation/+server.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { RequestHandler } from './$types';
2
+ import { collections } from '$lib/server/database';
3
+ import { ObjectId } from 'mongodb';
4
+
5
+ export const POST: RequestHandler = async (input) => {
6
+ const res = await collections.conversations.insertOne({
7
+ _id: new ObjectId(),
8
+ title:
9
+ 'Untitled ' +
10
+ ((await collections.conversations.countDocuments({ sessionId: input.locals.sessionId })) + 1),
11
+ messages: [],
12
+ createdAt: new Date(),
13
+ updatedAt: new Date(),
14
+ sessionId: input.locals.sessionId
15
+ });
16
+
17
+ return new Response(
18
+ JSON.stringify({
19
+ conversationId: res.insertedId.toString()
20
+ }),
21
+ { headers: { 'Content-Type': 'application/json' } }
22
+ );
23
+ };
src/routes/conversation/[id]/+page.server.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { PageServerLoad } from './$types';
2
+ import { collections } from '$lib/server/database';
3
+ import type { Conversation } from '$lib/types/Conversation';
4
+ import { ObjectId } from 'mongodb';
5
+ import { error } from '@sveltejs/kit';
6
+
7
+ export const load: PageServerLoad = async (event) => {
8
+ // todo: add validation on params.id
9
+ const conversation = await collections.conversations.findOne({
10
+ _id: new ObjectId(event.params.id),
11
+ sessionId: event.locals.sessionId
12
+ });
13
+
14
+ if (!conversation) {
15
+ throw error(404, 'Conversation not found');
16
+ }
17
+
18
+ return {
19
+ messages: conversation.messages
20
+ };
21
+ };
src/routes/conversation/[id]/+page.svelte ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import ChatWindow from '$lib/components/chat/ChatWindow.svelte';
3
+ import { pendingMessage } from '$lib/stores/pendingMessage';
4
+ import { onMount } from 'svelte';
5
+ import type { PageData } from './$types';
6
+ import { page } from '$app/stores';
7
+ import {
8
+ PUBLIC_ASSISTANT_MESSAGE_TOKEN,
9
+ PUBLIC_SEP_TOKEN,
10
+ PUBLIC_USER_MESSAGE_TOKEN
11
+ } from '$env/static/public';
12
+ import { HfInference } from '@huggingface/inference';
13
+
14
+ export let data: PageData;
15
+
16
+ $: messages = data.messages;
17
+
18
+ const userToken = PUBLIC_USER_MESSAGE_TOKEN;
19
+ const assistantToken = PUBLIC_ASSISTANT_MESSAGE_TOKEN;
20
+ const sepToken = PUBLIC_SEP_TOKEN;
21
+
22
+ const hf = new HfInference();
23
+ const model = hf.endpoint(`${$page.url.origin}/api/conversation`);
24
+
25
+ let loading = false;
26
+
27
+ async function getTextGenerationStream(inputs: string) {
28
+ const response = model.textGenerationStream(
29
+ {
30
+ inputs,
31
+ parameters: {
32
+ // Taken from https://huggingface.co/spaces/huggingface/open-assistant-private-testing/blob/main/app.py#L54
33
+ // @ts-ignore
34
+ stop: ['<|endoftext|>'],
35
+ max_new_tokens: 1024,
36
+ truncate: 1024,
37
+ typical_p: 0.2
38
+ }
39
+ },
40
+ {
41
+ use_cache: false
42
+ }
43
+ );
44
+
45
+ // Regex to check if the text finishes by "<" but is not a piece of code like "`<img>`"
46
+ const endOfTextRegex = /(?<!`)<(?!`)/;
47
+
48
+ for await (const data of response) {
49
+ if (!data) break;
50
+
51
+ if (!data.token.special) {
52
+ if (messages.at(-1)?.from !== 'assistant') {
53
+ // First token has a space at the beginning, trim it
54
+ messages = [...messages, { from: 'assistant', content: data.token.text.trimStart() }];
55
+ } else {
56
+ const isEndOfText = endOfTextRegex.test(data.token.text);
57
+
58
+ messages.at(-1)!.content += isEndOfText
59
+ ? data.token.text.replace('<', '')
60
+ : data.token.text;
61
+ messages = messages;
62
+
63
+ if (isEndOfText) break;
64
+ }
65
+ }
66
+ }
67
+
68
+ // todo: if everything went well, store message + response in DB
69
+ }
70
+
71
+ async function writeMessage(message: string) {
72
+ if (!message.trim()) return;
73
+
74
+ try {
75
+ loading = true;
76
+
77
+ messages = [...messages, { from: 'user', content: message }];
78
+ message = '';
79
+ const inputs =
80
+ messages
81
+ .map(
82
+ (m) =>
83
+ (m.from === 'user' ? userToken + m.content : assistantToken + m.content) +
84
+ (m.content.endsWith(sepToken) ? '' : sepToken)
85
+ )
86
+ .join('') + assistantToken;
87
+
88
+ await getTextGenerationStream(inputs);
89
+ } finally {
90
+ loading = false;
91
+ }
92
+ }
93
+
94
+ onMount(async () => {
95
+ if ($pendingMessage) {
96
+ const val = $pendingMessage;
97
+ $pendingMessage = '';
98
+
99
+ if (messages.length === 0) {
100
+ writeMessage(val);
101
+ }
102
+ }
103
+ });
104
+ </script>
105
+
106
+ <ChatWindow disabled={loading} {messages} on:message={(message) => writeMessage(message.detail)} />