unbrandedhuman commited on
Commit
1e95ed5
·
1 Parent(s): 8f3d5b1

Upload 83 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. src/app.d.ts +20 -0
  2. src/app.html +73 -0
  3. src/hooks.server.ts +95 -0
  4. src/lib/actions/snapScrollToBottom.ts +54 -0
  5. src/lib/buildPrompt.ts +36 -0
  6. src/lib/components/AnnouncementBanner.svelte +15 -0
  7. src/lib/components/CodeBlock.svelte +28 -0
  8. src/lib/components/CopyToClipBoardBtn.svelte +50 -0
  9. src/lib/components/EthicsModal.svelte +46 -0
  10. src/lib/components/MobileNav.svelte +62 -0
  11. src/lib/components/Modal.svelte +59 -0
  12. src/lib/components/ModelCardMetadata.svelte +53 -0
  13. src/lib/components/ModelsModal.svelte +80 -0
  14. src/lib/components/NavConversationItem.svelte +87 -0
  15. src/lib/components/NavMenu.svelte +71 -0
  16. src/lib/components/Portal.svelte +19 -0
  17. src/lib/components/ScrollToBottomBtn.svelte +46 -0
  18. src/lib/components/SettingsModal.svelte +65 -0
  19. src/lib/components/StopGeneratingBtn.svelte +17 -0
  20. src/lib/components/Switch.svelte +11 -0
  21. src/lib/components/Toast.svelte +19 -0
  22. src/lib/components/Tooltip.svelte +22 -0
  23. src/lib/components/chat/ChatInput.svelte +64 -0
  24. src/lib/components/chat/ChatIntroduction.svelte +91 -0
  25. src/lib/components/chat/ChatMessage.svelte +148 -0
  26. src/lib/components/chat/ChatMessages.svelte +65 -0
  27. src/lib/components/chat/ChatWindow.svelte +106 -0
  28. src/lib/components/icons/IconChevron.svelte +20 -0
  29. src/lib/components/icons/IconCopy.svelte +26 -0
  30. src/lib/components/icons/IconDazzled.svelte +36 -0
  31. src/lib/components/icons/IconLoading.svelte +31 -0
  32. src/lib/components/icons/Logo.svelte +27 -0
  33. src/lib/constants/publicSepToken.ts +1 -0
  34. src/lib/server/abortedGenerations.ts +29 -0
  35. src/lib/server/auth.ts +9 -0
  36. src/lib/server/database.ts +52 -0
  37. src/lib/server/modelEndpoint.ts +32 -0
  38. src/lib/server/models.ts +80 -0
  39. src/lib/shareConversation.ts +27 -0
  40. src/lib/stores/errors.ts +7 -0
  41. src/lib/stores/pendingMessage.ts +3 -0
  42. src/lib/stores/pendingMessageIdToRetry.ts +4 -0
  43. src/lib/switchTheme.ts +10 -0
  44. src/lib/types/AbortedGeneration.ts +8 -0
  45. src/lib/types/Conversation.ts +20 -0
  46. src/lib/types/Message.ts +5 -0
  47. src/lib/types/Model.ts +13 -0
  48. src/lib/types/Settings.ts +16 -0
  49. src/lib/types/SharedConversation.ts +12 -0
  50. src/lib/types/Timestamps.ts +4 -0
