@smarter.sh/ui-chat 0.0.1
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/LICENSE +671 -0
- package/README.md +143 -0
- package/dist/smarter-chat-library.es.js +28850 -0
- package/dist/smarter-chat-library.es.js.map +1 -0
- package/dist/smarter-chat-library.umd.js +807 -0
- package/dist/smarter-chat-library.umd.js.map +1 -0
- package/dist/ui-chat.css +1 -0
- package/package.json +91 -0
- package/src/components/ErrorModal/ErrorModal.css +35 -0
- package/src/components/ErrorModal/ErrorModal.jsx +56 -0
- package/src/components/ErrorModal/index.js +3 -0
- package/src/components/SmarterChat/ErrorBoundary.jsx +32 -0
- package/src/components/SmarterChat/SmarterChat.jsx +419 -0
- package/src/components/SmarterChat/api.js +233 -0
- package/src/components/SmarterChat/enums.js +17 -0
- package/src/components/SmarterChat/index.js +3 -0
- package/src/components/SmarterChat/styles.css +148 -0
- package/src/components/SmarterChat/utils.jsx +158 -0
- package/src/components/enums.js +1 -0
- package/src/components/index.js +2 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
//---------------------------------------------------------------------------------
|
|
2
|
+
// written by: Lawrence McDaniel
|
|
3
|
+
// https://lawrencemcdaniel.com
|
|
4
|
+
//
|
|
5
|
+
// date: Mar-2024
|
|
6
|
+
//---------------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
// React stuff
|
|
9
|
+
import React, { useRef, useState, useEffect } from "react";
|
|
10
|
+
|
|
11
|
+
// see: https://www.npmjs.com/package/styled-components
|
|
12
|
+
import styled from "styled-components";
|
|
13
|
+
|
|
14
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
15
|
+
import { faCheckCircle, faTimesCircle, faRocket } from "@fortawesome/free-solid-svg-icons";
|
|
16
|
+
|
|
17
|
+
// Chat UI stuff
|
|
18
|
+
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
|
|
19
|
+
import {
|
|
20
|
+
MainContainer,
|
|
21
|
+
ChatContainer,
|
|
22
|
+
MessageList,
|
|
23
|
+
Message,
|
|
24
|
+
MessageInput,
|
|
25
|
+
TypingIndicator,
|
|
26
|
+
ConversationHeader,
|
|
27
|
+
InfoButton,
|
|
28
|
+
AddUserButton,
|
|
29
|
+
} from "@chatscope/chat-ui-kit-react";
|
|
30
|
+
|
|
31
|
+
// this repo
|
|
32
|
+
import { ErrorModal } from "../ErrorModal/ErrorModal.jsx";
|
|
33
|
+
|
|
34
|
+
// This component
|
|
35
|
+
import "./styles.css";
|
|
36
|
+
import { MessageDirectionEnum, SenderRoleEnum } from "./enums.js";
|
|
37
|
+
import { setCookie, fetchConfig, fetchPrompt } from "./api.js";
|
|
38
|
+
import { cookieMetaFactory, messageFactory, chatMessages2RequestMessages, chatInit } from "./utils.jsx";
|
|
39
|
+
import { ErrorBoundary } from "./ErrorBoundary.jsx";
|
|
40
|
+
|
|
41
|
+
export const ContainerLayout = styled.div`
|
|
42
|
+
height: 100%;
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: row;
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
export const ContentLayout = styled.div`
|
|
48
|
+
flex: 1;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: row;
|
|
51
|
+
margin: 0;
|
|
52
|
+
padding: 0;
|
|
53
|
+
height: 100%;
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
export const ComponentLayout = styled.div`
|
|
57
|
+
flex-basis: 100%;
|
|
58
|
+
margin: 0;
|
|
59
|
+
padding: 5px;
|
|
60
|
+
height: 100%;
|
|
61
|
+
@media (max-width: 992px) {
|
|
62
|
+
flex-basis: 100%;
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const DEBUG_MODE = false;
|
|
67
|
+
|
|
68
|
+
// The main chat component. This is the top-level component that
|
|
69
|
+
// is exported and used in the index.js file. It is responsible for
|
|
70
|
+
// managing the chat message thread, sending messages to the backend
|
|
71
|
+
// Api, and rendering the chat UI.
|
|
72
|
+
function SmarterChat({
|
|
73
|
+
apiUrl,
|
|
74
|
+
apiKey,
|
|
75
|
+
toggleMetadata,
|
|
76
|
+
csrfCookieName,
|
|
77
|
+
debugCookieName,
|
|
78
|
+
debugCookieExpiration,
|
|
79
|
+
sessionCookieName,
|
|
80
|
+
sessionCookieExpiration,
|
|
81
|
+
}) {
|
|
82
|
+
const csrfCookie = cookieMetaFactory(csrfCookieName, null); // we read this but never set it.
|
|
83
|
+
const sessionCookie = cookieMetaFactory(sessionCookieName, sessionCookieExpiration);
|
|
84
|
+
const debugCookie = cookieMetaFactory(debugCookieName, debugCookieExpiration);
|
|
85
|
+
const cookies = {
|
|
86
|
+
csrfCookie: csrfCookie,
|
|
87
|
+
sessionCookie: sessionCookie,
|
|
88
|
+
debugCookie: debugCookie,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const [configApiUrl, setConfigApiUrl] = useState(apiUrl);
|
|
92
|
+
const [showMetadata, setShowMetadata] = useState(toggleMetadata);
|
|
93
|
+
|
|
94
|
+
const [config, setConfig] = useState({});
|
|
95
|
+
const [placeholderText, setPlaceholderText] = useState("");
|
|
96
|
+
const [assistantName, setAssistantName] = useState("");
|
|
97
|
+
const [infoUrl, setInfoUrl] = useState("");
|
|
98
|
+
const [fileAttachButton, setFileAttachButton] = useState(false);
|
|
99
|
+
const [isValid, setIsValid] = useState(false);
|
|
100
|
+
const [isDeployed, setIsDeployed] = useState(false);
|
|
101
|
+
const [debugMode, setDebugMode] = useState(DEBUG_MODE);
|
|
102
|
+
const [messages, setMessages] = useState([]);
|
|
103
|
+
const [title, setTitle] = useState("");
|
|
104
|
+
const [info, setInfo] = useState("");
|
|
105
|
+
|
|
106
|
+
// future use
|
|
107
|
+
// const [backgroundImageUrl, setBackgroundImageUrl] = useState('');
|
|
108
|
+
// const [sandboxMode, setSandboxMode] = useState(false);
|
|
109
|
+
|
|
110
|
+
// component internal state
|
|
111
|
+
const [isTyping, setIsTyping] = useState(false);
|
|
112
|
+
const fileInputRef = useRef(null);
|
|
113
|
+
|
|
114
|
+
const refetchConfig = async () => {
|
|
115
|
+
const newConfig = await fetchConfig(configApiUrl, cookies);
|
|
116
|
+
|
|
117
|
+
if (newConfig?.debug_mode) {
|
|
118
|
+
console.log("fetchAndSetConfig()...");
|
|
119
|
+
console.log("fetchAndSetConfig() config:", newConfig);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setConfig(newConfig);
|
|
123
|
+
return newConfig;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const fetchAndSetConfig = async () => {
|
|
127
|
+
try {
|
|
128
|
+
const newConfig = await refetchConfig();
|
|
129
|
+
|
|
130
|
+
console.log("fetchAndSetConfig() config:", newConfig);
|
|
131
|
+
|
|
132
|
+
setPlaceholderText(newConfig.chatbot.app_placeholder);
|
|
133
|
+
setConfigApiUrl(newConfig.chatbot.url_chatbot);
|
|
134
|
+
setAssistantName(newConfig.chatbot.app_assistant);
|
|
135
|
+
setInfoUrl(newConfig.chatbot.app_info_url);
|
|
136
|
+
setFileAttachButton(newConfig.chatbot.app_file_attachment);
|
|
137
|
+
setIsValid(newConfig.meta_data.is_valid);
|
|
138
|
+
setIsDeployed(newConfig.meta_data.is_deployed);
|
|
139
|
+
setDebugMode(newConfig.debug_mode);
|
|
140
|
+
|
|
141
|
+
// wrap up the rest of the initialization
|
|
142
|
+
const newHistory = newConfig.history?.chat_history || [];
|
|
143
|
+
const newThread = chatInit(
|
|
144
|
+
newConfig.chatbot.app_welcome_message,
|
|
145
|
+
newConfig.chatbot.default_system_role,
|
|
146
|
+
newConfig.chatbot.app_example_prompts,
|
|
147
|
+
newConfig.session_key,
|
|
148
|
+
newHistory,
|
|
149
|
+
"BACKEND_CHAT_MOST_RECENT_RESPONSE",
|
|
150
|
+
);
|
|
151
|
+
setMessages(newThread);
|
|
152
|
+
|
|
153
|
+
const newTitle = `${newConfig.chatbot.app_name} v${newConfig.chatbot.version || "1.0.0"}`;
|
|
154
|
+
setTitle(newTitle);
|
|
155
|
+
let newInfo = `${newConfig.chatbot.provider} ${newConfig.chatbot.default_model}`;
|
|
156
|
+
if (newConfig.plugins.meta_data.total_plugins > 0) {
|
|
157
|
+
newInfo += ` with ${newConfig.plugins.meta_data.total_plugins} additional plugins`;
|
|
158
|
+
}
|
|
159
|
+
setInfo(newInfo);
|
|
160
|
+
|
|
161
|
+
if (newConfig?.debug_mode) {
|
|
162
|
+
console.log("fetchAndSetConfig() done!");
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error("Failed to fetch config:", error);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Lifecycle hooks
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (debugMode) {
|
|
172
|
+
console.log("ChatApp() component mounted");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fetchAndSetConfig();
|
|
176
|
+
|
|
177
|
+
return () => {
|
|
178
|
+
if (debugMode) {
|
|
179
|
+
console.log("ChatApp() component unmounted");
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}, []);
|
|
183
|
+
|
|
184
|
+
// Error modal state management
|
|
185
|
+
function openErrorModal(title, msg) {
|
|
186
|
+
setIsModalOpen(true);
|
|
187
|
+
setmodalTitle(title);
|
|
188
|
+
setmodalMessage(msg);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function closeChatModal() {
|
|
192
|
+
setIsModalOpen(false);
|
|
193
|
+
}
|
|
194
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
195
|
+
const [modalMessage, setmodalMessage] = useState("");
|
|
196
|
+
const [modalTitle, setmodalTitle] = useState("");
|
|
197
|
+
|
|
198
|
+
const handleInfoButtonClick = () => {
|
|
199
|
+
const newValue = !showMetadata;
|
|
200
|
+
setShowMetadata(newValue);
|
|
201
|
+
if (debugMode) {
|
|
202
|
+
console.log("showMetadata:", newValue);
|
|
203
|
+
}
|
|
204
|
+
const newMessages = messages.map((message) => {
|
|
205
|
+
if (message.message === null) {
|
|
206
|
+
return { ...message, display: false };
|
|
207
|
+
}
|
|
208
|
+
if (["smarter", "system", "tool"].includes(message.sender)) {
|
|
209
|
+
// toggle backend messages
|
|
210
|
+
if (debugMode) {
|
|
211
|
+
//console.log("toggle message:", message);
|
|
212
|
+
}
|
|
213
|
+
return { ...message, display: newValue };
|
|
214
|
+
} else {
|
|
215
|
+
// always show user and assistant messages
|
|
216
|
+
return { ...message, display: true };
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
setMessages(newMessages);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const handleAddUserButtonClick = () => {
|
|
223
|
+
setCookie(cookies.sessionCookie, "");
|
|
224
|
+
fetchAndSetConfig();
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
async function handleApiRequest(input_text, base64_encode = false) {
|
|
228
|
+
// Api request handler. This function is indirectly called by UI event handlers
|
|
229
|
+
// inside this module. It asynchronously sends the user's input to the
|
|
230
|
+
// backend Api using the fetch() function. The response from the Api is
|
|
231
|
+
// then used to update the chat message thread and the UI via React state.
|
|
232
|
+
const newMessage = messageFactory({}, input_text, MessageDirectionEnum.OUTGOING, SenderRoleEnum.USER);
|
|
233
|
+
if (base64_encode) {
|
|
234
|
+
console.error("base64 encoding not implemented yet.");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
setMessages((prevMessages) => {
|
|
238
|
+
const updatedMessages = [...prevMessages, newMessage];
|
|
239
|
+
setIsTyping(true);
|
|
240
|
+
|
|
241
|
+
(async () => {
|
|
242
|
+
try {
|
|
243
|
+
if (debugMode) {
|
|
244
|
+
console.log("handleApiRequest() messages:", updatedMessages);
|
|
245
|
+
}
|
|
246
|
+
const msgs = chatMessages2RequestMessages(updatedMessages);
|
|
247
|
+
const response = await fetchPrompt(config, msgs, cookies);
|
|
248
|
+
|
|
249
|
+
if (response) {
|
|
250
|
+
const responseMessages = response.smarter.messages
|
|
251
|
+
.filter((message) => message.content !== null)
|
|
252
|
+
.map((message) => {
|
|
253
|
+
return messageFactory(message, message.content, MessageDirectionEnum.INCOMING, message.role);
|
|
254
|
+
});
|
|
255
|
+
setMessages((prevMessages) => [...prevMessages, ...responseMessages]);
|
|
256
|
+
setIsTyping(false);
|
|
257
|
+
refetchConfig();
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
setIsTyping(false);
|
|
261
|
+
console.error("Api error: ", error);
|
|
262
|
+
openErrorModal("Api error", error.message);
|
|
263
|
+
}
|
|
264
|
+
})();
|
|
265
|
+
|
|
266
|
+
return updatedMessages;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// file upload event handlers
|
|
271
|
+
const handleAttachClick = async () => {
|
|
272
|
+
fileInputRef.current.click();
|
|
273
|
+
};
|
|
274
|
+
function handleFileChange(event) {
|
|
275
|
+
const file = event.target.files[0];
|
|
276
|
+
const reader = new FileReader();
|
|
277
|
+
|
|
278
|
+
reader.onload = (event) => {
|
|
279
|
+
const fileContent = event.target.result;
|
|
280
|
+
handleApiRequest(fileContent, true);
|
|
281
|
+
};
|
|
282
|
+
reader.readAsText(file);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// send button event handler
|
|
286
|
+
const handleSend = (input_text) => {
|
|
287
|
+
// remove any HTML tags from the input_text. Pasting text into the
|
|
288
|
+
// input box (from any source) tends to result in HTML span tags being included
|
|
289
|
+
// in the input_text. This is a problem because the Api doesn't know how to
|
|
290
|
+
// handle HTML tags. So we remove them here.
|
|
291
|
+
const sanitized_input_text = input_text.replace(/<[^>]+>/g, "");
|
|
292
|
+
|
|
293
|
+
// check if the sanitized input text is empty or only contains whitespace
|
|
294
|
+
if (!sanitized_input_text.trim()) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
handleApiRequest(sanitized_input_text, false);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Creates a fancier title for the chat app which includes
|
|
301
|
+
// fontawesome icons for validation and deployment status.
|
|
302
|
+
function AppTitle({ title, isValid, isDeployed }) {
|
|
303
|
+
return (
|
|
304
|
+
<div>
|
|
305
|
+
{title}
|
|
306
|
+
{isValid ? (
|
|
307
|
+
<FontAwesomeIcon icon={faCheckCircle} style={{ color: "green" }} />
|
|
308
|
+
) : (
|
|
309
|
+
<FontAwesomeIcon icon={faTimesCircle} style={{ color: "red" }} />
|
|
310
|
+
)}
|
|
311
|
+
{isDeployed ? (
|
|
312
|
+
<>
|
|
313
|
+
|
|
314
|
+
<FontAwesomeIcon icon={faRocket} style={{ color: "orange" }} />
|
|
315
|
+
</>
|
|
316
|
+
) : null}
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function SmarterMessage({ i, message }) {
|
|
322
|
+
let messageClassNames = "";
|
|
323
|
+
if (message.sender === "smarter") {
|
|
324
|
+
messageClassNames = "smarter-message";
|
|
325
|
+
} else if (["tool", "system"].includes(message.sender)) {
|
|
326
|
+
messageClassNames = "system-message";
|
|
327
|
+
}
|
|
328
|
+
return <Message key={i} model={message} className={messageClassNames} />;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// UI widget styles
|
|
332
|
+
// note that most styling is intended to be created in Component.css
|
|
333
|
+
// these are outlying cases where inline styles are required in order to override the default styles
|
|
334
|
+
const fullWidthStyle = {
|
|
335
|
+
width: "100%",
|
|
336
|
+
};
|
|
337
|
+
const transparentBackgroundStyle = {
|
|
338
|
+
backgroundColor: "rgba(0,0,0,0.10)",
|
|
339
|
+
color: "lightgray",
|
|
340
|
+
};
|
|
341
|
+
const mainContainerStyle = {
|
|
342
|
+
// backgroundImage:
|
|
343
|
+
// "linear-gradient(rgba(255, 255, 255, 0.95), rgba(255, 255, 255, .75)), apiUrl('" +
|
|
344
|
+
// background_image_url +
|
|
345
|
+
// "')",
|
|
346
|
+
// backgroundSize: "cover",
|
|
347
|
+
// backgroundPosition: "center",
|
|
348
|
+
width: "100%",
|
|
349
|
+
height: "100%",
|
|
350
|
+
};
|
|
351
|
+
const chatContainerStyle = {
|
|
352
|
+
...fullWidthStyle,
|
|
353
|
+
...transparentBackgroundStyle,
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// render the chat app
|
|
357
|
+
return (
|
|
358
|
+
<div id="smarter_chat_component_container" className="SmarterChat">
|
|
359
|
+
<ContainerLayout>
|
|
360
|
+
<ContentLayout>
|
|
361
|
+
<ComponentLayout>
|
|
362
|
+
<div className="chat-app">
|
|
363
|
+
<MainContainer style={mainContainerStyle}>
|
|
364
|
+
<ErrorBoundary>
|
|
365
|
+
<ErrorModal
|
|
366
|
+
isModalOpen={isModalOpen}
|
|
367
|
+
title={modalTitle}
|
|
368
|
+
message={modalMessage}
|
|
369
|
+
onCloseClick={closeChatModal}
|
|
370
|
+
/>
|
|
371
|
+
</ErrorBoundary>
|
|
372
|
+
<ChatContainer style={chatContainerStyle}>
|
|
373
|
+
<ConversationHeader>
|
|
374
|
+
<ConversationHeader.Content
|
|
375
|
+
userName={<AppTitle title={title} isValid={isValid} isDeployed={isDeployed} />}
|
|
376
|
+
info={info}
|
|
377
|
+
/>
|
|
378
|
+
<ConversationHeader.Actions>
|
|
379
|
+
<AddUserButton onClick={handleAddUserButtonClick} title="Start a new chat" />
|
|
380
|
+
{toggleMetadata && <InfoButton onClick={handleInfoButtonClick} title="Toggle system meta data" />}
|
|
381
|
+
</ConversationHeader.Actions>
|
|
382
|
+
</ConversationHeader>
|
|
383
|
+
<MessageList
|
|
384
|
+
style={transparentBackgroundStyle}
|
|
385
|
+
scrollBehavior="auto"
|
|
386
|
+
typingIndicator={isTyping ? <TypingIndicator content={assistantName + " is typing"} /> : null}
|
|
387
|
+
>
|
|
388
|
+
{messages
|
|
389
|
+
.filter((message) => message.display)
|
|
390
|
+
.map((message, i) => {
|
|
391
|
+
return <SmarterMessage i={i} message={message} />;
|
|
392
|
+
})}
|
|
393
|
+
</MessageList>
|
|
394
|
+
<MessageInput
|
|
395
|
+
placeholder={placeholderText}
|
|
396
|
+
onSend={handleSend}
|
|
397
|
+
onAttachClick={handleAttachClick}
|
|
398
|
+
attachButton={fileAttachButton}
|
|
399
|
+
fancyScroll={false}
|
|
400
|
+
/>
|
|
401
|
+
</ChatContainer>
|
|
402
|
+
<input
|
|
403
|
+
type="file"
|
|
404
|
+
accept=".py"
|
|
405
|
+
title="Select a Python file"
|
|
406
|
+
ref={fileInputRef}
|
|
407
|
+
style={{ display: "none" }}
|
|
408
|
+
onChange={handleFileChange}
|
|
409
|
+
/>
|
|
410
|
+
</MainContainer>
|
|
411
|
+
</div>
|
|
412
|
+
</ComponentLayout>
|
|
413
|
+
</ContentLayout>
|
|
414
|
+
</ContainerLayout>
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export default SmarterChat;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/*-----------------------------------------------------------------------------
|
|
2
|
+
Description: This file contains the function that makes the API request to
|
|
3
|
+
the backend. It is called exclusively from chatApp/Component.jsx
|
|
4
|
+
|
|
5
|
+
Notes:
|
|
6
|
+
- The backend API is an AWS API Gateway endpoint that is configured to
|
|
7
|
+
call an AWS Lambda function. The Lambda function is written in Python
|
|
8
|
+
and calls the OpenAI API.
|
|
9
|
+
|
|
10
|
+
- The backend API is configured to require an API key. The API key is
|
|
11
|
+
passed in the header of the request. In real terms, the api key is
|
|
12
|
+
pointless because it is exposed in the client code. However, it is
|
|
13
|
+
required by the API Gateway configuration.
|
|
14
|
+
|
|
15
|
+
- The backend API is configured to allow CORS requests from the client.
|
|
16
|
+
This is necessary because the client and backend are served from
|
|
17
|
+
different domains.
|
|
18
|
+
|
|
19
|
+
Returns: the backend API is configured to return a JSON object that substantially
|
|
20
|
+
conforms to the following structure for all 200 responses:
|
|
21
|
+
v0.1.0 - v0.4.0: ./test/events/openai.response.v0.4.0.json
|
|
22
|
+
v0.5.0: ./test/events/langchain.response.v0.5.0.json
|
|
23
|
+
-----------------------------------------------------------------------------*/
|
|
24
|
+
|
|
25
|
+
// Set to true to enable local development mode,
|
|
26
|
+
// which will simulate the server-side API calls.
|
|
27
|
+
const developerMode = false;
|
|
28
|
+
const userAgent = "SmarterChat/1.0";
|
|
29
|
+
const applicationJson = "application/json";
|
|
30
|
+
|
|
31
|
+
function getCookie(cookie, defaultValue = null) {
|
|
32
|
+
let cookieValue = null;
|
|
33
|
+
if (document.cookie && document.cookie !== "") {
|
|
34
|
+
const cookies = document.cookie.split(";");
|
|
35
|
+
for (let i = 0; i < cookies.length; i++) {
|
|
36
|
+
const thisCookie = cookies[i].trim();
|
|
37
|
+
if (thisCookie.substring(0, cookie.name.length + 1) === cookie.name + "=") {
|
|
38
|
+
cookieValue = decodeURIComponent(thisCookie.substring(cookie.name.length + 1));
|
|
39
|
+
if (developerMode) {
|
|
40
|
+
console.log("getCookie(): found ", cookieValue, "for cookie", cookie.name);
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (developerMode && !cookieValue) {
|
|
47
|
+
console.warn("getCookie(): no value found for", cookie.name);
|
|
48
|
+
}
|
|
49
|
+
return cookieValue || defaultValue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function setCookie(cookie, value) {
|
|
53
|
+
const currentPath = window.location.pathname;
|
|
54
|
+
if (value) {
|
|
55
|
+
const expirationDate = new Date();
|
|
56
|
+
expirationDate.setTime(expirationDate.getTime() + cookie.expiration);
|
|
57
|
+
const expires = expirationDate.toUTCString();
|
|
58
|
+
const cookieData = `${cookie.name}=${value}; path=${currentPath}; SameSite=Lax; expires=${expires}`;
|
|
59
|
+
document.cookie = cookieData;
|
|
60
|
+
if (developerMode) {
|
|
61
|
+
console.log(
|
|
62
|
+
"setCookie(): ",
|
|
63
|
+
cookieData,
|
|
64
|
+
"now: ",
|
|
65
|
+
new Date().toUTCString(),
|
|
66
|
+
"expiration: ",
|
|
67
|
+
expirationDate.toUTCString(),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Unset the cookie by setting its expiration date to the past
|
|
72
|
+
const expirationDate = new Date(0);
|
|
73
|
+
const expires = expirationDate.toUTCString();
|
|
74
|
+
const cookieData = `${cookie.name}=; path=${currentPath}; SameSite=Lax; expires=${expires}`;
|
|
75
|
+
document.cookie = cookieData;
|
|
76
|
+
if (developerMode) {
|
|
77
|
+
console.log("setCookie(): Unsetting cookie", cookieData);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function promptRequestBodyFactory(messages, config) {
|
|
83
|
+
const body = {
|
|
84
|
+
session_key: config.session_key,
|
|
85
|
+
messages: messages,
|
|
86
|
+
};
|
|
87
|
+
return JSON.stringify(body);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function requestHeadersFactory(cookies) {
|
|
91
|
+
console.log("requestHeadersFactory(): cookies", cookies);
|
|
92
|
+
function getRequestCookies(cookies) {
|
|
93
|
+
// Ensure that csrftoken is not included in the Cookie header.
|
|
94
|
+
const cookiesArray = document.cookie.split(";").filter((cookie) => {
|
|
95
|
+
const trimmedCookie = cookie.trim();
|
|
96
|
+
return !trimmedCookie.startsWith(`${cookies.csrfCookie.name}=`);
|
|
97
|
+
});
|
|
98
|
+
const selectedCookies = cookiesArray.join("; ");
|
|
99
|
+
return selectedCookies;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const requestCookies = getRequestCookies(cookies);
|
|
103
|
+
const csrftoken = getCookie(cookies.csrfCookie, "");
|
|
104
|
+
const authToken = null; // FIX NOTE: add me.
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
Accept: applicationJson,
|
|
108
|
+
"Content-Type": applicationJson,
|
|
109
|
+
"X-CSRFToken": csrftoken,
|
|
110
|
+
Origin: window.location.origin,
|
|
111
|
+
Cookie: requestCookies,
|
|
112
|
+
Authorization: `Bearer ${authToken}`,
|
|
113
|
+
"User-Agent": userAgent,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function requestInitFactory(headers, body) {
|
|
118
|
+
return {
|
|
119
|
+
method: "POST",
|
|
120
|
+
credentials: "include",
|
|
121
|
+
mode: "cors",
|
|
122
|
+
headers: headers,
|
|
123
|
+
body: body,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function urlFactory(apiUrl, endpoint, sessionKey) {
|
|
128
|
+
if (!apiUrl.endsWith("/")) {
|
|
129
|
+
apiUrl += "/";
|
|
130
|
+
}
|
|
131
|
+
endpoint = endpoint || "";
|
|
132
|
+
let apiConfigUrl = new URL(endpoint, apiUrl);
|
|
133
|
+
if (sessionKey) {
|
|
134
|
+
apiConfigUrl.searchParams.append("session_key", sessionKey);
|
|
135
|
+
}
|
|
136
|
+
const url = apiConfigUrl.toString();
|
|
137
|
+
return url;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function getJsonResponse(url, init, cookies) {
|
|
141
|
+
const debugMode = getCookie(cookies.debugCookie) === "true";
|
|
142
|
+
try {
|
|
143
|
+
if (debugMode || developerMode) {
|
|
144
|
+
console.log("getJsonResponse(): url: ", url, ", init: ", init, ", cookies: ", cookies);
|
|
145
|
+
}
|
|
146
|
+
const response = await fetch(url, init);
|
|
147
|
+
const contentType = response.headers.get("content-type");
|
|
148
|
+
if (contentType && contentType.includes(applicationJson)) {
|
|
149
|
+
const status = await response.status;
|
|
150
|
+
if (response.ok) {
|
|
151
|
+
const responseJson = await response.json(); // Convert the ReadableStream to a JSON object
|
|
152
|
+
const responseJsonData = await responseJson.data; // ditto
|
|
153
|
+
if (debugMode || developerMode) {
|
|
154
|
+
console.log("getJsonResponse(): response: ", responseJson);
|
|
155
|
+
}
|
|
156
|
+
return responseJsonData;
|
|
157
|
+
} else {
|
|
158
|
+
/*
|
|
159
|
+
note:
|
|
160
|
+
- the responseBody object is not available when the status is 504, because
|
|
161
|
+
these responses are generated exclusively by API Gateway.
|
|
162
|
+
- the responseBody object is potentially not available when the status is 500
|
|
163
|
+
depending on whether the 500 response was generated by the Lambda or the API Gateway
|
|
164
|
+
- the responseBody object is intended to always be available when the status is 400.
|
|
165
|
+
However, there potentially COULD be a case where the response itself contains message text.
|
|
166
|
+
*/
|
|
167
|
+
console.error("getJsonResponse(): error: ", status, response.statusText);
|
|
168
|
+
return response;
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
const errorText = await response.text();
|
|
172
|
+
throw new Error(`getJsonResponse() Unexpected response format: ${errorText}`);
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function fetchPrompt(config, messages, cookies) {
|
|
180
|
+
console.log("fetchPrompt(): config", config);
|
|
181
|
+
const apiUrl = config.meta_data.url;
|
|
182
|
+
const sessionKey = getCookie(cookies.sessionCookie, "");
|
|
183
|
+
const url = urlFactory(apiUrl, null, sessionKey);
|
|
184
|
+
const headers = requestHeadersFactory(cookies);
|
|
185
|
+
const body = promptRequestBodyFactory(messages, config);
|
|
186
|
+
const init = requestInitFactory(headers, body);
|
|
187
|
+
const responseJson = await getJsonResponse(url, init, cookies);
|
|
188
|
+
if (responseJson && responseJson.body) {
|
|
189
|
+
const responseBody = await JSON.parse(responseJson.body);
|
|
190
|
+
return responseBody;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function fetchLocalConfig(configFile) {
|
|
196
|
+
const response = await fetch("../data/" + configFile);
|
|
197
|
+
const sampleConfig = await response.json();
|
|
198
|
+
return sampleConfig.data;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function fetchConfig(apiUrl, cookies) {
|
|
202
|
+
/*
|
|
203
|
+
Fetch the chat configuration from the backend server. This is a POST request with the
|
|
204
|
+
session key as the payload. The server will return the configuration
|
|
205
|
+
as a JSON object.
|
|
206
|
+
|
|
207
|
+
See class ChatConfigView(View, AccountMixin) in
|
|
208
|
+
https://github.com/smarter-sh/smarter/blob/main/smarter/smarter/apps/chatapp/views.py
|
|
209
|
+
Things to note:
|
|
210
|
+
- The session key is used to identify the user, the chatbot,
|
|
211
|
+
and the chat history.
|
|
212
|
+
- The session key is stored in a cookie that is specific to the path. Thus,
|
|
213
|
+
each chatbot has its own session key.
|
|
214
|
+
- The CSRF token is stored in a cookie and is managed by Django.
|
|
215
|
+
- debugMode is a boolean that is also stored in a cookie, managed by Django
|
|
216
|
+
based on a Waffle switch 'reactapp_debug_mode'
|
|
217
|
+
*/
|
|
218
|
+
if (developerMode) {
|
|
219
|
+
return fetchLocalConfig("sample-config.json");
|
|
220
|
+
}
|
|
221
|
+
const sessionKey = getCookie(cookies.sessionCookie, "");
|
|
222
|
+
const headers = requestHeadersFactory(cookies);
|
|
223
|
+
const body = JSON.stringify({ session_key: sessionKey });
|
|
224
|
+
const init = requestInitFactory(headers, body);
|
|
225
|
+
const url = urlFactory(apiUrl, "config/", sessionKey);
|
|
226
|
+
const newConfig = await getJsonResponse(url, init, cookies);
|
|
227
|
+
if (newConfig) {
|
|
228
|
+
setCookie(cookies.sessionCookie, newConfig.session_key);
|
|
229
|
+
setCookie(cookies.debugCookie, newConfig.debug_mode);
|
|
230
|
+
return newConfig;
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const MessageDirectionEnum = {
|
|
2
|
+
INCOMING: "incoming",
|
|
3
|
+
OUTGOING: "outgoing",
|
|
4
|
+
};
|
|
5
|
+
export const SenderRoleEnum = {
|
|
6
|
+
SYSTEM: "system",
|
|
7
|
+
ASSISTANT: "assistant",
|
|
8
|
+
USER: "user",
|
|
9
|
+
TOOL: "tool",
|
|
10
|
+
SMARTER: "smarter",
|
|
11
|
+
};
|
|
12
|
+
export const ValidMessageRolesEnum = [
|
|
13
|
+
SenderRoleEnum.SYSTEM,
|
|
14
|
+
SenderRoleEnum.ASSISTANT,
|
|
15
|
+
SenderRoleEnum.USER,
|
|
16
|
+
SenderRoleEnum.TOOL,
|
|
17
|
+
];
|