Spaces:
Runtime error
Runtime error
unbrandedhuman
commited on
Commit
·
1e95ed5
1
Parent(s):
8f3d5b1
Upload 83 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- src/app.d.ts +20 -0
- src/app.html +73 -0
- src/hooks.server.ts +95 -0
- src/lib/actions/snapScrollToBottom.ts +54 -0
- src/lib/buildPrompt.ts +36 -0
- src/lib/components/AnnouncementBanner.svelte +15 -0
- src/lib/components/CodeBlock.svelte +28 -0
- src/lib/components/CopyToClipBoardBtn.svelte +50 -0
- src/lib/components/EthicsModal.svelte +46 -0
- src/lib/components/MobileNav.svelte +62 -0
- src/lib/components/Modal.svelte +59 -0
- src/lib/components/ModelCardMetadata.svelte +53 -0
- src/lib/components/ModelsModal.svelte +80 -0
- src/lib/components/NavConversationItem.svelte +87 -0
- src/lib/components/NavMenu.svelte +71 -0
- src/lib/components/Portal.svelte +19 -0
- src/lib/components/ScrollToBottomBtn.svelte +46 -0
- src/lib/components/SettingsModal.svelte +65 -0
- src/lib/components/StopGeneratingBtn.svelte +17 -0
- src/lib/components/Switch.svelte +11 -0
- src/lib/components/Toast.svelte +19 -0
- src/lib/components/Tooltip.svelte +22 -0
- src/lib/components/chat/ChatInput.svelte +64 -0
- src/lib/components/chat/ChatIntroduction.svelte +91 -0
- src/lib/components/chat/ChatMessage.svelte +148 -0
- src/lib/components/chat/ChatMessages.svelte +65 -0
- src/lib/components/chat/ChatWindow.svelte +106 -0
- src/lib/components/icons/IconChevron.svelte +20 -0
- src/lib/components/icons/IconCopy.svelte +26 -0
- src/lib/components/icons/IconDazzled.svelte +36 -0
- src/lib/components/icons/IconLoading.svelte +31 -0
- src/lib/components/icons/Logo.svelte +27 -0
- src/lib/constants/publicSepToken.ts +1 -0
- src/lib/server/abortedGenerations.ts +29 -0
- src/lib/server/auth.ts +9 -0
- src/lib/server/database.ts +52 -0
- src/lib/server/modelEndpoint.ts +32 -0
- src/lib/server/models.ts +80 -0
- src/lib/shareConversation.ts +27 -0
- src/lib/stores/errors.ts +7 -0
- src/lib/stores/pendingMessage.ts +3 -0
- src/lib/stores/pendingMessageIdToRetry.ts +4 -0
- src/lib/switchTheme.ts +10 -0
- src/lib/types/AbortedGeneration.ts +8 -0
- src/lib/types/Conversation.ts +20 -0
- src/lib/types/Message.ts +5 -0
- src/lib/types/Model.ts +13 -0
- src/lib/types/Settings.ts +16 -0
- src/lib/types/SharedConversation.ts +12 -0
- 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("<", "<")}</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"> 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"> 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("<", "<")
|
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("<", "<");
|
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("&", "&")}</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 |
+
}
|