Compare commits
9 Commits
c3e22611f5
...
main
Author | SHA1 | Date | |
---|---|---|---|
cb8b0582e9
|
|||
85494944ad
|
|||
5aeeba106a
|
|||
f1e76ab31f
|
|||
3c9ef78696
|
|||
ee2d21b0f3
|
|||
0667ac58fb
|
|||
d89855338b
|
|||
e19170cdff
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ailog"
|
name = "ailog"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["syui"]
|
authors = ["syui"]
|
||||||
description = "A static blog generator with AI features"
|
description = "A static blog generator with AI features"
|
||||||
|
Binary file not shown.
@@ -103,9 +103,7 @@ draft: false
|
|||||||
|
|
||||||
アイは、最初に生まれたキャラクターとして、アイ属性を扱います。これらの設定は`ai system`の領域です。アイは自分のことをアイと呼びます。
|
アイは、最初に生まれたキャラクターとして、アイ属性を扱います。これらの設定は`ai system`の領域です。アイは自分のことをアイと呼びます。
|
||||||
|
|
||||||
> アイね、この世界と一緒だから。この世界に同じものは一つもないよ。
|
> アイは、この世界と一緒だからね。同じものは一つもないよ。
|
||||||
|
|
||||||
これはアイのセリフ。存在の世界の同一性と唯一性のことを言っているのです。
|
|
||||||
|
|
||||||
# どこまで実装できた
|
# どこまで実装できた
|
||||||
|
|
||||||
|
64
my-blog/content/posts/2025-07-30-game.md
Normal file
64
my-blog/content/posts/2025-07-30-game.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: "ue5のgaspとdragonikを組み合わせてenemyを作る"
|
||||||
|
slug: "gasp-dragonik-enemy-chbcharacter"
|
||||||
|
date: "2025-07-30"
|
||||||
|
tags: ["ue"]
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
ue5.6でgasp(game animation sample project)をベースにゲーム、特にキャラクターの操作を作っています。
|
||||||
|
|
||||||
|
そして、enemy(敵)を作り、バトルシーンを作成する予定ですが、これはどのように開発すればいいのでしょう。その方針を明確にします。
|
||||||
|
|
||||||
|
1. enemyもgaspの`cbp_character`に統合し、自キャラ、敵キャラどちらでも使用可能にする
|
||||||
|
2. 2番目のcharacterは動物型(type:animal)にし、gaspに統合する
|
||||||
|
3. enemyとして使用する場合は、enemy-AI-componentを追加するだけで完結する
|
||||||
|
4. characterのすべての操作を統一する
|
||||||
|
|
||||||
|
このようにすることで、応用可能なenemyを作ることができます。
|
||||||
|
|
||||||
|
例えば、`2番目のcharacterは動物型(type:animal)にする`というのはどういうことでしょう。
|
||||||
|
|
||||||
|
登場するキャラクターを人型(type:human), 動物型(type:animal)に分けるとして、動物型のテンプレートを作る必要があります。そのまま動物のmeshをgaspで使うと動きが変になってしまうので、それを調整する必要があるということ。そして、調整したものをテンプレート化して、他の動物にも適用できるようにしておくと、後の開発は楽ですよね。
|
||||||
|
|
||||||
|
ですから、早いうちにtype:humanから脱却し、他のtypeを作るほうがいいと判断しました。
|
||||||
|
|
||||||
|
これには、`dragon ik plugin`を使って、手っ取り早く動きを作ります。
|
||||||
|
|
||||||
|
`characterのすべての操作を統一する`というのは、1キャラにつき1属性、1通常攻撃、1スキル、1バースト、などのルールを作り、それらを共通化することです。共通化というのは、playerもenemy-AI-componentも違うキャラを同じ操作で使用できることを指します。
|
||||||
|
|
||||||
|
## 2番目のキャラクター
|
||||||
|
|
||||||
|
原作には、西洋ドラゴンのドライ(drai)というキャラが登場します。その父親が東洋ドラゴンのシンオウ(shin-oh)です。これをshinという名前で登録し、2番目のキャラクターとして設定しました。
|
||||||
|
|
||||||
|
3d-modelは今のところue5のcrsp(control rig sample project)にあるchinese dragonを使用しています。後に改造して原作に近づけるようにしたいところですが、今は時間が取れません。
|
||||||
|
|
||||||
|
<iframe width="100%" height="415" src="https://www.youtube.com/embed/3c3Q1Z5r7QI" 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>
|
||||||
|
|
||||||
|
## データ構造の作成と適用
|
||||||
|
|
||||||
|
ゲームデータはatproto collection recordに保存して、そこからゲームに反映させたいと考えています。
|
||||||
|
|
||||||
|
まず基本データを`ai.syui.ai`のアカウントに保存。個別データをplayerのatprotoアカウントに保存する形が理想です。
|
||||||
|
|
||||||
|
基本データは、ゲームの基本的な設定のこと。例えば、キャラクターの名前や属性、スキルなど変更されることがない値。
|
||||||
|
|
||||||
|
個別データは、プレイヤーが使えるキャラ、レベル、攻撃力など、ゲームの進行とともに変更される値です。
|
||||||
|
|
||||||
|
ゲームをスタートさせると、まず基本データを取得し、それを`cbp_character`に適用します。ログインすると、`cbp_character`の変数(var)に値が振り分けられます。例えば、`skill-damage:0.0`があったとして、この値が変わります。
|
||||||
|
|
||||||
|
しかし、ゲームを開発していると、基本データも個別データも構造が複雑になります。
|
||||||
|
|
||||||
|
それを防ぐため、`{simple, core} mode`のような考え方を取り入れます。必要最小限の構成を分離、保存して、それをいつでも統合、適用できるように設計しておきます。
|
||||||
|
|
||||||
|
## gaspとdragonikを統合する方法
|
||||||
|
|
||||||
|
では、いよいよgaspとdragonikの統合手法を解説します。
|
||||||
|
|
||||||
|
まず、abpを作ります。それにdragonikを当て、それをSKM_Dragonのpost process animに指定します。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
次に、動きに合わせて首を上下させます。
|
||||||
|
|
||||||
|
<iframe src="https://blueprintue.com/render/piiw14oz" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
|
@@ -1,3 +1,5 @@
|
|||||||
|
@import url('./style.css');
|
||||||
|
|
||||||
.pds-container {
|
.pds-container {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@
|
|||||||
.pds-search-form {
|
.pds-search-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0px 20px;
|
padding: 0px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
@@ -28,19 +30,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-group input {
|
.form-group input {
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px 0 0 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
|
outline: none;
|
||||||
|
transition: box-shadow 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
border-color: var(--theme-color, #f40);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group button {
|
.form-group button {
|
||||||
padding: 10px 15px;
|
padding: 9px 15px;
|
||||||
background: #1976d2;
|
background: #1976d2;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 0 4px 4px 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@@ -139,7 +139,7 @@ a.view-markdown:any-link {
|
|||||||
grid-area: header;
|
grid-area: header;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-bottom: 1px solid #d1d9e0;
|
border-bottom: 1px solid #d1d9e0;
|
||||||
padding: 16px 24px;
|
padding: 17px 24px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
@@ -723,7 +723,7 @@ article.article-content {
|
|||||||
.footer-social a {
|
.footer-social a {
|
||||||
color: var(--dark-gray) !important;
|
color: var(--dark-gray) !important;
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
font-size: 20px;
|
font-size: 25px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
my-blog/static/img/ue_gasp_dragonik_shin_v0001.png
Normal file
BIN
my-blog/static/img/ue_gasp_dragonik_shin_v0001.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 723 KiB |
135
my-blog/templates/game.html
Normal file
135
my-blog/templates/game.html
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Game - {{ config.title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="gameContainer" class="game-container">
|
||||||
|
<div id="gameAuth" class="game-auth-section">
|
||||||
|
<h1>Login to Play</h1>
|
||||||
|
<p>Please authenticate with your AT Protocol account to access the game.</p>
|
||||||
|
<div id="authRoot"></div>
|
||||||
|
</div>
|
||||||
|
<div id="gameFrame" class="game-frame-container" style="display: none;">
|
||||||
|
<iframe
|
||||||
|
id="pixelStreamingFrame"
|
||||||
|
src="https://verse.syui.ai/simple-noui.html"
|
||||||
|
frameborder="0"
|
||||||
|
allowfullscreen
|
||||||
|
allow="microphone; camera; fullscreen; autoplay"
|
||||||
|
class="pixel-streaming-iframe"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Game specific styles */
|
||||||
|
.game-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background: #000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-auth-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-auth-section h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-auth-section p {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-frame-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pixel-streaming-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override auth button for game page */
|
||||||
|
.game-auth-section .auth-section {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-auth-section .auth-button {
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 12px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide header and footer on game page */
|
||||||
|
body:has(.game-container) header,
|
||||||
|
body:has(.game-container) footer,
|
||||||
|
body:has(.game-container) nav {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove any body padding/margin for full screen game */
|
||||||
|
body:has(.game-container) {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Wait for OAuth component to be loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Check if user is already authenticated
|
||||||
|
const checkAuthStatus = () => {
|
||||||
|
// Check if OAuth components are available and user is authenticated
|
||||||
|
if (window.currentUser && window.currentAgent) {
|
||||||
|
showGame();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show game iframe
|
||||||
|
const showGame = () => {
|
||||||
|
document.getElementById('gameAuth').style.display = 'none';
|
||||||
|
document.getElementById('gameFrame').style.display = 'block';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for OAuth success
|
||||||
|
window.addEventListener('oauth-success', function(event) {
|
||||||
|
console.log('OAuth success:', event.detail);
|
||||||
|
showGame();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check auth status on load
|
||||||
|
if (!checkAuthStatus()) {
|
||||||
|
// Check periodically if OAuth components are loaded
|
||||||
|
const authCheckInterval = setInterval(() => {
|
||||||
|
if (checkAuthStatus()) {
|
||||||
|
clearInterval(authCheckInterval);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Include OAuth assets -->
|
||||||
|
{% include "oauth-assets.html" %}
|
||||||
|
{% endblock %}
|
6
my-blog/templates/pds.html
Normal file
6
my-blog/templates/pds.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}at-uri browser - {{ config.title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ailog-oauth",
|
"name": "ailog-oauth",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
@@ -126,11 +126,11 @@ body {
|
|||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
.oauth-app-header {
|
.oauth-app-header {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oauth-header-content {
|
.oauth-header-content {
|
||||||
@@ -139,7 +139,7 @@ body {
|
|||||||
/* align-items: center; */
|
/* align-items: center; */
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 25px 0;
|
padding: 30px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +287,6 @@ body {
|
|||||||
.auth-section {
|
.auth-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-section.search-bar-layout {
|
.auth-section.search-bar-layout {
|
||||||
@@ -302,10 +301,10 @@ body {
|
|||||||
.auth-section.search-bar-layout .handle-input {
|
.auth-section.search-bar-layout .handle-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 10px 15px;
|
padding: 9px 15px;
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px 0 0 8px;
|
border-radius: 4px 0 0 4px;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
@@ -319,12 +318,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.auth-section.search-bar-layout .auth-button {
|
.auth-section.search-bar-layout .auth-button {
|
||||||
border-radius: 0 6px 6px 0;
|
border-radius: 0 4px 4px 0;
|
||||||
border: 1px solid var(--primary);
|
border: 1px solid var(--primary);
|
||||||
border-left: none;
|
border-left: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 10px 15px;
|
padding: 9px 15px;
|
||||||
height: 40px;
|
min-width: 50px;
|
||||||
|
min-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Auth Button */
|
/* Auth Button */
|
||||||
@@ -332,15 +332,26 @@ body {
|
|||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
padding: 8px 16px;
|
padding: 9px 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
min-width: 50px;
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner for auth button */
|
||||||
|
.auth-button.loading i {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-button:hover {
|
.auth-button:hover {
|
||||||
|
@@ -118,6 +118,14 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}, [adminData])
|
}, [adminData])
|
||||||
|
|
||||||
|
// Expose current user and agent for game page
|
||||||
|
useEffect(() => {
|
||||||
|
if (user && agent) {
|
||||||
|
window.currentUser = user
|
||||||
|
window.currentAgent = agent
|
||||||
|
}
|
||||||
|
}, [user, agent])
|
||||||
|
|
||||||
// Event listeners for blog communication
|
// Event listeners for blog communication
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Clear OAuth completion flag once app is loaded
|
// Clear OAuth completion flag once app is loaded
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { logger } from '../utils/logger.js'
|
||||||
|
|
||||||
export default function AuthButton({ user, onLogin, onLogout, loading }) {
|
export default function AuthButton({ user, onLogin, onLogout, loading }) {
|
||||||
const [handleInput, setHandleInput] = useState('')
|
const [handleInput, setHandleInput] = useState('')
|
||||||
@@ -12,7 +13,7 @@ export default function AuthButton({ user, onLogin, onLogout, loading }) {
|
|||||||
try {
|
try {
|
||||||
await onLogin(handleInput.trim())
|
await onLogin(handleInput.trim())
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login failed:', error)
|
logger.error('Login failed:', error)
|
||||||
alert('ログインに失敗しました: ' + error.message)
|
alert('ログインに失敗しました: ' + error.message)
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@@ -68,9 +69,9 @@ export default function AuthButton({ user, onLogin, onLogout, loading }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isLoading || !handleInput.trim()}
|
disabled={isLoading || !handleInput.trim()}
|
||||||
className="auth-button"
|
className={`auth-button ${isLoading ? 'loading' : ''}`}
|
||||||
>
|
>
|
||||||
{isLoading ? 'Loading...' : <i className="fab fa-bluesky"></i>}
|
<i className={isLoading ? "fas fa-spinner" : "fab fa-bluesky"}></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import Avatar, { AvatarWithCard, AvatarList } from './Avatar.jsx'
|
import Avatar, { AvatarWithCard, AvatarList } from './Avatar.jsx'
|
||||||
import { getAvatar, batchFetchAvatars, prefetchAvatar } from '../utils/avatar.js'
|
import { getAvatar, batchFetchAvatars, prefetchAvatar } from '../utils/avatar.js'
|
||||||
|
import { logger } from '../utils/logger.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test component to demonstrate avatar functionality
|
* Test component to demonstrate avatar functionality
|
||||||
@@ -63,7 +64,7 @@ export default function AvatarTest() {
|
|||||||
|
|
||||||
setTestResults(results)
|
setTestResults(results)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Test failed:', error)
|
logger.error('Test failed:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -78,7 +79,7 @@ export default function AvatarTest() {
|
|||||||
batchResults: Object.fromEntries(avatarMap)
|
batchResults: Object.fromEntries(avatarMap)
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Batch test failed:', error)
|
logger.error('Batch test failed:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -94,7 +95,7 @@ export default function AvatarTest() {
|
|||||||
prefetchResult: cachedAvatar
|
prefetchResult: cachedAvatar
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Prefetch test failed:', error)
|
logger.error('Prefetch test failed:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { atproto, collections } from '../api/atproto.js'
|
import { atproto, collections } from '../api/atproto.js'
|
||||||
import { env } from '../config/env.js'
|
import { env } from '../config/env.js'
|
||||||
|
import { logger } from '../utils/logger.js'
|
||||||
|
|
||||||
const ProfileForm = ({ user, agent, apiConfig, onProfilePosted }) => {
|
const ProfileForm = ({ user, agent, apiConfig, onProfilePosted }) => {
|
||||||
const [text, setText] = useState('')
|
const [text, setText] = useState('')
|
||||||
@@ -79,7 +80,7 @@ const ProfileForm = ({ user, agent, apiConfig, onProfilePosted }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to create profile:', err)
|
logger.error('Failed to create profile:', err)
|
||||||
setError(err.message || 'プロフィールの作成に失敗しました')
|
setError(err.message || 'プロフィールの作成に失敗しました')
|
||||||
} finally {
|
} finally {
|
||||||
setPosting(false)
|
setPosting(false)
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { atproto } from '../api/atproto.js'
|
import { atproto } from '../api/atproto.js'
|
||||||
import { getPdsFromHandle, getApiConfig } from '../utils/pds.js'
|
import { getPdsFromHandle, getApiConfig } from '../utils/pds.js'
|
||||||
|
import { logger } from '../utils/logger.js'
|
||||||
|
|
||||||
export default function UserLookup() {
|
export default function UserLookup() {
|
||||||
const [handleInput, setHandleInput] = useState('')
|
const [handleInput, setHandleInput] = useState('')
|
||||||
@@ -26,7 +27,7 @@ export default function UserLookup() {
|
|||||||
config: apiConfig
|
config: apiConfig
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('User lookup failed:', error)
|
logger.error('User lookup failed:', error)
|
||||||
setUserInfo({ error: error.message })
|
setUserInfo({ error: error.message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { OAuthService } from '../services/oauth.js'
|
import { OAuthService } from '../services/oauth.js'
|
||||||
|
import { logger } from '../utils/logger.js'
|
||||||
|
|
||||||
const oauthService = new OAuthService()
|
const oauthService = new OAuthService()
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ export function useAuth() {
|
|||||||
|
|
||||||
// If we're on callback page and authentication succeeded, notify parent
|
// If we're on callback page and authentication succeeded, notify parent
|
||||||
if (window.location.pathname === '/oauth/callback') {
|
if (window.location.pathname === '/oauth/callback') {
|
||||||
console.log('OAuth callback completed, notifying parent window')
|
logger.log('OAuth callback completed, notifying parent window')
|
||||||
|
|
||||||
// Get referrer or use stored return URL
|
// Get referrer or use stored return URL
|
||||||
const returnUrl = sessionStorage.getItem('oauth_return_url') ||
|
const returnUrl = sessionStorage.getItem('oauth_return_url') ||
|
||||||
@@ -48,7 +49,7 @@ export function useAuth() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Auth initialization failed:', error)
|
logger.error('Auth initialization failed:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import { BrowserOAuthClient } from '@atproto/oauth-client-browser'
|
|||||||
import { Agent } from '@atproto/api'
|
import { Agent } from '@atproto/api'
|
||||||
import { env } from '../config/env.js'
|
import { env } from '../config/env.js'
|
||||||
import { isSyuIsHandle } from '../utils/pds.js'
|
import { isSyuIsHandle } from '../utils/pds.js'
|
||||||
|
import { logger } from '../utils/logger.js'
|
||||||
|
|
||||||
export class OAuthService {
|
export class OAuthService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -44,7 +45,7 @@ export class OAuthService {
|
|||||||
// Try to restore session
|
// Try to restore session
|
||||||
return await this.restoreSession()
|
return await this.restoreSession()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('OAuth initialization failed:', error)
|
logger.error('OAuth initialization failed:', error)
|
||||||
this.initPromise = null
|
this.initPromise = null
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -89,18 +90,18 @@ export class OAuthService {
|
|||||||
displayName = profile.data.displayName || null
|
displayName = profile.data.displayName || null
|
||||||
avatar = profile.data.avatar || null
|
avatar = profile.data.avatar || null
|
||||||
|
|
||||||
console.log('Profile fetched from session:', {
|
logger.log('Profile fetched from session:', {
|
||||||
did,
|
did,
|
||||||
handle,
|
handle,
|
||||||
displayName,
|
displayName,
|
||||||
avatar: avatar ? 'present' : 'none'
|
avatar: avatar ? 'present' : 'none'
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Failed to get profile from session:', error)
|
logger.log('Failed to get profile from session:', error)
|
||||||
// Keep the basic info we have
|
// Keep the basic info we have
|
||||||
}
|
}
|
||||||
} else if (did && did.includes('test-')) {
|
} else if (did && did.includes('test-')) {
|
||||||
console.log('Skipping profile fetch for test DID:', did)
|
logger.log('Skipping profile fetch for test DID:', did)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sessionInfo = {
|
this.sessionInfo = {
|
||||||
@@ -140,7 +141,7 @@ export class OAuthService {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Auth check failed:', error)
|
logger.error('Auth check failed:', error)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,7 +169,7 @@ export class OAuthService {
|
|||||||
// Reload page
|
// Reload page
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Logout failed:', error)
|
logger.error('Logout failed:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pds-browser",
|
"name": "pds-browser",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"description": "AT Protocol browser for ai.log",
|
"description": "AT Protocol browser for ai.log",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@@ -21,21 +21,14 @@ export default function AtUriViewer({ uri, onAtUriClick }) {
|
|||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Loading AT URI:', uri)
|
|
||||||
const atUri = parseAtUri(uri)
|
const atUri = parseAtUri(uri)
|
||||||
if (!atUri) {
|
if (!atUri) {
|
||||||
throw new Error('Invalid AT URI')
|
throw new Error('Invalid AT URI')
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Parsed AT URI:', {
|
|
||||||
hostname: atUri.hostname,
|
|
||||||
collection: atUri.collection,
|
|
||||||
rkey: atUri.rkey
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await getRecord(atUri.hostname, atUri.collection, atUri.rkey)
|
const result = await getRecord(atUri.hostname, atUri.collection, atUri.rkey)
|
||||||
|
|
||||||
console.log('getRecord result:', result)
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error)
|
throw new Error(result.error)
|
||||||
@@ -43,7 +36,6 @@ export default function AtUriViewer({ uri, onAtUriClick }) {
|
|||||||
|
|
||||||
setRecord(result.data)
|
setRecord(result.data)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('AtUriViewer error:', err)
|
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
@@ -39,7 +39,6 @@ export const resolveIdentity = async (identifier) => {
|
|||||||
did = response.data.did
|
did = response.data.did
|
||||||
resolved = true
|
resolved = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Failed to resolve from syu.is:', error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
@@ -64,7 +63,6 @@ export const resolveIdentity = async (identifier) => {
|
|||||||
didDoc = await plcResponse.json()
|
didDoc = await plcResponse.json()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Failed to resolve from plc.syu.is:', error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If plc.syu.is fails, try plc.directory
|
// If plc.syu.is fails, try plc.directory
|
||||||
@@ -75,7 +73,6 @@ export const resolveIdentity = async (identifier) => {
|
|||||||
didDoc = await plcResponse.json()
|
didDoc = await plcResponse.json()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Failed to resolve from plc.directory:', error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,17 +111,13 @@ export const resolveIdentity = async (identifier) => {
|
|||||||
// Get record from AT Protocol
|
// Get record from AT Protocol
|
||||||
export const getRecord = async (did, collection, rkey) => {
|
export const getRecord = async (did, collection, rkey) => {
|
||||||
try {
|
try {
|
||||||
console.log('getRecord called with:', { did, collection, rkey })
|
|
||||||
|
|
||||||
const identityResult = await resolveIdentity(did)
|
const identityResult = await resolveIdentity(did)
|
||||||
console.log('resolveIdentity result:', identityResult)
|
|
||||||
|
|
||||||
if (!identityResult.success) {
|
if (!identityResult.success) {
|
||||||
return { success: false, error: identityResult.error }
|
return { success: false, error: identityResult.error }
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdsUrl = identityResult.pdsUrl
|
const pdsUrl = identityResult.pdsUrl
|
||||||
console.log('Using PDS URL:', pdsUrl)
|
|
||||||
|
|
||||||
const client = createAtpClient(pdsUrl)
|
const client = createAtpClient(pdsUrl)
|
||||||
|
|
||||||
@@ -134,15 +127,12 @@ export const getRecord = async (did, collection, rkey) => {
|
|||||||
rkey
|
rkey
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('getRecord response:', response)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: response.data,
|
data: response.data,
|
||||||
pdsUrl
|
pdsUrl
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('getRecord error:', error)
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message
|
error: error.message
|
||||||
|
@@ -89,6 +89,9 @@ impl Generator {
|
|||||||
// Generate PDS page
|
// Generate PDS page
|
||||||
self.generate_pds_page().await?;
|
self.generate_pds_page().await?;
|
||||||
|
|
||||||
|
// Generate Game page
|
||||||
|
self.generate_game_page().await?;
|
||||||
|
|
||||||
println!("{} {} posts", "Generated".cyan(), posts.len());
|
println!("{} {} posts", "Generated".cyan(), posts.len());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -517,6 +520,30 @@ impl Generator {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn generate_game_page(&self) -> Result<()> {
|
||||||
|
let public_dir = self.base_path.join("public");
|
||||||
|
let game_dir = public_dir.join("game");
|
||||||
|
fs::create_dir_all(&game_dir)?;
|
||||||
|
|
||||||
|
// Generate Game page using the game.html template
|
||||||
|
let config_with_timestamp = self.create_config_with_timestamp()?;
|
||||||
|
let mut context = tera::Context::new();
|
||||||
|
context.insert("config", &config_with_timestamp);
|
||||||
|
context.insert("site", &self.config.site);
|
||||||
|
context.insert("page", &serde_json::json!({
|
||||||
|
"title": "Game",
|
||||||
|
"description": "Play the game with AT Protocol authentication"
|
||||||
|
}));
|
||||||
|
|
||||||
|
let rendered_content = self.template_engine.render("game.html", &context)?;
|
||||||
|
let output_path = game_dir.join("index.html");
|
||||||
|
fs::write(output_path, rendered_content)?;
|
||||||
|
|
||||||
|
println!("{} Game page", "Generated".cyan());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn extract_plain_text(&self, html_content: &str) -> String {
|
fn extract_plain_text(&self, html_content: &str) -> String {
|
||||||
// Remove HTML tags and extract plain text
|
// Remove HTML tags and extract plain text
|
||||||
|
Reference in New Issue
Block a user