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