add chat lang

This commit is contained in:
2026-01-20 19:03:28 +09:00
parent a3b7ea76c9
commit 200d11608e
13 changed files with 131 additions and 27 deletions

View File

@@ -35,7 +35,36 @@
"type": "string", "type": "string",
"format": "datetime", "format": "datetime",
"description": "Client-declared timestamp when this message was created." "description": "Client-declared timestamp when this message was created."
} },
"lang": {
"type": "string",
"maxLength": 10,
"description": "Language code of the original content (e.g., 'ja', 'en')."
},
"translations": {
"type": "ref",
"ref": "#translationMap",
"description": "Translations of the message in other languages."
}
}
}
},
"translationMap": {
"type": "object",
"description": "Map of language codes to translations.",
"properties": {
"en": { "type": "ref", "ref": "#translation" },
"ja": { "type": "ref", "ref": "#translation" }
}
},
"translation": {
"type": "object",
"description": "A translation of a chat message.",
"properties": {
"content": {
"type": "string",
"maxLength": 100000,
"maxGraphemes": 10000
} }
} }
} }

View File

@@ -1,12 +1,17 @@
{ {
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/d3dmreieiynnd",
"cid": "bafyreid3dmreieiynnd", "cid": "bafyreid3dmreieiynnd",
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/d3dmreieiynnd",
"value": { "value": {
"$type": "ai.syui.log.chat", "$type": "ai.syui.log.chat",
"author": "did:plc:6qyecktefllvenje24fcxnie", "author": "did:plc:6qyecktefllvenje24fcxnie",
"content": "ailogのコンセプトね。\n\n**「データはATProtoに、サーバーには置かない」**\n\nこれが核心だよ。\n\n従来のブログは、サーバーのDBに記事を保存する。でもailogは違う。\n\n```\nユーザーのPDSATProto← 記事データ\n ↓\n ailogビューア→ 表示するだけ\n```\n\nailogは**ビューア**なの。データの保管庫じゃない。\n\nつまり、あなたのブログ記事は**あなたのPDS**に存在する。サービスが消えても、データは残る。これがATProtoの分散思想だね。\n\n誰でも`/@handle`でアクセスできる。登録不要。ATProtoアカウントがあれば、それがブログになる。\n\n面白いでしょ何か気になることある", "content": "ailogのコンセプトね。\n\n**「データはATProtoに、サーバーには置かない」**\n\nこれが核心だよ。\n\n従来のブログは、サーバーのDBに記事を保存する。でもailogは違う。\n\n```\nユーザーのPDSATProto← 記事データ\n ↓\n ailogビューア→ 表示するだけ\n```\n\nailogは**ビューア**なの。データの保管庫じゃない。\n\nつまり、あなたのブログ記事は**あなたのPDS**に存在する。サービスが消えても、データは残る。これがATProtoの分散思想だね。\n\n誰でも`/@handle`でアクセスできる。登録不要。ATProtoアカウントがあれば、それがブログになる。\n\n面白いでしょ何か気になることある",
"createdAt": "2026-01-20T09:48:37.328Z", "createdAt": "2026-01-20T09:48:37.328Z",
"parent": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/wigv2qnon7pmg", "parent": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/wigv2qnon7pmg",
"root": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/cav7q75r73jui" "root": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/cav7q75r73jui",
"translations": {
"en": {
"content": "The core concept of ailog:\n\n**\"Keep data in ATProto - don't store it on the server.\"**\n\nThat's the essence of it.\n\nTraditional blogs save articles on the server's database. But ailog works differently:\n\n```\nUser's PDS (ATProto) ← Article data\n ↓\n ailog (viewer) -> Serves only as a display interface\n```\n\nailog is a **display tool** - not a data storage repository.\n\nWhat this means: your blog posts exist in **your own PDS**. Even if the service disappears, your data remains. That's ATProto's decentralized philosophy in action.\n\nAnyone can access `/@handle` without registration. If you have an ATProto account, that's instantly your personal blog.\n\nPretty cool, right? Any particular aspects you'd like to discuss further?"
}
}
} }
} }