src/app.d.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="@sveltejs/kit" />
2
+ /// <reference types="unplugin-icons/types/svelte" />
3
+
4
+ import type { ObjectId } from "mongodb";
5
+
6
+ // See https://kit.svelte.dev/docs/types#app
7
+ // for information about these interfaces
8
+ declare global {
9
+ namespace App {
10
+ // interface Error {}
11
+ interface Locals {
12
+ sessionId: string;
13
+ userId?: ObjectId;
14
+ }
15
+ // interface PageData {}
16
+ // interface Platform {}
17
+ }
18
+ }
19
+
20
+ export {};
src/app.html ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
7
+ <title>Macie</title>
8
+ <script>
9
+ if (
10
+ localStorage.theme === "dark" ||
11
+ (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)
12
+ ) {
13
+ document.documentElement.classList.add("dark");
14
+ }
15
+
16
+ // For some reason, Sveltekit doesn't let us load env variables from .env here, so we load it from hooks.server.ts
17
+ window.gaId = "%gaId%";
18
+ window.gaIdDeprecated = "%gaIdDeprecated%";
19
+ </script>
20
+ %sveltekit.head%
21
+ </head>
22
+ <body data-sveltekit-preload-data="hover" class="h-full dark:bg-gray-900">
23
+ <div id="app" class="contents h-full">%sveltekit.body%</div>
24
+
25
+ <!-- Google Tag Manager -->
26
+ <script>
27
+ if (window.gaId) {
28
+ const script = document.createElement("script");
29
+ script.src = "https://www.googletagmanager.com/gtag/js?id=" + window.gaId;
30
+ script.async = true;
31
+ document.head.appendChild(script);
32
+
33
+ window.dataLayer = window.dataLayer || [];
34
+ function gtag() {
35
+ dataLayer.push(arguments);
36
+ }
37
+ gtag("js", new Date());
38
+ /// ^ See https://developers.google.com/tag-platform/gtagjs/install
39
+ gtag("config", window.gaId);
40
+ gtag("consent", "default", { ad_storage: "denied", analytics_storage: "denied" });
41
+ /// ^ See https://developers.google.com/tag-platform/gtagjs/reference#consent
42
+ /// TODO: ask the user for their consent and update this with gtag('consent', 'update')
43
+ }
44
+ </script>
45
+
46
+ <!-- Google Analytics v3 (deprecated on 1 July 2023) -->
47
+ <script>
48
+ if (window.gaIdDeprecated) {
49
+ (function (i, s, o, g, r, a, m) {
50
+ i["GoogleAnalyticsObject"] = r;
51
+ (i[r] =
52
+ i[r] ||
53
+ function () {
54
+ (i[r].q = i[r].q || []).push(arguments);
55
+ }),
56
+ (i[r].l = 1 * new Date());
57
+ (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]);
58
+ a.async = 1;
59
+ a.src = g;
60
+ m.parentNode.insertBefore(a, m);
61
+ })(
62
+ window,
63
+ document,
64
+ "script",
65
+ "https://www.google-analytics.com/analytics.js",
66
+ "ganalytics"
67
+ );
68
+ ganalytics("create", window.gaIdDeprecated, "auto");
69
+ ganalytics("send", "pageview");
70
+ }
71
+ </script>
72
+ </body>
73
+ </html>
src/hooks.server.ts ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { dev } from "$app/environment";
2
+ import { COOKIE_NAME } from "$env/static/private";
3
+ import type { Handle } from "@sveltejs/kit";
4
+ import {
5
+ PUBLIC_GOOGLE_ANALYTICS_ID,
6
+ PUBLIC_DEPRECATED_GOOGLE_ANALYTICS_ID,
7
+ } from "$env/static/public";
8
+ import { addYears } from "date-fns";
9
+ import { collections } from "$lib/server/database";
10
+ import { base } from "$app/paths";
11
+ import { requiresUser } from "$lib/server/auth";
12
+
13
+ export const handle: Handle = async ({ event, resolve }) => {
14
+ const token = event.cookies.get(COOKIE_NAME);
15
+
16
+ event.locals.sessionId = token || crypto.randomUUID();
17
+
18
+ const user = await collections.users.findOne({ sessionId: event.locals.sessionId });
19
+
20
+ if (user) {
21
+ event.locals.userId = user._id;
22
+ }
23
+
24
+ if (
25
+ !event.url.pathname.startsWith(`${base}/admin`) &&
26
+ !["GET", "OPTIONS", "HEAD"].includes(event.request.method)
27
+ ) {
28
+ const sendJson =
29
+ event.request.headers.get("accept")?.includes("application/json") ||
30
+ event.request.headers.get("content-type")?.includes("application/json");
31
+
32
+ if (!user && requiresUser) {
33
+ return new Response(
34
+ sendJson
35
+ ? JSON.stringify({ error: "You need to be logged in first" })
36
+ : "You need to be logged in first",
37
+ {
38
+ status: 401,
39
+ headers: {
40
+ "content-type": sendJson ? "application/json" : "text/plain",
41
+ },
42
+ }
43
+ );
44
+ }
45
+
46
+ if (!event.url.pathname.startsWith(`${base}/settings`)) {
47
+ const hasAcceptedEthicsModal = await collections.settings.countDocuments({
48
+ sessionId: event.locals.sessionId,
49
+ ethicsModalAcceptedAt: { $exists: true },
50
+ });
51
+
52
+ if (!hasAcceptedEthicsModal) {
53
+ return new Response(
54
+ sendJson
55
+ ? JSON.stringify({ error: "You need to accept the welcome modal first" })
56
+ : "You need to accept the welcome modal first",
57
+ {
58
+ status: 405,
59
+ headers: {
60
+ "content-type": sendJson ? "application/json" : "text/plain",
61
+ },
62
+ }
63
+ );
64
+ }
65
+ }
66
+ }
67
+
68
+ // Refresh cookie expiration date
69
+ event.cookies.set(COOKIE_NAME, event.locals.sessionId, {
70
+ path: "/",
71
+ // So that it works inside the space's iframe
72
+ sameSite: dev ? "lax" : "none",
73
+ secure: !dev,
74
+ httpOnly: true,
75
+ expires: addYears(new Date(), 1),
76
+ });
77
+
78
+ let replaced = false;
79
+
80
+ const response = await resolve(event, {
81
+ transformPageChunk: (chunk) => {
82
+ // For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
83
+ if (replaced || !chunk.html.includes("%gaId%") || !chunk.html.includes("%gaIdDeprecated%")) {
84
+ return chunk.html;
85
+ }
86
+ replaced = true;
87
+
88
+ return chunk.html
89
+ .replace("%gaId%", PUBLIC_GOOGLE_ANALYTICS_ID)
90
+ .replace("%gaIdDeprecated%", PUBLIC_DEPRECATED_GOOGLE_ANALYTICS_ID);
91
+ },
92
+ });
93
+
94
+ return response;
95
+ };
src/lib/actions/snapScrollToBottom.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { navigating } from "$app/stores";
2
+ import { tick } from "svelte";
3
+ import { get } from "svelte/store";
4
+
5
+ const detachedOffset = 10;
6
+
7
+ /**
8
+ * @param node element to snap scroll to bottom
9
+ * @param dependency pass in a dependency to update scroll on changes.
10
+ */
11
+ export const snapScrollToBottom = (node: HTMLElement, dependency: unknown) => {
12
+ let prevScrollValue = node.scrollTop;
13
+ let isDetached = false;
14
+
15
+ const handleScroll = () => {
16
+ // if user scrolled up, we detach
17
+ if (node.scrollTop < prevScrollValue) {
18
+ isDetached = true;
19
+ }
20
+
21
+ // if user scrolled back to within 10px of bottom, we reattach
22
+ if (node.scrollTop - (node.scrollHeight - node.clientHeight) >= -detachedOffset) {
23
+ isDetached = false;
24
+ }
25
+
26
+ prevScrollValue = node.scrollTop;
27
+ };
28
+
29
+ const updateScroll = async (_options: { force?: boolean } = {}) => {
30
+ const defaultOptions = { force: false };
31
+ const options = { ...defaultOptions, ..._options };
32
+ const { force } = options;
33
+
34
+ if (!force && isDetached && !get(navigating)) return;
35
+
36
+ // wait for next tick to ensure that the DOM is updated
37
+ await tick();
38
+
39
+ node.scrollTo({ top: node.scrollHeight });
40
+ };
41
+
42
+ node.addEventListener("scroll", handleScroll);
43
+
44
+ if (dependency) {
45
+ updateScroll({ force: true });
46
+ }
47
+
48
+ return {
49
+ update: updateScroll,
50
+ destroy: () => {
51
+ node.removeEventListener("scroll", handleScroll);
52
+ },
53
+ };
54
+ };
src/lib/buildPrompt.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { BackendModel } from "./server/models";
2
+ import type { Message } from "./types/Message";
3
+
4
+ /**
5
+ * Convert [{user: "assistant", content: "hi"}, {user: "user", content: "hello"}] to:
6
+ *
7
+ * <|assistant|>hi<|endoftext|><|prompter|>hello<|endoftext|><|assistant|>
8
+ */
9
+ export function buildPrompt(
10
+ messages: Pick<Message, "from" | "content">[],
11
+ model: BackendModel
12
+ ): string {
13
+ const prompt =
14
+ messages
15
+ .map(
16
+ (m) =>
17
+ (m.from === "user"
18
+ ? model.userMessageToken + m.content
19
+ : model.assistantMessageToken + m.content) +
20
+ (model.messageEndToken
21
+ ? m.content.endsWith(model.messageEndToken)
22
+ ? ""
23
+ : model.messageEndToken
24
+ : "")
25
+ )
26
+ .join("") + model.assistantMessageToken;
27
+
28
+ // Not super precise, but it's truncated in the model's backend anyway
29
+ return (
30
+ model.preprompt +
31
+ prompt
32
+ .split(" ")
33
+ .slice(-(model.parameters?.truncate ?? 0))
34
+ .join(" ")
35
+ );
36
+ }
src/lib/components/AnnouncementBanner.svelte ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let title = "";
3
+ export let classNames = "";
4
+ </script>
5
+
6
+ <div class="flex items-center rounded-xl bg-gray-100 p-1 text-sm dark:bg-gray-800 {classNames}">
7
+ <span
8
+ class="mr-2 inline-flex items-center rounded-lg bg-gradient-to-br from-pink-300 px-2 py-1 text-xxs font-medium uppercase leading-3 text-pink-700 dark:from-[#373010] dark:text-pink-400"
9
+ >New!</span
10
+ >
11
+ {title}
12
+ <div class="ml-auto shrink-0">
13
+ <slot />
14
+ </div>
15
+ </div>
src/lib/components/CodeBlock.svelte ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { afterUpdate } from "svelte";
3
+ import CopyToClipBoardBtn from "./CopyToClipBoardBtn.svelte";
4
+
5
+ export let code = "";
6
+ export let lang = "";
7
+
8
+ $: highlightedCode = "";
9
+
10
+ afterUpdate(async () => {
11
+ const { default: hljs } = await import("highlight.js");
12
+ const language = hljs.getLanguage(lang);
13
+
14
+ highlightedCode = hljs.highlightAuto(code, language?.aliases).value;
15
+ });
16
+ </script>
17
+
18
+ <div class="group relative my-4 rounded-lg">
19
+ <!-- eslint-disable svelte/no-at-html-tags -->
20
+ <pre
21
+ class="scrollbar-custom overflow-auto px-5 scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20"><code
22
+ class="language-{lang}">{@html highlightedCode || code.replaceAll("<", "&lt;")}</code
23
+ ></pre>
24
+ <CopyToClipBoardBtn
25
+ classNames="absolute top-2 right-2 invisible opacity-0 group-hover:visible group-hover:opacity-100"
26
+ value={code}
27
+ />
28
+ </div>
src/lib/components/CopyToClipBoardBtn.svelte ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onDestroy } from "svelte";
3
+
4
+ import IconCopy from "./icons/IconCopy.svelte";
5
+ import Tooltip from "./Tooltip.svelte";
6
+
7
+ export let classNames = "";
8
+ export let value: string;
9
+
10
+ let isSuccess = false;
11
+ let timeout: ReturnType<typeof setTimeout>;
12
+
13
+ const handleClick = async () => {
14
+ // writeText() can be unavailable or fail in some cases (iframe, etc) so we try/catch
15
+ try {
16
+ await navigator.clipboard.writeText(value);
17
+
18
+ isSuccess = true;
19
+ if (timeout) {
20
+ clearTimeout(timeout);
21
+ }
22
+ timeout = setTimeout(() => {
23
+ isSuccess = false;
24
+ }, 1000);
25
+ } catch (err) {
26
+ console.error(err);
27
+ }
28
+ };
29
+
30
+ onDestroy(() => {
31
+ if (timeout) {
32
+ clearTimeout(timeout);
33
+ }
34
+ });
35
+ </script>
36
+
37
+ <button
38
+ class="btn rounded-lg border border-gray-200 px-2 py-2 text-sm shadow-sm transition-all hover:border-gray-300 active:shadow-inner dark:border-gray-600 dark:hover:border-gray-400 {classNames}
39
+ {!isSuccess && 'text-gray-200 dark:text-gray-200'}
40
+ {isSuccess && 'text-green-500'}
41
+ "
42
+ title={"Copy to clipboard"}
43
+ type="button"
44
+ on:click={handleClick}
45
+ >
46
+ <span class="relative">
47
+ <IconCopy />
48
+ <Tooltip classNames={isSuccess ? "opacity-100" : "opacity-0"} />
49
+ </span>
50
+ </button>
src/lib/components/EthicsModal.svelte ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { enhance } from "$app/forms";
3
+ import { base } from "$app/paths";
4
+ import { PUBLIC_VERSION } from "$env/static/public";
5
+ import Logo from "$lib/components/icons/Logo.svelte";
6
+ import Modal from "$lib/components/Modal.svelte";
7
+ import type { LayoutData } from "../../routes/$types";
8
+
9
+ export let settings: LayoutData["settings"];
10
+ </script>
11
+
12
+ <Modal>
13
+ <div
14
+ class="flex w-full flex-col items-center gap-6 bg-gradient-to-t from-yellow-500/40 via-yellow-500/10 to-yellow-500/0 px-4 pb-10 pt-9 text-center"
15
+ >
16
+ <h2 class="flex items-center text-2xl font-semibold text-gray-800">
17
+ <Logo classNames="text-3xl mr-1.5" />Macie
18
+ <div
19
+ class="ml-3 flex h-6 items-center rounded-lg border border-gray-100 bg-gray-50 px-2 text-base text-gray-400"
20
+ >
21
+ Duet-1
22
+ </div>
23
+ </h2>
24
+ <p class="px-4 text-lg font-semibold leading-snug text-gray-800 sm:px-12">
25
+ Macie Chat is in development phase.
26
+ </p>
27
+ <p class="text-gray-800">
28
+ Macie is in early stages of development. Don't take her advice as fact or to make big decisions. We ourselves don't know exactly what she says.
29
+ </p>
30
+ <p class="px-2 text-sm text-gray-500">
31
+ You're conversations will be shared with Macie Developers to improve and support a better world of AI.
32
+ </p>
33
+ <form action="{base}/settings" use:enhance method="POST">
34
+ <input type="hidden" name="ethicsModalAccepted" value={true} />
35
+ {#each Object.entries(settings) as [key, val]}
36
+ <input type="hidden" name={key} value={val} />
37
+ {/each}
38
+ <button
39
+ type="submit"
40
+ class="mt-2 rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-yellow-500"
41
+ >
42
+ Let's Go!
43
+ </button>
44
+ </form>
45
+ </div>
46
+ </Modal>
src/lib/components/MobileNav.svelte ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 | undefined;
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
33
+ class="flex h-12 items-center justify-between border-b bg-gray-50 px-4 dark:border-gray-800 dark:bg-gray-800/70 md:hidden"
34
+ >
35
+ <button
36
+ type="button"
37
+ class="-ml-3 flex h-9 w-9 shrink-0 items-center justify-center"
38
+ on:click={() => dispatch("toggle", true)}
39
+ aria-label="Open menu"
40
+ bind:this={openEl}><CarbonTextAlignJustify /></button
41
+ >
42
+ <span class="truncate px-4">{title}</span>
43
+ <a href={base || "/"} class="-mr-3 flex h-9 w-9 shrink-0 items-center justify-center"
44
+ ><CarbonAdd /></a
45
+ >
46
+ </nav>
47
+ <nav
48
+ class="fixed inset-0 z-30 grid max-h-screen grid-cols-1 grid-rows-[auto,auto,1fr,auto] bg-white bg-gradient-to-l from-gray-50 dark:bg-gray-900 dark:from-gray-800/30 {isOpen
49
+ ? 'block'
50
+ : 'hidden'}"
51
+ >
52
+ <div class="flex h-12 items-center px-4">
53
+ <button
54
+ type="button"
55
+ class="-mr-3 ml-auto flex h-9 w-9 items-center justify-center"
56
+ on:click={() => dispatch("toggle", false)}
57
+ aria-label="Close menu"
58
+ bind:this={closeEl}><CarbonClose /></button
59
+ >
60
+ </div>
61
+ <slot />
62
+ </nav>
src/lib/components/Modal.svelte ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher, onDestroy, onMount } from "svelte";
3
+ import { cubicOut } from "svelte/easing";
4
+ import { fade } from "svelte/transition";
5
+ import Portal from "./Portal.svelte";
6
+ import { browser } from "$app/environment";
7
+
8
+ export let width = "max-w-sm";
9
+
10
+ let backdropEl: HTMLDivElement;
11
+ let modalEl: HTMLDivElement;
12
+
13
+ const dispatch = createEventDispatcher<{ close: void }>();
14
+
15
+ function handleKeydown(event: KeyboardEvent) {
16
+ // close on ESC
17
+ if (event.key === "Escape") {
18
+ event.preventDefault();
19
+ dispatch("close");
20
+ }
21
+ }
22
+
23
+ function handleBackdropClick(event: MouseEvent) {
24
+ if (event.target === backdropEl) {
25
+ dispatch("close");
26
+ }
27
+ }
28
+
29
+ onMount(() => {
30
+ document.getElementById("app")?.setAttribute("inert", "true");
31
+ modalEl.focus();
32
+ });
33
+
34
+ onDestroy(() => {
35
+ if (!browser) return;
36
+ document.getElementById("app")?.removeAttribute("inert");
37
+ });
38
+ </script>
39
+
40
+ <Portal>
41
+ <div
42
+ role="presentation"
43
+ tabindex="-1"
44
+ bind:this={backdropEl}
45
+ on:click={handleBackdropClick}
46
+ transition:fade={{ easing: cubicOut, duration: 300 }}
47
+ class="fixed inset-0 z-40 flex items-center justify-center bg-black/80 p-8 backdrop-blur-sm dark:bg-black/50"
48
+ >
49
+ <div
50
+ role="dialog"
51
+ tabindex="-1"
52
+ bind:this={modalEl}
53
+ on:keydown={handleKeydown}
54
+ class="-mt-10 overflow-hidden rounded-2xl bg-white shadow-2xl outline-none md:-mt-20 {width}"
55
+ >
56
+ <slot />
57
+ </div>
58
+ </div>
59
+ </Portal>
src/lib/components/ModelCardMetadata.svelte ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import CarbonEarth from "~icons/carbon/earth";
3
+ import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
4
+ import type { Model } from "$lib/types/Model";
5
+
6
+ export let model: Pick<Model, "name" | "datasetName" | "websiteUrl">;
7
+
8
+ export let variant: "light" | "dark" = "light";
9
+ </script>
10
+
11
+ <div
12
+ class="flex items-center gap-5 rounded-xl bg-gray-100 px-3 py-2 text-sm
13
+ {variant === 'dark'
14
+ ? 'text-gray-600 dark:bg-gray-800 dark:text-gray-300'
15
+ : 'text-gray-800 dark:bg-gray-100 dark:text-gray-600'}"
16
+ >
17
+
18
+ <!-- <a
19
+ href="https://huggingface.co/{model.name}"
20
+ target="_blank"
21
+ rel="noreferrer"
22
+ class="flex items-center hover:underline"
23
+ ><CarbonArrowUpRight class="mr-1.5 shrink-0 text-xs text-gray-400" />
24
+ Model
25
+ <div class="max-sm:hidden">&nbsp;page</div></a
26
+ >
27
+ {#if model.datasetName}
28
+ <a
29
+ href="https://huggingface.co/datasets/{model.datasetName}"
30
+ target="_blank"
31
+ rel="noreferrer"
32
+ class="flex items-center hover:underline"
33
+ ><CarbonArrowUpRight class="mr-1.5 shrink-0 text-xs text-gray-400" />
34
+ Dataset
35
+ <div class="max-sm:hidden">&nbsp;page</div></a
36
+ >
37
+ {/if}
38
+ {#if model.websiteUrl}
39
+ <a
40
+ href={model.websiteUrl}
41
+ target="_blank"
42
+ class="ml-auto flex items-center hover:underline"
43
+ rel="noreferrer"
44
+ >
45
+ <CarbonEarth class="mr-1.5 shrink-0 text-xs text-gray-400" />
46
+ Website
47
+ </a>
48
+ {/if} -->
49
+
50
+ <a>
51
+ Macie Duet-1. Content and debugging still isn't 100%, and macie can get it wrong sometimes.
52
+ </a>
53
+ </div>
src/lib/components/ModelsModal.svelte ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ export let settings: LayoutData["settings"];
14
+ export let models: Array<Model>;
15
+
16
+ let selectedModelId = settings.activeModel;
17
+
18
+ const dispatch = createEventDispatcher<{ close: void }>();
19
+ </script>
20
+
21
+ <Modal width="max-w-lg" on:close>
22
+ <form
23
+ action="{base}/settings"
24
+ method="post"
25
+ use:enhance={() => {
26
+ dispatch("close");
27
+ }}
28
+ class="flex w-full flex-col gap-5 p-6"
29
+ >
30
+ {#each Object.entries(settings).filter(([k]) => k !== "activeModel") as [key, val]}
31
+ <input type="hidden" name={key} value={val} />
32
+ {/each}
33
+ <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
34
+ <h2>Models</h2>
35
+ <button type="button" class="group" on:click={() => dispatch("close")}>
36
+ <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
37
+ </button>
38
+ </div>
39
+
40
+ <div class="space-y-4">
41
+ {#each models as model}
42
+ <div
43
+ class="rounded-xl border border-gray-100 {model.id === selectedModelId
44
+ ? 'bg-gradient-to-r from-yellow-200/40 via-yellow-500/10'
45
+ : ''}"
46
+ >
47
+ <label class="group flex cursor-pointer p-3" on:change aria-label={model.displayName}>
48
+ <input
49
+ type="radio"
50
+ class="sr-only"
51
+ name="activeModel"
52
+ value={model.id}
53
+ bind:group={selectedModelId}
54
+ />
55
+ <span>
56
+ <span class="text-md block font-semibold leading-tight text-gray-800"
57
+ >{model.displayName}</span
58
+ >
59
+ {#if model.description}
60
+ <span class="text-xs text-[#9FA8B5]">{model.description}</span>
61
+ {/if}
62
+ </span>
63
+ <CarbonCheckmark
64
+ class="-mr-1 -mt-1 ml-auto shrink-0 text-xl {model.id === selectedModelId
65
+ ? 'text-yellow-400'
66
+ : 'text-transparent group-hover:text-gray-200'}"
67
+ />
68
+ </label>
69
+ <ModelCardMetadata {model} />
70
+ </div>
71
+ {/each}
72
+ </div>
73
+ <button
74
+ type="submit"
75
+ 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-colors hover:ring"
76
+ >
77
+ Apply
78
+ </button>
79
+ </form>
80
+ </Modal>
src/lib/components/NavConversationItem.svelte ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { base } from "$app/paths";
3
+ import { page } from "$app/stores";
4
+ import { createEventDispatcher } from "svelte";
5
+
6
+ import CarbonCheckmark from "~icons/carbon/checkmark";
7
+ import CarbonTrashCan from "~icons/carbon/trash-can";
8
+ import CarbonClose from "~icons/carbon/close";
9
+ import CarbonEdit from "~icons/carbon/edit";
10
+
11
+ export let conv: { id: string; title: string };
12
+
13
+ let confirmDelete = false;
14
+
15
+ const dispatch = createEventDispatcher<{
16
+ deleteConversation: string;
17
+ editConversationTitle: { id: string; title: string };
18
+ }>();
19
+ </script>
20
+
21
+ <a
22
+ data-sveltekit-noscroll
23
+ on:mouseleave={() => {
24
+ confirmDelete = false;
25
+ }}
26
+ href="{base}/conversation/{conv.id}"
27
+ class="group flex h-11 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 {conv.id ===
28
+ $page.params.id
29
+ ? 'bg-gray-100 dark:bg-gray-700'
30
+ : ''}"
31
+ >
32
+ <div class="flex-1 truncate">
33
+ {#if confirmDelete}
34
+ <span class="font-semibold"> Delete </span>
35
+ {/if}
36
+ {conv.title}
37
+ </div>
38
+
39
+ {#if confirmDelete}
40
+ <button
41
+ type="button"
42
+ class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
43
+ title="Confirm delete action"
44
+ on:click|preventDefault={() => dispatch("deleteConversation", conv.id)}
45
+ >
46
+ <CarbonCheckmark class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
47
+ </button>
48
+ <button
49
+ type="button"
50
+ class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
51
+ title="Cancel delete action"
52
+ on:click|preventDefault={() => {
53
+ confirmDelete = false;
54
+ }}
55
+ >
56
+ <CarbonClose class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
57
+ </button>
58
+ {:else}
59
+ <button
60
+ type="button"
61
+ class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
62
+ title="Edit conversation title"
63
+ on:click|preventDefault={() => {
64
+ const newTitle = prompt("Edit this conversation title:", conv.title);
65
+ if (!newTitle) return;
66
+ dispatch("editConversationTitle", { id: conv.id, title: newTitle });
67
+ }}
68
+ >
69
+ <CarbonEdit class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
70
+ </button>
71
+
72
+ <button
73
+ type="button"
74
+ class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
75
+ title="Delete conversation"
76
+ on:click|preventDefault={(event) => {
77
+ if (event.shiftKey) {
78
+ dispatch("deleteConversation", conv.id);
79
+ } else {
80
+ confirmDelete = true;
81
+ }
82
+ }}
83
+ >
84
+ <CarbonTrashCan class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
85
+ </button>
86
+ {/if}
87
+ </a>
src/lib/components/NavMenu.svelte ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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";
7
+ import { PUBLIC_ORIGIN } from "$env/static/public";
8
+ import NavConversationItem from "./NavConversationItem.svelte";
9
+
10
+ const dispatch = createEventDispatcher<{
11
+ shareConversation: { id: string; title: string };
12
+ clickSettings: void;
13
+ }>();
14
+
15
+ export let conversations: Array<{
16
+ id: string;
17
+ title: string;
18
+ }> = [];
19
+ </script>
20
+
21
+ <div class="sticky top-0 flex flex-none items-center justify-between px-3 py-3.5 max-sm:pt-0">
22
+ <a class="flex items-center rounded-xl text-lg font-semibold" href="{PUBLIC_ORIGIN}{base}/">
23
+ <Logo classNames="mr-1 text-3xl" />
24
+ Macie
25
+ </a>
26
+ <a
27
+ href={base || "/"}
28
+ class="flex rounded-lg border bg-white px-2 py-0.5 text-center shadow-sm hover:shadow-none dark:border-gray-600 dark:bg-gray-700"
29
+ >
30
+ New Conversation
31
+ </a>
32
+ </div>
33
+ <div
34
+ class="scrollbar-custom flex flex-col gap-1 overflow-y-auto rounded-r-xl bg-gradient-to-l from-gray-50 px-3 pb-3 pt-2 dark:from-gray-800/30"
35
+ >
36
+ {#each conversations as conv (conv.id)}
37
+ <NavConversationItem on:editConversationTitle on:deleteConversation {conv} />
38
+ {/each}
39
+ </div>
40
+ <div
41
+ class="mt-0.5 flex flex-col gap-1 rounded-r-xl bg-gradient-to-l from-gray-50 p-3 text-sm dark:from-gray-800/30"
42
+ >
43
+ <button
44
+ on:click={switchTheme}
45
+ type="button"
46
+ class="group flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
47
+ >
48
+ Theme
49
+ </button>
50
+ <!-- <button
51
+ on:click={() => dispatch("clickSettings")}
52
+ type="button"
53
+ class="group flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
54
+ >
55
+ Settings
56
+ </button> -->
57
+ <!-- <a
58
+ href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions"
59
+ target="_blank"
60
+ rel="noreferrer"
61
+ class="group flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
62
+ >
63
+ Feedback
64
+ </a> -->
65
+ <a
66
+ href="{base}/privacy"
67
+ class="group flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
68
+ >
69
+ Privacy and Security
70
+ </a>
71
+ </div>
src/lib/components/Portal.svelte ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from "svelte";
3
+
4
+ let el: HTMLElement;
5
+
6
+ onMount(() => {
7
+ el.ownerDocument.body.appendChild(el);
8
+ });
9
+
10
+ onDestroy(() => {
11
+ if (el?.parentNode) {
12
+ el.parentNode.removeChild(el);
13
+ }
14
+ });
15
+ </script>
16
+
17
+ <div bind:this={el} class="contents" hidden>
18
+ <slot />
19
+ </div>
src/lib/components/ScrollToBottomBtn.svelte ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { fade } from "svelte/transition";
3
+ import { onDestroy } from "svelte";
4
+ import IconChevron from "./icons/IconChevron.svelte";
5
+
6
+ export let scrollNode: HTMLElement;
7
+ export { className as class };
8
+
9
+ let visible = false;
10
+ let className = "";
11
+ let observer: ResizeObserver | null = null;
12
+
13
+ $: if (scrollNode) {
14
+ destroy();
15
+
16
+ if (window.ResizeObserver) {
17
+ observer = new ResizeObserver(() => {
18
+ updateVisibility();
19
+ });
20
+ observer.observe(scrollNode);
21
+ }
22
+ scrollNode.addEventListener("scroll", updateVisibility);
23
+ }
24
+
25
+ function updateVisibility() {
26
+ if (!scrollNode) return;
27
+ visible =
28
+ Math.ceil(scrollNode.scrollTop) + 200 < scrollNode.scrollHeight - scrollNode.clientHeight;
29
+ }
30
+
31
+ function destroy() {
32
+ observer?.disconnect();
33
+ scrollNode?.removeEventListener("scroll", updateVisibility);
34
+ }
35
+
36
+ onDestroy(destroy);
37
+ </script>
38
+
39
+ {#if visible}
40
+ <button
41
+ transition:fade|local={{ duration: 150 }}
42
+ on:click={() => scrollNode.scrollTo({ top: scrollNode.scrollHeight, behavior: "smooth" })}
43
+ class="btn absolute flex h-[41px] w-[41px] rounded-full border bg-white shadow-md transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:shadow-gray-950 dark:hover:bg-gray-600 {className}"
44
+ ><IconChevron classNames="mt-[2px]" /></button
45
+ >
46
+ {/if}
src/lib/components/SettingsModal.svelte ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 type { Settings } from "$lib/types/Settings";
8
+ import { enhance } from "$app/forms";
9
+ import { base } from "$app/paths";
10
+
11
+ export let settings: Pick<Settings, "shareConversationsWithModelAuthors">;
12
+
13
+ const dispatch = createEventDispatcher<{ close: void }>();
14
+ </script>
15
+
16
+ <Modal on:close>
17
+ <form
18
+ class="flex w-full flex-col gap-5 p-6"
19
+ use:enhance={() => {
20
+ dispatch("close");
21
+ }}
22
+ method="post"
23
+ action="{base}/settings"
24
+ >
25
+ <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
26
+ <h2>Settings</h2>
27
+ <button type="button" class="group" on:click={() => dispatch("close")}>
28
+ <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
29
+ </button>
30
+ </div>
31
+
32
+ <label class="flex cursor-pointer select-none items-center gap-2 text-gray-500">
33
+ {#each Object.entries(settings).filter(([k]) => k !== "shareConversationsWithModelAuthors") as [key, val]}
34
+ <input type="hidden" name={key} value={val} />
35
+ {/each}
36
+ <Switch
37
+ name="shareConversationsWithModelAuthors"
38
+ bind:checked={settings.shareConversationsWithModelAuthors}
39
+ />
40
+ Share conversations with model authors
41
+ </label>
42
+
43
+ <p class="text-gray-800">
44
+ Sharing your data will help improve the training data and make open models better over time.
45
+ </p>
46
+ <p class="text-gray-800">
47
+ You can change this setting at any time, it applies to all your conversations.
48
+ </p>
49
+ <p class="text-gray-800">
50
+ Read more about this model's authors,
51
+ <a
52
+ href="https://open-assistant.io/"
53
+ target="_blank"
54
+ rel="noreferrer"
55
+ class="underline decoration-gray-300 hover:decoration-gray-700">Open Assistant</a
56
+ >.
57
+ </p>
58
+ <button
59
+ type="submit"
60
+ 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-colors hover:ring"
61
+ >
62
+ Apply
63
+ </button>
64
+ </form>
65
+ </Modal>
src/lib/components/StopGeneratingBtn.svelte ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import CarbonPause from "~icons/carbon/pause-filled";
3
+
4
+ export let visible = false;
5
+ export let className = "";
6
+ </script>
7
+
8
+ <button
9
+ type="button"
10
+ on:click
11
+ class="btn absolute flex rounded-lg border bg-white px-3 py-1 shadow-sm transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:bg-gray-600
12
+ {className}
13
+ {visible ? 'visible opacity-100' : 'invisible opacity-0'}
14
+ "
15
+ >
16
+ <CarbonPause class="-ml-1 mr-1 h-[1.25rem] w-[1.1875rem] text-gray-400" /> Stop generating
17
+ </button>
src/lib/components/Switch.svelte ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let checked: boolean;
3
+ export let name: string;
4
+ </script>
5
+
6
+ <input bind:checked type="checkbox" {name} class="peer pointer-events-none absolute opacity-0" />
7
+ <div
8
+ class="relative inline-flex h-5 w-9 items-center rounded-full bg-gray-300 p-1 shadow-inner transition-all peer-checked:bg-black hover:bg-gray-400 peer-checked:[&>div]:translate-x-3.5"
9
+ >
10
+ <div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-all" />
11
+ </div>
src/lib/components/Toast.svelte ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { fade } from "svelte/transition";
3
+
4
+ import IconDazzled from "$lib/components/icons/IconDazzled.svelte";
5
+
6
+ export let message = "";
7
+ </script>
8
+
9
+ <div
10
+ transition:fade={{ duration: 300 }}
11
+ class="pointer-events-none fixed right-0 top-12 z-20 bg-gradient-to-bl from-red-500/20 via-red-500/0 to-red-500/0 pb-36 pl-36 pr-2 pt-2 md:top-0 md:pr-8 md:pt-5"
12
+ >
13
+ <div
14
+ class="pointer-events-auto flex items-center rounded-full bg-white/90 px-3 py-1 shadow-sm dark:bg-gray-900/80"
15
+ >
16
+ <IconDazzled classNames="text-2xl mr-2" />
17
+ <h2 class="font-semibold">{message}</h2>
18
+ </div>
19
+ </div>
src/lib/components/Tooltip.svelte ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ export let label = "Copied";
4
+ export let position = "left-1/2 top-full transform -translate-x-1/2 translate-y-2";
5
+ </script>
6
+
7
+ <div
8
+ class="
9
+ pointer-events-none absolute rounded bg-black px-2 py-1 font-normal leading-tight text-white shadow transition-opacity
10
+ {position}
11
+ {classNames}
12
+ "
13
+ >
14
+ <div
15
+ class="absolute bottom-full left-1/2 h-0 w-0 -translate-x-1/2 transform border-4 border-t-0 border-black"
16
+ style="
17
+ border-left-color: transparent;
18
+ border-right-color: transparent;
19
+ "
20
+ />
21
+ {label}
22
+ </div>
src/lib/components/chat/ChatInput.svelte ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher, onMount } 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 disabled = false;
9
+
10
+ // Approximate width from which we disable autofocus
11
+ const TABLET_VIEWPORT_WIDTH = 768;
12
+
13
+ let innerWidth = 0;
14
+ let textareaElement: HTMLTextAreaElement;
15
+
16
+ const dispatch = createEventDispatcher<{ submit: void }>();
17
+
18
+ $: minHeight = `${1 + minRows * 1.5}em`;
19
+ $: maxHeight = maxRows ? `${1 + maxRows * 1.5}em` : `auto`;
20
+
21
+ function handleKeydown(event: KeyboardEvent) {
22
+ // submit on enter
23
+ if (event.key === "Enter" && !event.shiftKey) {
24
+ event.preventDefault();
25
+ dispatch("submit"); // use a custom event instead of `event.target.form.requestSubmit()` as it does not work on Safari 14
26
+ }
27
+ }
28
+
29
+ onMount(() => {
30
+ if (innerWidth > TABLET_VIEWPORT_WIDTH) {
31
+ textareaElement.focus();
32
+ }
33
+ });
34
+ </script>
35
+
36
+ <svelte:window bind:innerWidth />
37
+
38
+ <div class="relative min-w-0 flex-1">
39
+ <pre
40
+ class="invisible whitespace-pre-wrap p-3"
41
+ aria-hidden="true"
42
+ style="min-height: {minHeight}; max-height: {maxHeight}">{(value || " ") + "\n"}</pre>
43
+
44
+ <textarea
45
+ enterkeyhint="send"
46
+ tabindex="0"
47
+ rows="1"
48
+ class="scrollbar-custom absolute top-0 m-0 h-full w-full resize-none scroll-p-3 overflow-x-hidden overflow-y-scroll border-0 bg-transparent p-3 outline-none focus:ring-0 focus-visible:ring-0"
49
+ bind:value
50
+ bind:this={textareaElement}
51
+ {disabled}
52
+ on:keydown={handleKeydown}
53
+ {placeholder}
54
+ />
55
+ </div>
56
+
57
+ <style>
58
+ pre,
59
+ textarea {
60
+ font-family: inherit;
61
+ box-sizing: border-box;
62
+ line-height: 1.5;
63
+ }
64
+ </style>
src/lib/components/chat/ChatIntroduction.svelte ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { PUBLIC_VERSION } from "$env/static/public";
3
+ import Logo from "$lib/components/icons/Logo.svelte";
4
+ import { createEventDispatcher } from "svelte";
5
+ import IconChevron from "$lib/components/icons/IconChevron.svelte";
6
+ import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
7
+ import AnnouncementBanner from "../AnnouncementBanner.svelte";
8
+ import ModelsModal from "../ModelsModal.svelte";
9
+ import type { Model } from "$lib/types/Model";
10
+ import ModelCardMetadata from "../ModelCardMetadata.svelte";
11
+ import type { LayoutData } from "../../../routes/$types";
12
+ import { findCurrentModel } from "$lib/utils/models";
13
+
14
+ export let currentModel: Model;
15
+ export let settings: LayoutData["settings"];
16
+ export let models: Model[];
17
+
18
+ let isModelsModalOpen = false;
19
+
20
+ $: currentModelMetadata = findCurrentModel(models, settings.activeModel);
21
+
22
+ const dispatch = createEventDispatcher<{ message: string }>();
23
+ </script>
24
+
25
+ <div class="my-auto grid gap-8 lg:grid-cols-3">
26
+ <div class="lg:col-span-1">
27
+ <div>
28
+ <div class="mb-3 flex items-center text-2xl font-semibold">
29
+ <Logo classNames="mr-1 text-pink-400" />
30
+ Macie
31
+ <div
32
+ class="ml-3 flex h-6 items-center rounded-lg border border-gray-100 bg-gray-50 px-2 text-base text-gray-400 dark:border-gray-700/60 dark:bg-gray-800"
33
+ >
34
+ Duet-1
35
+ </div>
36
+ </div>
37
+ <p class="text-base text-gray-600 dark:text-gray-400">
38
+ AI is for everyone. Let's make it work for everyone.
39
+ </p>
40
+ </div>
41
+ </div>
42
+ <div class="lg:col-span-2 lg:pl-24">
43
+ <AnnouncementBanner classNames="mb-4" title="We've moved to open source models!">
44
+ <a
45
+ target="_blank"
46
+ href="https://www.vibelobster.com/more/moving-to-open-source-ai"
47
+ class="mr-2 flex items-center underline hover:no-underline"
48
+ >
49
+ <CarbonArrowUpRight class="mr-1" />
50
+
51
+ Read.
52
+
53
+ </a>
54
+ </AnnouncementBanner>
55
+ {#if isModelsModalOpen}
56
+ <ModelsModal {settings} {models} on:close={() => (isModelsModalOpen = false)} />
57
+ {/if}
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">Latest Model</div>
62
+ <div class="font-semibold">Duet-1</div>
63
+ </div>
64
+ {#if models.length > 1}
65
+ <button
66
+ type="button"
67
+ on:click={() => (isModelsModalOpen = true)}
68
+ 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"
69
+ ><IconChevron /></button
70
+ >
71
+ {/if}
72
+ </div>
73
+ <ModelCardMetadata variant="dark" model={currentModel} />
74
+ </div>
75
+ </div>
76
+ {#if currentModelMetadata.promptExamples}
77
+ <div class="lg:col-span-3 lg:mt-12">
78
+ <p class="mb-3 text-gray-600 dark:text-gray-300">Examples</p>
79
+ <div class="grid gap-3 lg:grid-cols-3 lg:gap-5">
80
+ {#each currentModelMetadata.promptExamples as example}
81
+ <button
82
+ type="button"
83
+ class="rounded-xl border bg-gray-50 p-2.5 text-gray-600 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 sm:p-4"
84
+ on:click={() => dispatch("message", example.prompt)}
85
+ >
86
+ {example.title}
87
+ </button>
88
+ {/each}
89
+ </div>
90
+ </div>{/if}
91
+ </div>
src/lib/components/chat/ChatMessage.svelte ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { marked } from "marked";
3
+ import type { Message } from "$lib/types/Message";
4
+ import { afterUpdate, createEventDispatcher } from "svelte";
5
+ import { deepestChild } from "$lib/utils/deepestChild";
6
+ import { page } from "$app/stores";
7
+
8
+ import CodeBlock from "../CodeBlock.svelte";
9
+ import IconLoading from "../icons/IconLoading.svelte";
10
+ import CarbonRotate360 from "~icons/carbon/rotate-360";
11
+ import CarbonDownload from "~icons/carbon/download";
12
+ import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
13
+ import type { Model } from "$lib/types/Model";
14
+
15
+ function sanitizeMd(md: string) {
16
+ let ret = md
17
+ .replace(/<\|[a-z]*$/, "")
18
+ .replace(/<\|[a-z]+\|$/, "")
19
+ .replace(/<$/, "")
20
+ .replaceAll(PUBLIC_SEP_TOKEN, " ")
21
+ .replaceAll(/<\|[a-z]+\|>/g, " ")
22
+ .replaceAll(/<br\s?\/?>/gi, "\n")
23
+ .replaceAll("<", "&lt;")
24
+ .trim();
25
+
26
+ for (const stop of [...(model.parameters?.stop ?? []), "<|endoftext|>"]) {
27
+ if (ret.endsWith(stop)) {
28
+ ret = ret.slice(0, -stop.length).trim();
29
+ }
30
+ }
31
+
32
+ return ret;
33
+ }
34
+ function unsanitizeMd(md: string) {
35
+ return md.replaceAll("&lt;", "<");
36
+ }
37
+
38
+ export let model: Model;
39
+ export let message: Message;
40
+ export let loading = false;
41
+ export let readOnly = false;
42
+
43
+ const dispatch = createEventDispatcher<{ retry: void }>();
44
+
45
+ let contentEl: HTMLElement;
46
+ let loadingEl: IconLoading;
47
+ let pendingTimeout: ReturnType<typeof setTimeout>;
48
+
49
+ const renderer = new marked.Renderer();
50
+
51
+ // For code blocks with simple backticks
52
+ renderer.codespan = (code) => {
53
+ // Unsanitize double-sanitized code
54
+ return `<code>${code.replaceAll("&amp;", "&")}</code>`;
55
+ };
56
+
57
+ const options: marked.MarkedOptions = {
58
+ ...marked.getDefaults(),
59
+ gfm: true,
60
+ breaks: true,
61
+ renderer,
62
+ };
63
+
64
+ $: tokens = marked.lexer(sanitizeMd(message.content));
65
+
66
+ afterUpdate(() => {
67
+ loadingEl?.$destroy();
68
+ clearTimeout(pendingTimeout);
69
+
70
+ // Add loading animation to the last message if update takes more than 600ms
71
+ if (loading) {
72
+ pendingTimeout = setTimeout(() => {
73
+ if (contentEl) {
74
+ loadingEl = new IconLoading({
75
+ target: deepestChild(contentEl),
76
+ props: { classNames: "loading inline ml-2" },
77
+ });
78
+ }
79
+ }, 600);
80
+ }
81
+ });
82
+
83
+ $: downloadLink =
84
+ message.from === "user" ? `${$page.url.pathname}/message/${message.id}/prompt` : undefined;
85
+ </script>
86
+
87
+ {#if message.from === "assistant"}
88
+ <div class="flex items-start justify-start gap-4 leading-relaxed">
89
+ <img
90
+ alt=""
91
+ src="https://raw.githubusercontent.com/unbrandedhuman/macie-images/0da2d187b24452e36ce5918eed994a863856a55c/macie%20branding-02.svg?"
92
+ class="mt-5 h-12 w-12 flex-none select-none rounded-full shadow-lg"
93
+ />
94
+ <div
95
+ class="relative min-h-[calc(2rem+theme(spacing[3.5])*2)] min-w-[100px] rounded-2xl border border-gray-100 bg-gradient-to-br from-gray-50 px-5 py-3.5 text-gray-600 prose-pre:my-2 dark:border-gray-800 dark:from-gray-800/40 dark:text-gray-300"
96
+ >
97
+ {#if !message.content}
98
+ <IconLoading classNames="absolute inset-0 m-auto" />
99
+ {/if}
100
+ <div
101
+ class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
102
+ bind:this={contentEl}
103
+ >
104
+ {#each tokens as token}
105
+ {#if token.type === "code"}
106
+ <CodeBlock lang={token.lang} code={unsanitizeMd(token.text)} />
107
+ {:else}
108
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
109
+ {@html marked(token.raw, options)}
110
+ {/if}
111
+ {/each}
112
+ </div>
113
+ </div>
114
+ </div>
115
+ {/if}
116
+ {#if message.from === "user"}
117
+ <div class="group relative flex items-start justify-start gap-4 max-sm:text-sm">
118
+ <div class="mt-5 h-3 w-3 flex-none rounded-full" />
119
+ <div class="whitespace-break-spaces rounded-2xl px-5 py-3.5 text-gray-500 dark:text-gray-400">
120
+ {message.content.trim()}
121
+ </div>
122
+ {#if !loading}
123
+ <div class="absolute right-0 top-3.5 flex gap-2 lg:-right-2">
124
+ {#if downloadLink}
125
+ <a
126
+ class="rounded-lg border border-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 dark:border-gray-800 dark:text-gray-400 dark:hover:text-gray-300 md:hidden"
127
+ title="Download prompt and parameters"
128
+ type="button"
129
+ target="_blank"
130
+ href={downloadLink}
131
+ >
132
+ <CarbonDownload />
133
+ </a>
134
+ {/if}
135
+ {#if !readOnly}
136
+ <button
137
+ class="cursor-pointer rounded-lg border border-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 dark:border-gray-800 dark:text-gray-400 dark:hover:text-gray-300 md:hidden lg:-right-2"
138
+ title="Retry"
139
+ type="button"
140
+ on:click={() => dispatch("retry")}
141
+ >
142
+ <CarbonRotate360 />
143
+ </button>
144
+ {/if}
145
+ </div>
146
+ {/if}
147
+ </div>
148
+ {/if}
src/lib/components/chat/ChatMessages.svelte ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 { createEventDispatcher, tick } from "svelte";
6
+
7
+ import ChatIntroduction from "./ChatIntroduction.svelte";
8
+ import ChatMessage from "./ChatMessage.svelte";
9
+ import { randomUUID } from "$lib/utils/randomUuid";
10
+ import type { Model } from "$lib/types/Model";
11
+ import type { LayoutData } from "../../../routes/$types";
12
+
13
+ const dispatch = createEventDispatcher<{ retry: { id: Message["id"]; content: string } }>();
14
+
15
+ export let messages: Message[];
16
+ export let loading: boolean;
17
+ export let pending: boolean;
18
+ export let currentModel: Model;
19
+ export let settings: LayoutData["settings"];
20
+ export let models: Model[];
21
+ export let readOnly: boolean;
22
+
23
+ let chatContainer: HTMLElement;
24
+
25
+ async function scrollToBottom() {
26
+ await tick();
27
+ chatContainer.scrollTop = chatContainer.scrollHeight;
28
+ }
29
+
30
+ // If last message is from user, scroll to bottom
31
+ $: if (messages[messages.length - 1]?.from === "user") {
32
+ scrollToBottom();
33
+ }
34
+ </script>
35
+
36
+ <div
37
+ class="scrollbar-custom mr-1 h-full overflow-y-auto"
38
+ use:snapScrollToBottom={messages.length ? messages : false}
39
+ bind:this={chatContainer}
40
+ >
41
+ <div class="mx-auto flex h-full max-w-3xl flex-col gap-5 px-5 pt-6 sm:gap-8 xl:max-w-4xl">
42
+ {#each messages as message, i}
43
+ <ChatMessage
44
+ loading={loading && i === messages.length - 1}
45
+ {message}
46
+ model={currentModel}
47
+ {readOnly}
48
+ on:retry={() => dispatch("retry", { id: message.id, content: message.content })}
49
+ />
50
+ {:else}
51
+ <ChatIntroduction {settings} {models} {currentModel} on:message />
52
+ {/each}
53
+ {#if pending}
54
+ <ChatMessage
55
+ message={{ from: "assistant", content: "", id: randomUUID() }}
56
+ model={currentModel}
57
+ />
58
+ {/if}
59
+ <div class="h-32 flex-none" />
60
+ </div>
61
+ <ScrollToBottomBtn
62
+ class="bottom-36 right-4 max-md:hidden lg:right-10"
63
+ scrollNode={chatContainer}
64
+ />
65
+ </div>
src/lib/components/chat/ChatWindow.svelte ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { Message } from "$lib/types/Message";
3
+ import { createEventDispatcher } from "svelte";
4
+
5
+ import CarbonSendAltFilled from "~icons/carbon/send-alt-filled";
6
+ import CarbonExport from "~icons/carbon/export";
7
+
8
+ import ChatMessages from "./ChatMessages.svelte";
9
+ import ChatInput from "./ChatInput.svelte";
10
+ import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
11
+ import type { Model } from "$lib/types/Model";
12
+ import type { LayoutData } from "../../../routes/$types";
13
+
14
+ export let messages: Message[] = [];
15
+ export let loading = false;
16
+ export let pending = false;
17
+ export let currentModel: Model;
18
+ export let models: Model[];
19
+ export let settings: LayoutData["settings"];
20
+
21
+ $: isReadOnly = !models.some((model) => model.id === currentModel.id);
22
+
23
+ let message: string;
24
+
25
+ const dispatch = createEventDispatcher<{
26
+ message: string;
27
+ share: void;
28
+ stop: void;
29
+ retry: { id: Message["id"]; content: string };
30
+ }>();
31
+
32
+ const handleSubmit = () => {
33
+ if (loading) return;
34
+ dispatch("message", message);
35
+ message = "";
36
+ };
37
+ </script>
38
+
39
+ <div class="relative min-h-0 min-w-0">
40
+ <ChatMessages
41
+ {loading}
42
+ {pending}
43
+ {settings}
44
+ {currentModel}
45
+ {models}
46
+ {messages}
47
+ readOnly={isReadOnly}
48
+ on:message
49
+ on:retry={(ev) => {
50
+ if (!loading) dispatch("retry", ev.detail);
51
+ }}
52
+ />
53
+ <div
54
+ class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:border-t max-md:bg-white max-md:dark:bg-gray-900 sm:px-5 md:py-8 xl:max-w-4xl [&>*]:pointer-events-auto"
55
+ >
56
+ <StopGeneratingBtn
57
+ visible={loading}
58
+ className="right-5 mr-[1px] md:mr-0 md:right-7 top-6 md:top-10 z-10"
59
+ on:click={() => dispatch("stop")}
60
+ />
61
+ <form
62
+ on:submit|preventDefault={handleSubmit}
63
+ class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 focus-within:border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-gray-500
64
+ {isReadOnly ? 'opacity-30' : ''}"
65
+ >
66
+ <div class="flex w-full flex-1 border-none bg-transparent">
67
+ <ChatInput
68
+ placeholder="Ask anything"
69
+ bind:value={message}
70
+ on:submit={handleSubmit}
71
+ maxRows={4}
72
+ disabled={isReadOnly}
73
+ />
74
+ <button
75
+ class="btn mx-1 my-1 h-[2.4rem] self-end rounded-lg bg-transparent p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100"
76
+ disabled={!message || loading || isReadOnly}
77
+ type="submit"
78
+ >
79
+ <CarbonSendAltFilled />
80
+ </button>
81
+ </div>
82
+ </form>
83
+ <div class="mt-2 flex justify-between self-stretch px-1 text-xs text-gray-400/90 max-sm:gap-2">
84
+ <p>
85
+ <!-- Model: <a
86
+ href="https://huggingface.co/{currentModel.name}"
87
+ target="_blank"
88
+ rel="noreferrer"
89
+ class="hover:underline">{currentModel.displayName}</a
90
+ > <span class="max-sm:hidden">·</span><br class="sm:hidden" /> Generated content may be inaccurate
91
+ or false. -->
92
+ Using model Duet-1. Some content mau be biased, inaccurate, or not even real. 🤷
93
+ </p>
94
+ {#if messages.length}
95
+ <button
96
+ class="flex flex-none items-center hover:text-gray-400 hover:underline max-sm:rounded-lg max-sm:bg-gray-50 max-sm:px-2.5 dark:max-sm:bg-gray-800"
97
+ type="button"
98
+ on:click={() => dispatch("share")}
99
+ >
100
+ <CarbonExport class="text-[.6rem] sm:mr-1.5 sm:text-pink-500" />
101
+ <div class="max-sm:hidden">Share this conversation</div>
102
+ </button>
103
+ {/if}
104
+ </div>
105
+ </div>
106
+ </div>
src/lib/components/icons/IconChevron.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ width="1em"
7
+ height="1em"
8
+ viewBox="0 0 15 6"
9
+ class={classNames}
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path
14
+ d="M1.67236 1L7.67236 7L13.6724 1"
15
+ stroke="currentColor"
16
+ stroke-width="2"
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ />
20
+ </svg>
src/lib/components/icons/IconCopy.svelte ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ aria-hidden="true"
9
+ fill="currentColor"
10
+ focusable="false"
11
+ role="img"
12
+ width="1em"
13
+ height="1em"
14
+ preserveAspectRatio="xMidYMid meet"
15
+ viewBox="0 0 32 32"
16
+ >
17
+ <path
18
+ d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z"
19
+ transform="translate(0)"
20
+ />
21
+ <path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)" /><rect
22
+ fill="none"
23
+ width="32"
24
+ height="32"
25
+ />
26
+ </svg>
src/lib/components/icons/IconDazzled.svelte ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ width="1em"
8
+ height="1em"
9
+ class={classNames}
10
+ fill="none"
11
+ viewBox="0 0 26 23"
12
+ >
13
+ <path
14
+ fill="url(#a)"
15
+ d="M.93 10.65A10.17 10.17 0 0 1 11.11.48h4.67a9.45 9.45 0 0 1 0 18.89H4.53L1.62 22.2a.38.38 0 0 1-.69-.28V10.65Z"
16
+ />
17
+ <path
18
+ fill="#000"
19
+ fill-rule="evenodd"
20
+ d="M11.52 7.4a1.86 1.86 0 1 1-3.72 0 1.86 1.86 0 0 1 3.72 0Zm7.57 0a1.86 1.86 0 1 1-3.73 0 1.86 1.86 0 0 1 3.73 0ZM8.9 12.9a.55.55 0 0 0-.11.35.76.76 0 0 1-1.51 0c0-.95.67-1.94 1.76-1.94 1.09 0 1.76 1 1.76 1.94H9.3a.55.55 0 0 0-.12-.35c-.06-.07-.1-.08-.13-.08s-.08 0-.14.08Zm4.04 0a.55.55 0 0 0-.12.35h-1.51c0-.95.68-1.94 1.76-1.94 1.1 0 1.77 1 1.77 1.94h-1.51a.55.55 0 0 0-.12-.35c-.06-.07-.11-.08-.14-.08-.02 0-.07 0-.13.08Zm-1.89.79c-.02 0-.07-.01-.13-.08a.55.55 0 0 1-.12-.36h-1.5c0 .95.67 1.95 1.75 1.95 1.1 0 1.77-1 1.77-1.95h-1.51c0 .16-.06.28-.12.36-.06.07-.11.08-.14.08Zm4.04 0c-.03 0-.08-.01-.14-.08a.55.55 0 0 1-.12-.36h-1.5c0 .95.67 1.95 1.76 1.95 1.08 0 1.76-1 1.76-1.95h-1.51c0 .16-.06.28-.12.36-.06.07-.11.08-.13.08Zm1.76-.44c0-.16.05-.28.12-.35.06-.07.1-.08.13-.08s.08 0 .14.08c.06.07.11.2.11.35a.76.76 0 0 0 1.51 0c0-.95-.67-1.94-1.76-1.94-1.09 0-1.76 1-1.76 1.94h1.5Z"
21
+ clip-rule="evenodd"
22
+ />
23
+ <defs>
24
+ <radialGradient
25
+ id="a"
26
+ cx="0"
27
+ cy="0"
28
+ r="1"
29
+ gradientTransform="matrix(0 31.37 -34.85 0 13.08 -9.02)"
30
+ gradientUnits="userSpaceOnUse"
31
+ >
32
+ <stop stop-color="#FFD21E" />
33
+ <stop offset="1" stop-color="red" />
34
+ </radialGradient>
35
+ </defs>
36
+ </svg>
src/lib/components/icons/IconLoading.svelte ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ width="40px"
8
+ height="25px"
9
+ viewBox="0 0 60 40"
10
+ preserveAspectRatio="xMidYMid"
11
+ class={classNames}
12
+ >
13
+ {#each Array(3) as _, index}
14
+ <g transform={`translate(${20 * index + 10} 20)`}>
15
+ {index}
16
+ <circle cx="0" cy="0" r="6" fill="currentColor">
17
+ <animateTransform
18
+ attributeName="transform"
19
+ type="scale"
20
+ begin={`${-0.375 + 0.15 * index}s`}
21
+ calcMode="spline"
22
+ keySplines="0.3 0 0.7 1;0.3 0 0.7 1"
23
+ values="0.5;1;0.5"
24
+ keyTimes="0;0.5;1"
25
+ dur="1s"
26
+ repeatCount="indefinite"
27
+ />
28
+ </circle>
29
+ </g>
30
+ {/each}
31
+ </svg>
src/lib/components/icons/Logo.svelte ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <!-- <svg
6
+ width="1em"
7
+ height="1em"
8
+ class={classNames}
9
+ viewBox="0 0 13 12"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path
14
+ fill="#FFB6C1"
15
+ d="M1.76 5.63a3.7 3.7 0 0 1 3.7-3.7h1.7a3.43 3.43 0 0 1 0 6.87H3.07L2.01 9.83a.14.14 0 0 1-.25-.1v-4.1Z"
16
+ />
17
+ <path
18
+ fill="#32343D"
19
+ d="M7.37 4.8c.13.05.19.33.33.25a.54.54 0 0 0 .22-.73.54.54 0 0 0-.73-.22.54.54 0 0 0-.22.73c.06.13.27-.08.4-.03ZM4.83 4.8c-.14.05-.2.33-.33.25a.54.54 0 0 1-.23-.73A.54.54 0 0 1 5 4.1c.26.14.36.47.22.73-.06.13-.27-.08-.4-.03ZM6.12 7.4c1.06 0 1.4-.96 1.4-1.44 0-.49-.62.26-1.4.26-.77 0-1.4-.75-1.4-.26 0 .48.34 1.43 1.4 1.43Z"
20
+ />
21
+ <path
22
+ fill="#FF323D"
23
+ d="M6.97 7.12c-.2.16-.49.27-.85.27-.34 0-.6-.1-.81-.24a.94.94 0 0 1 .57-.49c.04-.01.09.06.13.14.05.07.1.15.14.15.05 0 .1-.08.14-.15.05-.08.1-.15.14-.13a.93.93 0 0 1 .54.45Z"
24
+ />
25
+ </svg> -->
26
+
27
+ <img src="https://raw.githubusercontent.com/unbrandedhuman/macie-images/0da2d187b24452e36ce5918eed994a863856a55c/macie%20branding-02.svg?" alt="Alternative text for the image" width="30" height="30">
src/lib/constants/publicSepToken.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export const PUBLIC_SEP_TOKEN = "</s>";
src/lib/server/abortedGenerations.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Shouldn't be needed if we dove into sveltekit internals, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850
2
+
3
+ import { setTimeout } from "node:timers/promises";
4
+ import { collections } from "./database";
5
+
6
+ let closed = false;
7
+ process.on("SIGINT", () => {
8
+ closed = true;
9
+ });
10
+
11
+ export let abortedGenerations: Map<string, Date> = new Map();
12
+
13
+ async function maintainAbortedGenerations() {
14
+ while (!closed) {
15
+ await setTimeout(1000);
16
+
17
+ try {
18
+ const aborts = await collections.abortedGenerations.find({}).sort({ createdAt: 1 }).toArray();
19
+
20
+ abortedGenerations = new Map(
21
+ aborts.map(({ conversationId, createdAt }) => [conversationId.toString(), createdAt])
22
+ );
23
+ } catch (err) {
24
+ console.error(err);
25
+ }
26
+ }
27
+ }
28
+
29
+ maintainAbortedGenerations();
src/lib/server/auth.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { HF_CLIENT_ID, HF_CLIENT_SECRET } from "$env/static/private";
2
+
3
+ export const requiresUser = !!HF_CLIENT_ID && !!HF_CLIENT_SECRET;
4
+
5
+ export const authCondition = (locals: App.Locals) => {
6
+ return locals.userId
7
+ ? { userId: locals.userId }
8
+ : { sessionId: locals.sessionId, userId: { $exists: false } };
9
+ };
src/lib/server/database.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MONGODB_URL, MONGODB_DB_NAME } from "$env/static/private";
2
+ import { MongoClient } from "mongodb";
3
+ import type { Conversation } from "$lib/types/Conversation";
4
+ import type { SharedConversation } from "$lib/types/SharedConversation";
5
+ import type { AbortedGeneration } from "$lib/types/AbortedGeneration";
6
+ import type { Settings } from "$lib/types/Settings";
7
+ import type { User } from "$lib/types/User";
8
+
9
+ const client = new MongoClient(MONGODB_URL, {
10
+ // directConnection: true
11
+ });
12
+
13
+ export const connectPromise = client.connect().catch(console.error);
14
+
15
+ const db = client.db(MONGODB_DB_NAME);
16
+
17
+ const conversations = db.collection<Conversation>("conversations");
18
+ const sharedConversations = db.collection<SharedConversation>("sharedConversations");
19
+ const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
20
+ const settings = db.collection<Settings>("settings");
21
+ const users = db.collection<User>("users");
22
+
23
+ export { client, db };
24
+ export const collections = {
25
+ conversations,
26
+ sharedConversations,
27
+ abortedGenerations,
28
+ settings,
29
+ users,
30
+ };
31
+
32
+ client.on("open", () => {
33
+ conversations
34
+ .createIndex(
35
+ { sessionId: 1, updatedAt: -1 },
36
+ { partialFilterExpression: { sessionId: { $exists: true } } }
37
+ )
38
+ .catch(console.error);
39
+ conversations
40
+ .createIndex(
41
+ { userId: 1, updatedAt: -1 },
42
+ { partialFilterExpression: { userId: { $exists: true } } }
43
+ )
44
+ .catch(console.error);
45
+ abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 }).catch(console.error);
46
+ abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(console.error);
47
+ sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(console.error);
48
+ settings.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(console.error);
49
+ settings.createIndex({ userId: 1 }, { unique: true, sparse: true }).catch(console.error);
50
+ users.createIndex({ hfUserId: 1 }, { unique: true }).catch(console.error);
51
+ users.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(console.error);
52
+ });
src/lib/server/modelEndpoint.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HF_ACCESS_TOKEN } from "$env/static/private";
2
+ import { sum } from "$lib/utils/sum";
3
+ import type { BackendModel } from "./models";
4
+
5
+ /**
6
+ * Find a random load-balanced endpoint
7
+ */
8
+ export function modelEndpoint(model: BackendModel): {
9
+ url: string;
10
+ authorization: string;
11
+ weight: number;
12
+ } {
13
+ if (!model.endpoints) {
14
+ return {
15
+ url: `https://api-inference.huggingface.co/models/${model.name}`,
16
+ authorization: `Bearer ${HF_ACCESS_TOKEN}`,
17
+ weight: 1,
18
+ };
19
+ }
20
+ const endpoints = model.endpoints;
21
+ const totalWeight = sum(endpoints.map((e) => e.weight));
22
+
23
+ let random = Math.random() * totalWeight;
24
+ for (const endpoint of endpoints) {
25
+ if (random < endpoint.weight) {
26
+ return endpoint;
27
+ }
28
+ random -= endpoint.weight;
29
+ }
30
+
31
+ throw new Error("Invalid config, no endpoint found");
32
+ }
src/lib/server/models.ts ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HF_ACCESS_TOKEN, MODELS, OLD_MODELS } from "$env/static/private";
2
+ import { z } from "zod";
3
+
4
+ const modelsRaw = z
5
+ .array(
6
+ z.object({
7
+ /** Used as an identifier in DB */
8
+ id: z.string().optional(),
9
+ /** Used to link to the model page, and for inference */
10
+ name: z.string().min(1),
11
+ displayName: z.string().min(1).optional(),
12
+ description: z.string().min(1).optional(),
13
+ websiteUrl: z.string().url().optional(),
14
+ datasetName: z.string().min(1).optional(),
15
+ userMessageToken: z.string().min(1),
16
+ assistantMessageToken: z.string().min(1),
17
+ messageEndToken: z.string().min(1).optional(),
18
+ preprompt: z.string().default(""),
19
+ prepromptUrl: z.string().url().optional(),
20
+ promptExamples: z
21
+ .array(
22
+ z.object({
23
+ title: z.string().min(1),
24
+ prompt: z.string().min(1),
25
+ })
26
+ )
27
+ .optional(),
28
+ endpoints: z
29
+ .array(
30
+ z.object({
31
+ url: z.string().url(),
32
+ authorization: z.string().min(1).default(`Bearer ${HF_ACCESS_TOKEN}`),
33
+ weight: z.number().int().positive().default(1),
34
+ })
35
+ )
36
+ .optional(),
37
+ parameters: z
38
+ .object({
39
+ temperature: z.number().min(0).max(1),
40
+ truncate: z.number().int().positive(),
41
+ max_new_tokens: z.number().int().positive(),
42
+ stop: z.array(z.string()).optional(),
43
+ })
44
+ .passthrough()
45
+ .optional(),
46
+ })
47
+ )
48
+ .parse(JSON.parse(MODELS));
49
+
50
+ export const models = await Promise.all(
51
+ modelsRaw.map(async (m) => ({
52
+ ...m,
53
+ id: m.id || m.name,
54
+ displayName: m.displayName || m.name,
55
+ preprompt: m.prepromptUrl ? await fetch(m.prepromptUrl).then((r) => r.text()) : m.preprompt,
56
+ }))
57
+ );
58
+
59
+ // Models that have been deprecated
60
+ export const oldModels = OLD_MODELS
61
+ ? z
62
+ .array(
63
+ z.object({
64
+ id: z.string().optional(),
65
+ name: z.string().min(1),
66
+ displayName: z.string().min(1).optional(),
67
+ })
68
+ )
69
+ .parse(JSON.parse(OLD_MODELS))
70
+ .map((m) => ({ ...m, id: m.id || m.name, displayName: m.displayName || m.name }))
71
+ : [];
72
+
73
+ export type BackendModel = (typeof models)[0];
74
+
75
+ export const defaultModel = models[0];
76
+
77
+ export const validateModel = (_models: BackendModel[]) => {
78
+ // Zod enum function requires 2 parameters
79
+ return z.enum([_models[0].id, ..._models.slice(1).map((m) => m.id)]);
80
+ };
src/lib/shareConversation.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+ import { ERROR_MESSAGES, error } from "$lib/stores/errors";
3
+ import { share } from "./utils/share";
4
+
5
+ export async function shareConversation(id: string, title: string) {
6
+ try {
7
+ const res = await fetch(`${base}/conversation/${id}/share`, {
8
+ method: "POST",
9
+ headers: {
10
+ "Content-Type": "application/json",
11
+ },
12
+ });
13
+
14
+ if (!res.ok) {
15
+ error.set("Error while sharing conversation, try again.");
16
+ console.error("Error while sharing conversation: " + (await res.text()));
17
+ return;
18
+ }
19
+
20
+ const { url } = await res.json();
21
+
22
+ share(url, title);
23
+ } catch (err) {
24
+ error.set(ERROR_MESSAGES.default);
25
+ console.error(err);
26
+ }
27
+ }
src/lib/stores/errors.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { writable } from "svelte/store";
2
+
3
+ export const ERROR_MESSAGES = {
4
+ default: "Oops, something went wrong.",
5
+ };
6
+
7
+ export const error = writable<string | null>(null);
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/stores/pendingMessageIdToRetry.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import type { Message } from "$lib/types/Message";
2
+ import { writable } from "svelte/store";
3
+
4
+ export const pendingMessageIdToRetry = writable<Message["id"] | null>(null);
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/lib/types/AbortedGeneration.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ // Ideally shouldn't be needed, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850
2
+
3
+ import type { Conversation } from "./Conversation";
4
+ import type { Timestamps } from "./Timestamps";
5
+
6
+ export interface AbortedGeneration extends Timestamps {
7
+ conversationId: Conversation["_id"];
8
+ }
src/lib/types/Conversation.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ObjectId } from "mongodb";
2
+ import type { Message } from "./Message";
3
+ import type { Timestamps } from "./Timestamps";
4
+ import type { User } from "./User";
5
+
6
+ export interface Conversation extends Timestamps {
7
+ _id: ObjectId;
8
+
9
+ sessionId?: string;
10
+ userId?: User["_id"];
11
+
12
+ model: string;
13
+
14
+ title: string;
15
+ messages: Message[];
16
+
17
+ meta?: {
18
+ fromShareId?: string;
19
+ };
20
+ }
src/lib/types/Message.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export interface Message {
2
+ from: "user" | "assistant";
3
+ id: ReturnType<typeof crypto.randomUUID>;
4
+ content: string;
5
+ }
src/lib/types/Model.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { BackendModel } from "$lib/server/models";
2
+
3
+ export type Model = Pick<
4
+ BackendModel,
5
+ | "id"
6
+ | "name"
7
+ | "displayName"
8
+ | "websiteUrl"
9
+ | "datasetName"
10
+ | "promptExamples"
11
+ | "parameters"
12
+ | "description"
13
+ >;
src/lib/types/Settings.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Timestamps } from "./Timestamps";
2
+ import type { User } from "./User";
3
+
4
+ export interface Settings extends Timestamps {
5
+ userId?: User["_id"];
6
+ sessionId?: string;
7
+
8
+ /**
9
+ * Note: Only conversations with this settings explictly set to true should be shared.
10
+ *
11
+ * This setting is explicitly set to true when users accept the ethics modal.
12
+ * */
13
+ shareConversationsWithModelAuthors: boolean;
14
+ ethicsModalAcceptedAt: Date | null;
15
+ activeModel: string;
16
+ }
src/lib/types/SharedConversation.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Message } from "./Message";
2
+ import type { Timestamps } from "./Timestamps";
3
+
4
+ export interface SharedConversation extends Timestamps {
5
+ _id: string;
6
+
7
+ hash: string;
8
+
9
+ model: string;
10
+ title: string;
11
+ messages: Message[];
12
+ }
src/lib/types/Timestamps.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export interface Timestamps {
2
+ createdAt: Date;
3
+ updatedAt: Date;
4
+ }