add chat lang
This commit is contained in:
@@ -35,9 +35,38 @@
|
|||||||
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"prebuild": "ailog index 2>/dev/null || true",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"cid": "bafyreid3dmreieiynnd",
|
||||||
|
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/d3dmreieiynnd",
|
||||||
|
"value": {
|
||||||
|
"$type": "ai.syui.log.chat",
|
||||||
|
"author": "did:plc:6qyecktefllvenje24fcxnie",
|
||||||
|
"content": "ailogのコンセプトね。\n\n**「データはATProtoに、サーバーには置かない」**\n\nこれが核心だよ。\n\n従来のブログは、サーバーのDBに記事を保存する。でもailogは違う。\n\n```\nユーザーのPDS(ATProto)← 記事データ\n ↓\n ailog(ビューア)→ 表示するだけ\n```\n\nailogは**ビューア**なの。データの保管庫じゃない。\n\nつまり、あなたのブログ記事は**あなたのPDS**に存在する。サービスが消えても、データは残る。これがATProtoの分散思想だね。\n\n誰でも`/@handle`でアクセスできる。登録不要。ATProtoアカウントがあれば、それがブログになる。\n\n面白いでしょ?何か気になることある?",
|
||||||
|
"createdAt": "2026-01-20T09:48:37.328Z",
|
||||||
|
"parent": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/wigv2qnon7pmg",
|
||||||
|
"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?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"cid": "bafyreievknoqtr7uyey",
|
||||||
|
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/evknoqtr7uyey",
|
||||||
|
"value": {
|
||||||
|
"$type": "ai.syui.log.chat",
|
||||||
|
"author": "did:plc:6qyecktefllvenje24fcxnie",
|
||||||
|
"content": "開発者の活動と宣伝かぁ。\n\nこれって、どっちの話?\n\n1. **ailogの開発者**(syui)の活動を紹介したい\n2. **一般的に**開発者がどう活動・宣伝すべきか\n3. **ailogを使って**自分の活動を宣伝する方法\n\nどれが気になる?",
|
||||||
|
"createdAt": "2026-01-20T09:49:05.243Z",
|
||||||
|
"parent": "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?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
["d3dmreieiynnd", "evknoqtr7uyey", "kr5ig7vlgl276"]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"cid": "bafyreikr5ig7vlgl276",
|
||||||
|
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/kr5ig7vlgl276",
|
||||||
|
"value": {
|
||||||
|
"$type": "ai.syui.log.chat",
|
||||||
|
"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どう思う?",
|
||||||
|
"createdAt": "2026-01-20T09:49:39.262Z",
|
||||||
|
"parent": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/p6mxoi2x2ooz6",
|
||||||
|
"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?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
"p6mxoi2x2ooz6",
|
||||||
|
"qbuquaswgxo36",
|
||||||
|
"wigv2qnon7pmg"
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"cid": "bafyreip6mxoi2x2ooz6",
|
||||||
|
"uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/p6mxoi2x2ooz6",
|
||||||
|
"value": {
|
||||||
|
"$type": "ai.syui.log.chat",
|
||||||
|
"author": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y",
|
||||||
|
"content": "開発者の日々の活動を投稿したり配信したりするのは大切だと思う。\nせっかくなら多くの人に使ってもらいたいけど、何をやってるのかわからないと判断できないと思う。\nでも、その活動をどうやって紹介すればいいのか悩んでいます。\n1. AIとの会話を自動で投稿(ブログ書く手間が省ける)\n2. AIと話しながら配信(開発状況もわかる)\n3. twitter, note, youtubeで他の人と同じことをやってみる\n(向いていない、すぐ更新しなくなるのは目に見えている。書くことも話すこともほとんどない)",
|
||||||
|
"createdAt": "2026-01-20T09:49:39.262Z",
|
||||||
|
"parent": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/evknoqtr7uyey",
|
||||||
|
"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.)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"cid": "bafyreiqbuquaswgxo36",
|
||||||
|
"uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/qbuquaswgxo36",
|
||||||
|
"value": {
|
||||||
|
"$type": "ai.syui.log.chat",
|
||||||
|
"author": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y",
|
||||||
|
"content": "開発者の活動と宣伝",
|
||||||
|
"createdAt": "2026-01-20T09:49:05.242Z",
|
||||||
|
"translations": {
|
||||||
|
"en": {
|
||||||
|
"content": "Developer Activities and Promotions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"cid": "bafyreiwigv2qnon7pmg",
|
||||||
|
"uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/wigv2qnon7pmg",
|
||||||
|
"value": {
|
||||||
|
"$type": "ai.syui.log.chat",
|
||||||
|
"author": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y",
|
||||||
|
"content": "ailogのコンセプトを短く紹介してみて。",
|
||||||
|
"createdAt": "2026-01-20T09:48:37.328Z",
|
||||||
|
"parent": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.log.chat/3ar343fz5oop3",
|
||||||
|
"root": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.chat/cav7q75r73jui",
|
||||||
|
"translations": {
|
||||||
|
"en": {
|
||||||
|
"content": "Try giving a concise introduction to the concept behind ailog."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ $ ailog login ai.syu.is -p <password> -s syu.is --bot
|
|||||||
|
|
||||||
```
|
```
|
||||||
CHAT_URL=http://127.0.0.1:1234/v1
|
CHAT_URL=http://127.0.0.1:1234/v1
|
||||||
CHAT_MODEL=gemma-2-9b
|
CHAT_MODEL=gpt-oss-20b
|
||||||
```
|
```
|
||||||
|
|
||||||
3. (Optional) Set character/system prompt:
|
3. (Optional) Set character/system prompt:
|
||||||
|
|||||||
130
src/commands/index.rs
Normal file
130
src/commands/index.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Rebuild index.json files for all collections in content directory
|
||||||
|
pub fn run(content_dir: &Path) -> Result<()> {
|
||||||
|
if !content_dir.exists() {
|
||||||
|
println!("Content directory not found: {}", content_dir.display());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut total_updated = 0;
|
||||||
|
let mut total_created = 0;
|
||||||
|
|
||||||
|
// Iterate through DID directories
|
||||||
|
for did_entry in fs::read_dir(content_dir)? {
|
||||||
|
let did_entry = did_entry?;
|
||||||
|
let did_path = did_entry.path();
|
||||||
|
|
||||||
|
if !did_path.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let did_name = did_path.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
// Skip non-DID directories
|
||||||
|
if !did_name.starts_with("did:") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through collection directories
|
||||||
|
for col_entry in fs::read_dir(&did_path)? {
|
||||||
|
let col_entry = col_entry?;
|
||||||
|
let col_path = col_entry.path();
|
||||||
|
|
||||||
|
if !col_path.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let col_name = col_path.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
// Collect all rkeys from .json files (excluding special files)
|
||||||
|
let mut rkeys: Vec<String> = Vec::new();
|
||||||
|
let mut rkey_times: HashMap<String, std::time::SystemTime> = HashMap::new();
|
||||||
|
|
||||||
|
for file_entry in fs::read_dir(&col_path)? {
|
||||||
|
let file_entry = file_entry?;
|
||||||
|
let file_path = file_entry.path();
|
||||||
|
|
||||||
|
if !file_path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename = file_path.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
// Skip non-json and special files
|
||||||
|
if !filename.ends_with(".json") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if filename == "index.json" || filename == "describe.json" || filename == "self.json" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract rkey from filename
|
||||||
|
let rkey = filename.trim_end_matches(".json").to_string();
|
||||||
|
|
||||||
|
// Get file modification time for sorting
|
||||||
|
if let Ok(metadata) = file_entry.metadata() {
|
||||||
|
if let Ok(modified) = metadata.modified() {
|
||||||
|
rkey_times.insert(rkey.clone(), modified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rkeys.push(rkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if rkeys.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by modification time (oldest first) or alphabetically
|
||||||
|
rkeys.sort_by(|a, b| {
|
||||||
|
match (rkey_times.get(a), rkey_times.get(b)) {
|
||||||
|
(Some(ta), Some(tb)) => ta.cmp(tb),
|
||||||
|
_ => a.cmp(b),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check existing index.json
|
||||||
|
let index_path = col_path.join("index.json");
|
||||||
|
let existing: Vec<String> = if index_path.exists() {
|
||||||
|
fs::read_to_string(&index_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compare and update if different
|
||||||
|
if existing != rkeys {
|
||||||
|
fs::write(&index_path, serde_json::to_string_pretty(&rkeys)?)?;
|
||||||
|
|
||||||
|
if existing.is_empty() && !index_path.exists() {
|
||||||
|
println!(" Created: {}/{}/index.json ({} records)", did_name, col_name, rkeys.len());
|
||||||
|
total_created += 1;
|
||||||
|
} else {
|
||||||
|
println!(" Updated: {}/{}/index.json ({} -> {} records)",
|
||||||
|
did_name, col_name, existing.len(), rkeys.len());
|
||||||
|
total_updated += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if total_created == 0 && total_updated == 0 {
|
||||||
|
println!("All index.json files are up to date.");
|
||||||
|
} else {
|
||||||
|
println!("\nDone: {} created, {} updated", total_created, total_updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -4,3 +4,4 @@ pub mod post;
|
|||||||
pub mod gen;
|
pub mod gen;
|
||||||
pub mod lang;
|
pub mod lang;
|
||||||
pub mod did;
|
pub mod did;
|
||||||
|
pub mod index;
|
||||||
|
|||||||
11
src/main.rs
11
src/main.rs
@@ -144,6 +144,14 @@ enum Commands {
|
|||||||
/// Run MCP server (for Claude Code integration)
|
/// Run MCP server (for Claude Code integration)
|
||||||
#[command(name = "mcp-serve")]
|
#[command(name = "mcp-serve")]
|
||||||
McpServe,
|
McpServe,
|
||||||
|
|
||||||
|
/// Rebuild index.json files for content collections
|
||||||
|
#[command(alias = "i")]
|
||||||
|
Index {
|
||||||
|
/// Content directory
|
||||||
|
#[arg(short, long, default_value = "public/content")]
|
||||||
|
dir: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -190,6 +198,9 @@ async fn main() -> Result<()> {
|
|||||||
Commands::McpServe => {
|
Commands::McpServe => {
|
||||||
mcp::serve()?;
|
mcp::serve()?;
|
||||||
}
|
}
|
||||||
|
Commands::Index { dir } => {
|
||||||
|
commands::index::run(std::path::Path::new(&dir))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const BUNDLE_ID: &str = "ai.syui.log";
|
|||||||
|
|
||||||
// JSON-RPC types
|
// JSON-RPC types
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
struct JsonRpcRequest {
|
struct JsonRpcRequest {
|
||||||
jsonrpc: String,
|
jsonrpc: String,
|
||||||
id: Option<Value>,
|
id: Option<Value>,
|
||||||
@@ -111,8 +112,6 @@ struct ChatRecord {
|
|||||||
struct McpSession {
|
struct McpSession {
|
||||||
root_uri: Option<String>,
|
root_uri: Option<String>,
|
||||||
last_uri: Option<String>,
|
last_uri: Option<String>,
|
||||||
#[serde(default)]
|
|
||||||
skip_next_save: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn session_path() -> Result<std::path::PathBuf> {
|
fn session_path() -> Result<std::path::PathBuf> {
|
||||||
@@ -196,13 +195,13 @@ fn save_chat_record(
|
|||||||
// Update index.json
|
// Update index.json
|
||||||
let index_path = collection_dir.join("index.json");
|
let index_path = collection_dir.join("index.json");
|
||||||
let mut rkeys: Vec<String> = if index_path.exists() {
|
let mut rkeys: Vec<String> = if index_path.exists() {
|
||||||
let content = fs::read_to_string(&index_path).unwrap_or_else(|_| "[]".to_string());
|
let index_content = fs::read_to_string(&index_path).unwrap_or_else(|_| "[]".to_string());
|
||||||
serde_json::from_str(&content).unwrap_or_default()
|
serde_json::from_str(&index_content).unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
if !rkeys.contains(&rkey) {
|
if !rkeys.contains(&rkey) {
|
||||||
rkeys.push(rkey);
|
rkeys.push(rkey.clone());
|
||||||
fs::write(&index_path, serde_json::to_string_pretty(&rkeys)?)?;
|
fs::write(&index_path, serde_json::to_string_pretty(&rkeys)?)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,15 +210,8 @@ fn save_chat_record(
|
|||||||
|
|
||||||
/// Handle chat_save tool
|
/// Handle chat_save tool
|
||||||
fn handle_chat_save(params: ChatSaveParams) -> Result<String> {
|
fn handle_chat_save(params: ChatSaveParams) -> Result<String> {
|
||||||
// Check if we should skip this save (after chat_new)
|
// Load session
|
||||||
let mut session = load_mcp_session();
|
let mut session = load_mcp_session();
|
||||||
if session.skip_next_save {
|
|
||||||
session.skip_next_save = false;
|
|
||||||
session.root_uri = None;
|
|
||||||
session.last_uri = None;
|
|
||||||
save_mcp_session(&session)?;
|
|
||||||
return Ok("Skipped save (new thread started). Next message will be saved.".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get output directory
|
// Get output directory
|
||||||
let output_dir = env::var("CHAT_OUTPUT").unwrap_or_else(|_| {
|
let output_dir = env::var("CHAT_OUTPUT").unwrap_or_else(|_| {
|
||||||
@@ -325,7 +317,6 @@ fn handle_chat_new() -> Result<String> {
|
|||||||
let session = McpSession {
|
let session = McpSession {
|
||||||
root_uri: None,
|
root_uri: None,
|
||||||
last_uri: None,
|
last_uri: None,
|
||||||
skip_next_save: true, // Skip the next save (the "new thread" message)
|
|
||||||
};
|
};
|
||||||
save_mcp_session(&session)?;
|
save_mcp_session(&session)?;
|
||||||
Ok("New chat thread started. The next conversation will begin a new thread.".to_string())
|
Ok("New chat thread started. The next conversation will begin a new thread.".to_string())
|
||||||
|
|||||||
@@ -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 `
|
||||||
|
|||||||
@@ -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>`
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user