diff --git a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatServer.kt b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatServer.kt
index b3bac850..dc3a5226 100644
--- a/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatServer.kt
+++ b/src/main/kotlin/com/github/simiacryptus/aicoder/actions/dev/CodeChatServer.kt
@@ -5,55 +5,42 @@ import com.intellij.openapi.project.Project
import com.simiacryptus.openai.OpenAIClient
import com.simiacryptus.skyenet.sessions.*
import com.simiacryptus.skyenet.util.ClasspathResource
-import com.simiacryptus.skyenet.util.MarkdownUtil
import org.eclipse.jetty.util.resource.Resource
-import org.intellij.lang.annotations.Language
class CodeChatServer(
val project: Project,
val language: String,
val codeSelection: String,
val api: OpenAIClient,
+ resourceBase: String = "codeChat",
) : ChatApplicationBase(
applicationName = "Code Chat",
+ resourceBase = resourceBase,
) {
- private val rootOperationID = (0..5).map { ('a'..'z').random() }.joinToString("")
- private var rootMessageTrail: String = ""
-
- override fun newSession(sessionId: String): ChatSession {
- val newSession = ChatSession(
- sessionId = sessionId,
- parent = this@CodeChatServer,
- model = AppSettingsState.instance.defaultChatModel(),
- api = api,
- visiblePrompt = """
-
Code:
-
${htmlEscape(codeSelection)}
-
- """.trimIndent().trim(),
- hiddenPrompt = "",
- systemPrompt = """
- |You are a helpful AI that helps people with coding.
- |
- |You will be answering questions about the following code:
- |
- |```$language
- |$codeSelection
- |```
- |
- |Responses may use markdown formatting.
- """.trimMargin(),
- )
- @Language("HTML") val html = """
- Code:
-
${htmlEscape(codeSelection)}
-
- """.trimIndent().trim()
- rootMessageTrail = """$rootOperationID,$html"""
- newSession.send(rootMessageTrail)
- return newSession
- }
+ override fun newSession(sessionId: String) = ChatSession(
+ sessionId = sessionId,
+ parent = this@CodeChatServer,
+ model = AppSettingsState.instance.defaultChatModel(),
+ api = api,
+ visiblePrompt = """
+ |Code:
+ |
${htmlEscape(codeSelection)}
+ |
+ """.trimMargin().trim(),
+ hiddenPrompt = "",
+ systemPrompt = """
+ |You are a helpful AI that helps people with coding.
+ |
+ |You will be answering questions about the following code:
+ |
+ |```$language
+ |$codeSelection
+ |```
+ |
+ |Responses may use markdown formatting.
+ """.trimMargin(),
+ )
override fun processMessage(
sessionId: String,
diff --git a/src/main/resources/codeChat/chat.css b/src/main/resources/codeChat/chat.css
new file mode 100644
index 00000000..b1e6adaa
--- /dev/null
+++ b/src/main/resources/codeChat/chat.css
@@ -0,0 +1,278 @@
+#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;
+}
+
+#form {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ margin: 0;
+ padding: 5px;
+ flex-shrink: 0;
+}
+
+.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;
+}
+
+#disconnected-overlay {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 999;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: white;
+ font-size: 24px;
+}
+
+#disconnected-overlay p {
+ font-size: 3rem;
+ line-height: 1.5;
+ margin-bottom: 20px;
+ animation-name: bounce;
+ animation-duration: 0.5s;
+ animation-iteration-count: infinite;
+ animation-direction: alternate;
+ left: 10%;
+ position: relative;
+ color: firebrick;
+}
+
+@keyframes bounce {
+ 0% {
+ transform: translateY(0);
+ }
+ 100% {
+ transform: translateY(-10px);
+ }
+}
+
+.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/src/main/resources/codeChat/chat.js b/src/main/resources/codeChat/chat.js
new file mode 100644
index 00000000..f946c020
--- /dev/null
+++ b/src/main/resources/codeChat/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/src/main/resources/codeChat/favicon.svg b/src/main/resources/codeChat/favicon.svg
new file mode 100644
index 00000000..2cbd9fdb
--- /dev/null
+++ b/src/main/resources/codeChat/favicon.svg
@@ -0,0 +1,724 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/codeChat/index.html b/src/main/resources/codeChat/index.html
new file mode 100644
index 00000000..79979eb8
--- /dev/null
+++ b/src/main/resources/codeChat/index.html
@@ -0,0 +1,37 @@
+
+
+
+
+ WebSocket Client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Disconnected. Attempting to reconnect...
+
+
+
+
+
diff --git a/src/main/resources/codeChat/main.js b/src/main/resources/codeChat/main.js
new file mode 100644
index 00000000..2d899aa1
--- /dev/null
+++ b/src/main/resources/codeChat/main.js
@@ -0,0 +1,128 @@
+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.querySelector('.close').addEventListener('click', closeModal);
+
+ window.addEventListener('click', (event) => {
+ if (event.target === document.getElementById('modal')) {
+ closeModal();
+ }
+ });
+
+ const form = document.getElementById('form');
+ const messageInput = document.getElementById('message');
+
+ form.addEventListener('submit', (event) => {
+ event.preventDefault();
+ send(messageInput.value);
+ messageInput.value = '';
+ });
+
+ messageInput.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter' && !event.shiftKey) {
+ event.preventDefault();
+ form.dispatchEvent(new Event('submit'));
+ }
+ });
+
+ connect(undefined, onWebSocketText);
+
+ document.body.addEventListener('click', (event) => {
+ const target = event.target;
+ if (target.classList.contains('play-button')) {
+ const messageId = target.getAttribute('data-id');
+ send('!' + messageId + ',run');
+ } else if (target.classList.contains('regen-button')) {
+ const messageId = target.getAttribute('data-id');
+ send('!' + messageId + ',regen');
+ } else if (target.classList.contains('cancel-button')) {
+ const messageId = target.getAttribute('data-id');
+ send('!' + messageId + ',stop');
+ } else if (target.classList.contains('href-link')) {
+ const messageId = target.getAttribute('data-id');
+ send('!' + messageId + ',link');
+ } else if (target.classList.contains('text-submit-button')) {
+ const messageId = target.getAttribute('data-id');
+ const text = document.querySelector('.reply-input[data-id="' + messageId + '"]').value;
+ send('!' + messageId + ',userTxt,' + text);
+ }
+ });
+
+ 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);
+ });
+
+});
+