test update

This commit is contained in:
2025-06-15 15:06:50 +09:00
parent 67b241f1e8
commit c12d42882c
16 changed files with 1363 additions and 742 deletions

View File

@ -4,15 +4,9 @@ VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json
VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
# Collection names for OAuth app
VITE_COLLECTION_COMMENT=ai.syui.log
VITE_COLLECTION_USER=ai.syui.log.user
VITE_COLLECTION_CHAT=ai.syui.log.chat
# Collection names for ailog (backward compatibility)
AILOG_COLLECTION_COMMENT=ai.syui.log
AILOG_COLLECTION_USER=ai.syui.log.user
AILOG_COLLECTION_CHAT=ai.syui.log.chat
# Base collection for OAuth app and ailog (all others are derived)
VITE_OAUTH_COLLECTION=ai.syui.log
# [user, chat, chat.lang, chat.comment]
# AI Configuration
VITE_AI_ENABLED=true

View File

@ -3,7 +3,7 @@ import { OAuthCallback } from './components/OAuthCallback';
import { AIChat } from './components/AIChat';
import { authService, User } from './services/auth';
import { atprotoOAuthService } from './services/atproto-oauth';
import { appConfig } from './config/app';
import { appConfig, getCollectionNames } from './config/app';
import './App.css';
function App() {
@ -46,8 +46,10 @@ function App() {
const [isPostingUserList, setIsPostingUserList] = useState(false);
const [userListRecords, setUserListRecords] = useState<any[]>([]);
const [showJsonFor, setShowJsonFor] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'comments' | 'ai-chat'>('comments');
const [activeTab, setActiveTab] = useState<'comments' | 'ai-chat' | 'lang-en' | 'ai-comment'>('comments');
const [aiChatHistory, setAiChatHistory] = useState<any[]>([]);
const [langEnRecords, setLangEnRecords] = useState<any[]>([]);
const [aiCommentRecords, setAiCommentRecords] = useState<any[]>([]);
useEffect(() => {
// Setup Jetstream WebSocket for real-time comments (optional)
@ -55,17 +57,18 @@ function App() {
try {
const ws = new WebSocket('wss://jetstream2.us-east.bsky.network/subscribe');
const collections = getCollectionNames(appConfig.collections.base);
ws.onopen = () => {
console.log('Jetstream connected');
ws.send(JSON.stringify({
wantedCollections: [appConfig.collections.comment]
wantedCollections: [collections.comment]
}));
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.collection === appConfig.collections.comment && data.commit?.operation === 'create') {
if (data.collection === collections.comment && data.commit?.operation === 'create') {
console.log('New comment detected via Jetstream:', data);
// Optionally reload comments
// loadAllComments(window.location.href);
@ -190,6 +193,9 @@ function App() {
};
checkAuth();
// Load AI generated content (public)
loadAIGeneratedContent();
return () => {
window.removeEventListener('popstate', handlePopState);
@ -274,6 +280,45 @@ function App() {
}
};
// Load AI generated content from admin DID
const loadAIGeneratedContent = async () => {
try {
const adminDid = appConfig.adminDid;
const bskyApi = appConfig.bskyPublicApi || 'https://public.api.bsky.app';
const collections = getCollectionNames(appConfig.collections.base);
// Load lang:en records
const langResponse = await fetch(`${bskyApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.chatLang)}&limit=100`);
if (langResponse.ok) {
const langData = await langResponse.json();
const langRecords = langData.records || [];
// Filter by current page URL if on post page
const filteredLangRecords = appConfig.rkey
? langRecords.filter(record => record.value.url === window.location.href)
: langRecords.slice(0, 3); // Top page: latest 3
setLangEnRecords(filteredLangRecords);
}
// Load AI comment records
const commentResponse = await fetch(`${bskyApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.chatComment)}&limit=100`);
if (commentResponse.ok) {
const commentData = await commentResponse.json();
const commentRecords = commentData.records || [];
// Filter by current page URL if on post page
const filteredCommentRecords = appConfig.rkey
? commentRecords.filter(record => record.value.url === window.location.href)
: commentRecords.slice(0, 3); // Top page: latest 3
setAiCommentRecords(filteredCommentRecords);
}
} catch (err) {
console.error('Failed to load AI generated content:', err);
}
};
const loadUserComments = async (did: string) => {
try {
console.log('Loading comments for DID:', did);
@ -454,7 +499,8 @@ function App() {
console.log(`Fetching comments from user: ${user.handle} (${user.did}) at ${user.pds}`);
// Public API使用認証不要
const response = await fetch(`${user.pds}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(user.did)}&collection=${encodeURIComponent(appConfig.collections.comment)}&limit=100`);
const collections = getCollectionNames(appConfig.collections.base);
const response = await fetch(`${user.pds}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(user.did)}&collection=${encodeURIComponent(collections.comment)}&limit=100`);
if (!response.ok) {
console.warn(`Failed to fetch from ${user.handle} (${response.status}): ${response.statusText}`);
@ -1043,6 +1089,18 @@ function App() {
AI Chat History ({aiChatHistory.length})
</button>
)}
<button
className={`tab-button ${activeTab === 'lang-en' ? 'active' : ''}`}
onClick={() => setActiveTab('lang-en')}
>
Lang: EN ({langEnRecords.length})
</button>
<button
className={`tab-button ${activeTab === 'ai-comment' ? 'active' : ''}`}
onClick={() => setActiveTab('ai-comment')}
>
AI Comment ({aiCommentRecords.length})
</button>
</div>
{/* Comments List */}
@ -1118,7 +1176,7 @@ function App() {
</div>
<div className="comment-meta">
{record.value.url && (
<small><a href={record.value.url} target="_blank" rel="noopener noreferrer">{record.value.url}</a></small>
<small><a href={record.value.url}>{record.value.url}</a></small>
)}
</div>
@ -1204,7 +1262,7 @@ function App() {
</div>
<div className="comment-meta">
{record.value.url && (
<small><a href={record.value.url} target="_blank" rel="noopener noreferrer">{record.value.url}</a></small>
<small><a href={record.value.url}>{record.value.url}</a></small>
)}
</div>
@ -1223,6 +1281,88 @@ function App() {
</div>
)}
{/* Lang: EN List */}
{activeTab === 'lang-en' && (
<div className="lang-en-list">
{langEnRecords.length === 0 ? (
<p className="no-content">No English translations yet</p>
) : (
langEnRecords.map((record, index) => (
<div key={index} className="lang-item">
<div className="lang-header">
<img
src={record.value.author?.avatar || generatePlaceholderAvatar(record.value.author?.handle || 'AI')}
alt="AI Avatar"
className="comment-avatar"
/>
<div className="comment-author-info">
<span className="comment-author">
{record.value.author?.displayName || 'AI Translator'}
</span>
<span className="comment-handle">
@{record.value.author?.handle || 'ai'}
</span>
</div>
<span className="comment-date">
{new Date(record.value.createdAt).toLocaleString()}
</span>
</div>
<div className="lang-content">
<div className="lang-type">Type: {record.value.type || 'en'}</div>
<div className="lang-body">{record.value.body}</div>
</div>
<div className="comment-meta">
{record.value.url && (
<small><a href={record.value.url}>{record.value.url}</a></small>
)}
</div>
</div>
))
)}
</div>
)}
{/* AI Comment List */}
{activeTab === 'ai-comment' && (
<div className="ai-comment-list">
{aiCommentRecords.length === 0 ? (
<p className="no-content">No AI comments yet</p>
) : (
aiCommentRecords.map((record, index) => (
<div key={index} className="ai-comment-item">
<div className="ai-comment-header">
<img
src={record.value.author?.avatar || generatePlaceholderAvatar(record.value.author?.handle || 'AI')}
alt="AI Avatar"
className="comment-avatar"
/>
<div className="comment-author-info">
<span className="comment-author">
{record.value.author?.displayName || 'AI Commenter'}
</span>
<span className="comment-handle">
@{record.value.author?.handle || 'ai'}
</span>
</div>
<span className="comment-date">
{new Date(record.value.createdAt).toLocaleString()}
</span>
</div>
<div className="ai-comment-content">
<div className="ai-comment-type">Type: {record.value.type || 'comment'}</div>
<div className="ai-comment-body">{record.value.body}</div>
</div>
<div className="comment-meta">
{record.value.url && (
<small><a href={record.value.url}>{record.value.url}</a></small>
)}
</div>
</div>
))
)}
</div>
)}
{/* Comment Form - Only show on post pages */}
{user && appConfig.rkey && (
<div className="comment-form">

View File

@ -2,9 +2,7 @@
export interface AppConfig {
adminDid: string;
collections: {
comment: string;
user: string;
chat: string;
base: string; // Base collection like "ai.syui.log"
};
host: string;
rkey?: string; // Current post rkey if on post page
@ -16,10 +14,21 @@ export interface AppConfig {
bskyPublicApi: string;
}
// Collection name builders (similar to Rust implementation)
export function getCollectionNames(base: string) {
return {
comment: base,
user: `${base}.user`,
chat: `${base}.chat`,
chatLang: `${base}.chat.lang`,
chatComment: `${base}.chat.comment`,
};
}
// Generate collection names from host
// Format: ${reg}.${name}.${sub}
// Example: log.syui.ai -> ai.syui.log
function generateCollectionNames(host: string): { comment: string; user: string; chat: string } {
function generateBaseCollectionFromHost(host: string): string {
try {
// Remove protocol if present
const cleanHost = host.replace(/^https?:\/\//, '');
@ -34,21 +43,11 @@ function generateCollectionNames(host: string): { comment: string; user: string;
// Reverse the parts for collection naming
// log.syui.ai -> ai.syui.log
const reversedParts = parts.reverse();
const collectionBase = reversedParts.join('.');
return {
comment: collectionBase,
user: `${collectionBase}.user`,
chat: `${collectionBase}.chat`
};
return reversedParts.join('.');
} catch (error) {
console.warn('Failed to generate collection names from host:', host, error);
// Fallback to default collections
return {
comment: 'ai.syui.log',
user: 'ai.syui.log.user',
chat: 'ai.syui.log.chat'
};
console.warn('Failed to generate collection base from host:', host, error);
// Fallback to default
return 'ai.syui.log';
}
}
@ -66,11 +65,9 @@ export function getAppConfig(): AppConfig {
const adminDid = import.meta.env.VITE_ADMIN_DID || 'did:plc:uqzpqmrjnptsxezjx4xuh2mn';
// Priority: Environment variables > Auto-generated from host
const autoGeneratedCollections = generateCollectionNames(host);
const autoGeneratedBase = generateBaseCollectionFromHost(host);
const collections = {
comment: import.meta.env.VITE_COLLECTION_COMMENT || autoGeneratedCollections.comment,
user: import.meta.env.VITE_COLLECTION_USER || autoGeneratedCollections.user,
chat: import.meta.env.VITE_COLLECTION_CHAT || autoGeneratedCollections.chat,
base: import.meta.env.VITE_OAUTH_COLLECTION || autoGeneratedBase,
};
const rkey = extractRkeyFromUrl();