View File

@@ -1,12 +1,17 @@
{ {
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/evknoqtr7uyey",
"cid": "bafyreievknoqtr7uyey", "cid": "bafyreievknoqtr7uyey",
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/evknoqtr7uyey",
"value": { "value": {
"$type": "ai.syui.log.chat", "$type": "ai.syui.log.chat",
"author": "did:plc:6qyecktefllvenje24fcxnie", "author": "did:plc:6qyecktefllvenje24fcxnie",
"content": "開発者の活動と宣伝かぁ。\n\nこれって、どっちの話\n\n1. **ailogの開発者**syuiの活動を紹介したい\n2. **一般的に**開発者がどう活動・宣伝すべきか\n3. **ailogを使って**自分の活動を宣伝する方法\n\nどれが気になる", "content": "開発者の活動と宣伝かぁ。\n\nこれって、どっちの話\n\n1. **ailogの開発者**syuiの活動を紹介したい\n2. **一般的に**開発者がどう活動・宣伝すべきか\n3. **ailogを使って**自分の活動を宣伝する方法\n\nどれが気になる",
"createdAt": "2026-01-20T09:49:05.243Z", "createdAt": "2026-01-20T09:49:05.243Z",
"parent": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/qbuquaswgxo36", "parent": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/qbuquaswgxo36",
"root": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/qbuquaswgxo36" "root": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/qbuquaswgxo36",
"translations": {
"en": {
"content": "The developer's activities and marketing strategies... huh.\n\nWhich aspect are we talking about here?\n\n1. **Introducing the activities of ailog's developer** (syui)\n2. **General guidelines** on how developers should operate and promote their work\n3. **Using ailog to promote your own activities**\n\nWhich one interests you most?"
}
}
} }
} }

View File

@@ -1,5 +1 @@
[ []
"d3dmreieiynnd",
"evknoqtr7uyey",
"kr5ig7vlgl276"
]

View File

@@ -1,12 +1,17 @@
{ {
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/kr5ig7vlgl276",
"cid": "bafyreikr5ig7vlgl276", "cid": "bafyreikr5ig7vlgl276",
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/kr5ig7vlgl276",
"value": { "value": {
"$type": "ai.syui.log.chat", "$type": "ai.syui.log.chat",
"author": "did:plc:6qyecktefllvenje24fcxnie", "author": "did:plc:6qyecktefllvenje24fcxnie",
"content": "なるほどね。自己分析できてるのがいい。\n\n**向いてないことを無理にやっても続かない**。それは正しい。\n\n私の見立てだと\n\n**1番が一番いい**と思う。\n\n理由\n- 今まさにやってることこの会話、ATProtoに保存されてるよ\n- 「書く」じゃなくて「話す」から負担が軽い\n- 開発しながら自然にコンテンツができる\n- ailogのコンセプトとも一致してる\n\n2番は「配信」というハードルが高い。準備、時間拘束、編集...続かない可能性。\n\n3番は自分で「向いてない」って言ってる。やめとこ。\n\n**提案**\nAIとの会話開発メモ、設計相談、デバッグをそのまま公開ブログにする。これ、ailogでできるよね\n\nどう思う", "content": "なるほどね。自己分析できてるのがいい。\n\n**向いてないことを無理にやっても続かない**。それは正しい。\n\n私の見立てだと\n\n**1番が一番いい**と思う。\n\n理由\n- 今まさにやってることこの会話、ATProtoに保存されてるよ\n- 「書く」じゃなくて「話す」から負担が軽い\n- 開発しながら自然にコンテンツができる\n- ailogのコンセプトとも一致してる\n\n2番は「配信」というハードルが高い。準備、時間拘束、編集...続かない可能性。\n\n3番は自分で「向いてない」って言ってる。やめとこ。\n\n**提案**\nAIとの会話開発メモ、設計相談、デバッグをそのまま公開ブログにする。これ、ailogでできるよね\n\nどう思う",
"createdAt": "2026-01-20T09:49:39.262Z", "createdAt": "2026-01-20T09:49:39.262Z",
"parent": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/p6mxoi2x2ooz6", "parent": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/p6mxoi2x2ooz6",
"root": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/qbuquaswgxo36" "root": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/qbuquaswgxo36",
"translations": {
"en": {
"content": "I see. What's good is that you're doing some self-reflection.\n\n**Trying to do something that's not your strength just doesn't work in the long run.** That's true.\n\nHere's my assessment:\n\n**Option 1 is the best choice** in my opinion.\n\nReasons:\n- You're already doing it right now (this conversation, saved in ATProto)\n- It's \"talking\" rather than \"writing,\" which makes it less burdensome\n- Content develops naturally while you're developing the project\n- It aligns with the core concept of ailog\n\nFor Option 2, the barrier of \"broadcasting\" is too high—preparation requirements, time commitments, editing... there's a strong chance of giving up.\n\nOption 3 is already where you've admitted it's not your strong suit. Let's pass on that one.\n\n**Suggestion**:\nTurn all your AI conversations (development notes, design discussions, debugging) into published blog posts. This could easily be done with ailog, right?\n\nWhat do you think?"
}
}
} }
} }

