@ims360/svelte-ivory 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENCE +23 -0
- package/README.md +15 -0
- package/dist/components/ai/AiMessage.svelte +115 -0
- package/dist/components/ai/AiMessage.svelte.d.ts +18 -0
- package/dist/components/ai/AttachedFile.svelte +28 -0
- package/dist/components/ai/AttachedFile.svelte.d.ts +9 -0
- package/dist/components/ai/Chat.svelte +150 -0
- package/dist/components/ai/Chat.svelte.d.ts +40 -0
- package/dist/components/ai/Markdown.svelte +59 -0
- package/dist/components/ai/Markdown.svelte.d.ts +16 -0
- package/dist/components/ai/UserMessage.svelte +53 -0
- package/dist/components/ai/UserMessage.svelte.d.ts +16 -0
- package/dist/components/ai/index.d.ts +5 -0
- package/dist/components/ai/index.js +5 -0
- package/dist/components/basic/checkbox/Checkbox.svelte +79 -0
- package/dist/components/basic/checkbox/Checkbox.svelte.d.ts +18 -0
- package/dist/components/basic/index.d.ts +2 -0
- package/dist/components/basic/index.js +2 -0
- package/dist/components/basic/toggle/Toggle.svelte +47 -0
- package/dist/components/basic/toggle/Toggle.svelte.d.ts +12 -0
- package/dist/components/buttons/CopyToClipboardButton.svelte +33 -0
- package/dist/components/buttons/CopyToClipboardButton.svelte.d.ts +9 -0
- package/dist/components/index.d.ts +0 -0
- package/dist/components/index.js +1 -0
- package/dist/components/layout/heading/Heading.svelte +33 -0
- package/dist/components/layout/heading/Heading.svelte.d.ts +16 -0
- package/dist/components/layout/heading/index.d.ts +5 -0
- package/dist/components/layout/heading/index.js +5 -0
- package/dist/components/layout/hiddenBackground/HiddenBackground.svelte +48 -0
- package/dist/components/layout/hiddenBackground/HiddenBackground.svelte.d.ts +13 -0
- package/dist/components/layout/hiddenBackground/index.js +6 -0
- package/dist/components/layout/index.d.ts +7 -0
- package/dist/components/layout/index.js +7 -0
- package/dist/components/layout/modal/Modal.svelte +114 -0
- package/dist/components/layout/modal/Modal.svelte.d.ts +30 -0
- package/dist/components/layout/modal/ModalTest.svelte +16 -0
- package/dist/components/layout/modal/ModalTest.svelte.d.ts +9 -0
- package/dist/components/layout/popover/Popover.svelte +108 -0
- package/dist/components/layout/popover/Popover.svelte.d.ts +36 -0
- package/dist/components/layout/portal/Portal.svelte +23 -0
- package/dist/components/layout/portal/Portal.svelte.d.ts +15 -0
- package/dist/components/layout/tabs/Tab.svelte +80 -0
- package/dist/components/layout/tabs/Tab.svelte.d.ts +21 -0
- package/dist/components/layout/tabs/TabPanel.svelte +23 -0
- package/dist/components/layout/tabs/TabPanel.svelte.d.ts +10 -0
- package/dist/components/layout/tabs/Tabs.svelte +86 -0
- package/dist/components/layout/tabs/Tabs.svelte.d.ts +21 -0
- package/dist/components/layout/tabs/index.d.ts +26 -0
- package/dist/components/layout/tabs/index.js +8 -0
- package/dist/components/layout/tooltip/Tooltip.svelte +111 -0
- package/dist/components/layout/tooltip/Tooltip.svelte.d.ts +32 -0
- package/dist/components/toast/Toast.svelte +100 -0
- package/dist/components/toast/Toast.svelte.d.ts +16 -0
- package/dist/components/toast/index.d.ts +2 -0
- package/dist/components/toast/index.js +2 -0
- package/dist/components/toast/toasts.svelte.d.ts +26 -0
- package/dist/components/toast/toasts.svelte.js +67 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.js +2 -0
- package/dist/utils/actions/clickOutside.d.ts +11 -0
- package/dist/utils/actions/clickOutside.js +23 -0
- package/dist/utils/actions/focusTrap.d.ts +4 -0
- package/dist/utils/actions/focusTrap.js +64 -0
- package/dist/utils/actions/index.d.ts +5 -0
- package/dist/utils/actions/index.js +5 -0
- package/dist/utils/actions/portal.d.ts +9 -0
- package/dist/utils/actions/portal.js +39 -0
- package/dist/utils/actions/shortcut.d.ts +10 -0
- package/dist/utils/actions/shortcut.js +25 -0
- package/dist/utils/actions/visible.d.ts +5 -0
- package/dist/utils/actions/visible.js +14 -0
- package/dist/utils/functions/cookie.d.ts +12 -0
- package/dist/utils/functions/cookie.js +36 -0
- package/dist/utils/functions/index.d.ts +3 -0
- package/dist/utils/functions/index.js +3 -0
- package/dist/utils/functions/pseudoRandomId.d.ts +1 -0
- package/dist/utils/functions/pseudoRandomId.js +3 -0
- package/dist/utils/functions/queryParams.d.ts +1 -0
- package/dist/utils/functions/queryParams.js +14 -0
- package/package.json +107 -0
- package/src/lib/components/ai/AiMessage.svelte +115 -0
- package/src/lib/components/ai/AttachedFile.svelte +28 -0
- package/src/lib/components/ai/Chat.svelte +150 -0
- package/src/lib/components/ai/Markdown.svelte +59 -0
- package/src/lib/components/ai/UserMessage.svelte +53 -0
- package/src/lib/components/ai/index.ts +5 -0
- package/src/lib/components/basic/checkbox/Checkbox.svelte +79 -0
- package/src/lib/components/basic/checkbox/checkbox.svelte.spec.ts +39 -0
- package/src/lib/components/basic/index.ts +2 -0
- package/src/lib/components/basic/toggle/Toggle.svelte +47 -0
- package/src/lib/components/basic/toggle/toggle.svelte.spec.ts +19 -0
- package/src/lib/components/buttons/CopyToClipboardButton.svelte +33 -0
- package/src/lib/components/index.ts +0 -0
- package/src/lib/components/layout/heading/Heading.svelte +33 -0
- package/src/lib/components/layout/heading/index.ts +7 -0
- package/src/lib/components/layout/hiddenBackground/HiddenBackground.svelte +48 -0
- package/src/lib/components/layout/hiddenBackground/index.ts +8 -0
- package/src/lib/components/layout/index.ts +7 -0
- package/src/lib/components/layout/modal/Modal.svelte +114 -0
- package/src/lib/components/layout/modal/ModalTest.svelte +16 -0
- package/src/lib/components/layout/modal/modal.svelte.spec.ts +39 -0
- package/src/lib/components/layout/popover/Popover.svelte +108 -0
- package/src/lib/components/layout/portal/Portal.svelte +23 -0
- package/src/lib/components/layout/tabs/Tab.svelte +80 -0
- package/src/lib/components/layout/tabs/TabPanel.svelte +23 -0
- package/src/lib/components/layout/tabs/Tabs.svelte +86 -0
- package/src/lib/components/layout/tabs/Tabs.test.svelte +5 -0
- package/src/lib/components/layout/tabs/index.ts +10 -0
- package/src/lib/components/layout/tooltip/Tooltip.svelte +111 -0
- package/src/lib/components/toast/Toast.svelte +100 -0
- package/src/lib/components/toast/index.ts +2 -0
- package/src/lib/components/toast/toasts.svelte.ts +89 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/utils/actions/clickOutside.svelte.spec.ts +67 -0
- package/src/lib/utils/actions/clickOutside.ts +38 -0
- package/src/lib/utils/actions/focusTrap.ts +65 -0
- package/src/lib/utils/actions/index.ts +5 -0
- package/src/lib/utils/actions/portal.ts +43 -0
- package/src/lib/utils/actions/shortcut.svelte.spec.ts +19 -0
- package/src/lib/utils/actions/shortcut.ts +35 -0
- package/src/lib/utils/actions/visible.ts +28 -0
- package/src/lib/utils/functions/cookie.svelte.spec.ts +55 -0
- package/src/lib/utils/functions/cookie.ts +46 -0
- package/src/lib/utils/functions/index.ts +3 -0
- package/src/lib/utils/functions/pseudoRandomId.spec.ts +19 -0
- package/src/lib/utils/functions/pseudoRandomId.ts +4 -0
- package/src/lib/utils/functions/queryParams.spec.ts +25 -0
- package/src/lib/utils/functions/queryParams.ts +15 -0
package/LICENCE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ims360 GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
|
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Ivory, more than just a [skeleton](https://www.skeleton.dev/)
|
|
2
|
+
|
|
3
|
+
A svelte library with a bunch of components and utilities, building upon [skeletons](https://www.skeleton.dev/) design system.
|
|
4
|
+
Currently this library is primarily intended for our ([ims360](https://ims360.de/)) internal use, so documentation is sparse at the moment.
|
|
5
|
+
Eventually we will add more documentation and examples to make it easier for devs outside our organization to use.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
Include this line in your `app.css` file, so the TW classes used in the lib are included:
|
|
10
|
+
|
|
11
|
+
```css
|
|
12
|
+
@source "../node_modules/@ims360/svelte-lib";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
_You may need to adjust the path if your `app.css` is not located in `/src`_
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The default for AI Messages in the Chat component.
|
|
4
|
+
Can be customized using the `class` and `loading` props.
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import { ThumbsDown, ThumbsUp } from '@lucide/svelte';
|
|
9
|
+
import clsx from 'clsx';
|
|
10
|
+
import type { Snippet } from 'svelte';
|
|
11
|
+
import type { ClassValue } from 'svelte/elements';
|
|
12
|
+
import { twMerge } from 'tailwind-merge';
|
|
13
|
+
import CopyToClipboardButton from '../buttons/CopyToClipboardButton.svelte';
|
|
14
|
+
import type { AiChatMessage } from './Chat.svelte';
|
|
15
|
+
import Markdown from './Markdown.svelte';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
b_message: AiChatMessage;
|
|
19
|
+
messageText?: Snippet<[{ message: AiChatMessage }]>;
|
|
20
|
+
class?: ClassValue;
|
|
21
|
+
minHeight?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
b_message = $bindable(),
|
|
26
|
+
class: clazz,
|
|
27
|
+
messageText = defaultMessage,
|
|
28
|
+
minHeight
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
// const uuidRegex =
|
|
32
|
+
// /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g;
|
|
33
|
+
// const icon = `<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" class="h-[1em] origin-center overflow-visible align-[-0.125rem]" viewBox="0 0 512 512"><g transform="translate(256 256)" transform-origin="128 0"><path d="M336 0c-8.8 0-16 7.2-16 16s7.2 16 16 16l121.4 0L212.7 276.7c-6.2 6.2-6.2 16.4 0 22.6s16.4 6.2 22.6 0L480 54.6 480 176c0 8.8 7.2 16 16 16s16-7.2 16-16l0-160c0-8.8-7.2-16-16-16L336 0zM64 32C28.7 32 0 60.7 0 96L0 448c0 35.3 28.7 64 64 64l352 0c35.3 0 64-28.7 64-64l0-144c0-8.8-7.2-16-16-16s-16 7.2-16 16l0 144c0 17.7-14.3 32-32 32L64 480c-17.7 0-32-14.3-32-32L32 96c0-17.7 14.3-32 32-32l144 0c8.8 0 16-7.2 16-16s-7.2-16-16-16L64 32z" fill="currentColor" transform="translate(-256 -256)"></path><!----></g></svg>`;
|
|
34
|
+
|
|
35
|
+
// async function replacerFactory(): Promise<(match: string) => string> {
|
|
36
|
+
// const docs = await documents.value;
|
|
37
|
+
// return (match: string) => {
|
|
38
|
+
// const doc = docs.find((d) => d.id === match);
|
|
39
|
+
|
|
40
|
+
// return `<span class="bg-surface-100-900 py-1 px-2 rounded flex flex-row items-center gap-2 hover:shadow-lg w-fit transition-all">
|
|
41
|
+
// ${doc?.title ?? 'Unbekanntes Dokument'}
|
|
42
|
+
// <a href="/documents/${doc?.id}" target="_blank">
|
|
43
|
+
// ${icon}
|
|
44
|
+
// </a>
|
|
45
|
+
// </span>`;
|
|
46
|
+
// };
|
|
47
|
+
// }
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<div
|
|
51
|
+
class={twMerge(clsx('group flex w-full flex-col items-start', clazz))}
|
|
52
|
+
style={minHeight ? `min-height: ${minHeight}px;` : undefined}
|
|
53
|
+
>
|
|
54
|
+
{@render messageText({
|
|
55
|
+
message: b_message
|
|
56
|
+
})}
|
|
57
|
+
<div
|
|
58
|
+
class={[
|
|
59
|
+
'text-surface-500 flex -translate-x-3 flex-row items-center transition-all group-hover:opacity-100',
|
|
60
|
+
b_message.liked || b_message.disliked ? 'opacity-100' : 'opacity-0'
|
|
61
|
+
]}
|
|
62
|
+
>
|
|
63
|
+
<CopyToClipboardButton
|
|
64
|
+
text={b_message.message}
|
|
65
|
+
toastMessage="Nachricht wurde in die Zwischenablage kopiert"
|
|
66
|
+
/>
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
class="btn-icon"
|
|
70
|
+
onclick={() => {
|
|
71
|
+
b_message.liked = !b_message.liked;
|
|
72
|
+
b_message.disliked = false;
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<ThumbsUp class={[b_message.liked && 'fill-surface-500/50']} />
|
|
76
|
+
</button>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
class="btn-icon"
|
|
80
|
+
onclick={() => {
|
|
81
|
+
b_message.liked = false;
|
|
82
|
+
b_message.disliked = !b_message.disliked;
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<ThumbsDown class={[b_message.disliked && 'fill-surface-500/50']} />
|
|
86
|
+
</button>
|
|
87
|
+
{#if b_message.time}
|
|
88
|
+
<p class="text-surface-400-600 pl-2">
|
|
89
|
+
{b_message.time.toLocaleString('de')}
|
|
90
|
+
</p>
|
|
91
|
+
{/if}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{#snippet defaultMessage({ message }: { message: AiChatMessage })}
|
|
96
|
+
{#if message}
|
|
97
|
+
<Markdown source={message.message} />
|
|
98
|
+
{:else}
|
|
99
|
+
<p class="flex flex-row">
|
|
100
|
+
<span class="h-4 animate-bounce rounded-full pl-1">.</span>
|
|
101
|
+
<span
|
|
102
|
+
class="h-4 animate-bounce rounded-full"
|
|
103
|
+
style="animation-delay: 125ms !important;"
|
|
104
|
+
>
|
|
105
|
+
.
|
|
106
|
+
</span>
|
|
107
|
+
<span
|
|
108
|
+
class="h-4 animate-bounce rounded-full"
|
|
109
|
+
style="animation-delay: 250ms !important;"
|
|
110
|
+
>
|
|
111
|
+
.
|
|
112
|
+
</span>
|
|
113
|
+
</p>
|
|
114
|
+
{/if}
|
|
115
|
+
{/snippet}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ClassValue } from 'svelte/elements';
|
|
3
|
+
import type { AiChatMessage } from './Chat.svelte';
|
|
4
|
+
interface Props {
|
|
5
|
+
b_message: AiChatMessage;
|
|
6
|
+
messageText?: Snippet<[{
|
|
7
|
+
message: AiChatMessage;
|
|
8
|
+
}]>;
|
|
9
|
+
class?: ClassValue;
|
|
10
|
+
minHeight?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The default for AI Messages in the Chat component.
|
|
14
|
+
* Can be customized using the `class` and `loading` props.
|
|
15
|
+
*/
|
|
16
|
+
declare const AiMessage: import("svelte").Component<Props, {}, "b_message">;
|
|
17
|
+
type AiMessage = ReturnType<typeof AiMessage>;
|
|
18
|
+
export default AiMessage;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { X } from '@lucide/svelte';
|
|
3
|
+
import type { ClassValue } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
file: File;
|
|
7
|
+
onremove?: (f: File) => void;
|
|
8
|
+
class?: ClassValue;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { file, onremove, class: clazz }: Props = $props();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
15
|
+
<svelte:element
|
|
16
|
+
this={onremove ? 'button' : 'div'}
|
|
17
|
+
type={onremove ? 'button' : undefined}
|
|
18
|
+
onclick={onremove ? () => onremove(file) : undefined}
|
|
19
|
+
class={[
|
|
20
|
+
'bg-primary-500/25 group flex flex-row items-center gap-1 overflow-hidden rounded-full px-4 py-1 whitespace-nowrap',
|
|
21
|
+
clazz
|
|
22
|
+
]}
|
|
23
|
+
>
|
|
24
|
+
<p>{file.name}</p>
|
|
25
|
+
{#if onremove}
|
|
26
|
+
<X size={16} class="group-hover:text-primary-500" />
|
|
27
|
+
{/if}
|
|
28
|
+
</svelte:element>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ClassValue } from 'svelte/elements';
|
|
2
|
+
interface Props {
|
|
3
|
+
file: File;
|
|
4
|
+
onremove?: (f: File) => void;
|
|
5
|
+
class?: ClassValue;
|
|
6
|
+
}
|
|
7
|
+
declare const AttachedFile: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type AttachedFile = ReturnType<typeof AttachedFile>;
|
|
9
|
+
export default AttachedFile;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
An AI chat component that can be used to create a chatbot.
|
|
4
|
+
Comes with default styles for the chat messages, but can be customized with the `userMessage` and `systemMessage` props.
|
|
5
|
+
The input component has to be provided as a child component, and the `submit` function has to be provided as a callback.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
<script lang="ts" module>
|
|
9
|
+
import clsx from 'clsx';
|
|
10
|
+
import { tick, type Snippet } from 'svelte';
|
|
11
|
+
import type { ClassValue } from 'svelte/elements';
|
|
12
|
+
import { twMerge } from 'tailwind-merge';
|
|
13
|
+
import AiMessage from './AiMessage.svelte';
|
|
14
|
+
import UserMessage from './UserMessage.svelte';
|
|
15
|
+
|
|
16
|
+
export interface AiChatMessage {
|
|
17
|
+
from: 'user' | 'system';
|
|
18
|
+
message: string;
|
|
19
|
+
time?: Date;
|
|
20
|
+
liked?: boolean;
|
|
21
|
+
disliked?: boolean;
|
|
22
|
+
files?: File[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AiChat {
|
|
26
|
+
messages: AiChatMessage[];
|
|
27
|
+
loading?: boolean;
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<script lang="ts">
|
|
32
|
+
interface Props {
|
|
33
|
+
class?: ClassValue;
|
|
34
|
+
b_chat: AiChat;
|
|
35
|
+
userMessage?: Snippet<[{ message: AiChatMessage; i: number }]>;
|
|
36
|
+
systemMessage?: Snippet<[{ message: AiChatMessage; i: number; minHeight?: number }]>;
|
|
37
|
+
placeholder?: Snippet;
|
|
38
|
+
children: Snippet<[{ onsubmit: (message: AiChatMessage) => Promise<void> }]>;
|
|
39
|
+
submit: (message: AiChatMessage) => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let {
|
|
43
|
+
class: clazz,
|
|
44
|
+
b_chat: chat = $bindable(),
|
|
45
|
+
userMessage = defaultUserMessage,
|
|
46
|
+
systemMessage = defaultSystemMessage,
|
|
47
|
+
placeholder,
|
|
48
|
+
children,
|
|
49
|
+
submit: externalSubmit
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
let chatContainer = $state<HTMLDivElement>();
|
|
53
|
+
let lastMessageMinHeight = $state(0);
|
|
54
|
+
|
|
55
|
+
function getLastMessageMinHeight() {
|
|
56
|
+
if (!chatContainer) return 0;
|
|
57
|
+
const secondToLastElement = chatContainer.children[chatContainer.children.length - 2];
|
|
58
|
+
const rect = secondToLastElement?.getBoundingClientRect();
|
|
59
|
+
const remainHeight = chatContainer.clientHeight - rect.height - 16;
|
|
60
|
+
return remainHeight;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function scrollToBottom() {
|
|
64
|
+
if (!chatContainer) return;
|
|
65
|
+
await tick();
|
|
66
|
+
await tick();
|
|
67
|
+
lastMessageMinHeight = getLastMessageMinHeight();
|
|
68
|
+
await tick();
|
|
69
|
+
// ensure we don't scroll if the newly generated message is already in view
|
|
70
|
+
chatContainer.scrollTo({
|
|
71
|
+
top: chatContainer.scrollHeight,
|
|
72
|
+
behavior: 'smooth'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function submit(message: AiChatMessage) {
|
|
77
|
+
if (chat.loading) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
chat.messages.push({
|
|
82
|
+
...message,
|
|
83
|
+
from: 'user',
|
|
84
|
+
time: new Date()
|
|
85
|
+
});
|
|
86
|
+
// prevent the user from sending another message while we are loading the ai response
|
|
87
|
+
chat.loading = true;
|
|
88
|
+
|
|
89
|
+
// add an empty system message to the chat, this will indicate a loading state
|
|
90
|
+
chat.messages.push({
|
|
91
|
+
from: 'system',
|
|
92
|
+
message: '',
|
|
93
|
+
time: new Date()
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await scrollToBottom();
|
|
97
|
+
|
|
98
|
+
await externalSubmit(message);
|
|
99
|
+
|
|
100
|
+
chat.loading = false;
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<div class={twMerge(clsx('flex grow flex-col gap-2 overflow-hidden', clazz))}>
|
|
105
|
+
<div
|
|
106
|
+
class="flex grow flex-col gap-4 overflow-auto pr-2 [scrollbar-gutter:stable]"
|
|
107
|
+
bind:this={chatContainer}
|
|
108
|
+
>
|
|
109
|
+
{#if chat.messages.length === 0 && placeholder}
|
|
110
|
+
{@render placeholder()}
|
|
111
|
+
{/if}
|
|
112
|
+
{#each chat.messages as _, i}
|
|
113
|
+
{@const message = chat.messages[i]}
|
|
114
|
+
{#if message.from === 'user'}
|
|
115
|
+
{@render userMessage({
|
|
116
|
+
message,
|
|
117
|
+
i
|
|
118
|
+
})}
|
|
119
|
+
{:else}
|
|
120
|
+
{@render systemMessage({
|
|
121
|
+
message,
|
|
122
|
+
i,
|
|
123
|
+
minHeight: i === chat.messages.length - 1 ? lastMessageMinHeight : 0
|
|
124
|
+
})}
|
|
125
|
+
{/if}
|
|
126
|
+
{/each}
|
|
127
|
+
</div>
|
|
128
|
+
{@render children({ onsubmit: submit })}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{#snippet defaultSystemMessage({
|
|
132
|
+
i,
|
|
133
|
+
minHeight
|
|
134
|
+
}: {
|
|
135
|
+
i: number;
|
|
136
|
+
message: AiChatMessage;
|
|
137
|
+
minHeight?: number;
|
|
138
|
+
})}
|
|
139
|
+
<AiMessage bind:b_message={chat.messages[i]} {minHeight} />
|
|
140
|
+
{/snippet}
|
|
141
|
+
|
|
142
|
+
{#snippet defaultUserMessage({
|
|
143
|
+
message
|
|
144
|
+
}: {
|
|
145
|
+
i: number;
|
|
146
|
+
message: AiChatMessage;
|
|
147
|
+
minHeight?: number;
|
|
148
|
+
})}
|
|
149
|
+
<UserMessage {message} />
|
|
150
|
+
{/snippet}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { ClassValue } from 'svelte/elements';
|
|
3
|
+
export interface AiChatMessage {
|
|
4
|
+
from: 'user' | 'system';
|
|
5
|
+
message: string;
|
|
6
|
+
time?: Date;
|
|
7
|
+
liked?: boolean;
|
|
8
|
+
disliked?: boolean;
|
|
9
|
+
files?: File[];
|
|
10
|
+
}
|
|
11
|
+
export interface AiChat {
|
|
12
|
+
messages: AiChatMessage[];
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
}
|
|
15
|
+
interface Props {
|
|
16
|
+
class?: ClassValue;
|
|
17
|
+
b_chat: AiChat;
|
|
18
|
+
userMessage?: Snippet<[{
|
|
19
|
+
message: AiChatMessage;
|
|
20
|
+
i: number;
|
|
21
|
+
}]>;
|
|
22
|
+
systemMessage?: Snippet<[{
|
|
23
|
+
message: AiChatMessage;
|
|
24
|
+
i: number;
|
|
25
|
+
minHeight?: number;
|
|
26
|
+
}]>;
|
|
27
|
+
placeholder?: Snippet;
|
|
28
|
+
children: Snippet<[{
|
|
29
|
+
onsubmit: (message: AiChatMessage) => Promise<void>;
|
|
30
|
+
}]>;
|
|
31
|
+
submit: (message: AiChatMessage) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* An AI chat component that can be used to create a chatbot.
|
|
35
|
+
* Comes with default styles for the chat messages, but can be customized with the `userMessage` and `systemMessage` props.
|
|
36
|
+
* The input component has to be provided as a child component, and the `submit` function has to be provided as a callback.
|
|
37
|
+
*/
|
|
38
|
+
declare const Chat: import("svelte").Component<Props, {}, "b_chat">;
|
|
39
|
+
type Chat = ReturnType<typeof Chat>;
|
|
40
|
+
export default Chat;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Renders markdown to html.
|
|
4
|
+
Uses the [marked](https://marked.js.org/) library for rendering and [dompurify](https://github.com/cure53/DOMPurify) for sanitizing the html.
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import clsx from 'clsx';
|
|
9
|
+
import DomPurify from 'dompurify';
|
|
10
|
+
import { marked } from 'marked';
|
|
11
|
+
import type { ClassValue } from 'svelte/elements';
|
|
12
|
+
import { twMerge } from 'tailwind-merge';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
source: string;
|
|
16
|
+
class?: ClassValue;
|
|
17
|
+
replace?: {
|
|
18
|
+
regex: RegExp;
|
|
19
|
+
replacerFactory: () => Promise<(match: string) => string> | ((match: string) => string);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let { source, replace, class: clazz }: Props = $props();
|
|
24
|
+
|
|
25
|
+
const html = $derived.by(async () => {
|
|
26
|
+
// replace 0 width characters
|
|
27
|
+
const cleanedSource = source.replace(
|
|
28
|
+
// eslint-disable-next-line no-misleading-character-class
|
|
29
|
+
/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/,
|
|
30
|
+
''
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const html = await marked.parse(cleanedSource);
|
|
34
|
+
// const docs = await documents.value;
|
|
35
|
+
|
|
36
|
+
if (replace) {
|
|
37
|
+
const replacer = await replace.replacerFactory();
|
|
38
|
+
const replaced = html.replaceAll(replace.regex, replacer);
|
|
39
|
+
return DomPurify.sanitize(replaced);
|
|
40
|
+
} else {
|
|
41
|
+
return DomPurify.sanitize(html);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<div
|
|
47
|
+
class={twMerge(
|
|
48
|
+
clsx(
|
|
49
|
+
'text-surface-950-50 prose prose-strong:text-surface-950-50 prose-p:my-1 flex flex-col items-start gap-1',
|
|
50
|
+
clazz
|
|
51
|
+
)
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
{#await html then html}
|
|
55
|
+
<!-- this is fine since we purify the string -->
|
|
56
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
57
|
+
{@html html}
|
|
58
|
+
{/await}
|
|
59
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ClassValue } from 'svelte/elements';
|
|
2
|
+
interface Props {
|
|
3
|
+
source: string;
|
|
4
|
+
class?: ClassValue;
|
|
5
|
+
replace?: {
|
|
6
|
+
regex: RegExp;
|
|
7
|
+
replacerFactory: () => Promise<(match: string) => string> | ((match: string) => string);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Renders markdown to html.
|
|
12
|
+
* Uses the [marked](https://marked.js.org/) library for rendering and [dompurify](https://github.com/cure53/DOMPurify) for sanitizing the html.
|
|
13
|
+
*/
|
|
14
|
+
declare const Markdown: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type Markdown = ReturnType<typeof Markdown>;
|
|
16
|
+
export default Markdown;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import type { ClassValue } from 'svelte/elements';
|
|
5
|
+
import { twMerge } from 'tailwind-merge';
|
|
6
|
+
import AttachedFile from './AttachedFile.svelte';
|
|
7
|
+
import type { AiChatMessage } from './Chat.svelte';
|
|
8
|
+
import Markdown from './Markdown.svelte';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
class?: ClassValue;
|
|
12
|
+
message: AiChatMessage;
|
|
13
|
+
/** How attached files should be rendered */
|
|
14
|
+
attachedFile?: Snippet<[file: File]>;
|
|
15
|
+
/** How the message string should be rendered */
|
|
16
|
+
messageText?: Snippet<[{ message: AiChatMessage }]>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
class: clazz,
|
|
21
|
+
message,
|
|
22
|
+
attachedFile = defaultAttachedFile,
|
|
23
|
+
messageText = defaultMessageText
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<div class={twMerge(clsx('flex w-full flex-col items-end gap-1', clazz))}>
|
|
28
|
+
{@render messageText({ message })}
|
|
29
|
+
{#if message.files}
|
|
30
|
+
<div class="flex flex-row items-center gap-2">
|
|
31
|
+
{#each message.files as file}
|
|
32
|
+
{@render attachedFile(file)}
|
|
33
|
+
{/each}
|
|
34
|
+
</div>
|
|
35
|
+
{/if}
|
|
36
|
+
<div class="flex flex-row items-center gap-2">
|
|
37
|
+
{#if message.time}
|
|
38
|
+
<p class="text-surface-400-600 text-sm">
|
|
39
|
+
{message.time.toLocaleString('de')}
|
|
40
|
+
</p>
|
|
41
|
+
{/if}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{#snippet defaultAttachedFile(file: File)}
|
|
46
|
+
<AttachedFile {file} />
|
|
47
|
+
{/snippet}
|
|
48
|
+
|
|
49
|
+
{#snippet defaultMessageText({ message }: { message: AiChatMessage })}
|
|
50
|
+
<div class="bg-surface-200-800 text-surface-contrast-200-800 rounded px-2 py-0.5">
|
|
51
|
+
<Markdown source={message.message} />
|
|
52
|
+
</div>
|
|
53
|
+
{/snippet}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ClassValue } from 'svelte/elements';
|
|
3
|
+
import type { AiChatMessage } from './Chat.svelte';
|
|
4
|
+
interface Props {
|
|
5
|
+
class?: ClassValue;
|
|
6
|
+
message: AiChatMessage;
|
|
7
|
+
/** How attached files should be rendered */
|
|
8
|
+
attachedFile?: Snippet<[file: File]>;
|
|
9
|
+
/** How the message string should be rendered */
|
|
10
|
+
messageText?: Snippet<[{
|
|
11
|
+
message: AiChatMessage;
|
|
12
|
+
}]>;
|
|
13
|
+
}
|
|
14
|
+
declare const UserMessage: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type UserMessage = ReturnType<typeof UserMessage>;
|
|
16
|
+
export default UserMessage;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as AiMessage } from './AiMessage.svelte';
|
|
2
|
+
export { default as AttachedFile } from './AttachedFile.svelte';
|
|
3
|
+
export { default as Chat, type AiChat, type AiChatMessage } from './Chat.svelte';
|
|
4
|
+
export { default as Markdown } from './Markdown.svelte';
|
|
5
|
+
export { default as UserMessage } from './UserMessage.svelte';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as AiMessage } from './AiMessage.svelte';
|
|
2
|
+
export { default as AttachedFile } from './AttachedFile.svelte';
|
|
3
|
+
export { default as Chat } from './Chat.svelte';
|
|
4
|
+
export { default as Markdown } from './Markdown.svelte';
|
|
5
|
+
export { default as UserMessage } from './UserMessage.svelte';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
It's a checkbox
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { type Icon as LucideIcon, Minus, icons } from '@lucide/svelte';
|
|
8
|
+
import clsx from 'clsx';
|
|
9
|
+
import type { ClassValue } from 'svelte/elements';
|
|
10
|
+
import { twMerge } from 'tailwind-merge';
|
|
11
|
+
|
|
12
|
+
const Check = $derived(icons.Check);
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
class?: ClassValue;
|
|
16
|
+
/** `checked` has prioriy over `partial` */
|
|
17
|
+
checked?: boolean | null;
|
|
18
|
+
/** `checked` has prioriy over `partial` */
|
|
19
|
+
partial?: boolean | null;
|
|
20
|
+
id?: string;
|
|
21
|
+
/** if true, the onclick handler will not be called */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
onclick?: () => void;
|
|
24
|
+
/** data-testid */
|
|
25
|
+
testId?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
class: clazz,
|
|
30
|
+
checked = false,
|
|
31
|
+
partial = false,
|
|
32
|
+
id,
|
|
33
|
+
disabled = false,
|
|
34
|
+
onclick,
|
|
35
|
+
testId
|
|
36
|
+
}: Props = $props();
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
icon: Icon,
|
|
40
|
+
innerClass,
|
|
41
|
+
style
|
|
42
|
+
}: { icon?: typeof LucideIcon; innerClass?: string; style?: string } = $derived.by(() => {
|
|
43
|
+
if (!checked && !partial) return { innerClass: 'border-surface-500' };
|
|
44
|
+
if (checked)
|
|
45
|
+
return {
|
|
46
|
+
icon: Check,
|
|
47
|
+
innerClass: 'bg-primary-500 border-primary-500 text-surface-50'
|
|
48
|
+
};
|
|
49
|
+
if (partial)
|
|
50
|
+
return {
|
|
51
|
+
icon: Minus,
|
|
52
|
+
innerClass: 'border-primary-700 text-primary-500'
|
|
53
|
+
};
|
|
54
|
+
return {};
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<!-- svelte-ignore a11y_no_static_element_interactions-->
|
|
59
|
+
<svelte:element
|
|
60
|
+
this={onclick ? 'button' : 'div'}
|
|
61
|
+
type="button"
|
|
62
|
+
{id}
|
|
63
|
+
{disabled}
|
|
64
|
+
{style}
|
|
65
|
+
class={twMerge(
|
|
66
|
+
clsx(
|
|
67
|
+
'box-border flex h-5 w-5 items-center justify-center overflow-hidden rounded border-2 transition-colors',
|
|
68
|
+
disabled && 'cursor-not-allowed opacity-70',
|
|
69
|
+
innerClass,
|
|
70
|
+
clazz
|
|
71
|
+
)
|
|
72
|
+
)}
|
|
73
|
+
{onclick}
|
|
74
|
+
data-testid={testId}
|
|
75
|
+
>
|
|
76
|
+
{#if Icon}
|
|
77
|
+
<Icon class="h-full w-full" size={16} strokeWidth={3} />
|
|
78
|
+
{/if}
|
|
79
|
+
</svelte:element>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ClassValue } from 'svelte/elements';
|
|
2
|
+
type Props = {
|
|
3
|
+
class?: ClassValue;
|
|
4
|
+
/** `checked` has prioriy over `partial` */
|
|
5
|
+
checked?: boolean | null;
|
|
6
|
+
/** `checked` has prioriy over `partial` */
|
|
7
|
+
partial?: boolean | null;
|
|
8
|
+
id?: string;
|
|
9
|
+
/** if true, the onclick handler will not be called */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
onclick?: () => void;
|
|
12
|
+
/** data-testid */
|
|
13
|
+
testId?: string;
|
|
14
|
+
};
|
|
15
|
+
/** It's a checkbox */
|
|
16
|
+
declare const Checkbox: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type Checkbox = ReturnType<typeof Checkbox>;
|
|
18
|
+
export default Checkbox;
|