Mercurial Hosting > lang
changeset 4:b1adec083e44
chat work
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Tue, 08 Jul 2025 22:15:41 -0600 |
parents | eee6d4f59811 |
children | a970b7a01a74 |
files | src/chat.css src/chat.html.luan src/chat.js src/delete_chat.js.luan src/images/menu.svg src/index.html.luan src/lib/Chat.luan src/lib/Db.luan src/lib/Shared.luan src/rename_chat.js.luan src/save_rename_chat.js.luan src/site.css src/site.js |
diffstat | 13 files changed, 375 insertions(+), 61 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/chat.css Tue Jul 08 22:15:41 2025 -0600 @@ -0,0 +1,38 @@ +div[top] { + display: flex; + justify-content: space-between; + align-items: center; +} + +div[top] span[pulldown] > div { + right: 0; +} + +[waiting-ai-icon] { + width: 100px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + z-index: 3000; + display: none; +} + +[ai_container] div[ask] { + padding-top: 1em; + padding-bottom: 1em; + xpadding-left: 12px; + xpadding-right: 12px; + display: flex; + gap: 8px; + align-items: flex-end; +} +[ai_container] textarea { + flex-grow: 1; + max-height: 150px; + resize: none; +} + +dialog[rename] input { + width: 300px; +}
--- a/src/chat.html.luan Tue Jul 08 16:02:29 2025 -0600 +++ b/src/chat.html.luan Tue Jul 08 22:15:41 2025 -0600 @@ -5,41 +5,89 @@ local Shared = require "site:/lib/Shared.luan" local head = Shared.head or error() local header = Shared.header or error() +local started = Shared.started or error() +local User = require "site:/lib/User.luan" +local current_user = User.current or error() +local Chat = require "site:/lib/Chat.luan" +local get_chat_by_id = Chat.get_by_id or error() -local function get_ai_thread(ai_key) - return "thread" -end - return function() - local ai_key = "whatever" - local thread = get_ai_thread(ai_key) + local user = current_user() + if user == nil then + Http.response.send_redirect("/login.html") + return + end + local chat_id = Http.request.parameters.chat + local chat + if chat_id ~= nil then + chat = get_chat_by_id(chat_id) or error() + else + chat = Chat.new{ + user_id = user.id + name = "whatever" + } + chat.save() + end Io.stdout = Http.response.text_writer() %> <!doctype html> <html lang="en"> <head> <% head() %> + <style> + @import "/chat.css?s=<%=started%>"; + </style> + <script> + let chatId = <%=chat_id%>; + </script> + <script src="/chat.js?s=<%=started%>"></script> </head> <body> <% header() %> - <div content> - <h1>Chat</h1> - <div ai_container="<%=ai_key%>" > - <div flex> - <div scroll> - <h2>Let's chat</h2> - <div messages> - </div> + <div content ai_container> + <div top> + <h3 name><%= chat.name_html() %></h3> + <span pulldown> + <img onclick="clickMenu(this)" src="/images/menu.svg"> + <div> + <span onclick="renameChat()">Rename Chat</span> + <span onclick="deleteChat()">Delete Chat</span> </div> - <div ask> - <textarea autofocus oninput="fixTextarea(event)" onkeydown="textareaKey('<%=ai_key%>',event)"></textarea> - <button onclick="askAi('<%=ai_key%>')" title="Send"><img src="/images/send.svg"></button> - </div> + </span> + </div> + <div scroll> + <div messages> </div> - <img waiting-ai-icon src="/images/spinner_green.gif"> + </div> + <div ask> + <textarea autofocus oninput="fixTextarea(event)" onkeydown="textareaKey(event)"></textarea> + <button onclick="askAi()" title="Send"><img src="/images/send.svg"></button> </div> + <img waiting-ai-icon src="/images/spinner_green.gif"> </div> + <dialog rename> + <h2>Rename Chat</h2> + <form action="javascript:saveRenameChat()"> + <p> + <label>Chat name</label><br> + <input name=name required><br> + <span error></span> + </p> + <div buttons> + <button type=button onclick="closeModal(this)">Cancel</button> + <button type=submit>Rename</button> + </div> + </form> + </dialog> + <dialog delete> + <h2>Delete Chat</h2> + <p>Are you sure that you want to delete this chat?</p> + <div buttons> + <button onclick="closeModal(this)">Cancel</button> + <button onclick="doDeleteChat(this)">Delete</button> + </div> + </dialog> </body> </html> <%
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/chat.js Tue Jul 08 22:15:41 2025 -0600 @@ -0,0 +1,56 @@ +'use strict'; + +function openRenameChat(name) { + let dialog = document.querySelector('dialog[rename]'); + dialog.querySelector('input[name=name]').value = name; + dialog.showModal(); +} + +function renameChat() { + ajax(`rename_chat.js?chat=${chatId}`); +} + +function saveRenameChat() { + let dialog = document.querySelector('dialog[rename]'); + let name = dialog.querySelector('input[name=name]').value; + ajax(`save_rename_chat.js?chat=${chatId}&name=${encodeURIComponent(name)}`); + dialog.close(); +} + +function deleteChat() { + let dialog = document.querySelector('dialog[delete]'); + dialog.showModal(); +} + +function doDeleteChat(el) { + closeModal(el); + ajax(`delete_chat.js?chat=${chatId}`); +} + +function showWaitingAiIcon() { + document.querySelector('[waiting-ai-icon]').style.display = 'block'; +} + +const isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; + +function textareaKey(event) { + if( event.keyCode===13 && !event.shiftKey && !event.ctrlKey && !isMobile ) { + event.preventDefault(); + askAi(); + } +} + +function fixTextarea(event) { + let textarea = event.target; + textarea.style.height = 'initial'; + textarea.style.height = (textarea.scrollHeight+2) + 'px'; + textarea.scrollIntoViewIfNeeded(false); +} + +function askAi() { + let input = document.querySelector('textarea'); + let url = `ai_ask.js?key=${chatId}&input=${encodeURIComponent(input.value)}`; + ajax(url); + input.value = ''; + showWaitingAiIcon(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/delete_chat.js.luan Tue Jul 08 22:15:41 2025 -0600 @@ -0,0 +1,20 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local Io = require "luan:Io.luan" +local Http = require "luan:http/Http.luan" +local User = require "site:/lib/User.luan" +local current_user = User.current or error() +local Chat = require "site:/lib/Chat.luan" +local get_chat_by_id = Chat.get_by_id or error() + + +return function() + local chat = Http.request.parameters.chat or error() + chat = get_chat_by_id(chat) or error() + chat.user_id == current_user().id or error() + chat.delete() + Io.stdout = Http.response.text_writer() +%> + location = '/'; +<% +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/images/menu.svg Tue Jul 08 22:15:41 2025 -0600 @@ -0,0 +1,1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg> \ No newline at end of file
--- a/src/index.html.luan Tue Jul 08 16:02:29 2025 -0600 +++ b/src/index.html.luan Tue Jul 08 22:15:41 2025 -0600 @@ -1,13 +1,24 @@ local Luan = require "luan:Luan.luan" local error = Luan.error +local ipairs = Luan.ipairs or error() local Io = require "luan:Io.luan" local Http = require "luan:http/Http.luan" local Shared = require "site:/lib/Shared.luan" local head = Shared.head or error() local header = Shared.header or error() +local User = require "site:/lib/User.luan" +local current_user = User.current or error() +local Chat = require "site:/lib/Chat.luan" +local chat_search = Chat.search or error() return function() + local user = current_user() + if user == nil then + Http.response.send_redirect("/login.html") + return + end + local chats = chat_search( "chat_user_id:"..user.id, "chat_updated desc" ) Io.stdout = Http.response.text_writer() %> <!doctype html> @@ -19,7 +30,10 @@ <% header() %> <div content> <h1>Lang</h1> - <p><a href="chat.html">chat</a></p> +<% for _, chat in ipairs(chats) do %> + <p><a href="chat.html?chat=<%=chat.id%>"><%= chat.name_html() %></a></p> +<% end %> + <p><a href="chat.html">new chat</a></p> </div> </body> </html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/Chat.luan Tue Jul 08 22:15:41 2025 -0600 @@ -0,0 +1,72 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local ipairs = Luan.ipairs or error() +local Number = require "luan:Number.luan" +local long = Number.long or error() +local Time = require "luan:Time.luan" +local time_now = Time.now or error() +local Html = require "luan:Html.luan" +local html_encode = Html.encode or error() +local Db = require "site:/lib/Db.luan" + + +local Chat = {} + +local function from_doc(doc) + doc.type == "chat" or error "wrong type" + return Chat.new { + id = doc.id + user_id = doc.chat_user_id + updated = doc.chat_updated + name = doc.name + ai_key = doc.ai_key + } +end + +local function to_doc(chat) + return { + type = "chat" + id = chat.id + chat_user_id = long(chat.user_id) + chat_updated = long(chat.updated) + name = chat.name or error() + ai_key = chat.ai_key -- or error() + } +end + +function Chat.new(chat) + chat.updated = chat.updated or time_now() + + function chat.save() + local doc = to_doc(chat) + Db.save(doc) + chat.id = doc.id + end + + function chat.delete() + Db.delete("id:"..chat.id) + end + + function chat.name_html() + return html_encode(chat.name) + end + + return chat +end + +function Chat.search(query,sort,rows) + rows = rows or 1000000 + local chats = {} + local docs = Db.search(query,1,rows,{sort=sort}) + for _, doc in ipairs(docs) do + chats[#chats+1] = from_doc(doc) + end + return chats +end + +function Chat.get_by_id(id) + local doc = Db.get_document("id:"..id) + return doc and doc.type=="chat" and from_doc(doc) or nil +end + +return Chat
--- a/src/lib/Db.luan Tue Jul 08 16:02:29 2025 -0600 +++ b/src/lib/Db.luan Tue Jul 08 22:15:41 2025 -0600 @@ -18,6 +18,9 @@ Db.indexed_fields.user_email = Lucene.type.lowercase +Db.indexed_fields.chat_user_id = Lucene.type.long +Db.indexed_fields.chat_updated = Lucene.type.long + Db.restore_from_log() if Http.is_serving then
--- a/src/lib/Shared.luan Tue Jul 08 16:02:29 2025 -0600 +++ b/src/lib/Shared.luan Tue Jul 08 22:15:41 2025 -0600 @@ -12,6 +12,7 @@ local Shared = {} local started = Time.now() +Shared.started = started function Shared.head() %>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/rename_chat.js.luan Tue Jul 08 22:15:41 2025 -0600 @@ -0,0 +1,18 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local Parsers = require "luan:Parsers.luan" +local json_string = Parsers.json_string or error() +local Io = require "luan:Io.luan" +local Http = require "luan:http/Http.luan" +local Chat = require "site:/lib/Chat.luan" +local get_chat_by_id = Chat.get_by_id or error() + + +return function() + local chat = Http.request.parameters.chat or error() + chat = get_chat_by_id(chat) or error() + Io.stdout = Http.response.text_writer() +%> + openRenameChat(<%=json_string(chat.name)%>); +<% +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/save_rename_chat.js.luan Tue Jul 08 22:15:41 2025 -0600 @@ -0,0 +1,28 @@ +local Luan = require "luan:Luan.luan" +local error = Luan.error +local Parsers = require "luan:Parsers.luan" +local json_string = Parsers.json_string or error() +local Io = require "luan:Io.luan" +local Http = require "luan:http/Http.luan" +local Chat = require "site:/lib/Chat.luan" +local get_chat_by_id = Chat.get_by_id or error() +local User = require "site:/lib/User.luan" +local current_user = User.current or error() +local Db = require "site:/lib/Db.luan" +local run_in_transaction = Db.run_in_transaction or error() + + +return function() + local chat = Http.request.parameters.chat or error() + local name = Http.request.parameters.name or error() + run_in_transaction( function() + chat = get_chat_by_id(chat) or error() + chat.user_id == current_user().id or error() + chat.name = name + chat.save() + end ) + Io.stdout = Http.response.text_writer() +%> + document.querySelector('[content] [name]').textContent = <%=json_string(name)%>; +<% +end
--- a/src/site.css Tue Jul 08 16:02:29 2025 -0600 +++ b/src/site.css Tue Jul 08 22:15:41 2025 -0600 @@ -14,6 +14,14 @@ text-decoration: underline; } +button, +img[onclick], +label[clickable], +input[type="radio"], +input[type="submit"] { + cursor: pointer; +} + div[header] { font-size: 14px; background-color: #ddd; @@ -28,27 +36,30 @@ margin-bottom: 2em; } -[waiting-ai-icon] { - width: 100px; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%,-50%); - z-index: 3000; - display: none; +span[pulldown] { + position: relative; } -[ai_container] div[ask] { - padding-top: 1em; - padding-bottom: 1em; - padding-left: 12px; - padding-right: 12px; - display: flex; - gap: 8px; - align-items: flex-end; +span[pulldown] > div { + display: none; + right: 100%; + z-index: 2; + position: absolute; + border: 1px solid #cccccc; + border-radius: 4px; + text-align: left; + background-color: #eeeeee; + padding: 5px 0; } -[ai_container] textarea { - flex-grow: 1; - max-height: 150px; - resize: none; + +span[pulldown] > div > span { + white-space: nowrap; + display: block; + padding: 8px 16px; + cursor: pointer; } + +span[pulldown] > div > span:hover { + color: #ffffff; + background-color: #428bca; +}
--- a/src/site.js Tue Jul 08 16:02:29 2025 -0600 +++ b/src/site.js Tue Jul 08 22:15:41 2025 -0600 @@ -70,31 +70,35 @@ } -function showWaitingAiIcon() { - document.querySelector('[waiting-ai-icon]').style.display = 'block'; -} +let currentPulldown = null; +let newPulldown = null; -const isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; - -function textareaKey(aiKey,event) { - if( event.keyCode===13 && !event.shiftKey && !event.ctrlKey && !isMobile ) { - event.preventDefault(); - askAi(aiKey); +function clickMenu(clicked,display) { + //console.log('clickMenu'); + let pulldown = clicked.parentNode.querySelector('div'); + if( pulldown !== currentPulldown ) { + pulldown.style.display = display || 'block'; + newPulldown = pulldown; + window.onclick = function() { + //console.log('window.onclick'); + if( currentPulldown ) { + currentPulldown.style.display = 'none'; + if( !newPulldown ) + window.onclick = null; + } + currentPulldown = newPulldown; + newPulldown = null; + }; + pulldown.scrollIntoViewIfNeeded(false); } } -function fixTextarea(event) { - let textarea = event.target; - textarea.style.height = 'initial'; - textarea.style.height = (textarea.scrollHeight+2) + 'px'; - textarea.scrollIntoViewIfNeeded(false); +function getEnclosingDialog(el) { + while( el.nodeName !== 'DIALOG' ) + el = el.parentNode; + return el; } -function askAi(aiKey) { - let aiDiv = document.querySelector(`[ai_container="${aiKey}"]`); - let input = aiDiv.querySelector('textarea'); - let url = `ai_ask.js?key=${aiKey}&input=${encodeURIComponent(input.value)}`; - ajax(url); - input.value = ''; - showWaitingAiIcon(); +function closeModal(el) { + getEnclosingDialog(el).close(); }