diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/AppServerBase.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/AppServerBase.kt
index 8a52cc2d..1f2362f5 100644
--- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/AppServerBase.kt
+++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/AppServerBase.kt
@@ -1,15 +1,16 @@
package com.simiacryptus.skyenet.servers
+import com.google.api.services.oauth2.model.Userinfo
import com.simiacryptus.openai.OpenAIClient
import com.simiacryptus.skyenet.OutputInterceptor
-import com.simiacryptus.skyenet.util.AwsUtil.decryptResource
import com.simiacryptus.skyenet.servlet.AuthenticatedWebsite
import com.simiacryptus.skyenet.servlet.UsageServlet
-import com.simiacryptus.skyenet.sessions.ApplicationBase
import com.simiacryptus.skyenet.servlet.UserInfoServlet
import com.simiacryptus.skyenet.servlet.UserSettingsServlet
+import com.simiacryptus.skyenet.sessions.ApplicationBase
import com.simiacryptus.skyenet.sessions.WebSocketServer
+import com.simiacryptus.skyenet.util.AwsUtil.decryptResource
import jakarta.servlet.DispatcherType
import jakarta.servlet.Servlet
import jakarta.servlet.http.HttpServlet
@@ -30,6 +31,7 @@ import java.util.*
abstract class AppServerBase(
private val localName: String = "localhost",
+ private val publicName: String = "localhost",
private val port: Int = 8081,
) {
var domainName: String = ""
@@ -38,11 +40,12 @@ abstract class AppServerBase(
data class ChildWebApp(
val path: String,
val server: WebSocketServer,
- val isAuthenticated: Boolean = false
+ val isAuthenticated: Boolean = false,
+ val isPublicOnly: Boolean = false
)
private fun domainName(isServer: Boolean) =
- if (isServer) "https://apps.simiacrypt.us" else "http://$localName:$port"
+ if (isServer) "https://$publicName" else "http://$localName:$port"
val welcomeResources = Resource.newResource(javaClass.classLoader.getResource("welcome"))
val userInfoServlet = UserInfoServlet()
@@ -103,17 +106,18 @@ abstract class AppServerBase(
inner class WelcomeServlet() : HttpServlet() {
override fun doGet(req: HttpServletRequest?, resp: HttpServletResponse?) {
- val requestURI = req?.requestURI ?: "/"
+ val user = AuthenticatedWebsite.getUser(req!!)
+ val requestURI = req.requestURI ?: "/"
resp?.contentType = when (requestURI) {
"/" -> "text/html"
else -> ApplicationBase.getMimeType(requestURI)
}
when {
- requestURI == "/" -> resp?.writer?.write(homepage().trimIndent())
- requestURI == "/index.html" -> resp?.writer?.write(homepage().trimIndent())
- requestURI.startsWith("/userInfo") -> userInfoServlet.doGet(req!!, resp!!)
- requestURI.startsWith("/userSettings") -> userSettingsServlet.doGet(req!!, resp!!)
- requestURI.startsWith("/usage") -> usageServlet.doGet(req!!, resp!!)
+ requestURI == "/" -> resp?.writer?.write(homepage(user).trimIndent())
+ requestURI == "/index.html" -> resp?.writer?.write(homepage(user).trimIndent())
+ requestURI.startsWith("/userInfo") -> userInfoServlet.doGet(req, resp!!)
+ requestURI.startsWith("/userSettings") -> userSettingsServlet.doGet(req, resp!!)
+ requestURI.startsWith("/usage") -> usageServlet.doGet(req, resp!!)
else -> try {
val inputStream = welcomeResources.addPath(requestURI)?.inputStream
inputStream?.copyTo(resp?.outputStream!!)
@@ -132,36 +136,41 @@ abstract class AppServerBase(
}
}
- @Language("HTML")
- private fun homepage() = """
-
-
-
- SimiaCryptus Skyenet Apps
-
-
-
-
-
-
-
-
-
-
-
-
- ${
- childWebApps.joinToString("
") {
- """
${it.server.applicationName}"""
+ private fun homepage(user: Userinfo?): String {
+ @Language("HTML")
+ val html = """
+
+
+
+
SimiaCryptus Skyenet Apps
+
+
+
+
+
+
+
+
+
+
+
+
+ ${
+ childWebApps.filter {
+ !it.isAuthenticated || (user != null && !it.isPublicOnly)
+ }.joinToString("
") {
+ """
${it.server.applicationName}"""
+ }
}
+
+
+
+
+ """
+ return html
}
-
-
-
-
- """
private fun start(
port: Int,
diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ReadOnlyApp.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ReadOnlyApp.kt
index 415da5e4..09898679 100644
--- a/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ReadOnlyApp.kt
+++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/servers/ReadOnlyApp.kt
@@ -1,6 +1,5 @@
package com.simiacryptus.skyenet.servers
-import com.simiacryptus.openai.OpenAIClient
import com.simiacryptus.skyenet.sessions.*
import org.slf4j.LoggerFactory
@@ -8,11 +7,11 @@ open class ReadOnlyApp(
applicationName: String,
temperature: Double = 0.3,
oauthConfig: String? = null,
- val api: OpenAIClient,
) : ApplicationBase(
applicationName = applicationName,
oauthConfig = oauthConfig,
temperature = temperature,
+ resourceBase = "readOnly",
) {
companion object {
@@ -20,12 +19,7 @@ open class ReadOnlyApp(
}
override fun newSession(sessionId: String): SessionInterface {
- return BasicChatSession(
- parent = this@ReadOnlyApp,
- model = OpenAIClient.Models.GPT35Turbo,
- sessionId = sessionId,
- api = api
- )
+ throw UnsupportedOperationException()
}
diff --git a/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionDataStorage.kt b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionDataStorage.kt
index 1b8fc0c2..432619a2 100644
--- a/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionDataStorage.kt
+++ b/webui/src/main/kotlin/com/simiacryptus/skyenet/sessions/SessionDataStorage.kt
@@ -39,7 +39,7 @@ open class SessionDataStorage(
val files = dataDir.listFiles()?.flatMap { it.listFiles()?.toList() ?: listOf() }?.filter { sessionDir ->
val operationDir = File(sessionDir, "messages")
if (!operationDir.exists()) false else {
- val listFiles = operationDir.listFiles()
+ val listFiles = operationDir.listFiles().filter { it.isFile && !it.name.startsWith("aaa") }
(listFiles?.size ?: 0) > 0
}
}
diff --git a/webui/src/main/resources/readOnly/chat.css b/webui/src/main/resources/readOnly/chat.css
new file mode 100644
index 00000000..02cac400
--- /dev/null
+++ b/webui/src/main/resources/readOnly/chat.css
@@ -0,0 +1,228 @@
+#messages {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 40px;
+ overflow-y: auto;
+ padding: 10px;
+ padding-top: 40px;
+ flex-grow: 1;
+ flex-shrink: 1;
+}
+
+.message {
+ background-color: #f0f0f0;
+ border-radius: 5px;
+ padding: 10px;
+ margin-bottom: 10px;
+ overflow: scroll;
+ z-index: 100;
+}
+
+.reply-form {
+ width: 100%;
+}
+
+.chat-input {
+ resize: vertical;
+}
+
+.reply-input {
+ resize: vertical;
+}
+
+.href-link {
+ text-decoration: underline;
+ color: blue;
+}
+
+#message {
+ flex-grow: 1;
+ margin-right: 5px;
+}
+
+.spinner-border {
+ display: block;
+ width: 40px;
+ height: 40px;
+ border: 4px solid rgba(0, 0, 0, 0.1);
+ border-left-color: #007bff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+#toolbar {
+ background-color: #f1f1f1;
+ padding: 5px;
+ position: fixed;
+ top: 0;
+ width: 100%;
+ horiz-align: left;
+ z-index: 1;
+}
+
+#toolbar a {
+ color: #000;
+ text-decoration: none;
+ padding: 5px;
+}
+
+#toolbar a:hover {
+ background-color: #ddd;
+}
+
+
+#namebar {
+ background-color: #f1f1f1;
+ padding: 5px;
+ position: fixed;
+ top: 0;
+ right: 0;
+ horiz-align: right;
+ z-index: 2;
+}
+
+#namebar a {
+ color: #000;
+ text-decoration: none;
+ padding: 5px;
+}
+
+#namebar a:hover {
+ background-color: #ddd;
+}
+
+/* Modal */
+.modal {
+ display: none;
+ position: fixed;
+ z-index: 1000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(0, 0, 0, 0.4);
+}
+
+.modal-content {
+ background-color: #fefefe;
+ margin: 15% auto;
+ padding: 20px;
+ border: 1px solid #888;
+ width: 80%;
+ position: relative;
+}
+
+.close {
+ color: #aaa;
+ float: right;
+ font-size: 28px;
+ font-weight: bold;
+ cursor: pointer;
+}
+
+.close:hover,
+.close:focus {
+ color: #000;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+pre {
+ white-space: pre-wrap; /* Since CSS modules do not automatically apply this */
+}
+
+#container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh; /* Adjust this value based on your preferred container height */
+}
+
+.play-button {
+ font-size: 48px;
+ font-weight: bold;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ transition: transform 0.1s;
+}
+
+.play-button:focus {
+ outline: none;
+}
+
+.play-button:active {
+ transform: scale(0.9);
+}
+
+
+.regen-button {
+ font-size: 48px;
+ font-weight: bold;
+ color: chartreuse;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ transition: transform 0.1s;
+}
+
+.regen-button:focus {
+ outline: none;
+}
+
+.regen-button:active {
+ transform: scale(0.9);
+}
+
+.message-container {
+ position: relative; /* Add this line to set the position property of the parent div */
+}
+
+.cancel-button {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ font-size: 16px;
+ font-weight: bold;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ transition: transform 0.1s;
+}
+
+.cancel-button:focus {
+ outline: none;
+}
+
+.cancel-button:active {
+ transform: scale(0.9);
+}
+
+.collapsible-content {
+ overflow: hidden;
+ max-height: 0;
+ transition: max-height 0.2s ease-out;
+}
diff --git a/webui/src/main/resources/readOnly/chat.js b/webui/src/main/resources/readOnly/chat.js
new file mode 100644
index 00000000..f946c020
--- /dev/null
+++ b/webui/src/main/resources/readOnly/chat.js
@@ -0,0 +1,65 @@
+
+let socket;
+
+function getSessionId() {
+ if (!window.location.hash) {
+ fetch('newSession')
+ .then(response => {
+ if (response.ok) {
+ return response.text();
+ } else {
+ throw new Error('Failed to get new session ID');
+ }
+ })
+ .then(sessionId => {
+ window.location.hash = sessionId;
+ connect(sessionId);
+ });
+ } else {
+ return window.location.hash.substring(1);
+ }
+}
+
+function send(message) {
+ console.log('Sending message:', message);
+ socket.send(message);
+}
+
+function connect(sessionId, customReceiveFunction) {
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const host = window.location.hostname;
+ const port = window.location.port;
+ let path = window.location.pathname;
+ let strings = path.split('/');
+ if(strings.length >= 2 && strings[1] !== '' && strings[1] !== 'index.html') {
+ path = '/' + strings[1] + '/';
+ } else {
+ path = '/';
+ }
+
+ socket = new WebSocket(`${protocol}//${host}:${port}${path}ws?sessionId=${sessionId}`);
+
+ socket.addEventListener('open', (event) => {
+ console.log('WebSocket connected:', event);
+ showDisconnectedOverlay(false);
+ });
+
+ socket.addEventListener('message', customReceiveFunction || onWebSocketText);
+
+ socket.addEventListener('close', (event) => {
+ console.log('WebSocket closed:', event);
+ showDisconnectedOverlay(true);
+ setTimeout(() => {
+ connect(getSessionId(), customReceiveFunction);
+ }, 3000);
+ });
+
+ socket.addEventListener('error', (event) => {
+ console.error('WebSocket error:', event);
+ });
+}
+
+function showDisconnectedOverlay(show) {
+ const overlay = document.getElementById('disconnected-overlay');
+ overlay.style.display = show ? 'block' : 'none';
+}
diff --git a/webui/src/main/resources/readOnly/favicon.svg b/webui/src/main/resources/readOnly/favicon.svg
new file mode 100644
index 00000000..2cbd9fdb
--- /dev/null
+++ b/webui/src/main/resources/readOnly/favicon.svg
@@ -0,0 +1,724 @@
+
+
diff --git a/webui/src/main/resources/readOnly/index.html b/webui/src/main/resources/readOnly/index.html
new file mode 100644
index 00000000..aeb8b50a
--- /dev/null
+++ b/webui/src/main/resources/readOnly/index.html
@@ -0,0 +1,45 @@
+
+
+
+
+ WebSocket Client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Disconnected. Attempting to reconnect...
+
+
+
+
+
diff --git a/webui/src/main/resources/readOnly/main.js b/webui/src/main/resources/readOnly/main.js
new file mode 100644
index 00000000..7629f48c
--- /dev/null
+++ b/webui/src/main/resources/readOnly/main.js
@@ -0,0 +1,130 @@
+function showModal(endpoint) {
+ fetchData(endpoint);
+ document.getElementById('modal').style.display = 'block';
+}
+
+function closeModal() {
+ document.getElementById('modal').style.display = 'none';
+}
+
+async function fetchData(endpoint) {
+ try {
+ // Add session id to the endpoint as a path parameter
+ const sessionId = getSessionId();
+ if (sessionId) {
+ endpoint = endpoint + "?sessionId=" + sessionId;
+ }
+ const response = await fetch(endpoint);
+ const text = await response.text();
+ document.getElementById('modal-content').innerHTML = "" + text + "
";
+ Prism.highlightAll();
+ } catch (error) {
+ console.error('Error fetching data:', error);
+ }
+}
+
+let messageVersions = {};
+
+function onWebSocketText(event) {
+ console.log('WebSocket message:', event);
+ const messagesDiv = document.getElementById('messages');
+
+ // Parse message e.g. "id,version,content"
+ const firstCommaIndex = event.data.indexOf(',');
+ const secondCommaIndex = event.data.indexOf(',', firstCommaIndex + 1);
+ const messageId = event.data.substring(0, firstCommaIndex);
+ const messageVersion = event.data.substring(firstCommaIndex + 1, secondCommaIndex);
+ const messageContent = event.data.substring(secondCommaIndex + 1);
+ // If messageVersion isn't more than the version for the messageId using the version map, then ignore the message
+ if (messageVersion <= (messageVersions[messageId] || 0)) {
+ console.log("Ignoring message with id " + messageId + " and version " + messageVersion);
+ return;
+ } else {
+ messageVersions[messageId] = messageVersion;
+ }
+
+ let messageDiv = document.getElementById(messageId);
+
+ if (messageDiv) {
+ messageDiv.innerHTML = messageContent;
+ } else {
+ messageDiv = document.createElement('div');
+ messageDiv.className = 'message message-container'; // Add the message-container class
+ messageDiv.id = messageId;
+ messageDiv.innerHTML = messageContent;
+ messagesDiv.appendChild(messageDiv);
+ }
+
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
+ Prism.highlightAll();
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+
+ document.getElementById('history').addEventListener('click', () => showModal('sessions'));
+ document.getElementById('settings').addEventListener('click', () => showModal('settings'));
+ document.querySelector('.close').addEventListener('click', closeModal);
+
+ window.addEventListener('click', (event) => {
+ if (event.target === document.getElementById('modal')) {
+ closeModal();
+ }
+ });
+
+ const messageInput = document.getElementById('message');
+ const usage = document.getElementById('usage');
+
+ const sessionId = getSessionId();
+ if (sessionId) {
+ connect(sessionId, onWebSocketText);
+ usage.href = '/usage/?sessionId=' + sessionId;
+ } else {
+ connect(undefined, onWebSocketText);
+ }
+
+ document.getElementById("files").addEventListener("click", function (event) {
+ event.preventDefault(); // Prevent the default behavior of the anchor tag
+ const sessionId = getSessionId();
+ const url = "fileIndex/" + sessionId + "/";
+ window.open(url, "_blank"); // Open the URL in a new tab
+ });
+
+ const loginLink = document.getElementById('username');
+ if (loginLink) {
+ loginLink.href = '/googleLogin?redirect=' + encodeURIComponent(window.location.pathname);
+ }
+
+ fetch('appInfo')
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ return response.json();
+ })
+ .then(data => {
+ if (data.applicationName) {
+ document.title = data.applicationName;
+ }
+ })
+ .catch(error => {
+ console.error('There was a problem with the fetch operation:', error);
+ });
+
+ fetch('userInfo')
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ return response.json();
+ })
+ .then(data => {
+ if (data.name && loginLink) {
+ loginLink.innerHTML = data.name;
+ loginLink.href = "/userSettings";
+ }
+ })
+ .catch(error => {
+ console.error('There was a problem with the fetch operation:', error);
+ });
+});
+
diff --git a/webui/src/main/resources/welcome/chat.css b/webui/src/main/resources/welcome/chat.css
index 830deeae..0be5f864 100644
--- a/webui/src/main/resources/welcome/chat.css
+++ b/webui/src/main/resources/welcome/chat.css
@@ -19,6 +19,11 @@ pre {
height: 100vh; /* Adjust this value based on your preferred container height */
}
+#applist {
+ top: 3em;
+ left: 10px;
+ position: absolute;
+}
#namebar {
background-color: #f1f1f1;
diff --git a/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt b/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt
index be5a4083..95ca2f6f 100644
--- a/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt
+++ b/webui/src/test/kotlin/com/simiacryptus/skyenet/ActorTestAppServer.kt
@@ -24,7 +24,6 @@ object ActorTestAppServer : AppServerBase(port = 8082) {
interface JokeParser : Function
override val childWebApps by lazy {
- val api = com.simiacryptus.openai.OpenAIClient()
listOf(
ChildWebApp("/test_simple", SimpleActorTestApp(SimpleActor("Translate the user's request into pig latin.", "PigLatin"))),
ChildWebApp("/test_parsed_joke", ParsedActorTestApp(ParsedActor(JokeParser::class.java, "Tell me a joke"))),