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); + }); + +}); +