@katechat/ui 1.0.2 → 1.0.3
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/dist/cjs/index.css +491 -0
- package/dist/cjs/index.css.map +7 -0
- package/dist/cjs/index.js +75305 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/esm/index.css +491 -0
- package/dist/esm/index.css.map +7 -0
- package/dist/esm/index.js +75304 -0
- package/dist/esm/index.js.map +7 -0
- package/dist/index.css +1 -0
- package/dist/index.js +539 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +27 -4
- package/.prettierrc +0 -9
- package/esbuild.js +0 -56
- package/jest.config.js +0 -24
- package/postcss.config.cjs +0 -14
- package/src/__mocks__/fileMock.js +0 -1
- package/src/__mocks__/styleMock.js +0 -1
- package/src/components/chat/ChatMessagesContainer.module.scss +0 -77
- package/src/components/chat/ChatMessagesContainer.tsx +0 -151
- package/src/components/chat/ChatMessagesList.tsx +0 -216
- package/src/components/chat/index.ts +0 -4
- package/src/components/chat/input/ChatInput.module.scss +0 -113
- package/src/components/chat/input/ChatInput.tsx +0 -259
- package/src/components/chat/input/index.ts +0 -1
- package/src/components/chat/message/ChatMessage.Carousel.module.scss +0 -7
- package/src/components/chat/message/ChatMessage.module.scss +0 -378
- package/src/components/chat/message/ChatMessage.tsx +0 -271
- package/src/components/chat/message/ChatMessagePreview.tsx +0 -22
- package/src/components/chat/message/LinkedChatMessage.tsx +0 -64
- package/src/components/chat/message/MessageStatus.tsx +0 -38
- package/src/components/chat/message/controls/CopyMessageButton.tsx +0 -32
- package/src/components/chat/message/index.ts +0 -4
- package/src/components/icons/ProviderIcon.tsx +0 -49
- package/src/components/icons/index.ts +0 -1
- package/src/components/index.ts +0 -3
- package/src/components/modal/ImagePopup.tsx +0 -97
- package/src/components/modal/index.ts +0 -1
- package/src/controls/FileDropzone/FileDropzone.module.scss +0 -15
- package/src/controls/FileDropzone/FileDropzone.tsx +0 -120
- package/src/controls/index.ts +0 -1
- package/src/core/ai.ts +0 -1
- package/src/core/index.ts +0 -4
- package/src/core/message.ts +0 -59
- package/src/core/model.ts +0 -23
- package/src/core/user.ts +0 -8
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useIntersectionObserver.ts +0 -24
- package/src/hooks/useTheme.tsx +0 -66
- package/src/index.ts +0 -5
- package/src/lib/__tests__/markdown.parser.test.ts +0 -289
- package/src/lib/__tests__/markdown.parser.testUtils.ts +0 -31
- package/src/lib/__tests__/markdown.parser_sanitizeUrl.test.ts +0 -130
- package/src/lib/assert.ts +0 -14
- package/src/lib/markdown.parser.ts +0 -189
- package/src/setupTests.ts +0 -1
- package/src/types/scss.d.ts +0 -4
- package/tsconfig.json +0 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@katechat/ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "KateChat - AI Chat Interface modules collection",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,19 +8,42 @@
|
|
|
8
8
|
"directory": "packages/katechat-ui"
|
|
9
9
|
},
|
|
10
10
|
"keywords": [
|
|
11
|
-
"chatbot"
|
|
11
|
+
"chatbot",
|
|
12
|
+
"chat",
|
|
13
|
+
"ui",
|
|
14
|
+
"react",
|
|
15
|
+
"mantine",
|
|
16
|
+
"ai"
|
|
12
17
|
],
|
|
13
18
|
"license": "MIT",
|
|
14
19
|
"bugs": {
|
|
15
20
|
"url": "https://github.com/artiz/kate-chat/issues"
|
|
16
21
|
},
|
|
17
22
|
"homepage": "https://katechat.tech",
|
|
18
|
-
"
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/cjs/index.js",
|
|
25
|
+
"module": "./dist/esm/index.js",
|
|
26
|
+
"types": "./dist/types/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/types/index.d.ts",
|
|
30
|
+
"import": "./dist/esm/index.js",
|
|
31
|
+
"require": "./dist/cjs/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
19
39
|
"publishConfig": {
|
|
20
40
|
"access": "public"
|
|
21
41
|
},
|
|
22
42
|
"scripts": {
|
|
23
|
-
"build": "
|
|
43
|
+
"build": "npm run build:js && npm run build:types",
|
|
44
|
+
"build:js": "node --experimental-default-type=module esbuild.js",
|
|
45
|
+
"build:types": "tsc --emitDeclarationOnly --declaration --declarationMap --outDir dist/types",
|
|
46
|
+
"prepublishOnly": "npm run build",
|
|
24
47
|
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,scss}\"",
|
|
25
48
|
"typecheck": "tsc --noEmit",
|
|
26
49
|
"test": "jest --passWithNoTests --coverage"
|
package/.prettierrc
DELETED
package/esbuild.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import esbuild from "esbuild";
|
|
2
|
-
import { clean } from "esbuild-plugin-clean";
|
|
3
|
-
import { sassPlugin, postcssModules } from "esbuild-sass-plugin";
|
|
4
|
-
import fs from "fs";
|
|
5
|
-
|
|
6
|
-
// Create directory if it doesn't exist
|
|
7
|
-
if (!fs.existsSync("./dist")) {
|
|
8
|
-
fs.mkdirSync("./dist");
|
|
9
|
-
}
|
|
10
|
-
// Production build configuration
|
|
11
|
-
esbuild
|
|
12
|
-
.build({
|
|
13
|
-
entryPoints: ["./src/index.ts"],
|
|
14
|
-
outdir: "./dist",
|
|
15
|
-
bundle: true,
|
|
16
|
-
minify: true,
|
|
17
|
-
format: "iife",
|
|
18
|
-
splitting: false,
|
|
19
|
-
loader: {
|
|
20
|
-
".js": "jsx",
|
|
21
|
-
".svg": "dataurl",
|
|
22
|
-
".png": "dataurl",
|
|
23
|
-
".jpg": "dataurl",
|
|
24
|
-
".gif": "dataurl",
|
|
25
|
-
".woff": "file",
|
|
26
|
-
".woff2": "file",
|
|
27
|
-
".ttf": "file",
|
|
28
|
-
".eot": "file",
|
|
29
|
-
},
|
|
30
|
-
plugins: [
|
|
31
|
-
clean({ patterns: ["./dist/*.*"] }),
|
|
32
|
-
sassPlugin({
|
|
33
|
-
filter: /\.module\.scss$/,
|
|
34
|
-
transform: postcssModules({}),
|
|
35
|
-
}),
|
|
36
|
-
sassPlugin({
|
|
37
|
-
filter: /\.scss$/,
|
|
38
|
-
}),
|
|
39
|
-
],
|
|
40
|
-
define: {
|
|
41
|
-
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "production"),
|
|
42
|
-
},
|
|
43
|
-
metafile: true,
|
|
44
|
-
})
|
|
45
|
-
.then(result => {
|
|
46
|
-
console.log("⚡ Build complete! Bundle created in ./dist");
|
|
47
|
-
console.log(`📦 Config: APP_API_URL: ${process.env.APP_API_URL}`);
|
|
48
|
-
const outputSize = Object.entries(result.metafile.outputs).reduce((total, [name, data]) => {
|
|
49
|
-
return total + data.bytes;
|
|
50
|
-
}, 0);
|
|
51
|
-
console.log(`📦 Total bundle size: ${(outputSize / 1024 / 1024).toFixed(2)}MB`);
|
|
52
|
-
})
|
|
53
|
-
.catch(e => {
|
|
54
|
-
console.error("❌ Build failed:", e);
|
|
55
|
-
process.exit(1);
|
|
56
|
-
});
|
package/jest.config.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/** @type {import('jest').Config} */
|
|
2
|
-
module.exports = {
|
|
3
|
-
preset: "ts-jest",
|
|
4
|
-
testEnvironment: "jsdom",
|
|
5
|
-
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
|
|
6
|
-
moduleNameMapper: {
|
|
7
|
-
"^@/(.*)$": "<rootDir>/src/$1",
|
|
8
|
-
"\\.(css|less|scss|sass)$": "<rootDir>/src/__mocks__/styleMock.js",
|
|
9
|
-
"\\.(jpg|jpeg|png|gif|webp|svg)$": "<rootDir>/src/__mocks__/fileMock.js",
|
|
10
|
-
},
|
|
11
|
-
transform: {
|
|
12
|
-
"^.+\\.[tj]sx?$": [
|
|
13
|
-
"ts-jest",
|
|
14
|
-
{
|
|
15
|
-
tsconfig: {
|
|
16
|
-
jsx: "react-jsx",
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
],
|
|
20
|
-
},
|
|
21
|
-
testMatch: ["**/__tests__/**/*.test.(ts|tsx)"],
|
|
22
|
-
coverageDirectory: "coverage",
|
|
23
|
-
collectCoverageFrom: ["src/**/*.{ts,tsx}", "!src/**/*.d.ts"],
|
|
24
|
-
};
|
package/postcss.config.cjs
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
plugins: {
|
|
3
|
-
"postcss-preset-mantine": {},
|
|
4
|
-
"postcss-simple-vars": {
|
|
5
|
-
variables: {
|
|
6
|
-
"mantine-breakpoint-xs": "36em",
|
|
7
|
-
"mantine-breakpoint-sm": "48em",
|
|
8
|
-
"mantine-breakpoint-md": "62em",
|
|
9
|
-
"mantine-breakpoint-lg": "75em",
|
|
10
|
-
"mantine-breakpoint-xl": "88em",
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = "test-file-stub";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = {};
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
@import "@mantine/core/styles.css";
|
|
2
|
-
|
|
3
|
-
.messagesContainer {
|
|
4
|
-
flex-grow: 1;
|
|
5
|
-
position: relative;
|
|
6
|
-
display: none;
|
|
7
|
-
flex-direction: column;
|
|
8
|
-
border: 1px solid var(--app-shell-border-color);
|
|
9
|
-
border-radius: var(--mantine-radius-md);
|
|
10
|
-
/*
|
|
11
|
-
* Two-level scrolling architecture:
|
|
12
|
-
* The parent (.messagesContainer) uses overflow-y: hidden to prevent unwanted scrollbars and control the visible area.
|
|
13
|
-
* The child (.scroller) uses overflow-y: scroll to enable vertical scrolling for the message list.
|
|
14
|
-
* This pattern is intentional; do not merge the scroll behavior into the parent.
|
|
15
|
-
*/
|
|
16
|
-
overflow-y: hidden;
|
|
17
|
-
opacity: 1;
|
|
18
|
-
transition: all 0.3s ease;
|
|
19
|
-
|
|
20
|
-
.messagesList {
|
|
21
|
-
opacity: 0;
|
|
22
|
-
margin-bottom: 1rem;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
&.empty {
|
|
26
|
-
flex-grow: 0;
|
|
27
|
-
opacity: 0;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.scroller {
|
|
31
|
-
overflow-y: scroll;
|
|
32
|
-
flex-grow: 1;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
&.loadCompleted {
|
|
36
|
-
.scroller {
|
|
37
|
-
scroll-behavior: smooth;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
display: flex;
|
|
41
|
-
|
|
42
|
-
.messagesList {
|
|
43
|
-
opacity: 1;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.anchorContainer {
|
|
49
|
-
position: absolute;
|
|
50
|
-
width: 40px;
|
|
51
|
-
left: calc(50% - 40px);
|
|
52
|
-
bottom: 0;
|
|
53
|
-
height: 48px;
|
|
54
|
-
z-index: 2;
|
|
55
|
-
display: flex;
|
|
56
|
-
flex-direction: column;
|
|
57
|
-
justify-items: center;
|
|
58
|
-
align-items: center;
|
|
59
|
-
opacity: 0;
|
|
60
|
-
transition: all 0.5s ease;
|
|
61
|
-
|
|
62
|
-
&.visible {
|
|
63
|
-
opacity: 1;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.anchor {
|
|
67
|
-
background-color: var(--mantine-color-body);
|
|
68
|
-
border-radius: 50%;
|
|
69
|
-
height: 32px;
|
|
70
|
-
width: 32px;
|
|
71
|
-
opacity: 0.75;
|
|
72
|
-
|
|
73
|
-
&:hover {
|
|
74
|
-
opacity: 1;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
2
|
-
import { Group, Loader } from "@mantine/core";
|
|
3
|
-
import { IconCircleChevronDown } from "@tabler/icons-react";
|
|
4
|
-
import { Message, Model, PluginProps } from "@/core";
|
|
5
|
-
import { useIntersectionObserver } from "@/hooks";
|
|
6
|
-
import { ChatMessagesList } from "./ChatMessagesList";
|
|
7
|
-
|
|
8
|
-
import classes from "./ChatMessagesContainer.module.scss";
|
|
9
|
-
|
|
10
|
-
interface IProps {
|
|
11
|
-
messages?: Message[];
|
|
12
|
-
models: Model[];
|
|
13
|
-
addChatMessage: (message: Message) => void;
|
|
14
|
-
removeMessages: (args: { messagesToDelete?: Message[]; deleteAfter?: Message }) => void;
|
|
15
|
-
loadMoreMessages?: () => void;
|
|
16
|
-
plugins?: React.FC<PluginProps<Message>>[];
|
|
17
|
-
detailsPlugins?: ((message: Message) => React.ReactNode)[];
|
|
18
|
-
streaming?: boolean;
|
|
19
|
-
loading?: boolean;
|
|
20
|
-
loadCompleted?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ChatMessagesContainerRef {
|
|
24
|
-
scrollToBottom: () => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const ChatMessagesContainer = React.forwardRef<ChatMessagesContainerRef, IProps>(
|
|
28
|
-
(
|
|
29
|
-
{
|
|
30
|
-
messages,
|
|
31
|
-
models,
|
|
32
|
-
addChatMessage,
|
|
33
|
-
removeMessages,
|
|
34
|
-
loadMoreMessages,
|
|
35
|
-
plugins,
|
|
36
|
-
detailsPlugins,
|
|
37
|
-
streaming = false,
|
|
38
|
-
loadCompleted = true,
|
|
39
|
-
loading = false,
|
|
40
|
-
},
|
|
41
|
-
ref
|
|
42
|
-
) => {
|
|
43
|
-
const [showAnchorButton, setShowAnchorButton] = useState<boolean>(false);
|
|
44
|
-
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
|
45
|
-
const anchorTimer = useRef<NodeJS.Timeout | null>(null);
|
|
46
|
-
|
|
47
|
-
// #region Scrolling
|
|
48
|
-
const scrollToBottom = useCallback(() => {
|
|
49
|
-
messagesContainerRef.current?.scrollTo(0, messagesContainerRef.current?.scrollHeight ?? 0);
|
|
50
|
-
}, [messagesContainerRef]);
|
|
51
|
-
|
|
52
|
-
// Expose scrollToBottom method to parent via ref
|
|
53
|
-
useImperativeHandle(
|
|
54
|
-
ref,
|
|
55
|
-
() => ({
|
|
56
|
-
scrollToBottom,
|
|
57
|
-
}),
|
|
58
|
-
[scrollToBottom]
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
const autoScroll = useCallback(() => {
|
|
62
|
-
if (!showAnchorButton) {
|
|
63
|
-
scrollToBottom();
|
|
64
|
-
}
|
|
65
|
-
}, [scrollToBottom, showAnchorButton]);
|
|
66
|
-
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
autoScroll();
|
|
69
|
-
}, [messages, autoScroll]);
|
|
70
|
-
|
|
71
|
-
const handleScroll = useCallback(
|
|
72
|
-
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
73
|
-
const { scrollTop, scrollHeight, clientHeight } = e.target as HTMLDivElement;
|
|
74
|
-
anchorTimer.current && clearTimeout(anchorTimer.current);
|
|
75
|
-
|
|
76
|
-
if (scrollHeight - scrollTop - clientHeight < 2) {
|
|
77
|
-
setShowAnchorButton(false);
|
|
78
|
-
} else if (messages?.length) {
|
|
79
|
-
if (streaming) {
|
|
80
|
-
anchorTimer.current = setTimeout(() => {
|
|
81
|
-
setShowAnchorButton(true);
|
|
82
|
-
}, 100);
|
|
83
|
-
} else {
|
|
84
|
-
setShowAnchorButton(true);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
[messages?.length, streaming]
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const anchorHandleClick = useCallback(() => {
|
|
92
|
-
setShowAnchorButton(false);
|
|
93
|
-
scrollToBottom();
|
|
94
|
-
}, [scrollToBottom]);
|
|
95
|
-
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
if (loadCompleted) {
|
|
98
|
-
setShowAnchorButton(false);
|
|
99
|
-
setTimeout(scrollToBottom, 200);
|
|
100
|
-
}
|
|
101
|
-
}, [loadCompleted]);
|
|
102
|
-
|
|
103
|
-
const firstMessageRef = useIntersectionObserver<HTMLDivElement>(
|
|
104
|
-
() => loadMoreMessages?.(),
|
|
105
|
-
[loadMoreMessages],
|
|
106
|
-
200
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// #endregion
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<div
|
|
113
|
-
className={[
|
|
114
|
-
classes.messagesContainer,
|
|
115
|
-
loadCompleted ? classes.loadCompleted : "",
|
|
116
|
-
loadCompleted && messages?.length === 0 ? classes.empty : "",
|
|
117
|
-
].join(" ")}
|
|
118
|
-
>
|
|
119
|
-
<div className={classes.scroller} ref={messagesContainerRef} onScroll={handleScroll}>
|
|
120
|
-
<div ref={firstMessageRef} />
|
|
121
|
-
{loading && (
|
|
122
|
-
<Group justify="center" align="center" py="xl">
|
|
123
|
-
<Loader />
|
|
124
|
-
</Group>
|
|
125
|
-
)}
|
|
126
|
-
|
|
127
|
-
<div className={classes.messagesList}>
|
|
128
|
-
{messages && (
|
|
129
|
-
<ChatMessagesList
|
|
130
|
-
messages={messages}
|
|
131
|
-
onMessageDeleted={removeMessages} // Reload messages after deletion
|
|
132
|
-
onAddMessage={addChatMessage}
|
|
133
|
-
models={models}
|
|
134
|
-
plugins={plugins}
|
|
135
|
-
detailsPlugins={detailsPlugins}
|
|
136
|
-
/>
|
|
137
|
-
)}
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<div className={[classes.anchorContainer, showAnchorButton ? classes.visible : ""].join(" ")}>
|
|
142
|
-
<div className={classes.anchor}>
|
|
143
|
-
<IconCircleChevronDown size={32} color="teal" style={{ cursor: "pointer" }} onClick={anchorHandleClick} />
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
ChatMessagesContainer.displayName = "ChatMessagesContainer";
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useRef, useState } from "react";
|
|
2
|
-
import { Stack, Group } from "@mantine/core";
|
|
3
|
-
import { notifications } from "@mantine/notifications";
|
|
4
|
-
|
|
5
|
-
import { notEmpty, ok } from "@/lib/assert";
|
|
6
|
-
import { Message, Model, PluginProps } from "@/core";
|
|
7
|
-
import { ChatMessage } from "./message/ChatMessage";
|
|
8
|
-
import { ImagePopup } from "../modal/ImagePopup";
|
|
9
|
-
|
|
10
|
-
interface ChatMessagesProps {
|
|
11
|
-
messages: Message[];
|
|
12
|
-
onMessageDeleted?: (args: { messagesToDelete?: Message[]; deleteAfter?: Message }) => void;
|
|
13
|
-
onAddMessage?: (message: Message) => void;
|
|
14
|
-
plugins?: React.FC<PluginProps<Message>>[];
|
|
15
|
-
detailsPlugins?: ((message: Message) => React.ReactNode)[];
|
|
16
|
-
models: Model[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const ChatMessagesList: React.FC<ChatMessagesProps> = ({
|
|
20
|
-
messages,
|
|
21
|
-
onMessageDeleted,
|
|
22
|
-
onAddMessage,
|
|
23
|
-
plugins = [],
|
|
24
|
-
detailsPlugins = [],
|
|
25
|
-
models = [],
|
|
26
|
-
}) => {
|
|
27
|
-
const componentRef = useRef<HTMLDivElement>(null);
|
|
28
|
-
|
|
29
|
-
const [imageToShow, setImageToShow] = useState<string | undefined>();
|
|
30
|
-
const [imageFileName, setImageFileName] = useState<string | undefined>();
|
|
31
|
-
const [updatedMessages, setUpdatedMessages] = useState<Set<string>>(new Set());
|
|
32
|
-
|
|
33
|
-
const addEditedMessage = (messageId: string) => setUpdatedMessages(prev => new Set(prev).add(messageId));
|
|
34
|
-
|
|
35
|
-
const clearEditedMessage = (messageId: string) => {
|
|
36
|
-
setUpdatedMessages(prev => {
|
|
37
|
-
const set = new Set(prev);
|
|
38
|
-
set.delete(messageId);
|
|
39
|
-
return set;
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const resetSelectedImage = () => {
|
|
44
|
-
setImageToShow(undefined);
|
|
45
|
-
setImageFileName(undefined);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// common messages interaction logic
|
|
49
|
-
const handleMessageClick = useCallback(
|
|
50
|
-
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
51
|
-
if (!e.target) return;
|
|
52
|
-
|
|
53
|
-
// common clicks logic to simplify code in ChatMessage component
|
|
54
|
-
const classesToFind = ["code-copy-btn", "code-toggle-all", "copy-message-btn", "code-header", "message-image"];
|
|
55
|
-
|
|
56
|
-
let el: HTMLElement = e.target as HTMLElement;
|
|
57
|
-
for (const cls of classesToFind) {
|
|
58
|
-
const trg = el.closest(`.${cls}`);
|
|
59
|
-
if (trg) {
|
|
60
|
-
el = trg as HTMLElement;
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!el) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const target = el as HTMLElement;
|
|
70
|
-
const toggleCodeBlock = (header: HTMLElement) => {
|
|
71
|
-
const codeBlock = header?.nextElementSibling as HTMLElement;
|
|
72
|
-
if (codeBlock.classList.contains("collapsed")) {
|
|
73
|
-
header.classList.remove("collapsed");
|
|
74
|
-
codeBlock && codeBlock.classList.remove("collapsed");
|
|
75
|
-
} else {
|
|
76
|
-
header.classList.add("collapsed");
|
|
77
|
-
codeBlock && codeBlock.classList.add("collapsed");
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// copy code block
|
|
82
|
-
if (target.classList.contains("code-copy-btn")) {
|
|
83
|
-
const data = target.parentElement?.parentElement?.nextElementSibling?.querySelector(
|
|
84
|
-
".code-data"
|
|
85
|
-
) as HTMLElement;
|
|
86
|
-
if (data) {
|
|
87
|
-
const code = decodeURIComponent(data.dataset.code || "").trim();
|
|
88
|
-
navigator.clipboard.writeText(code);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const copyIcon = target.querySelector(".copy-icon") as HTMLElement;
|
|
92
|
-
const checkIcon = target.querySelector(".check-icon") as HTMLElement;
|
|
93
|
-
if (copyIcon && checkIcon) {
|
|
94
|
-
copyIcon.style.display = "none";
|
|
95
|
-
checkIcon.style.display = "block";
|
|
96
|
-
setTimeout(() => {
|
|
97
|
-
copyIcon.style.display = "block";
|
|
98
|
-
checkIcon.style.display = "none";
|
|
99
|
-
}, 2000);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
// code toggle btn
|
|
103
|
-
else if (target.classList.contains("code-header")) {
|
|
104
|
-
toggleCodeBlock(target);
|
|
105
|
-
}
|
|
106
|
-
// all code blocks toggle
|
|
107
|
-
else if (target.classList.contains("code-toggle-all")) {
|
|
108
|
-
componentRef.current?.querySelectorAll(".code-header").forEach(header => {
|
|
109
|
-
toggleCodeBlock(header as HTMLElement);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
// copy message
|
|
113
|
-
else if (target.classList.contains("copy-message-btn")) {
|
|
114
|
-
if (target.dataset["messageId"]) {
|
|
115
|
-
const index = target.dataset["messageIndex"];
|
|
116
|
-
const linkedIndex = target.dataset["messageLinkedIndex"];
|
|
117
|
-
const messageId = target.dataset["messageId"];
|
|
118
|
-
|
|
119
|
-
let msg: Message | undefined =
|
|
120
|
-
index != undefined ? messages[Number(index)] : messages.find(m => m.id === messageId);
|
|
121
|
-
if (linkedIndex != undefined) {
|
|
122
|
-
msg = msg?.linkedMessages?.[Number(linkedIndex)];
|
|
123
|
-
}
|
|
124
|
-
ok(msg, "Message should exist to copy");
|
|
125
|
-
const content = (msg.content || "").trim();
|
|
126
|
-
const html = msg.html || [];
|
|
127
|
-
|
|
128
|
-
if (html.length && html[0]) {
|
|
129
|
-
const blobHTML = new Blob([html.join("<br/>")], { type: "text/html" });
|
|
130
|
-
const blobPlain = new Blob([content], { type: "text/plain" });
|
|
131
|
-
navigator.clipboard
|
|
132
|
-
.write([new ClipboardItem({ [blobHTML.type]: blobHTML, [blobPlain.type]: blobPlain })])
|
|
133
|
-
.catch(err =>
|
|
134
|
-
notifications.show({
|
|
135
|
-
title: "Error",
|
|
136
|
-
message: err.message || "Failed to copy message",
|
|
137
|
-
color: "red",
|
|
138
|
-
})
|
|
139
|
-
);
|
|
140
|
-
} else {
|
|
141
|
-
navigator.clipboard.writeText(content);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const checkIcon = target.parentElement?.querySelector(".check-icon") as HTMLElement;
|
|
145
|
-
if (checkIcon) {
|
|
146
|
-
target.style.display = "none";
|
|
147
|
-
checkIcon.style.display = "inline-block";
|
|
148
|
-
setTimeout(() => {
|
|
149
|
-
target.style.display = "inline-block";
|
|
150
|
-
checkIcon.style.display = "none";
|
|
151
|
-
}, 2000);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// code toggle btn
|
|
156
|
-
else if (target.classList.contains("message-image")) {
|
|
157
|
-
const fileName = target.dataset["fileName"];
|
|
158
|
-
const imageUrl = (target as HTMLImageElement).src;
|
|
159
|
-
|
|
160
|
-
setImageToShow(imageUrl);
|
|
161
|
-
setImageFileName(fileName);
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
[messages]
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
const pluginsLoader = useCallback(
|
|
168
|
-
(msg: Message) => {
|
|
169
|
-
return (
|
|
170
|
-
<>
|
|
171
|
-
{plugins.map((PluginComponent, idx) => (
|
|
172
|
-
<PluginComponent
|
|
173
|
-
key={idx}
|
|
174
|
-
message={msg}
|
|
175
|
-
onAddMessage={onAddMessage}
|
|
176
|
-
onAction={addEditedMessage}
|
|
177
|
-
onActionEnd={clearEditedMessage}
|
|
178
|
-
onMessageDeleted={onMessageDeleted}
|
|
179
|
-
disabled={updatedMessages.has(msg.id)}
|
|
180
|
-
/>
|
|
181
|
-
))}
|
|
182
|
-
</>
|
|
183
|
-
);
|
|
184
|
-
},
|
|
185
|
-
[plugins, onAddMessage, onMessageDeleted, updatedMessages, addEditedMessage, clearEditedMessage]
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
const messageDetailsLoader = useCallback(
|
|
189
|
-
(msg: Message) => {
|
|
190
|
-
const details = detailsPlugins.map((plugin, idx) => plugin(msg)).filter(notEmpty);
|
|
191
|
-
return details.length ? details : null;
|
|
192
|
-
},
|
|
193
|
-
[plugins, detailsPlugins]
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
return (
|
|
197
|
-
<>
|
|
198
|
-
<Stack gap="xs" ref={componentRef} onClick={handleMessageClick}>
|
|
199
|
-
{messages.map((msg, index) => (
|
|
200
|
-
<Group key={msg.id} align="flex-start" gap="xs">
|
|
201
|
-
<ChatMessage
|
|
202
|
-
message={msg}
|
|
203
|
-
index={index}
|
|
204
|
-
disabled={updatedMessages.has(msg.id)}
|
|
205
|
-
pluginsLoader={pluginsLoader}
|
|
206
|
-
messageDetailsLoader={messageDetailsLoader}
|
|
207
|
-
models={models}
|
|
208
|
-
/>
|
|
209
|
-
</Group>
|
|
210
|
-
))}
|
|
211
|
-
</Stack>
|
|
212
|
-
|
|
213
|
-
<ImagePopup fileName={imageFileName ?? ""} fileUrl={imageToShow ?? ""} onClose={resetSelectedImage} />
|
|
214
|
-
</>
|
|
215
|
-
);
|
|
216
|
-
};
|