changeset 74:64e35a92d163

add translation
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 28 Aug 2025 13:31:46 -0600
parents 60ebb333b40c
children b96cf27e719d
files src/chat.css src/chat.html.luan src/chat.js src/lib/Chat.luan src/lib/ai/claude/Ai_chat.luan src/lib/ai/claude/Claude.luan src/lib/ai/claude/Translator.luan src/site.js src/translate.js.luan
diffstat 9 files changed, 145 insertions(+), 35 deletions(-) [+]
line wrap: on
line diff
--- a/src/chat.css	Thu Aug 28 05:16:32 2025 -0600
+++ b/src/chat.css	Thu Aug 28 13:31:46 2025 -0600
@@ -39,9 +39,21 @@
 	width: 100%;
 }
 
-div[role=assistant]:has(option[hide_text]:checked) div[message] {
+div[role=assistant]:has(option[value=hide_text]:checked) div[message] {
+	filter: blur(5px);
+}
+div[role=assistant]:has(option[value=hide_ruby]:checked) div[message] rt {
 	filter: blur(5px);
 }
-div[role=assistant]:has(option[hide_ruby]:checked) div[message] rt {
-	filter: blur(5px);
+div[role=assistant] div[trans] {
+	display: none;
+	border-top: 1px solid #cccccc;
+	white-space: pre-wrap;
 }
+div[role=assistant]:has(option[value=show_trans]:checked) div[trans] {
+	display: block;
+}
+
+div[controls] {
+	margin-top: 16px;
+}
--- a/src/chat.html.luan	Thu Aug 28 05:16:32 2025 -0600
+++ b/src/chat.html.luan	Thu Aug 28 13:31:46 2025 -0600
@@ -92,11 +92,11 @@
 				</p>
 				<p>
 					<select name=show_text>
-						<option value=show_text>Show text</option>
+						<option value=hide_text>Hide text</option>
 <%	if chat.has_ruby then %>
 						<option value=hide_ruby>Hide pronunciation</option>
 <%	end %>
-						<option value=hide_text>Hide text</option>
+						<option value=show_text>Show text</option>
 					</select>
 				</p>
 				<p>
--- a/src/chat.js	Thu Aug 28 05:16:32 2025 -0600
+++ b/src/chat.js	Thu Aug 28 13:31:46 2025 -0600
@@ -159,3 +159,26 @@
 		recorder.stop();
 	}
 }
+
+function showSelected(select) {
+	if( select.value !== 'show_trans' )
+		return;
+	let top = select.parentNode.parentNode;
+	let trans = top.querySelector('div[trans=needed]');
+	if( !trans )
+		return;
+	let msg = top.getAttribute('msg');
+	let message = top.querySelector('div[message]');
+	let text = textContent(message);
+	let url = `translate.js?msg=${msg}&text=${encodeURIComponent(text)}&language=${chat.language}`;
+	ajax(url);
+	showWaitingAiIcon();
+}
+
+function translated(msg,text) {
+	hideWaitingAiIcon();
+	let top = document.querySelector(`div[msg="${msg}"]`);
+	let trans = top.querySelector('div[trans]');
+	trans.setAttribute('trans','');
+	trans.innerHTML = text;
+}
--- a/src/lib/Chat.luan	Thu Aug 28 05:16:32 2025 -0600
+++ b/src/lib/Chat.luan	Thu Aug 28 13:31:46 2025 -0600
@@ -90,6 +90,7 @@
 			autoplay = chat.autoplay
 			is_private = chat.is_private
 			stt_prompt = chat.stt_prompt
+			language = chat.language
 		}
 	end
 
@@ -112,21 +113,25 @@
 	local function option(name,text)
 		local selected = name==chat.show_text and " selected" or ""
 %>
