test update
This commit is contained in:
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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();
|
||||
|
Reference in New Issue
Block a user