9 Commits

Author SHA1 Message Date
7800a655f3 fix profile 2025-07-13 08:12:40 +09:00
76c797e4d8 add blog post 2025-07-13 07:52:43 +09:00
d1a1c92842 update binary 2025-07-11 13:38:22 +09:00
9da1f87640 fix update version 2025-07-11 13:09:15 +09:00
ddfc43512c add md msg 2025-07-11 08:52:34 +09:00
b3ccd61935 add my-blog msg 2025-07-11 08:51:46 +09:00
a243b6a44e fix post filename 2025-07-05 15:42:36 +09:00
e3c1cf4790 fix build err 2025-07-05 15:31:04 +09:00
a6236661bf post 2025-07-05 15:30:55 +09:00
22 changed files with 761 additions and 52 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "ailog"
version = "0.2.6"
version = "0.2.7"
edition = "2021"
authors = ["syui"]
description = "A static blog generator with AI features"

Binary file not shown.

View File

@ -155,3 +155,21 @@ fn main() {
console.log("Hello, world!");
```
## msg
[msg type="info" content="これは情報メッセージです。重要な情報を読者に伝えるために使用します。"]
{{< msg type="warning" content="これは警告メッセージです。注意が必要な情報を示します。" >}}
[msg type="error" content="これはエラーメッセージです。問題やエラーを示します。"]
{{< msg type="success" content="これは成功メッセージです。操作が成功したことを示します。" >}}
[msg type="note" content="これはノートメッセージです。補足情報や備考を示します。"]
[msg content="これはデフォルトメッセージです。タイプが指定されていない場合、自動的に情報メッセージとして表示されます。"]
## img-compare
[img-compare before="/img/ue_blender_model_ai_v0401.png" after="/img/ue_blender_model_ai_v0501.png" width="800" height="300"]

View File

@ -20,7 +20,13 @@ oauthを`bsky.social`, `syu.is`ともに動くようにしました。
usernameは`handle`という`domain`の形を採用しています。
didの名前解決をしているのが`plc`です。pdsuserのdataを保存しています。timelineに配信したり表示しているのがbsky, bgsです。
didの名前解決(dns)をしているのが`plc`です。`pds`userのdataを保存しています。timelineに配信したり表示しているのが`bsky(appview)`, 統合しているのが`bgs`です。
その他、`social-app`がclientで、`ozone`がmoderationです。
```sh
"6qyecktefllvenje24fcxnie" -> "ai.syu.is"
```
## oauthでハマったところ
@ -36,15 +42,22 @@ $ curl -sL https://plc.directory/$did|jq .alsoKnownAs
[ "at://ai.syu.is" ]
```
しかし、みて分かる通り、pds, plcは`@ai.syu.is`で登録されており、handle-changeが更新されていないようです。
しかし、みて分かる通り、bskyではhandle-changeが反映されていますが、pds, plcは`@ai.syu.is`で登録されており、更新されていないようです。
```sh
$ handle=ai.syui.ai
$ curl -sL "https://syu.is/xrpc/com.atproto.identity.resolveHandle?handle=$handle" | jq -r .did
did:plc:6qyecktefllvenje24fcxnie
$ curl -sL "https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=$handle" | jq -r .did
null
$ curl -sL "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=$handle" | jq -r .did
did:plc:6qyecktefllvenje24fcxnie
```
[msg type="warning" content="現在はbsky.teamのplc, pdsにもhandle-changeが反映されています。"]
oauthは、そのままではbsky.teamのpds, plcを使って名前解決を行います。この場合、まず、それらのserverにdidが登録されている必要があります。
次に、handleの更新が反映されている必要があります。もし反映されていない場合、handleとpasswordが一致しません。

View File

@ -1,5 +1,5 @@
---
title: "world systemのupdateとmodelの改良"
title: "world system v0.2"
slug: "ue"
date: 2025-06-30
tags: ["ue", "blender"]
@ -33,36 +33,7 @@ draft: false
5. 横から惑星に突入できるようになった
```
## blender
まず、昔のmodelはクオリティの関係もあり、一時的にnahidaのmodelを参考にしていました。今回はオリジナリティを強化したため、クオリティは下がりましたが、素体と衣装を別々に作り組み合わせました。また、materialも分離したため、装飾がピカピカ光るようになりました。
blenderの使い方が少しわかってきたのでやってよかったです。
> vroid(vrm) -> blender(nahida) -> blender(original)
[img-compare before="/img/ue_blender_model_ai_v0401.png" after="/img/ue_blender_model_ai_v0501.png" width="800" height="300"]
[img-compare before="/img/ue_blender_model_ai_v0402.png" after="/img/ue_blender_model_ai_v0502.png" width="800" height="300"]
特に難しかったのは、指のウェイトペイントです。これは指全体をまんべんなく塗ることで解決しました。
また、昔からあった衣装のガビガビは重複する面を削除することで解消できました。
```md
全選択A キー)
Mesh → Clean Up → Merge by Distance
距離を0.000にして実行
```
しかし、まだまだ問題があり、細かな調整が必要です。
```sh
[issue]
1. 衣装同士、あるいは体が多少すり抜ける事がある
2. 指先、足先がちょっと気になる。ボーンの調整が完璧ではない
3. 後ろの装飾衣装を考えている。ひらひらのマントぽいものがあるといい
```
面白い動画ではありませんが、現状を記録しておきます。

View File

@ -0,0 +1,81 @@
---
title: "aiosを作り直した"
slug: "aios"
date: 2025-07-05
tags: ["os"]
draft: false
---
`aios`とは自作osのことで、archlinuxをベースにしていました。
```sh
#!/bin/zsh
git clone https://gitlab.archlinux.org/archlinux/archiso
cp -rf ./cfg/profiledef.sh ./archiso/configs/releng/profiledef.sh
cp -rf ./cfg/profiledef.sh ./archiso/configs/baseline/profiledef.sh
cp -rf ./scpt/mkarchiso ./archiso/archiso/mkarchiso
./archiso/archiso/mkarchiso -v -o ./ ./archiso/configs/releng/
tar xf aios-bootstrap*.tar.gz
mkdir -p root.x86_64/var/lib/machines/arch
pacstrap -c root.x86_64/var/lib/machines/arch base
echo -e 'Server = http://mirrors.cat.net/archlinux/$repo/os/$arch
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch' >> ./root.x86_64/etc/pacman.d/mirrorlist
sed -i s/CheckSpace/#CheckeSpace/ root.x86_64/etc/pacman.conf
arch-chroot root.x86_64 /bin/sh -c 'pacman-key --init'
arch-chroot root.x86_64 /bin/sh -c 'pacman-key --populate archlinux'
arch-chroot root.x86_64 /bin/sh -c 'pacman -Syu --noconfirm base base-devel linux'
tar -zcvf aios-bootstrap.tar.gz root.x86_64/
```
```sh:./cfg/profiledef.sh
#!/usr/bin/env bash
# shellcheck disable=SC2034
iso_name="aios"
iso_label="AI_$(date --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y%m)"
iso_publisher="ai os <https://git.syui.ai/ai/os>"
iso_application="ai os Live/Rescue DVD"
iso_version="$(date --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y.%m.%d)"
install_dir="ai"
#buildmodes=('iso')
buildmodes=('bootstrap')
bootmodes=('bios.syslinux.mbr' 'bios.syslinux.eltorito'
'uefi-ia32.grub.esp' 'uefi-x64.grub.esp'
'uefi-ia32.grub.eltorito' 'uefi-x64.grub.eltorito')
arch="x86_64"
pacman_conf="pacman.conf"
airootfs_image_type="squashfs"
airootfs_image_tool_options=('-comp' 'xz' '-Xbcj' 'x86' '-b' '1M' '-Xdict-size' '1M')
file_permissions=(
["/etc/shadow"]="0:0:400"
["/root"]="0:0:750"
["/root/.automated_script.sh"]="0:0:755"
["/root/.gnupg"]="0:0:700"
["/usr/local/bin/choose-mirror"]="0:0:755"
["/usr/local/bin/Installation_guide"]="0:0:755"
["/usr/local/bin/livecd-sound"]="0:0:755"
)
```
## rust + unix
一からosを作りたいと思っていたので、rustでunixのosを作り始めました。
![](/img/aios_v0201.png)
名前は`Aios`にして、今回は`syui`のprojectとして作り始めました。
後に`ai/os`と統合するかもしれません。
1. [https://git.syui.ai/ai/os](https://git.syui.ai/ai/os)
2. [https://git.syui.ai/syui/aios](https://git.syui.ai/syui/aios)
```sh
#!/bin/zsh
d=${0:a:h:h}
cd $d/kernel
cargo bootimage --release
BOOT_IMAGE="../target/x86_64-unknown-none/release/bootimage-aios-kernel.bin"
qemu-system-x86_64 -drive format=raw,file="$BOOT_IMAGE"
```

View File

@ -0,0 +1,114 @@
---
title: "yui system v0.2.1"
slug: "blender"
date: 2025-07-11
tags: ["blender", "ue", "vmc"]
draft: false
---
`yui system`をupdateしました。別名、`unique system`ともいい、プレイヤーの唯一性を担保するためのもので、キャラクターのモデルもここで管理します。
今回は、blenderでモデルを作り直している話になります。
## blenderで作るvrm
モデルをblenderで作り直すことにしました。
vroidからblenderに移行。blenderでmodelを作る作業はとても大変でした。
今回は、素体と衣装を別々に作り組み合わせています。完成度の高いモデルをいくつか参考にしています。
materialも分離したため、ue5で指定しやすくなりました。これによって変身時にue5のmaterialを指定しています。eyeのmaterialを分離して色を付けています。
![](/img/ue_blender_model_ai_v0604.png)
## modelの変遷
[img-compare before="/img/ue_blender_model_ai_v0601.png" after="/img/ue_blender_model_ai_v0602.png" width="800" height="300"]
[msg type="info" content="v0.1: vroidからblenderへ移行。blenderは初めてなので簡単なことだけ実行。"]
[img-compare before="/img/ue_blender_model_ai_v0602.png" after="/img/ue_blender_model_ai_v0603.png" width="800" height="300"]
[msg type="info" content="v0.2: blenderの使い方を次の段階へシフト。最初から作り直す。様々な問題が発生したが、大部分を解消した。"]
しかし、まだまだ問題があり、細かな調整が必要です。
[msg type="error" content="衣装同士、あるいは体が多少すり抜ける事がある。ウェイトペイントやボーンの調整が完璧ではない。"]
## eyeが動かない問題を解決
`vmc`で目玉であるeyeだけ動かないことに気づいて修正しました。
`eye`の部分だけvroid(vrm)のboneを使うことで解決できました。しかし、新たにblenderかvrm-addonのbugに遭遇しました。具体的にはboneがxyz軸で動かせなくなるbugです。これは不定期で発生していました。boneを動かせるときと動かせなくなるときがあり、ファイルは同じものを使用。また、スクリプト画面ではboneを動かせます。
## 指先がうまく動かない問題を解決
vmcで指先の動きがおかしくなるので、ウェイトペイントを塗り直すと治りました。
## worldscapeで足が浮いてしまう問題を解決
worldscapeでは陸地に降り立つとプレイヤーが浮いてしまいます。
gaspのabpでfoot placementを外す必要がありました。これは、モデルの問題ではなく、gaspのキャラクターすべてで発生します。
ここの処理を削除します。
<iframe src="https://blueprintue.com/render/wrrxz9vm" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
## 衣装のガビガビを解決
昔からあった衣装のガビガビは重複する面を削除することで解消できました。
```md
全選択A キー)
Mesh → Clean Up → Merge by Distance
距離を0.000にして実行
```
## materialの裏表を解決
これはue5で解消したほうがいいでしょう。編集していると、面の裏表の管理が面倒なことがあります。
materialで`Two Sided`を有効にします。
## キャラクターのエフェクトを改良
これらの処理を簡略化できました。最初は雑に書いていましたが、vrmは何度も修正し、上書きされますから、例えば、`SK_Mesh`でmaterialを設定する方法はよくありません。
<iframe src="https://blueprintue.com/render/gue0vayu" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
## gameplay camera pluginをue5.6に対応
ue5.5と5.6では関数も他の処理も変わっていて、rotationを`BP_Player`でsetすると、crashするbugがあります。
基本的には、`Blueprints/Cameras/CameraRigPrefab_BasicThiredPersonBehavior`をみてください。
<iframe src="https://blueprintue.com/render/-e0r7oxq" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
![](https://git.syui.ai/attachments/019d2079-1450-4271-8816-ded92f60b3c9)
キャラクターが動く場合は、`Update Rotation Pre CMC`にある`Use Controller Desired Rotation`, `Orient Rotation To Movement`の処理です。両方を`true`にしましょう。
`vmc`時もこれで対処します。
## gaspでidle, sprintをオリジナルに変更
これはabpで設定します。設定方法はue5.5と変わりません。
[https://ue-book.syui.ai/gasp/11_run.html](https://ue-book.syui.ai/gasp/11_run.html)
## vrm4uのvmcに対応
まず、clientはwabcam motion captureが最も自然に動作しています。
[msg type="warning" content="これは1年くらい前の検証結果です。現在はもっとよいvmc clientの選択肢があるかもしれません。"]
次に、`ABP_Pose_$NAME`が作られますが、vrmはよく更新しますので、`SK_Mesh`でcustom ABPを指定すると楽でしょう。
![](https://git.syui.ai/attachments/758407eb-5e77-4876-830b-ba4a78884e8d)
## youtube
<iframe width="100%" height="420" src="https://www.youtube.com/embed/qggHtmkMIko?vq=hd1080&rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

View File

@ -0,0 +1,36 @@
---
title: "yui system v0.2.2"
slug: "blender2"
date: 2025-07-11
tags: ["blender", "ue", "vmc"]
draft: false
---
新しい問題を発見したので、それらを解消しました。
## wingがbodyに入り込んでしまう
wingとmodelは分離させています。衣装の着せ替えを簡単にできるようにすること。それが新しく作ったblender modelの方針でした。
ただ、調整が難しくなったのも事実で、例えば、colliderの調整ができません。これによってbodyに入り込んでしまうことが多くなりました。
これは、とりあえず、wingのcolliderやboneを追加すること、そして、modelのneckに変更することで解消しました。
ただし、この方法も完璧ではないかもしれません。
## vmcではwingが追従しない
modelと分離しているので、vmc時には追従しません。したがって、wingのabpでmodelと同じvmcを入れます。これで解消できました。
## vrmでcustom abpを使用するとueがcrashする
vrm4uで`.vrm`をimportすると`SK_$NAME`にcustom abpを設定していた場合はueがcrashします。
上書きimportするならこれをnone(clear)に変更します。
## modelの頭身を調整
比較画像を出した際に、少しmodelのバランスが悪かったので調整しました。
具体的には、髪の毛を少し下げました。

View File

@ -0,0 +1,157 @@
---
title: "自作ゲームのsystemを説明する"
slug: "game"
date: 2025-07-12
tags: ["ue"]
draft: false
---
現在、自作ゲームを開発しています。
このゲームには4つの柱があり、それらはsystemで分けられています。そして、systemは根本的な2つの価値観に基づきます。
根本的な2つの価値観は、(1)現実を反映すること、(2)この世界に同じものは一つもないという唯一性になります。
1. 現実の反映
2. 唯一性の担保
では、各systemについて説明していきます。
# system
## world system
別名、planet systemといいます。
現実の反映という価値観から、ゲーム世界もできる限り現実に合わせようと思いworld systemを作っています。
ゲームは通常、平面世界です。これはゲームエンジンのルールであり、基本的にゲーム世界は平面をベースにしています。
ですから、例えば、上に行っても、下に行っても、あるいは右に行っても、左に行っても、ずっと地平線が広がっています。
しかし、現実世界では、上に行けば、やがて大気圏を越え、宇宙に出ます。
最初は昔から認知されていた地球、月、太陽という3つの星を現実に合わせて作りました。
そして、マップをできる限り惑星形式にします。
これは非常に難しいことで、現在もいくつか問題を抱えています。
ただし、このworld systemの問題がゲームプレイに影響するかと言われると、殆どの場合、影響しません。ゲームプレイの領域は、最初は非常に狭い範囲で作ろうと思っています。小さなところから完璧に作っていきたいという思いがあります。
つまり、プレイヤーは空にも宇宙にも到達できません。それが見えるかどうかもわかりません。しかし、見えない部分もしっかりと作り、世界があるということが私にとって大切です。
まずは、狭いけど完璧な空間を作り、そこでゲームシステムを完成させます。広い世界はできる限り見えないようにしたほうがいいでしょう。夢の世界のような狭い空間を作り、そこでシンプルで小さいゲームができます。もちろん、広い世界に出ることはできません。そもそもこのゲーム、見えない部分をちゃんと作る、そこにも世界がちゃんとあるというのをテーマにしているので、広い世界で何かをやるようなゲームを目指していなかったりします。なにかのときに垣間見える、かもしれない外の世界、広い世界。それを感じられることがある、ということ。それが重要なので、このsystem自体は背景に過ぎないのです。
最初から広い世界があるのではなく`狭い世界 -> 広い世界`への移行が重要だと考えています。この移行に関しては、演出というテーマに基づき、設計する必要があります。それがゲームとしての面白さを作る、ということなのだと思います。
## yui system
別名、unique systemといいます。プレイヤーの唯一性を担保するためのsystemです。
とはいえ、色々なものがここに詰め込まれるでしょう。characterのモデリングとかもそうですね。
どのように担保していくかは未定ですが、いくつか案があります。配信との連携、vmcでモーションキャプチャなどを考えていました。
## ai system
別名、ability systemといいます。
主に、ゲーム性に関することです。ゲーム性とはなにか。それは、永続するということです。
例えば、将棋やオセロを考えてみてください。無限の組み合わせがあり、可能であればずっと遊んでいられる。そのような仕組みを目指します。
まずは属性を物語から考えます。物語は最も小さい物質の探求です。アクシオンやバリオンなどの架空の物質、そして、中性子や原子などの現実の物質が属性となり、1キャラクターにつき1属性を持ちます。
## at system
別名、account systemといいます。
プレイヤーが現実のアカウントを使用してプレイできることを目指します。`atproto`を採用して、ゲームデータを個人のアカウントが所有することを目指しています。
# 現実の反映とはなにか
わかり易い言葉で「現実の反映」を目指すと言いましたが、これはどういうことでしょう。
私の中では「同一性」とも言い換えられます。
例えば、現実の世界とゲームの世界があるのではなく「すべてが現実である」という考え方をします。言い換えると「すべて同じもの」ということ。
もし多くの人が現実世界とゲーム世界を別物と捉えているなら、できる限りその認識を壊す方向で考えます。
例えば、`at system`では現実のsnsアカウントをゲームアカウントに使用したり、現実の出来事をゲームに反映したり、またはゲームの出来事を現実に反映する仕組みを考えます。
全ては一つ、一つはすべて。
同一性と唯一性は一見して矛盾しますが、その統合を考えます。
# 物語と実装
```md
# 物語-存在
同一性
唯一性
# system-実装
world system
yui system
ai system
at system
```
物語では、この世界のものは全て存在であると説きます。存在しかない世界。存在だけがある世界。そして、あらゆる存在を構築しているこの世界で最も小さいものが「存在子」です。存在子は別名、アイといいます。そして、このアイにも同じものはありません。すべての存在子は異なるもの、別の意識。
アイは、最初に生まれたキャラクターとして、アイ属性を扱います。これらの設定は`ai system`の領域です。アイは自分のことをアイと呼びます。
> アイね、この世界と一緒だから。この世界に同じものは一つもないよ。
これはアイのセリフ。存在の世界の同一性と唯一性のことを言っているのです。
# どこまで実装できた
実は、上記のsystemは既にすべてを実装したことがあります。
```md
[at system]
ゲームが始まると、atprotoのaccountでloginでき、取得したアイテムなどはatproto(pds)に保存されます。
[ai system]
キャラクターは属性攻撃ができます。
[world system]
上へ上へと飛んでいけば、雲を超え、宇宙空間に出られます。
[yui system]
配信環境やvmcでキャラクターを動かすことができます。
```
しかし、ue5.5で作っていたsystemも、ue5.6にupdateすると全て動かなくなりました。また一から作り直しています。私は、モデルの作り方から、ゲームの作り方まで初心者ですから、何度も作り直すことで、ゲーム作りを覚えられます。
そして、まだ革新的なアイディアを見つけられていません。それはシンプルで身近にあり、人々が面白いと思うもの。まだゲームになっていない、あるいはあまり知られていないものである必要があります。
例えば、ウマ娘でいうと競馬、ポケモンでいうと捕獲、になります。
それを見つけ、ゲームに取り込む事ができれば完成と言えるでしょう。
そして、ゲームに取り込むことが複雑で難しすぎるようなものではありません。シンプルで単純でわかりやすいものでなければなりません。
## versionを付ける
そろそろversionを付けるかどうか迷っています。
今までモヤモヤしていたものが、最近はよりはっきりしてきたと感じます。ただ、versionはあまり覚えていないし、付ける意味もない。これまではそうでした。
もしかすると今もそうかもしれません。色々なものがバラバラで管理しきれないのです。
ですが、今までやってきたことを総合すると、現在は、`v0.2`くらいだと思います。
最初、はじめてueを触ったときに宇宙マップを使って構築しました。これをv0.0としましょう。
次に、city sampleと宇宙を統合しました。これがv0.1です。
最近はworldscapeを使ってマップを構築しています。これがv0.2です。
aiというキャラクターモデルの変遷も大体を3つの段階に分けられると思います。初めてモデルを作った、vroidで作ったのがv0.0、blenderを初めて触ったのがv0.1、現在がv0.2です。
とはいえ、この設定もそのうち忘れ、どこかで圧縮されてしまうかもしれませんが、覚えているならここから徐々にversionが上がっていくでしょう。

View File

@ -1340,3 +1340,112 @@ article.article-content {
}
}
/* Message Components */
.msg {
display: flex;
align-items: flex-start;
margin: 20px 0;
padding: 16px;
border-radius: 8px;
border-left: 4px solid;
font-size: 14px;
line-height: 1.5;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.msg-symbol {
font-size: 18px;
font-weight: bold;
margin-right: 12px;
margin-top: 2px;
min-width: 20px;
text-align: center;
}
.msg-content {
flex: 1;
}
.msg-content p {
margin: 0;
color: inherit;
}
/* Message type styles */
.msg.message {
background-color: #f0f8ff;
border-left-color: #2196f3;
color: #1565c0;
}
.msg.message .msg-symbol {
color: #2196f3;
}
.msg.warning {
background-color: #fffbf0;
border-left-color: #ff9800;
color: #f57c00;
}
.msg.warning .msg-symbol {
color: #ff9800;
}
.msg.error {
background-color: #fff5f5;
border-left-color: #f44336;
color: #d32f2f;
}
.msg.error .msg-symbol {
color: #f44336;
}
.msg.success {
background-color: #f0fff0;
border-left-color: #4caf50;
color: #388e3c;
}
.msg.success .msg-symbol {
color: #4caf50;
}
.msg.note {
background-color: #faf5ff;
border-left-color: #9c27b0;
color: #7b1fa2;
}
.msg.note .msg-symbol {
color: #9c27b0;
}
/* Responsive message styles */
@media (max-width: 768px) {
.msg {
margin: 15px 0;
padding: 12px;
font-size: 13px;
}
.msg-symbol {
font-size: 16px;
margin-right: 8px;
}
}
@media (max-width: 480px) {
.msg {
margin: 10px 0;
padding: 10px;
font-size: 12px;
}
.msg-symbol {
font-size: 14px;
margin-right: 6px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -1,6 +1,6 @@
{
"name": "ailog-oauth",
"version": "0.2.5",
"version": "0.2.7",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -1,6 +1,36 @@
import React from 'react'
import React, { useState } from 'react'
// Helper function to get correct web URL based on avatar URL
function getCorrectWebUrl(avatarUrl) {
if (!avatarUrl) return 'https://bsky.app'
// If avatar is from bsky.app (main Bluesky), use bsky.app
if (avatarUrl.includes('cdn.bsky.app') || avatarUrl.includes('bsky.app')) {
return 'https://bsky.app'
}
// If avatar is from syu.is, use web.syu.is
if (avatarUrl.includes('bsky.syu.is') || avatarUrl.includes('syu.is')) {
return 'https://syu.is'
}
// Default to bsky.app
return 'https://bsky.app'
}
export default function ChatRecordList({ chatPairs, apiConfig, user = null, agent = null, onRecordDeleted = null }) {
const [expandedRecords, setExpandedRecords] = useState(new Set())
const toggleJsonView = (key) => {
const newExpanded = new Set(expandedRecords)
if (newExpanded.has(key)) {
newExpanded.delete(key)
} else {
newExpanded.add(key)
}
setExpandedRecords(newExpanded)
}
if (!chatPairs || chatPairs.length === 0) {
return (
<section>
@ -68,10 +98,30 @@ export default function ChatRecordList({ chatPairs, apiConfig, user = null, agen
</div>
)}
<div className="user-info">
<div className="display-name">{chatPair.question.value.author?.displayName || chatPair.question.value.author?.handle}</div>
<div className="display-name">
{chatPair.question.value.author?.displayName || chatPair.question.value.author?.handle}
{chatPair.question.value.author?.handle === 'syui' && <span className="admin-badge"> Admin</span>}
</div>
<div className="handle">
<a
href={`${getCorrectWebUrl(chatPair.question.value.author?.avatar)}/profile/${chatPair.question.value.author?.did}`}
target="_blank"
rel="noopener noreferrer"
className="handle-link"
>
@{chatPair.question.value.author?.handle}
</a>
</div>
</div>
{canDelete(chatPair) && (
<div className="record-actions">
<div className="record-actions">
<button
onClick={() => toggleJsonView(`${chatPair.rkey}-question`)}
className={`btn btn-sm ${expandedRecords.has(`${chatPair.rkey}-question`) ? 'btn-outline' : 'btn-primary'}`}
title="Show/Hide JSON"
>
{expandedRecords.has(`${chatPair.rkey}-question`) ? 'hide' : 'json'}
</button>
{canDelete(chatPair) && (
<button
onClick={() => handleDelete(chatPair)}
className="btn btn-danger btn-sm"
@ -79,9 +129,16 @@ export default function ChatRecordList({ chatPairs, apiConfig, user = null, agen
>
delete
</button>
</div>
)}
)}
</div>
</div>
{expandedRecords.has(`${chatPair.rkey}-question`) && (
<div className="json-display">
<pre className="json-content">
{JSON.stringify(chatPair.question, null, 2)}
</pre>
</div>
)}
<div className="message-content">{chatPair.question.value.text}</div>
</div>
)}
@ -102,9 +159,37 @@ export default function ChatRecordList({ chatPairs, apiConfig, user = null, agen
</div>
)}
<div className="user-info">
<div className="display-name">{chatPair.answer.value.author?.displayName || chatPair.answer.value.author?.handle}</div>
<div className="display-name">
{chatPair.answer.value.author?.displayName || chatPair.answer.value.author?.handle}
</div>
<div className="handle">
<a
href={`${getCorrectWebUrl(chatPair.answer.value.author?.avatar)}/profile/${chatPair.answer.value.author?.did}`}
target="_blank"
rel="noopener noreferrer"
className="handle-link"
>
@{chatPair.answer.value.author?.handle}
</a>
</div>
</div>
<div className="record-actions">
<button
onClick={() => toggleJsonView(`${chatPair.rkey}-answer`)}
className={`btn btn-sm ${expandedRecords.has(`${chatPair.rkey}-answer`) ? 'btn-outline' : 'btn-primary'}`}
title="Show/Hide JSON"
>
{expandedRecords.has(`${chatPair.rkey}-answer`) ? 'hide' : 'json'}
</button>
</div>
</div>
{expandedRecords.has(`${chatPair.rkey}-answer`) && (
<div className="json-display">
<pre className="json-content">
{JSON.stringify(chatPair.answer, null, 2)}
</pre>
</div>
)}
<div className="message-content">{chatPair.answer.value.text}</div>
</div>
)}

View File

@ -1,6 +1,35 @@
import React from 'react'
import React, { useState } from 'react'
// Helper function to get correct web URL based on avatar URL
function getCorrectWebUrl(avatarUrl) {
if (!avatarUrl) return 'https://bsky.app'
// If avatar is from bsky.app (main Bluesky), use bsky.app
if (avatarUrl.includes('cdn.bsky.app') || avatarUrl.includes('bsky.app')) {
return 'https://bsky.app'
}
// If avatar is from syu.is, use web.syu.is
if (avatarUrl.includes('bsky.syu.is') || avatarUrl.includes('syu.is')) {
return 'https://syu.is'
}
// Default to bsky.app
return 'https://bsky.app'
}
export default function ProfileRecordList({ profileRecords, apiConfig, user = null, agent = null, onRecordDeleted = null }) {
const [expandedRecords, setExpandedRecords] = useState(new Set())
const toggleJsonView = (uri) => {
const newExpanded = new Set(expandedRecords)
if (newExpanded.has(uri)) {
newExpanded.delete(uri)
} else {
newExpanded.add(uri)
}
setExpandedRecords(newExpanded)
}
if (!profileRecords || profileRecords.length === 0) {
return (
<section>
@ -62,9 +91,26 @@ export default function ProfileRecordList({ profileRecords, apiConfig, user = nu
<span className="admin-badge"> Admin</span>
)}
</div>
<div className="handle">
<a
href={`${getCorrectWebUrl(profile.value.author?.avatar)}/profile/${profile.value.author?.did}`}
target="_blank"
rel="noopener noreferrer"
className="handle-link"
>
@{profile.value.author?.handle}
</a>
</div>
</div>
{canDelete(profile) && (
<div className="record-actions">
<div className="record-actions">
<button
onClick={() => toggleJsonView(profile.uri)}
className={`btn btn-sm ${expandedRecords.has(profile.uri) ? 'btn-outline' : 'btn-primary'}`}
title="Show/Hide JSON"
>
{expandedRecords.has(profile.uri) ? 'hide' : 'json'}
</button>
{canDelete(profile) && (
<button
onClick={() => handleDelete(profile)}
className="btn btn-danger btn-sm"
@ -72,9 +118,16 @@ export default function ProfileRecordList({ profileRecords, apiConfig, user = nu
>
delete
</button>
</div>
)}
)}
</div>
</div>
{expandedRecords.has(profile.uri) && (
<div className="json-display">
<pre className="json-content">
{JSON.stringify(profile, null, 2)}
</pre>
</div>
)}
<div className="message-content">{profile.value.text}</div>
</div>
))}

View File

@ -11,13 +11,18 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
logger.log('RecordTabs: activeTab is', activeTab)
// Filter records based on page context
const filterRecords = (records) => {
const filterRecords = (records, isProfile = false) => {
if (pageContext.isTopPage) {
// Top page: show latest 3 records
return records.slice(0, 3)
} else {
// Individual page: show records matching the URL
return records.filter(record => {
// Profile records should always be shown
if (isProfile || record.value?.type === 'profile') {
return true
}
const recordUrl = record.value?.post?.url
if (!recordUrl) return false
@ -44,7 +49,7 @@ export default function RecordTabs({ langRecords, commentRecords, userComments,
if (a.value.profileType !== 'admin' && b.value.profileType === 'admin') return 1
return 0
})
const filteredProfileRecords = filterRecords(sortedProfileRecords)
const filteredProfileRecords = filterRecords(sortedProfileRecords, true)
return (
<div className="record-tabs">

View File

@ -13,6 +13,7 @@ impl ShortcodeProcessor {
// Register built-in shortcodes
processor.register_img_compare();
processor.register_message();
processor
}
@ -24,14 +25,21 @@ impl ShortcodeProcessor {
);
}
fn register_message(&mut self) {
self.shortcodes.insert(
"msg".to_string(),
Box::new(|attrs| Self::parse_message_shortcode(attrs)),
);
}
pub fn process(&self, content: &str) -> String {
let mut processed = content.to_string();
// Process {{< shortcode >}} format (Hugo-style)
let hugo_regex = Regex::new(r#"\{\{\<\s*(\w+(?:-\w+)*)\s+([^>]*)\s*\>\}\}"#).unwrap();
let hugo_regex = Regex::new(r#"\{\{<\s*(\w+(?:-\w+)*)\s*([^>]*)\s*>\}\}"#).unwrap();
processed = hugo_regex.replace_all(&processed, |caps: &regex::Captures| {
let shortcode_name = &caps[1];
let attrs = &caps[2];
let attrs = caps.get(2).map(|m| m.as_str()).unwrap_or("");
if let Some(handler) = self.shortcodes.get(shortcode_name) {
handler(attrs)
@ -41,10 +49,10 @@ impl ShortcodeProcessor {
}).to_string();
// Process [shortcode] format (Bracket-style)
let bracket_regex = Regex::new(r#"\[(\w+(?:-\w+)*)\s+([^\]]*)\]"#).unwrap();
let bracket_regex = Regex::new(r#"\[(\w+(?:-\w+)*)\s*([^\]]*)\]"#).unwrap();
processed = bracket_regex.replace_all(&processed, |caps: &regex::Captures| {
let shortcode_name = &caps[1];
let attrs = &caps[2];
let attrs = caps.get(2).map(|m| m.as_str()).unwrap_or("");
if let Some(handler) = self.shortcodes.get(shortcode_name) {
handler(attrs)
@ -113,6 +121,29 @@ impl ShortcodeProcessor {
)
}
fn parse_message_shortcode(attrs: &str) -> String {
let attributes = Self::parse_attributes(attrs);
let msg_type = attributes.get("type").map(|s| s.as_str()).unwrap_or("info");
let content = attributes.get("content").map(|s| s.as_str()).unwrap_or("");
let (symbol, class_suffix) = match msg_type {
"info" => ("!", "message"),
"warning" => ("", "warning"),
"error" => ("", "error"),
"success" => ("", "success"),
"note" => ("📝", "note"),
_ => ("!", "message"),
};
format!(r#"
<aside class="msg {}"><span class="msg-symbol">{}</span><div class="msg-content">
<p>{}</p>
</div></aside>"#,
class_suffix, symbol, content
)
}
/// Register a custom shortcode handler
#[allow(dead_code)]
pub fn register_shortcode<F>(&mut self, name: &str, handler: F)
@ -189,4 +220,37 @@ mod tests {
assert_eq!(attributes.get("after").unwrap(), "test2.jpg");
assert_eq!(attributes.get("width").unwrap(), "800");
}
#[test]
fn test_message_shortcode_info() {
let processor = ShortcodeProcessor::new();
let input = r#"[msg type="info" content="This is an info message"]"#;
let result = processor.process(input);
assert!(result.contains("msg message"));
assert!(result.contains("This is an info message"));
assert!(result.contains("!"));
}
#[test]
fn test_message_shortcode_warning() {
let processor = ShortcodeProcessor::new();
let input = r#"{{< msg type="warning" content="This is a warning" >}}"#;
let result = processor.process(input);
assert!(result.contains("msg warning"));
assert!(result.contains("This is a warning"));
assert!(result.contains(""));
}
#[test]
fn test_message_shortcode_default() {
let processor = ShortcodeProcessor::new();
let input = r#"[msg content="Default message"]"#;
let result = processor.process(input);
assert!(result.contains("msg message"));
assert!(result.contains("Default message"));
assert!(result.contains("!"));
}
}

View File

@ -74,7 +74,9 @@ pub struct LanguageMapping {
#[derive(Debug, Clone)]
pub struct LanguageInfo {
#[allow(dead_code)]
pub name: String,
#[allow(dead_code)]
pub code: String,
pub ollama_prompt: String,
}

View File

@ -56,6 +56,7 @@ impl OllamaTranslator {
Ok(translated.to_string())
}
#[allow(dead_code)]
fn build_translation_prompt(&self, text: &str, config: &TranslationConfig) -> Result<String> {
let source_info = self.language_mapping.get_language_info(&config.source_lang)
.ok_or_else(|| anyhow::anyhow!("Unsupported source language: {}", config.source_lang))?;