Commit df84396d by chenway

init

parents
node_modules
<a name="readme-top"></a>
<br />
<div align="center">
<h3 align="center">Kodi front-end website</h3>
<p align="center">
Front-end Web for Kodi
<br />
<a href="https://cd.i.strikingly.com/ai-support/web"><strong>Explore the docs »</strong></a>
<br />
<br />
<a href="http://ai-support.pages.i.strikingly.com/web/">View Demo</a>
</p>
</div>
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#about-the-project">About The Project</a>
<ul>
<li><a href="#built-with">Built With</a></li>
</ul>
</li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#installation">Installation</a></li>
</ul>
</li>
<li><a href="#usage">Usage</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#acknowledgments">Acknowledgments</a></li>
</ol>
</details>
<!-- ABOUT THE PROJECT -->
## About The Project
This project is the front-end website of Kodi server. It's a website built with React. On the website, the users are able to enter question, and then click Kodi-Template or Kodi-Support to get corresponding answers. Every generated answer will be sent to our google sheet. After that, they can click "Copy Good Answer" is the answer is great, or click "Thumb down" or "Regenerate" is the answer is not good.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
### Built With
* [![React][React.js]][React-url]
* [![Bootstrap][Bootstrap.com]][Bootstrap-url]
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- GETTING STARTED -->
## Getting Started
This is an example of how you may give instructions on setting up your project locally.
To get a local copy up and running follow these simple example steps.
### Prerequisites
This is an example of how to list things you need to use the software and how to install them.
* npm
```sh
npm install npm@latest -g
```
### Installation
1. Clone the repo
```sh
git clone git@cd.i.strikingly.com:ai-support/web.git
```
2. Install NPM packages
```sh
npm install
```
3. Runs the app in the development mode. Open http://localhost:3000 to view it in your browser.
```sh
npm start
```
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- USAGE EXAMPLES -->
## Usage
### Explaination of core files
* App.js: The main file of the website. It defines all the main functionalities of this web, including entering the question, sending the question to the server by fetch, getting streaming response from the server, and send feedback of the response.
* modal.js: Include 2 modal objects. The first one EmailModal is the Modal Popup that will show when the user first opens the website. An email is needed to be submitted to use the web. The second one is the FeedbackModal. When the user click the button "Feedback", the FeedbackModal will occur where user can write their feedback and send to the target google sheet.
* .gitlab-ci.yml: The .gitlab-ci.yml file is a configuration file used by GitLab CI/CD to define the pipeline stages and jobs for building and deploying this project. It's required for this project to be deployed on GitLab pages.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- CONTACT -->
## Contact
Project Link: [https://cd.i.strikingly.com/ai-support/web](https://cd.i.strikingly.com/ai-support/web)
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- ACKNOWLEDGMENTS -->
## Acknowledgments
* [React Bootstrap](https://react-bootstrap.github.io/)
* [React](https://react.dev/)
* [Javascript](https://www.javascript.com/)
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB
[React-url]: https://reactjs.org/
[Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white
[Bootstrap-url]: https://getbootstrap.com
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "kodi",
"version": "0.1.0",
"private": true,
"homepage": "http://ai-support.pages.i.strikingly.com/web",
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-bootstrap": "^2.7.4",
"react-dom": "^18.2.0",
"react-icons": "^4.8.0",
"react-router-dom": "^6.14.2",
"react-scripts": "5.0.1",
"socket.io": "^4.5.1",
"socket.io-client": "^4.5.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css'>
<title>chat</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<!-- <div id="root"></div> -->
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css'>
<title>Kodi</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<div id="message-box" class="col s12 m8 l9 white z-depth-1"></div>
</body>
</html>
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.spacer {
margin-bottom: 150px; /* Adjust the value as needed */
}
\ No newline at end of file
import React from 'react';
import { Route, Routes } from "react-router-dom";
import Chat from './Components/Chat';
import Kodi from './Components/Kodi';
function App() {
return (
<Routes>
<Route path="/" element={<Kodi />} />
<Route path="/web" element={<Kodi />} />
<Route path="/chat" element={<Chat />} />
</Routes>
);
}
export default App;
\ No newline at end of file
import { useState } from "react";
import {
Button,
Col,
Container,
Form,
FormLabel,
Navbar,
Row,
} from "react-bootstrap";
import io from "socket.io-client";
import "../App.css";
import Chatbox from "./Chatbox";
let messageBox = document.querySelector("#message-box");
// const url = 'https://8788-103-84-218-44.ngrok-free.app'
const url = 'https://strk-ai-support.ngrok.app/'
// const url = 'http://localhost:8000'
const socket = io(url, {
extraHeaders: {
"ngrok-skip-browser-warning": "123",
},
});
// socket.emit("join", {
// username: "test",
// room: "default",
// });
socket.on("connect", (data) => {
console.log(data);
});
socket.on("send msg", function (data) {
console.log(data);
console.log(socket.id);
let msg = null;
let msgbox = document.createElement("div");
msgbox.className = "row";
msg = `
<div class="col">
<div class="tag z-depth-3">
<span class="teal-text"><b>${data.user}</b>: ${data.message}
</span>
</div>
</div>`;
msgbox.innerHTML = msg;
messageBox.appendChild(msgbox);
messageBox.scrollTop = messageBox.scrollHeight;
});
function Chat() {
const [languageSupport, setLanguageSupport] = useState("English");
const [languageUser, setLanguageUser] = useState("Simple Chinese");
const [userMessage, setUserMessage] = useState("");
const [supportMessage, setSupportMessage] = useState("");
const [supportName, setSupportName] = useState("");
const [userName, setUserName] = useState("");
const sendUserMessage = (event, user) => {
if (!supportName || !userName) {
alert("Please enter user name and support name");
return;
}
if (userMessage) {
socket.emit("send msg", {
user: "user",
message: userMessage,
languageSupport: languageSupport,
languageUser: languageUser,
});
setUserMessage("");
} else {
alert("Message cannot be empty");
}
};
const sendSupportMessage = (event, user) => {
if (!supportName || !userName) {
alert("Please enter user name and support name");
return;
}
if (supportMessage) {
socket.emit("send msg", {
user: "support",
message: supportMessage,
languageSupport: languageSupport,
languageUser: languageUser,
});
setSupportMessage("");
} else {
alert("Message cannot be empty");
}
};
const setName = () => {
console.log(supportName);
console.log(userName);
if (supportName && userName) {
socket.emit("set room", {
support_name: supportName,
user_name: userName,
});
socket.emit("join", {
username: supportName,
room: supportName + '-' + userName,
});
}
}
return (
<Container>
<Navbar bg="dark" variant="dark">
<Navbar.Brand href="#home">Chat Room</Navbar.Brand>
<Navbar.Collapse className="justify-content-end"></Navbar.Collapse>
</Navbar>
<Row>
<Col>
<FormLabel>user name</FormLabel>
<Form.Control
type="text"
value={userName}
onChange={(e) => setUserName(e.target.value)}
placeholder="Enter user name"
/>
</Col>
<Col>
<FormLabel>support name</FormLabel>
<Form.Control
type="text"
value={supportName}
onChange={(e) => setSupportName(e.target.value)}
placeholder="Enter support name"
/>
</Col>
<Button onClick={(e) => setName()}>Submit(You should set user name and support name first!)</Button>
</Row>
<Row>
<Col>
<FormLabel>user</FormLabel>
<Form.Control
as="select"
onChange={(e) => setLanguageUser(e.target.value)}
>
<option value="Simplified Chinese">Simplified Chinese</option>
<option value="Traditional Chinese">Traditional Chinese</option>
<option value="Italiano">Italiano</option>
<option value="German">German</option>
<option value="Japanese">Japanese</option>
<option value="French">French</option>
<option value="Arabic">Arabic</option>
<option value="Spanish">Spanish</option>
<option value="Dutch">Dutch</option>
<option value="Portuguese">Portuguese</option>
<option value="Finnish">Finnish</option>
<option value="Norwegian">Norwegian</option>
<option value="Swedish">Swedish</option>
<option value="Czech">Czech</option>
<option value="Romanian">Romanian</option>
<option value="Indonesian">Indonesian</option>
<option value="Polish">Polish</option>
<option value="Vietnamese">Vietnamese</option>
<option value="Korean">Korean</option>
</Form.Control>
<Form.Control
type="text"
value={userMessage}
onChange={(e) => setUserMessage(e.target.value)}
placeholder="Enter message"
/>
<Button onClick={(e) => sendUserMessage(e, "user")}>Send</Button>
</Col>
<Col>
<FormLabel>support</FormLabel>
<Form.Control
as="select"
onChange={(e) => setLanguageSupport(e.target.value)}
>
<option value="English">English</option>
<option value="Simplified Chinese">Simplified Chinese</option>
{/* add other languages here */}
</Form.Control>
<Form.Control
type="text"
value={supportMessage}
onChange={(e) => setSupportMessage(e.target.value)}
placeholder="Enter message"
/>
<Button onClick={(e) => sendSupportMessage(e, "support")}>
Send
</Button>
</Col>
</Row>
<Chatbox />
</Container>
);
}
export default Chat;
import React from "react";
// import ChatFeed from "react-chat-ui";/
import { ChatFeed, ChatBubble, BubbleGroup, Message } from "react-chat-ui";
const styles = {
button: {
backgroundColor: "#fff",
borderColor: "#1D2129",
borderStyle: "solid",
borderRadius: 20,
borderWidth: 2,
color: "#1D2129",
fontSize: 18,
fontWeight: "300",
paddingTop: 8,
paddingBottom: 8,
paddingLeft: 16,
paddingRight: 16,
outline: "none"
},
selected: {
color: "#fff",
backgroundColor: "#0084FF",
borderColor: "#0084FF"
}
};
const users = {
0: "You",
Mark: "Mark",
2: "Evan"
};
const customBubble = props => (
<div>
<p>{`${props.message.senderName} ${props.message.id ? "says" : "said"}: ${
props.message.message
}`}</p>
</div>
);
class Chat extends React.Component {
constructor() {
super();
this.state = {
messages: [
new Message({ id: "Mary", message: "Hey guys!", senderName: "Mary" }),
new Message({
id: 2,
message: (
<p>
<span>11:50:</span>Hey! Eve here. react-chat-ui is pretty dooope.
</p>
),
senderName: "Eve"
})
],
useCustomBubble: false,
curr_user: 0
};
}
onPress(user) {
this.setState({ curr_user: user });
}
onMessageSubmit(e) {
const input = this.message;
e.preventDefault();
if (!input.value) {
return false;
}
this.pushMessage(this.state.curr_user, input.value);
input.value = "";
return true;
}
pushMessage(recipient, message) {
const prevState = this.state;
const newMessage = new Message({
id: recipient,
message,
senderName: users[recipient]
});
prevState.messages.push(newMessage);
this.setState(this.state);
}
render() {
return (
<div className="container">
<div className="chatfeed-wrapper">
<ChatFeed
chatBubble={this.state.useCustomBubble && customBubble}
maxHeight={250}
messages={this.state.messages} // Boolean: list of message objects
showSenderName
/>
<form onSubmit={e => this.onMessageSubmit(e)}>
<input
ref={m => {
this.message = m;
}}
placeholder="Type a message..."
className="message-input"
/>
</form>
<div style={{ display: "flex", justifyContent: "space-around" }}>
<button
style={{
...styles.button,
...(this.state.curr_user === 0 ? styles.selected : {})
}}
onClick={() => this.onPress(0)}
>
You
</button>
<button
style={{
...styles.button,
...(this.state.curr_user === "Mary" ? styles.selected : {})
}}
onClick={() => this.onPress("Mary")}
>
Mary
</button>
<button
style={{
...styles.button,
...(this.state.curr_user === 2 ? styles.selected : {})
}}
onClick={() => this.onPress(2)}
>
Eve
</button>
</div>
<div
style={{ display: "flex", justifyContent: "center", marginTop: 10 }}
>
<button
style={{
...styles.button,
...(this.state.useCustomBubble ? styles.selected : {})
}}
onClick={() =>
this.setState({ useCustomBubble: !this.state.useCustomBubble })
}
>
Custom Bubbles
</button>
</div>
</div>
</div>
);
}
}
export default Chatbox;
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.spacer {
margin-bottom: 150px; /* Adjust the value as needed */
}
\ No newline at end of file
import React, { useState } from "react";
import Col from "react-bootstrap/Col";
import Row from "react-bootstrap/Row";
import Toast from "react-bootstrap/Toast";
import "../App.css";
import { EmailPopup, FeedbackModal } from "./Modal";
function Kodi() {
const [aiSupportConversation, setAiSupportConversation] = useState("");
const [aiSupportResult, setAiSupportResult] = useState("");
const [originalResult, setOriginalResult] = useState("");
const [sourceArea, setSourceArea] = useState("");
const [buttonStatus, setButtonStatus] = useState("default");
// 1.default:Make the button enabled for all
// 2.generating:Disable the button during the process
// 3.needRating:Disable functional buttons before rating
const [webLanguage, setWebLanguage] = useState("en-us");
const [sourceFrom, setSourceFrom] = useState("Strikingly");
const [responseSubmitted, setResponseSubmitted] = useState(false);
const [helpType, setHelpType] = useState("");
const [isCancelled, setIsCancelled] = useState(false);
const [controller, setController] = useState(null);
const [copyToast, setCopyToast] = useState(false);
const [thumbDowntoast, setThumbDownToast] = useState(false);
const [showFeedbackModal, setShowFeedbackModal] = useState(false); //Control Pop-up Display
const [url, setUrl] = useState("https://strk-ai-support.ngrok.app");
// const [url, setUrl] = useState('https://8ac5a21b602f.ngrok.app')
// const [action, setAction] = useState('Generate')
const clearMessage = () => {
setAiSupportConversation("");
setAiSupportResult("");
setSourceArea("");
setButtonStatus("default"); //Make the button enabled for all
};
const copyMessage = () => {
const question = aiSupportConversation;
let answer = originalResult;
if (!question || !answer) {
setAiSupportResult(
"Please use this button only when you have question and answer in the text area"
);
setButtonStatus("default");
} else {
if (answer.startsWith("<br><br>")) {
answer = answer.replace("<br><br>", "");
}
// navigator.clipboard.writeText(answer);
unsecuredCopyToClipboard(answer);
setCopyToast(true);
// After copy message to clipboard, we send all the required data to our server
if (responseSubmitted) {
// If the response is submitted, we do not send the response to the server again
setButtonStatus("default");
return;
}
// const sendMessage = (question, answer, type, range, helpAction=null, reasonForEscalation=null)
sendMessage(question, answer, helpType, "Good");
sendMessage(question, answer, helpType, "All", "Copy", null);
setButtonStatus("default");
}
};
const thumbDown = () => {
setButtonStatus("generating"); //Disable the button during the process
const question = aiSupportConversation;
let answer = originalResult;
if (!question || !answer) {
setAiSupportResult(
"Please use this button only when you have question and answer in the text area"
);
setButtonStatus("default");
} else {
if (answer.startsWith("<br><br>")) {
answer = answer.replace("<br><br>", "");
}
if (responseSubmitted) {
// If the response is submitted, we do not send the response to the server again
setButtonStatus("default");
return;
}
setResponseSubmitted(true);
// const sendMessage = (question, answer, type, range, helpAction=null, reasonForEscalation=null)
sendMessage(question, answer, helpType, "Bad", null, "Thumb Down");
sendMessage(question, answer, helpType, "All", "Thumb Down", null);
setThumbDownToast(true);
setTimeout(() => {
setShowFeedbackModal(true);
}, 1000);
setButtonStatus("default");
}
};
function unsecuredCopyToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand("copy");
} catch (err) {
console.error("Unable to copy to clipboard", err);
}
document.body.removeChild(textArea);
}
const regenerateResponse = () => {
const question = aiSupportConversation;
let answer = originalResult;
if (!question || !answer) {
setAiSupportResult(
"Please use this button only when you have question and answer in the text area"
);
setButtonStatus("default");
} else {
setButtonStatus("generating"); //Disable the button during the process
console.log("inside the regenerate, buttonStatus:", buttonStatus);
const previousCommand = helpType;
if (previousCommand === "KB") {
articleHelp("Regenerate");
} else if (previousCommand === "Template") {
templateHelp("Regenerate");
}
if (answer.startsWith("<br><br>")) {
answer = answer.replace("<br><br>", "");
}
if (responseSubmitted) {
// If the response is submitted, we do not send the response to the server again
// setIsDisabled(false);
return;
} else {
// const sendMessage = (question, answer, type, range, helpAction=null, reasonForEscalation=null)
sendMessage(
question,
answer,
previousCommand,
"Bad",
null,
"Regenerate"
);
sendMessage(
question,
answer,
previousCommand,
"All",
"Regenerate",
null
);
setButtonStatus("generating"); //Disable the button during the process
}
// setIsDisabled(true);
setButtonStatus("default");
}
};
const cancelStreaming = () => {
setIsCancelled(true);
if (controller) {
controller.abort();
}
setButtonStatus("default");
};
const sendMessage = (
question,
answer,
type,
range,
helpAction = null,
reasonForEscalation = null
) => {
const storedEmail = localStorage.getItem("email");
const timestamp = new Date();
const options = { timeZone: "Asia/Shanghai" };
const date = timestamp.toLocaleDateString("en-US", options); // Get date in format mm/dd/yy
const time = timestamp.toLocaleTimeString("en-US", options); // Get time in format hh:mm:ss
const dateTime = `${date} ${time} UTC+8`; // Combine date and time into one string
const feedbackData = {
email: storedEmail,
time: dateTime,
type: type,
language: webLanguage,
question: question,
answer: answer,
range: range,
reasonForEscalation: reasonForEscalation,
action: helpAction,
};
console.log(feedbackData);
const apiUrl = `${url}/v1/ai-support/feedbacks`;
fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(feedbackData),
})
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw "some error text";
}
})
.then((data) => {
console.log(data);
if (range !== "All" && helpAction !== "Regenerate") {
setResponseSubmitted(true);
}
})
.catch((error) => {
setButtonStatus("default");
setAiSupportResult(
"Error. Try again later. If the error persists, please contact the developer."
);
console.error("Request failed:", error);
});
};
const articleHelp = async (helpAction = "Generate") => {
setIsCancelled(false);
setHelpType("KB");
const abortController = new AbortController();
setController(abortController); // create a new abort controller
setButtonStatus("generating"); //Disable the button during the process
console.log(webLanguage);
setSourceArea("");
const chat = aiSupportConversation;
if (!chat) {
setAiSupportResult(
"Please provide question in the Message box on the left"
);
setButtonStatus("default");
} else {
// setIsStreaming(true)
setAiSupportResult("");
console.log(webLanguage);
const data = {
message: chat,
language: webLanguage,
source: sourceFrom,
};
try {
const apiUrl = `${url}/v1/ai-support/article-support`;
const response = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
signal: abortController.signal,
timeout: 20000,
});
if (!response.ok) {
throw await response.json(); // Throw the error response from the server
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let message = "";
let htmlMessage = "";
let responseText = "";
let plainResponse = "";
while (true) {
const { done, value } = await reader.read();
if (done || isCancelled) break;
message = decoder.decode(value, { stream: true });
if (message.includes("Source: (Internal-only)")) {
let source = message;
setSourceArea(source);
} else {
plainResponse += message;
htmlMessage = message.replace(/\n/g, "<br>");
responseText += htmlMessage;
setAiSupportResult(responseText);
}
}
setOriginalResult(plainResponse);
// const sendMessage = (question, answer, type, range, helpAction=null, reasonForEscalation=null)
sendMessage(chat, plainResponse, "KB", "All", helpAction, null);
setResponseSubmitted(false);
setButtonStatus("needRating"); //Disable functional buttons before rating
} catch (error) {
setButtonStatus("default");
setIsCancelled(false);
if (error.name !== "AbortError") {
setAiSupportResult(
"Error. Try again later. If the error persists, please contact the developer."
);
console.error("Request failed:", error.error);
sendMessage(chat, error.error, "KB", "All", helpAction, null);
}
}
}
};
function renderSourceArea() {
const urlRegex = /(https?:\/\/[^\s]+)/g;
const urls = sourceArea.match(urlRegex);
let replacedSourceArea = sourceArea;
if (urls) {
urls.forEach((url) => {
const urlHtml = `<a href="${url}" target="_blank">${url}</a>`;
replacedSourceArea = replacedSourceArea.replace(url, urlHtml);
});
}
replacedSourceArea = replacedSourceArea.replace(/\n/g, "<br>");
return replacedSourceArea;
}
const templateHelp = async (helpAction = "Generate") => {
setIsCancelled(false);
setHelpType("Template");
const abortController = new AbortController();
setController(abortController); // create a new abort controller
setButtonStatus("generating");
setSourceArea("");
const chat = aiSupportConversation;
if (!chat) {
console.log("No chat");
setAiSupportResult(
"Please provide question in the Message box on the left"
);
setButtonStatus("default");
} else {
console.log(chat);
// setIsStreaming(true)
setAiSupportResult("");
const data = {
message: chat,
language: webLanguage,
source: sourceFrom,
};
try {
const apiUrl = `${url}/v1/ai-support/template-recommendation`;
const response = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
signal: abortController.signal,
timeout: 20000,
});
if (!response.ok) {
throw await response.json(); // Throw the error response from the server
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let message = "";
let htmlMessage = "";
let responseText = "";
let plainResponse = "";
while (true) {
const { done, value } = await reader.read();
if (done || isCancelled) break;
message = decoder.decode(value, { stream: true });
// if (message.trim() !== "Template")
// {
// plainResponse += message
// }
plainResponse += message;
htmlMessage = message.replace(/\n/g, "<br>");
// if (htmlMessage.trim() !== "Template")
// {
// responseText += htmlMessage
// }
responseText += htmlMessage;
setAiSupportResult(responseText);
}
setOriginalResult(plainResponse);
const regex = /(https?:\/\/[^\s\]]+\.[^\s\]]+)(?=[\n<\]])/g;
let modifiedText = plainResponse.replace(
regex,
'<a href="$1" target="_blank">$1</a>'
);
modifiedText = modifiedText.replace(/\n/g, "<br>");
setAiSupportResult(modifiedText);
// const sendMessage = (question, answer, type, range, helpAction=null, reasonForEscalation=null)
sendMessage(chat, plainResponse, "Template", "All", helpAction, null);
setResponseSubmitted(false);
setButtonStatus("needRating"); //Disable functional buttons before rating
setSourceArea("");
} catch (error) {
setIsCancelled(false);
setButtonStatus("default");
if (error.name !== "AbortError") {
setAiSupportResult(
"Error. Try again later. If the error persists, please contact the developer."
);
console.error("Request failed:", error.error);
sendMessage(chat, error.error, "Template", "All", helpAction, null);
}
}
}
};
const handleDivClick = (event) => {
if (event.target.tagName === "A") {
// handle hyperlink click event
window.open(event.target.href);
event.preventDefault();
}
};
const handleLanguageChange = (event) => {
console.log("function called");
console.log(event.target.value);
const language = event.target.value;
setWebLanguage(language);
};
const handleSourceSelect = (event) => {
console.log("function called");
console.log(event.target.value);
const template = event.target.value;
setSourceFrom(template);
};
function AutohideCopyToast() {
return (
<Col>
<Col xs={12} className="mb-2">
<button
id="copyButton"
className="btn btn-success"
onClick={copyMessage}
disabled={buttonStatus === "generating"}
>
Copy Good Response
</button>
</Col>
<Col xs={12}>
<Toast
onClose={() => setCopyToast(false)}
show={copyToast}
delay={1500}
autohide
>
<Toast.Body>Response copied successfully</Toast.Body>
</Toast>
</Col>
</Col>
);
}
function AutohideThumbDownToast() {
return (
<Row>
<Col xs={12} className="mb-2">
<button
id="thumbDown"
className="btn btn-warning mt-1"
onClick={thumbDown}
disabled={buttonStatus === "generating"}
>
<span class="bi bi-hand-thumbs-down"></span> Thumb Down
</button>
</Col>
<Col xs={12}>
<Toast
onClose={() => setThumbDownToast(false)}
show={thumbDowntoast}
delay={500}
autohide
>
<Toast.Body>Thumb Down feedback sent</Toast.Body>
</Toast>
</Col>
<FeedbackModal
showFeedbackModal={showFeedbackModal}
onClose={() => setShowFeedbackModal(false)}
/>
</Row>
);
}
const handleMouseDown = (event) => {
event.preventDefault();
};
const handleSelectStart = (event) => {
event.preventDefault();
};
const handleDragStart = (event) => {
event.preventDefault();
};
return (
<div className="container-fluid App" style={{ marginTop: "10px" }}>
<div className="col">
<div className="row-md-2">
<div className="row">
<div className="col-md-3">
<div className="float-right" style={{ marginTop: "10px" }}>
<div className="form-floating">
<select
className="form-control"
id="webLanguageSelect"
defaultValue="en-us"
onChange={handleLanguageChange}
>
<option value="en-us">English (US)</option>
<option value="ar">العربية</option>
<option value="cs">Čeština</option>
<option value="de-de">Deutsch (Deutschland)</option>
<option value="es">Español</option>
<option value="fi">Suomi</option>
<option value="fr">Français</option>
<option value="id">Bahasa Indonesia</option>
<option value="it">Italiano</option>
<option value="ja">日本語</option>
<option value="nl-nl">Nederlands (Nederland)</option>
<option value="no">Norsk</option>
<option value="pl">Polski</option>
<option value="pt-pt">Português (Portugal)</option>
<option value="ro">Română</option>
<option value="sk">Slovenčina</option>
<option value="sv">svenska</option>
<option value="vi">Tiếng Vit</option>
<option value="zh-cn">简体中文</option>
<option value="zh-tw">繁體中文</option>
</select>
<label htmlFor="webLanguageSelect">
Web Language (For KB sources)
</label>
</div>
</div>
</div>
<div className="col-md-2">
<div className="float-right" style={{ marginTop: "10px" }}>
<div className="form-floating">
<select
className="form-control"
id="SourceSelect"
defaultValue="Strikingly"
onChange={handleSourceSelect}
>
<option value="Strikingly">Strikingly.com</option>
<option value="sxl">sxl.cn</option>
</select>
<label htmlFor="SourceSelect">Sources From</label>
</div>
</div>
</div>
<div
className="col-md-6"
style={{ position: "fixed", top: "0", right: "0" }}
>
<button
className="btn btn-warning"
onClick={() => setShowFeedbackModal(true)}
>
Enter Feedback
</button>
<FeedbackModal
showFeedbackModal={showFeedbackModal}
onClose={() => setShowFeedbackModal(false)}
/>
</div>
<div
className="col-md-2"
style={{ position: "fixed", top: "0", right: "0" }}
>
<EmailPopup />
</div>
</div>
</div>
<div className="row-md-10">
<div
class="container-fluid"
className="main"
style={{ marginTop: "10px" }}
>
<div class="row">
<div class="col-md-5">
<div class="form-floating">
<textarea
class="form-control"
id="aiSupportConversation"
value={aiSupportConversation}
onChange={(e) => setAiSupportConversation(e.target.value)}
style={{ height: "600px", textAlign: "left" }}
></textarea>
<label for="aiSupportConversation">Message</label>
</div>
</div>
<div class="col-md-2">
<div class="mb-4">
<button
className="btn btn-primary"
onClick={() => templateHelp("Generate")}
disabled={
buttonStatus === "generating" ||
buttonStatus === "needRating"
}
>
Kodi - Template
</button>
</div>
<div class="mb-4">
<button
id="aiArticleButton"
className="btn btn-primary"
onClick={() => articleHelp("Generate")}
disabled={
buttonStatus === "generating" ||
buttonStatus === "needRating"
}
>
Kodi - Support
</button>
</div>
<div class="mb-4">
<button
id="clearMessageButton"
class="btn btn-primary"
onClick={clearMessage}
disabled={
buttonStatus === "generating" ||
buttonStatus === "needRating"
}
>
Clear Message
</button>
</div>
<div class="mb-3">
<button
id="cancelButton"
className="btn btn-secondary"
onClick={cancelStreaming}
disabled={buttonStatus !== "generating"}
>
{" "}
Cancel{" "}
</button>
</div>
<div class="mb-3">
<div class="spacer"></div>
</div>
<div class="mb-4">
<AutohideCopyToast />
</div>
<div class="mb-4">
<button
id="regenerateButton"
className="btn btn-warning"
onClick={regenerateResponse}
disabled={buttonStatus === "generating"}
>
Regenerate
</button>
</div>
<div class="mb-1">
<AutohideThumbDownToast />
</div>
</div>
<div class="col-md-5">
<div class="form-floating">
<div
class="form-control"
contentEditable="true"
id="aiSupportResult"
style={{
height: "auto",
minHeight: "600px",
textAlign: "left",
}}
dangerouslySetInnerHTML={{ __html: aiSupportResult }}
onClick={handleDivClick}
onMouseDown={handleMouseDown}
onSelectStart={handleSelectStart}
onDragStart={handleDragStart}
></div>
<label for="aiSupportResult">Response</label>
</div>
<div
id="sourceArea"
style={{ textAlign: "left" }}
dangerouslySetInnerHTML={{ __html: renderSourceArea() }}
></div>
{/* <div class="mb-1">
<button id="regenerateButton" className="btn btn-warning" onClick={regenerateResponse} disabled={isDisabled} >Regenerate</button>
</div> */}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default Kodi;
import React, { useState, useEffect } from 'react';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import Modal from 'react-bootstrap/Modal';
import Col from 'react-bootstrap/Col';
import Row from 'react-bootstrap/Row';
import Toast from 'react-bootstrap/Toast';
export function EmailPopup() {
const [show, setShow] = useState(false);
const storedEmail = localStorage.getItem('email')
useEffect(() => {
if (storedEmail === null) {
setShow(true);
}
}, [storedEmail]);
const [email, setEmail] = useState(storedEmail);
const [validEmail, setValidEmail] = useState(false);
// const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const handleEmailChange = (e) => {
const email = e.target.value;
setValidEmail(/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(email));
setEmail(email);
};
const handleSubmit = (e) => {
e.preventDefault();
const storedEmail = localStorage.getItem('email')
if (validEmail) {
localStorage.setItem('email', email);
console.log(storedEmail)
setShow(false);
}
};
return (
<>
<Button variant="dark" onClick={handleShow}>
{email}
</Button>
{/* <Modal show={show} onHide={handleClose}> */}
{/* No onHide so that the user cannot close the popup without submitting an email */}
<Modal show={show}>
<Modal.Header>
<Modal.Title>Please Enter Email</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form>
<Form.Group className="mb-3" controlId="emailAddress">
<Form.Label>Email address</Form.Label>
<Form.Control
type="email"
placeholder="name@example.com"
onChange={handleEmailChange}
autoFocus
/>
</Form.Group>
</Form>
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={handleSubmit}>
Submit
</Button>
</Modal.Footer>
</Modal>
</>
);
}
export function FeedbackModal({showFeedbackModal, onClose}) {
const [show, setShow] = useState(showFeedbackModal);
const [feedback, setFeedback] = useState('');
const [submitToast, setSubmitToast] = useState(false);
const handleClose = () => {
setShow(false);
onClose();
};
const handleClear = () => setFeedback('')
const handleFeedbackChange = (event) => {
setFeedback(event.target.value);
}
const handleSubmit = () => {
// do something with the feedback
if (feedback === '') {
setFeedback("Please do not send empty feedback. Please click 'Clear' and write you feedback");
return
}
const storedEmail = localStorage.getItem('email')
const timestamp = new Date();
const options = { timeZone: 'Asia/Shanghai' };
const date = timestamp.toLocaleDateString('en-US', options); // Get date in format mm/dd/yy
const time = timestamp.toLocaleTimeString('en-US', options); // Get time in format hh:mm:ss
const dateTime = `${date} ${time} UTC+8`; // Combine date and time into one string
const feedbackData = {
email: storedEmail,
time: dateTime,
feedback:feedback,
range: "Feedback"
};
console.log(feedbackData)
fetch("https://strk-ai-support.ngrok.app/v1/ai-support/feedbacks", {
// fetch("https://13313306f1aa.ngrok.app/v1/ai-support/feedbacks", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(feedbackData)
})
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw "some error text";
}
})
.then((data) => {
console.log(data)
setSubmitToast(true)
})
.catch((error) => {
setFeedback("Error. Try again later. If the error persists, please contact the developer.");
console.error("Request failed:", error);
})
// handleClose();
}
function AutohideSubmitToast() {
return (
<Row>
<Col xs={12} className="mb-2">
<Button variant="secondary" onClick={handleClear} className="mr-2">
Clear
</Button>
<Button variant="primary" onClick={handleSubmit} className="ms-2">
Submit
</Button>
</Col>
<Col xs={12}>
<Toast onClose={() => setSubmitToast(false)} show={submitToast} delay={1500} autohide>
<Toast.Body>Feedback sent successfully</Toast.Body>
</Toast>
</Col>
</Row>
);
}
return (
<>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Feedback</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form>
<Form.Group
className="mb-3"
controlId="exampleForm.ControlTextarea1"
>
<Form.Label>Note: The feedback will be sent directly to Kodi devs.</Form.Label>
<Form.Control as="textarea" rows={8} value={feedback} onChange={handleFeedbackChange}/>
</Form.Group>
</Form>
</Modal.Body>
<Modal.Footer className="d-flex justify-content-start align-items-end">
<AutohideSubmitToast/>
</Modal.Footer>
</Modal>
</>
);
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
#message-box {
float: left;
width: 90%;
margin-right: 5%;
margin-left: 5%;
}
\ No newline at end of file
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from "react-router-dom";
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment