Commit
•
1b66f8d
1
Parent(s):
067b52a
Split app structure & create convos (#20)
Browse files- src/hooks.server.ts +11 -8
- src/lib/Types.ts +0 -28
- src/lib/components/chat/ChatInput.svelte +7 -5
- src/lib/components/chat/ChatMessage.svelte +2 -2
- src/lib/components/chat/ChatMessages.svelte +23 -0
- src/lib/components/chat/ChatWindow.svelte +65 -0
- src/lib/stores/pendingMessage.ts +3 -0
- src/lib/types/Message.ts +1 -1
- src/routes/{+page.server.ts → +layout.server.ts} +2 -2
- src/routes/+layout.svelte +54 -1
- src/routes/+page.svelte +26 -197
- src/routes/api/conversation/+server.ts +1 -1
- src/routes/conversation/+server.ts +23 -0
- src/routes/conversation/[id]/+page.server.ts +21 -0
- src/routes/conversation/[id]/+page.svelte +106 -0
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 |
-
//
|
10 |
-
event.
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
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/
|
3 |
|
4 |
export let message: Message;
|
5 |
</script>
|
6 |
|
7 |
-
{#if message.from === '
|
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:
|
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 {
|
2 |
import { collections } from '$lib/server/database';
|
3 |
import type { Conversation } from '$lib/types/Conversation';
|
4 |
|
5 |
-
export const load:
|
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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
3 |
-
|
4 |
-
import {
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
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 |
-
|
77 |
-
|
78 |
-
|
79 |
-
messages = messages;
|
80 |
-
|
81 |
-
if (isEndOfText) break;
|
82 |
-
}
|
83 |
-
}
|
84 |
-
} catch (error) {
|
85 |
-
console.error(error);
|
86 |
-
break;
|
87 |
}
|
88 |
-
}
|
89 |
-
}
|
90 |
|
91 |
-
|
92 |
-
if (!message) return;
|
93 |
|
94 |
-
|
95 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
106 |
}
|
107 |
</script>
|
108 |
|
109 |
-
<
|
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)} />
|