9 Commits

Author SHA1 Message Date
cb8b0582e9 rm log 2025-08-01 21:25:52 +09:00
85494944ad rm log 2025-08-01 20:36:35 +09:00
5aeeba106a add post 2025-07-30 19:30:01 +09:00
f1e76ab31f fix post 2025-07-27 05:04:01 +09:00
3c9ef78696 add binary 2025-07-26 20:54:23 +09:00
ee2d21b0f3 update 2025-07-26 20:00:16 +09:00
0667ac58fb test game 2025-07-26 19:51:55 +09:00
d89855338b fix css 2025-07-18 10:57:42 +09:00
e19170cdff add pds.html 2025-07-18 00:05:04 +09:00
22 changed files with 309 additions and 64 deletions

View File

@@ -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.

View File

@@ -103,9 +103,7 @@ draft: false
アイは、最初に生まれたキャラクターとして、アイ属性を扱います。これらの設定は`ai system`の領域です。アイは自分のことをアイと呼びます。 アイは、最初に生まれたキャラクターとして、アイ属性を扱います。これらの設定は`ai system`の領域です。アイは自分のことをアイと呼びます。
> アイ、この世界と一緒だから。この世界に同じものは一つもないよ。 > アイ、この世界と一緒だから。同じものは一つもないよ。
これはアイのセリフ。存在の世界の同一性と唯一性のことを言っているのです。
# どこまで実装できた # どこまで実装できた

View 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に指定します。
![](/img/ue_gasp_dragonik_shin_v0001.png)
次に、動きに合わせて首を上下させます。
<iframe src="https://blueprintue.com/render/piiw14oz" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>

View File

@@ -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;

View File

@@ -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;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

135
my-blog/templates/game.html Normal file
View 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 %}

View File

@@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block title %}at-uri browser - {{ config.title }}{% endblock %}
{% block content %}
{% endblock %}

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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

View File

@@ -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>
) )

View File

@@ -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)
} }

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
} }

View File

@@ -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)
} }
} }

View File

@@ -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",

View File

@@ -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)

View File

@@ -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

View File

@@ -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(())
@@ -518,6 +521,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
let mut text = String::new(); let mut text = String::new();