-						<option <%=name%><%=selected%>><%=text%></option>
+						<option value=<%=name%><%=selected%>><%=text%></option>
 <%
 	end
 
 	local function assistant_controls()
 		return `%>
+				<div trans=needed>Translating...</div>
 				<div controls>
 					<audio controls preload=none></audio>
-					<select>
+					<select onchange="showSelected(this)">
 <%
-						option("show_text","Show text")
+						option("hide_text","Hide text")
 			if chat.has_ruby then
 						option("hide_ruby","Hide pronunciation")
 			end
-						option("hide_text","Hide text")
+						option("show_text","Show text")
+			if chat.language ~= "en" then
+						option("show_trans","Show translation")
+			end
 %>
 					</select>
 				</div>
--- a/src/lib/ai/claude/Ai_chat.luan	Thu Aug 28 05:16:32 2025 -0600
+++ b/src/lib/ai/claude/Ai_chat.luan	Thu Aug 28 13:31:46 2025 -0600
@@ -13,6 +13,8 @@
 local Thread = require "luan:Thread.luan"
 local Claude = require "site:/lib/ai/claude/Claude.luan"
 local claude_chat = Claude.chat or error()
+local Utils = require "site:/lib/Utils.luan"
+local deep_copy = Utils.deep_copy or error()
 local Logging = require "luan:logging/Logging.luan"
 local logger = Logging.logger "claude/Ai_chat"
 
@@ -51,8 +53,8 @@
 		local function output(text)
 			text = html_encode(text)
 %>
-			<h3><%=who%></h3>
-			<div role="<%=role%>">
+			<div role="<%=role%>" msg="<%=i%>">
+				<h3><%=who%></h3>
 				<div message markdown><%=text%></div>
 <%			if role=="assistant" then %>
 <%=				assistant_controls %>
@@ -164,6 +166,24 @@
 	return #thread.messages > 0
 end
 
+local function chit_chat(thread)
+	thread = deep_copy(thread)
+	local messages = thread.messages or error()
+	for _, message in ipairs(messages) do
+		local content = message.content or error()
+		if type(content) == "string" then
+			content = {{
+				type = "text"
+				text = content
+			}}
+			message.content = content
+		end
+	end
+	local content = messages[#messages].content or error()
+	content[#content].cache_control = { type = "ephemeral" }
+	return claude_chat(thread)
+end
+
 local function ask(thread)
 	local messages = thread.messages or error
 --[=[
@@ -179,7 +199,7 @@
 	end
 --]=]
 	-- logger.info(json_string(thread))
-	local resultJson = claude_chat(thread)
+	local resultJson = chit_chat(thread)
 	local result = json_parse(resultJson)
 	-- logger.info(json_string(result))
 	result.type == "message" or error()
--- a/src/lib/ai/claude/Claude.luan	Thu Aug 28 05:16:32 2025 -0600
+++ b/src/lib/ai/claude/Claude.luan	Thu Aug 28 13:31:46 2025 -0600
@@ -7,8 +7,6 @@
 local Parsers = require "luan:Parsers.luan"
 local json_string = Parsers.json_string or error()
 local Config = require "site:/private/Config.luan"
-local Utils = require "site:/lib/Utils.luan"
-local deep_copy = Utils.deep_copy or error()
 local Logging = require "luan:logging/Logging.luan"
 local logger = Logging.logger "claude/Claude"
 
@@ -27,22 +25,8 @@
 local max_tokens = 8192
 
 function Claude.chat(thread)
-	thread = deep_copy(thread)
 	thread.model = thread.model or model
 	thread.max_tokens = thread.max_tokens or max_tokens
-	local messages = thread.messages or error()
-	for _, message in ipairs(messages) do
-		local content = message.content or error()
-		if type(content) == "string" then
-			content = {{
-				type = "text"
-				text = content
-			}}
-			message.content = content
-		end
-	end
-	local content = messages[#messages].content or error()
-	content[#content].cache_control = { type = "ephemeral" }
 	local options = {
 		method = "POST"
 		headers = headers
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib/ai/claude/Translator.luan	Thu Aug 28 13:31:46 2025 -0600
@@ -0,0 +1,40 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Parsers = require "luan:Parsers.luan"
+local json_parse = Parsers.json_parse or error()
+local json_string = Parsers.json_string or error()
+local Claude = require "site:/lib/ai/claude/Claude.luan"
+local claude_chat = Claude.chat or error()
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "claude/Translator"
+
+
+local Translator = {}
+
+function Translator.translate(text,lang)
+	local thread = {
+		system = `%>
+Translate <%=lang%> in the text you get to English.
+Preserve formatting.
+The text may also contain English.  Just leave that unchanged.
+<%		`
+		messages = {{
+			role = "user"
+			content = text
+		}}
+		temperature = 0
+	}
+	local resultJson = claude_chat(thread)
+	local result = json_parse(resultJson)
+	-- logger.info(json_string(result))
+	result.type == "message" or error()
+	result.role == "assistant" or error()
+	result.stop_reason == "end_turn" or result.stop_reason == "tool_use" or error()
+	local content = result.content or error()
+	#content==1 or error()
+	content = content[1]
+	content.type == "text" or error()
+	return content.text or error()
+end
+
+return Translator
--- a/src/site.js	Thu Aug 28 05:16:32 2025 -0600
+++ b/src/site.js	Thu Aug 28 13:31:46 2025 -0600
@@ -151,6 +151,15 @@
 	}
 }
 
+function textContent(div) {
+	mdDiv.innerHTML = div.innerHTML;
+	let rts = mdDiv.querySelectorAll('rt');
+	for( let rt of rts ) {
+		rt.remove();
+	}
+	return mdDiv.textContent;
+}
+
 function handleMarkdown(voice,instructions) {
 	let converter = window.markdownit();
 	let divs = document.querySelectorAll('[markdown]');
@@ -160,13 +169,7 @@
 		div.removeAttribute('markdown');
 		let parent = div.parentNode;
 		if( parent.getAttribute('role')==='assistant' ) {
-			mdDiv.innerHTML = div.innerHTML;
-			let rts = mdDiv.querySelectorAll('rt');
-			for( let rt of rts ) {
-				rt.remove();
-			}
-			//console.log(mdDiv.textContent);
-			parent.querySelector('audio').src = `/tts.wav?voice=${voice}&instructions=${encodeURIComponent(instructions)}&text=${encodeURIComponent(mdDiv.textContent)}`;
+			parent.querySelector('audio').src = `/tts.wav?voice=${voice}&instructions=${encodeURIComponent(instructions)}&text=${encodeURIComponent(textContent(div))}`;
 		}
 	}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/translate.js.luan	Thu Aug 28 13:31:46 2025 -0600
@@ -0,0 +1,23 @@
+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 Shared = require "site:/lib/Shared.luan"
+local languages = Shared.languages or error()
+local Translator = require "site:/lib/ai/claude/Translator.luan"
+local translate = Translator.translate or error()
+
+
+return function()
+	local msg = Http.request.parameters.msg or error()
+	local text = Http.request.parameters.text or error()
+	local language = Http.request.parameters.language or error()
+	language = languages[language] or error()
+	text = translate(text,language)
+	Io.stdout = Http.response.text_writer()
+%>
+	translated('<%=msg%>',<%=json_string(text)%>);
+<%
+end