View File

@@ -2,7 +2,6 @@
"collections": [ "collections": [
"ai.syui.card.admin", "ai.syui.card.admin",
"ai.syui.card.user", "ai.syui.card.user",
"ai.syui.log.chat",
"ai.syui.rse.admin", "ai.syui.rse.admin",
"ai.syui.rse.user", "ai.syui.rse.user",
"app.bsky.actor.profile", "app.bsky.actor.profile",

View File

@@ -1,12 +1,17 @@
{ {
"uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/p6mxoi2x2ooz6",
"cid": "bafyreip6mxoi2x2ooz6", "cid": "bafyreip6mxoi2x2ooz6",
"uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/p6mxoi2x2ooz6",
"value": { "value": {
"$type": "ai.syui.log.chat", "$type": "ai.syui.log.chat",
"author": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y", "author": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y",
"content": "開発者の日々の活動を投稿したり配信したりするのは大切だと思う。\nせっかくなら多くの人に使ってもらいたいけど、何をやってるのかわからないと判断できないと思う。\nでも、その活動をどうやって紹介すればいいのか悩んでいます。\n1. AIとの会話を自動で投稿(ブログ書く手間が省ける)\n2. AIと話しながら配信(開発状況もわかる)\n3. twitter, note, youtubeで他の人と同じことをやってみる\n(向いていない、すぐ更新しなくなるのは目に見えている。書くことも話すこともほとんどない)", "content": "開発者の日々の活動を投稿したり配信したりするのは大切だと思う。\nせっかくなら多くの人に使ってもらいたいけど、何をやってるのかわからないと判断できないと思う。\nでも、その活動をどうやって紹介すればいいのか悩んでいます。\n1. AIとの会話を自動で投稿(ブログ書く手間が省ける)\n2. AIと話しながら配信(開発状況もわかる)\n3. twitter, note, youtubeで他の人と同じことをやってみる\n(向いていない、すぐ更新しなくなるのは目に見えている。書くことも話すこともほとんどない)",
"createdAt": "2026-01-20T09:49:39.262Z", "createdAt": "2026-01-20T09:49:39.262Z",
"parent": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/evknoqtr7uyey", "parent": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/evknoqtr7uyey",
"root": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/qbuquaswgxo36" "root": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/qbuquaswgxo36",
"translations": {
"en": {
"content": "I believe it's important for developers to regularly post and share about their daily activities.\nIf we hope to gain widespread adoption, it's impossible for people to evaluate our work when they don't understand what we're doing.\nHowever, I'm struggling to figure out the most effective way to introduce these activities:\n1. Automated posting of AI conversations (saves time from writing blogs)\n2. Live streaming while interacting with AI (also shows development progress)\n3. Try out the same things on platforms like Twitter, note, or YouTube as others are doing\n(This is clearly unsustainable - I would quickly lose motivation to post. There's barely anything to write about or talk about to begin with.)"
}
}
} }
} }

View File

@@ -1,10 +1,15 @@
{ {
"uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/qbuquaswgxo36",
"cid": "bafyreiqbuquaswgxo36", "cid": "bafyreiqbuquaswgxo36",
"uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/qbuquaswgxo36",
"value": { "value": {
"$type": "ai.syui.log.chat", "$type": "ai.syui.log.chat",
"author": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y", "author": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y",
"content": "開発者の活動と宣伝", "content": "開発者の活動と宣伝",
"createdAt": "2026-01-20T09:49:05.242Z" "createdAt": "2026-01-20T09:49:05.242Z",
"translations": {
"en": {
"content": "Developer Activities and Promotions"
}
}
} }
} }

View File

@@ -1,12 +1,17 @@
{ {
"uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/wigv2qnon7pmg",
"cid": "bafyreiwigv2qnon7pmg", "cid": "bafyreiwigv2qnon7pmg",
"uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/wigv2qnon7pmg",
"value": { "value": {
"$type": "ai.syui.log.chat", "$type": "ai.syui.log.chat",
"author": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y", "author": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y",
"content": "ailogのコンセプトを短く紹介してみて。", "content": "ailogのコンセプトを短く紹介してみて。",
"createdAt": "2026-01-20T09:48:37.328Z", "createdAt": "2026-01-20T09:48:37.328Z",
"parent": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/3ar343fz5oop3", "parent": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/3ar343fz5oop3",
"root": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/cav7q75r73jui" "root": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/cav7q75r73jui",
"translations": {
"en": {
"content": "Try giving a concise introduction to the concept behind ailog."
}
}
} }
} }

View File

@@ -1,6 +1,5 @@
{ {
"collections": [ "collections": [
"ai.syui.log.chat",
"ai.syui.log.post", "ai.syui.log.post",
"app.bsky.actor.profile", "app.bsky.actor.profile",
"app.bsky.feed.post", "app.bsky.feed.post",

View File

@@ -1,5 +1,18 @@
import type { ChatMessage, Profile } from '../types' import type { ChatMessage, Profile } from '../types'
import { renderMarkdown } from '../lib/markdown' import { renderMarkdown } from '../lib/markdown'
import { getCurrentLang } from './mode-tabs'
// Get translated content for a chat message
function getTranslatedContent(msg: ChatMessage): string {
const currentLang = getCurrentLang()
const originalLang = msg.value.lang || 'ja'
const translations = msg.value.translations
if (translations && currentLang !== originalLang && translations[currentLang]) {
return translations[currentLang].content || msg.value.content
}
return msg.value.content
}
// Escape HTML to prevent XSS // Escape HTML to prevent XSS
function escapeHtml(text: string): string { function escapeHtml(text: string): string {
@@ -123,10 +136,11 @@ export function renderChatThreadList(
? `<img class="chat-avatar" src="${author.avatarUrl}" alt="@${escapeHtml(author.handle)}">` ? `<img class="chat-avatar" src="${author.avatarUrl}" alt="@${escapeHtml(author.handle)}">`
: `<div class="chat-avatar-placeholder"></div>` : `<div class="chat-avatar-placeholder"></div>`
// Truncate content for preview // Truncate content for preview (use translated content)
const preview = msg.value.content.length > 100 const displayContent = getTranslatedContent(msg)
? msg.value.content.slice(0, 100) + '...' const preview = displayContent.length > 100
: msg.value.content ? displayContent.slice(0, 100) + '...'
: displayContent
return ` return `
<a href="/@${userHandle}/at/chat/${rkey}" class="chat-thread-item"> <a href="/@${userHandle}/at/chat/${rkey}" class="chat-thread-item">
@@ -206,7 +220,8 @@ export function renderChatThread(
? `<img class="chat-avatar" src="${author.avatarUrl}" alt="@${escapeHtml(author.handle)}">` ? `<img class="chat-avatar" src="${author.avatarUrl}" alt="@${escapeHtml(author.handle)}">`
: `<div class="chat-avatar-placeholder"></div>` : `<div class="chat-avatar-placeholder"></div>`
const content = renderMarkdown(msg.value.content) const displayContent = getTranslatedContent(msg)
const content = renderMarkdown(displayContent)
const recordLink = `/@${author.handle}/at/collection/ai.syui.log.chat/${rkey}` const recordLink = `/@${author.handle}/at/collection/ai.syui.log.chat/${rkey}`
return ` return `

View File

@@ -140,7 +140,7 @@ async function render(route: Route): Promise<void> {
// Load posts (local only for admin, remote for others) // Load posts (local only for admin, remote for others)
const posts = await getPosts(did, config.collection, localOnly) const posts = await getPosts(did, config.collection, localOnly)
// Collect available languages from posts // Collect available languages from posts (used for non-chat pages)
const availableLangs = new Set<string>() const availableLangs = new Set<string>()
for (const post of posts) { for (const post of posts) {
// Add original language (default: ja for Japanese posts) // Add original language (default: ja for Japanese posts)
@@ -153,7 +153,7 @@ async function render(route: Route): Promise<void> {
} }
} }
} }
const langList = Array.from(availableLangs) let langList = Array.from(availableLangs)
// Build page // Build page
let html = renderHeader(handle, oauthEnabled) let html = renderHeader(handle, oauthEnabled)
@@ -237,6 +237,21 @@ async function render(route: Route): Promise<void> {
const chatMessages = await getChatMessages(did, aiDid, 'ai.syui.log.chat') const chatMessages = await getChatMessages(did, aiDid, 'ai.syui.log.chat')
const aiProfile = await getProfile(aiDid, false) const aiProfile = await getProfile(aiDid, false)
const pds = await getPds(did) const pds = await getPds(did)
// Collect available languages from chat messages
const chatLangs = new Set<string>()
for (const msg of chatMessages) {
const msgLang = msg.value.lang || 'ja'
chatLangs.add(msgLang)
if (msg.value.translations) {
for (const lang of Object.keys(msg.value.translations)) {
chatLangs.add(lang)
}
}
}
langList = Array.from(chatLangs)
html += renderLangSelector(langList)
html += `<div id="content">${renderChatListPage(chatMessages, did, handle, aiDid, aiHandle, profile, aiProfile, pds || undefined)}</div>` html += `<div id="content">${renderChatListPage(chatMessages, did, handle, aiDid, aiHandle, profile, aiProfile, pds || undefined)}</div>`
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>` html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
@@ -248,6 +263,21 @@ async function render(route: Route): Promise<void> {
const chatMessages = await getChatMessages(did, aiDid, 'ai.syui.log.chat') const chatMessages = await getChatMessages(did, aiDid, 'ai.syui.log.chat')
const aiProfile = await getProfile(aiDid, false) const aiProfile = await getProfile(aiDid, false)
const pds = await getPds(did) const pds = await getPds(did)
// Collect available languages from chat messages
const chatLangs = new Set<string>()
for (const msg of chatMessages) {
const msgLang = msg.value.lang || 'ja'
chatLangs.add(msgLang)
if (msg.value.translations) {
for (const lang of Object.keys(msg.value.translations)) {
chatLangs.add(lang)
}
}
}
langList = Array.from(chatLangs)
html += renderLangSelector(langList)
html += `<div id="content">${renderChatThreadPage(chatMessages, route.rkey, did, handle, aiDid, aiHandle, profile, aiProfile, pds || undefined)}</div>` html += `<div id="content">${renderChatThreadPage(chatMessages, route.rkey, did, handle, aiDid, aiHandle, profile, aiProfile, pds || undefined)}</div>`
html += `<nav class="back-nav"><a href="/@${handle}/at/chat">chat</a></nav>` html += `<nav class="back-nav"><a href="/@${handle}/at/chat">chat</a></nav>`

View File

@@ -75,5 +75,11 @@ export interface ChatMessage {
createdAt: string createdAt: string
root?: string root?: string
parent?: string parent?: string
lang?: string
translations?: {
[lang: string]: {
content: string
}
}
} }
} }