This commit is contained in:
		@@ -1,7 +1,16 @@
 | 
			
		||||
/* Theme Colors */
 | 
			
		||||
:root {
 | 
			
		||||
  --theme-color: #FF4500;
 | 
			
		||||
  --white: #fff;
 | 
			
		||||
  --light-gray: #aaa;
 | 
			
		||||
  --dark-gray: #666;
 | 
			
		||||
  --background: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.app {
 | 
			
		||||
  min-height: 100vh;
 | 
			
		||||
  background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
 | 
			
		||||
  color: #333333;
 | 
			
		||||
  background: linear-gradient(180deg, #f8f9fa 0%, var(--background) 100%);
 | 
			
		||||
  color: var(--dark-gray);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.app-header {
 | 
			
		||||
@@ -41,15 +50,15 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-button.active {
 | 
			
		||||
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
			
		||||
  color: white;
 | 
			
		||||
  border: 1px solid #667eea;
 | 
			
		||||
  box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
 | 
			
		||||
  background: var(--theme-color);
 | 
			
		||||
  color: var(--white);
 | 
			
		||||
  border: 1px solid var(--theme-color);
 | 
			
		||||
  box-shadow: 0 4px 16px rgba(255, 69, 0, 0.4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-button.active:hover {
 | 
			
		||||
  transform: translateY(-2px);
 | 
			
		||||
  box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
 | 
			
		||||
  box-shadow: 0 6px 20px rgba(255, 69, 0, 0.5);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.app-header h1 {
 | 
			
		||||
@@ -99,9 +108,9 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.login-button {
 | 
			
		||||
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | 
			
		||||
  color: white;
 | 
			
		||||
  border: 1px solid #667eea;
 | 
			
		||||
  background: var(--theme-color);
 | 
			
		||||
  color: var(--white);
 | 
			
		||||
  border: 1px solid var(--theme-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.backup-button {
 | 
			
		||||
@@ -124,7 +133,7 @@
 | 
			
		||||
 | 
			
		||||
.login-button:hover {
 | 
			
		||||
  transform: translateY(-2px);
 | 
			
		||||
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
 | 
			
		||||
  box-shadow: 0 4px 12px rgba(255, 69, 0, 0.4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.backup-button:hover {
 | 
			
		||||
@@ -268,8 +277,8 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.atproto-button {
 | 
			
		||||
  background: #1185fe;
 | 
			
		||||
  color: white;
 | 
			
		||||
  background: var(--theme-color);
 | 
			
		||||
  color: var(--white);
 | 
			
		||||
  border: none;
 | 
			
		||||
  padding: 12px 24px;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
@@ -281,9 +290,9 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.atproto-button:hover {
 | 
			
		||||
  background: #0d6efd;
 | 
			
		||||
  filter: brightness(1.1);
 | 
			
		||||
  transform: translateY(-2px);
 | 
			
		||||
  box-shadow: 0 4px 12px rgba(17, 133, 254, 0.4);
 | 
			
		||||
  box-shadow: 0 4px 12px rgba(255, 69, 0, 0.4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.username-input-section {
 | 
			
		||||
@@ -407,8 +416,8 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post-button {
 | 
			
		||||
  background: #28a745;
 | 
			
		||||
  color: white;
 | 
			
		||||
  background: var(--theme-color);
 | 
			
		||||
  color: var(--white);
 | 
			
		||||
  border: none;
 | 
			
		||||
  padding: 10px 20px;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
@@ -419,9 +428,9 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post-button:hover:not(:disabled) {
 | 
			
		||||
  background: #218838;
 | 
			
		||||
  filter: brightness(1.1);
 | 
			
		||||
  transform: translateY(-2px);
 | 
			
		||||
  box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
 | 
			
		||||
  box-shadow: 0 4px 12px rgba(255, 69, 0, 0.4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post-button:disabled {
 | 
			
		||||
@@ -455,8 +464,8 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.comments-toggle-button {
 | 
			
		||||
  background: #1185fe;
 | 
			
		||||
  color: white;
 | 
			
		||||
  background: var(--theme-color);
 | 
			
		||||
  color: var(--white);
 | 
			
		||||
  border: none;
 | 
			
		||||
  padding: 8px 16px;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
@@ -467,9 +476,9 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.comments-toggle-button:hover {
 | 
			
		||||
  background: #0d6efd;
 | 
			
		||||
  filter: brightness(1.1);
 | 
			
		||||
  transform: translateY(-2px);
 | 
			
		||||
  box-shadow: 0 4px 12px rgba(17, 133, 254, 0.4);
 | 
			
		||||
  box-shadow: 0 4px 12px rgba(255, 69, 0, 0.4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.comment-item {
 | 
			
		||||
@@ -714,8 +723,8 @@
 | 
			
		||||
 | 
			
		||||
/* JSON Display Styles */
 | 
			
		||||
.json-button {
 | 
			
		||||
  background: #4caf50;
 | 
			
		||||
  color: white;
 | 
			
		||||
  background: var(--theme-color);
 | 
			
		||||
  color: var(--white);
 | 
			
		||||
  border: none;
 | 
			
		||||
  padding: 4px 8px;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
@@ -726,7 +735,7 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.json-button:hover {
 | 
			
		||||
  background: #45a049;
 | 
			
		||||
  filter: brightness(1.1);
 | 
			
		||||
  transform: scale(1.05);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -759,4 +768,108 @@
 | 
			
		||||
  color: #333;
 | 
			
		||||
  max-height: 400px;
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Tab Navigation */
 | 
			
		||||
.tab-navigation {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  border-bottom: 2px solid #e1e5e9;
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tab-button {
 | 
			
		||||
  background: none;
 | 
			
		||||
  border: none;
 | 
			
		||||
  padding: 12px 20px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  color: #656d76;
 | 
			
		||||
  border-bottom: 2px solid transparent;
 | 
			
		||||
  transition: all 0.2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tab-button:hover {
 | 
			
		||||
  color: var(--theme-color);
 | 
			
		||||
  background: #f6f8fa;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tab-button.active {
 | 
			
		||||
  color: var(--theme-color);
 | 
			
		||||
  border-bottom-color: var(--theme-color);
 | 
			
		||||
  background: #f6f8fa;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* AI Chat History */
 | 
			
		||||
.ai-chat-list {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  border: 1px solid #ddd;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chat-item {
 | 
			
		||||
  border: 1px solid #d1d9e0;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  padding: 16px;
 | 
			
		||||
  margin-bottom: 16px;
 | 
			
		||||
  background: #ffffff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chat-header {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  margin-bottom: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chat-actions {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chat-type-button {
 | 
			
		||||
  background: var(--theme-color);
 | 
			
		||||
  color: var(--white);
 | 
			
		||||
  border: none;
 | 
			
		||||
  padding: 4px 8px;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  cursor: default;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  margin-left: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chat-type-text {
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
  margin-left: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.chat-date {
 | 
			
		||||
  color: #656d76;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chat-content {
 | 
			
		||||
  background: #f6f8fa;
 | 
			
		||||
  padding: 12px;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  border-left: 4px solid #d1d9e0;
 | 
			
		||||
  margin-bottom: 8px;
 | 
			
		||||
  white-space: pre-wrap;
 | 
			
		||||
  line-height: 1.5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chat-meta {
 | 
			
		||||
  font-size: 11px;
 | 
			
		||||
  color: #656d76;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.no-chat {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding: 40px 20px;
 | 
			
		||||
  color: #656d76;
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
@@ -46,6 +46,8 @@ 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 [aiChatHistory, setAiChatHistory] = useState<any[]>([]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // Setup Jetstream WebSocket for real-time comments (optional)
 | 
			
		||||
@@ -151,6 +153,9 @@ function App() {
 | 
			
		||||
        console.log('OAuth session found, loading all comments...');
 | 
			
		||||
        loadAllComments();
 | 
			
		||||
        
 | 
			
		||||
        // Load AI chat history
 | 
			
		||||
        loadAiChatHistory(userProfile.did);
 | 
			
		||||
        
 | 
			
		||||
        // Load user list records if admin
 | 
			
		||||
        if (userProfile.did === appConfig.adminDid) {
 | 
			
		||||
          loadUserListRecords();
 | 
			
		||||
@@ -221,6 +226,50 @@ function App() {
 | 
			
		||||
    return `https://via.placeholder.com/48x48/1185fe/ffffff?text=${initial}`;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const loadAiChatHistory = async (did: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
      console.log('Loading AI chat history for DID:', did);
 | 
			
		||||
      const agent = atprotoOAuthService.getAgent();
 | 
			
		||||
      if (!agent) {
 | 
			
		||||
        console.log('No agent available');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Get AI chat records from current user
 | 
			
		||||
      const response = await agent.api.com.atproto.repo.listRecords({
 | 
			
		||||
        repo: did,
 | 
			
		||||
        collection: appConfig.collections.chat,
 | 
			
		||||
        limit: 100,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      console.log('AI chat history loaded:', response.data);
 | 
			
		||||
      const chatRecords = response.data.records || [];
 | 
			
		||||
      
 | 
			
		||||
      // Filter out old records with invalid AI profile data (temporary fix for migration)
 | 
			
		||||
      const validRecords = chatRecords.filter(record => {
 | 
			
		||||
        if (record.value.answer) {
 | 
			
		||||
          // This is an AI answer - check if it has valid AI profile
 | 
			
		||||
          return record.value.author?.handle && 
 | 
			
		||||
                 record.value.author?.handle !== 'ai-assistant' &&
 | 
			
		||||
                 record.value.author?.displayName !== 'AI Assistant';
 | 
			
		||||
        }
 | 
			
		||||
        return true; // Keep all questions
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      console.log(`Filtered ${chatRecords.length} records to ${validRecords.length} valid records`);
 | 
			
		||||
      
 | 
			
		||||
      // Sort by creation time and group question-answer pairs
 | 
			
		||||
      const sortedRecords = validRecords.sort((a, b) => 
 | 
			
		||||
        new Date(a.value.createdAt).getTime() - new Date(b.value.createdAt).getTime()
 | 
			
		||||
      );
 | 
			
		||||
      
 | 
			
		||||
      setAiChatHistory(sortedRecords);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error('Failed to load AI chat history:', err);
 | 
			
		||||
      setAiChatHistory([]);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const loadUserComments = async (did: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
      console.log('Loading comments for DID:', did);
 | 
			
		||||
@@ -305,7 +354,7 @@ function App() {
 | 
			
		||||
              if (user.did && user.did.includes('-placeholder')) {
 | 
			
		||||
                console.log(`Resolving placeholder DID for ${user.handle}`);
 | 
			
		||||
                try {
 | 
			
		||||
                  const profileResponse = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(user.handle)}`);
 | 
			
		||||
                  const profileResponse = await fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(user.handle)}`);
 | 
			
		||||
                  if (profileResponse.ok) {
 | 
			
		||||
                    const profileData = await profileResponse.json();
 | 
			
		||||
                    if (profileData.did) {
 | 
			
		||||
@@ -456,7 +505,7 @@ function App() {
 | 
			
		||||
          if (!record.value.author?.avatar && record.value.author?.handle) {
 | 
			
		||||
            try {
 | 
			
		||||
              // Public API でプロフィール取得
 | 
			
		||||
              const profileResponse = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(record.value.author.handle)}`);
 | 
			
		||||
              const profileResponse = await fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(record.value.author.handle)}`);
 | 
			
		||||
              
 | 
			
		||||
              if (profileResponse.ok) {
 | 
			
		||||
                const profileData = await profileResponse.json();
 | 
			
		||||
@@ -683,7 +732,7 @@ function App() {
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
          // Public APIでプロフィールを取得してDIDを解決
 | 
			
		||||
          const profileResponse = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`);
 | 
			
		||||
          const profileResponse = await fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`);
 | 
			
		||||
          if (profileResponse.ok) {
 | 
			
		||||
            const profileData = await profileResponse.json();
 | 
			
		||||
            if (profileData.did) {
 | 
			
		||||
@@ -974,11 +1023,30 @@ function App() {
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          {/* Tab Navigation */}
 | 
			
		||||
          <div className="tab-navigation">
 | 
			
		||||
            <button 
 | 
			
		||||
              className={`tab-button ${activeTab === 'comments' ? 'active' : ''}`}
 | 
			
		||||
              onClick={() => setActiveTab('comments')}
 | 
			
		||||
            >
 | 
			
		||||
              Comments ({comments.filter(shouldShowComment).length})
 | 
			
		||||
            </button>
 | 
			
		||||
            {user && (
 | 
			
		||||
              <button 
 | 
			
		||||
                className={`tab-button ${activeTab === 'ai-chat' ? 'active' : ''}`}
 | 
			
		||||
                onClick={() => setActiveTab('ai-chat')}
 | 
			
		||||
              >
 | 
			
		||||
                AI Chat History ({aiChatHistory.length})
 | 
			
		||||
              </button>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          {/* Comments List */}
 | 
			
		||||
          <div className="comments-list">
 | 
			
		||||
            <div className="comments-header">
 | 
			
		||||
              <h3>Comments</h3>
 | 
			
		||||
            </div>
 | 
			
		||||
          {activeTab === 'comments' && (
 | 
			
		||||
            <div className="comments-list">
 | 
			
		||||
              <div className="comments-header">
 | 
			
		||||
                <h3>Comments</h3>
 | 
			
		||||
              </div>
 | 
			
		||||
            {comments.filter(shouldShowComment).length === 0 ? (
 | 
			
		||||
              <p className="no-comments">
 | 
			
		||||
                {appConfig.rkey ? `No comments for this post yet` : `No comments yet`}
 | 
			
		||||
@@ -988,9 +1056,25 @@ function App() {
 | 
			
		||||
                <div key={index} className="comment-item">
 | 
			
		||||
                  <div className="comment-header">
 | 
			
		||||
                    <img 
 | 
			
		||||
                      src={record.value.author?.avatar || generatePlaceholderAvatar(record.value.author?.handle || 'unknown')} 
 | 
			
		||||
                      src={generatePlaceholderAvatar(record.value.author?.handle || 'unknown')} 
 | 
			
		||||
                      alt="User Avatar" 
 | 
			
		||||
                      className="comment-avatar"
 | 
			
		||||
                      ref={(img) => {
 | 
			
		||||
                        // Fetch fresh avatar from API when component mounts
 | 
			
		||||
                        if (img && record.value.author?.did) {
 | 
			
		||||
                          fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(record.value.author.did)}`)
 | 
			
		||||
                            .then(res => res.json())
 | 
			
		||||
                            .then(data => {
 | 
			
		||||
                              if (data.avatar && img) {
 | 
			
		||||
                                img.src = data.avatar;
 | 
			
		||||
                              }
 | 
			
		||||
                            })
 | 
			
		||||
                            .catch(err => {
 | 
			
		||||
                              console.warn('Failed to fetch fresh avatar:', err);
 | 
			
		||||
                              // Keep placeholder on error
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <div className="comment-author-info">
 | 
			
		||||
                      <span className="comment-author">
 | 
			
		||||
@@ -1047,7 +1131,92 @@ function App() {
 | 
			
		||||
                </div>
 | 
			
		||||
              ))
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          {/* AI Chat History List */}
 | 
			
		||||
          {activeTab === 'ai-chat' && user && (
 | 
			
		||||
            <div className="ai-chat-list">
 | 
			
		||||
              <div className="chat-header">
 | 
			
		||||
                <h3>AI Chat History</h3>
 | 
			
		||||
              </div>
 | 
			
		||||
              {aiChatHistory.length === 0 ? (
 | 
			
		||||
                <p className="no-chat">No AI conversations yet. Start chatting with Ask AI!</p>
 | 
			
		||||
              ) : (
 | 
			
		||||
                aiChatHistory.map((record, index) => (
 | 
			
		||||
                  <div key={index} className="chat-item">
 | 
			
		||||
                    <div className="chat-header">
 | 
			
		||||
                      <img 
 | 
			
		||||
                        src={generatePlaceholderAvatar(record.value.author?.handle || 'unknown')} 
 | 
			
		||||
                        alt="User Avatar" 
 | 
			
		||||
                        className="comment-avatar"
 | 
			
		||||
                        ref={(img) => {
 | 
			
		||||
                          // Fetch fresh avatar from API when component mounts
 | 
			
		||||
                          if (img && record.value.author?.did) {
 | 
			
		||||
                            fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(record.value.author.did)}`)
 | 
			
		||||
                              .then(res => res.json())
 | 
			
		||||
                              .then(data => {
 | 
			
		||||
                                if (data.avatar && img) {
 | 
			
		||||
                                  img.src = data.avatar;
 | 
			
		||||
                                }
 | 
			
		||||
                              })
 | 
			
		||||
                              .catch(err => {
 | 
			
		||||
                                console.warn('Failed to fetch fresh avatar:', err);
 | 
			
		||||
                                // Keep placeholder on error
 | 
			
		||||
                              });
 | 
			
		||||
                          }
 | 
			
		||||
                        }}
 | 
			
		||||
                      />
 | 
			
		||||
                      <div className="comment-author-info">
 | 
			
		||||
                        <span className="comment-author">
 | 
			
		||||
                          {record.value.author?.displayName || record.value.author?.handle || 'unknown'}
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <a 
 | 
			
		||||
                          href={generateProfileUrl(record.value.author?.handle || '', record.value.author?.did || '')}
 | 
			
		||||
                          target="_blank"
 | 
			
		||||
                          rel="noopener noreferrer"
 | 
			
		||||
                          className="comment-handle"
 | 
			
		||||
                        >
 | 
			
		||||
                          @{record.value.author?.handle || 'unknown'}
 | 
			
		||||
                        </a>
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <span className="comment-date">
 | 
			
		||||
                        {new Date(record.value.createdAt).toLocaleString()}
 | 
			
		||||
                      </span>
 | 
			
		||||
                      <div className="comment-actions">
 | 
			
		||||
                        <button 
 | 
			
		||||
                          onClick={() => toggleJsonDisplay(record.uri)}
 | 
			
		||||
                          className="json-button"
 | 
			
		||||
                          title="Show/Hide JSON"
 | 
			
		||||
                        >
 | 
			
		||||
                          {showJsonFor === record.uri ? 'Hide' : 'JSON'}
 | 
			
		||||
                        </button>
 | 
			
		||||
                        <button className="chat-type-button">
 | 
			
		||||
                          {record.value.question ? 'Question' : 'Answer'}
 | 
			
		||||
                        </button>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="comment-content">
 | 
			
		||||
                      {record.value.question || record.value.answer}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="comment-meta">
 | 
			
		||||
                      <small>{record.uri}</small>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    {/* JSON Display */}
 | 
			
		||||
                    {showJsonFor === record.uri && (
 | 
			
		||||
                      <div className="json-display">
 | 
			
		||||
                        <h5>JSON Record:</h5>
 | 
			
		||||
                        <pre className="json-content">
 | 
			
		||||
                          {JSON.stringify(record, null, 2)}
 | 
			
		||||
                        </pre>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </div>
 | 
			
		||||
                ))
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          {/* Comment Form - Only show on post pages */}
 | 
			
		||||
          {user && appConfig.rkey && (
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								oauth/src/components/AIChat-access.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								oauth/src/components/AIChat-access.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
// Cloudflare Access対応版の例
 | 
			
		||||
const response = await fetch(`${aiConfig.host}/api/generate`, {
 | 
			
		||||
  method: 'POST',
 | 
			
		||||
  headers: {
 | 
			
		||||
    'Content-Type': 'application/json',
 | 
			
		||||
    // Cloudflare Access Service Token
 | 
			
		||||
    'CF-Access-Client-Id': import.meta.env.VITE_CF_ACCESS_CLIENT_ID,
 | 
			
		||||
    'CF-Access-Client-Secret': import.meta.env.VITE_CF_ACCESS_CLIENT_SECRET,
 | 
			
		||||
  },
 | 
			
		||||
  body: JSON.stringify({
 | 
			
		||||
    model: aiConfig.model,
 | 
			
		||||
    prompt: prompt,
 | 
			
		||||
    stream: false,
 | 
			
		||||
    options: {
 | 
			
		||||
      temperature: 0.9,
 | 
			
		||||
      top_p: 0.9,
 | 
			
		||||
      num_predict: 80,
 | 
			
		||||
      repeat_penalty: 1.1,
 | 
			
		||||
    }
 | 
			
		||||
  }),
 | 
			
		||||
});
 | 
			
		||||
@@ -23,11 +23,15 @@ export const AIChat: React.FC<AIChatProps> = ({ user, isEnabled }) => {
 | 
			
		||||
    host: import.meta.env.VITE_AI_HOST || 'https://ollama.syui.ai',
 | 
			
		||||
    systemPrompt: import.meta.env.VITE_AI_SYSTEM_PROMPT || 'You are a helpful AI assistant trained on this blog\'s content.',
 | 
			
		||||
    aiDid: import.meta.env.VITE_AI_DID || 'did:plc:uqzpqmrjnptsxezjx4xuh2mn',
 | 
			
		||||
    bskyPublicApi: import.meta.env.VITE_BSKY_PUBLIC_API || 'https://public.api.bsky.app',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Fetch AI profile on load
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchAIProfile = async () => {
 | 
			
		||||
      console.log('=== AI PROFILE FETCH START ===');
 | 
			
		||||
      console.log('AI DID:', aiConfig.aiDid);
 | 
			
		||||
      
 | 
			
		||||
      if (!aiConfig.aiDid) {
 | 
			
		||||
        console.log('No AI DID configured');
 | 
			
		||||
        return;
 | 
			
		||||
@@ -42,51 +46,48 @@ export const AIChat: React.FC<AIChatProps> = ({ user, isEnabled }) => {
 | 
			
		||||
          console.log('AI profile fetched successfully:', profile.data);
 | 
			
		||||
          const profileData = {
 | 
			
		||||
            did: aiConfig.aiDid,
 | 
			
		||||
            handle: profile.data.handle || 'ai-assistant',
 | 
			
		||||
            displayName: profile.data.displayName || 'AI Assistant',
 | 
			
		||||
            avatar: profile.data.avatar || null,
 | 
			
		||||
            description: profile.data.description || null
 | 
			
		||||
            handle: profile.data.handle,
 | 
			
		||||
            displayName: profile.data.displayName,
 | 
			
		||||
            avatar: profile.data.avatar,
 | 
			
		||||
            description: profile.data.description
 | 
			
		||||
          };
 | 
			
		||||
          console.log('Setting aiProfile to:', profileData);
 | 
			
		||||
          setAiProfile(profileData);
 | 
			
		||||
          
 | 
			
		||||
          // Dispatch event to update Ask AI button
 | 
			
		||||
          window.dispatchEvent(new CustomEvent('aiProfileLoaded', { detail: profileData }));
 | 
			
		||||
          console.log('=== AI PROFILE FETCH SUCCESS (AGENT) ===');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Fallback to public API
 | 
			
		||||
        console.log('No agent available, trying public API for AI profile');
 | 
			
		||||
        const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(aiConfig.aiDid)}`);
 | 
			
		||||
        const response = await fetch(`${aiConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(aiConfig.aiDid)}`);
 | 
			
		||||
        if (response.ok) {
 | 
			
		||||
          const profileData = await response.json();
 | 
			
		||||
          console.log('AI profile fetched via public API:', profileData);
 | 
			
		||||
          const profile = {
 | 
			
		||||
            did: aiConfig.aiDid,
 | 
			
		||||
            handle: profileData.handle || 'ai-assistant',
 | 
			
		||||
            displayName: profileData.displayName || 'AI Assistant',
 | 
			
		||||
            avatar: profileData.avatar || null,
 | 
			
		||||
            description: profileData.description || null
 | 
			
		||||
            handle: profileData.handle,
 | 
			
		||||
            displayName: profileData.displayName,
 | 
			
		||||
            avatar: profileData.avatar,
 | 
			
		||||
            description: profileData.description
 | 
			
		||||
          };
 | 
			
		||||
          console.log('Setting aiProfile to:', profile);
 | 
			
		||||
          setAiProfile(profile);
 | 
			
		||||
          
 | 
			
		||||
          // Dispatch event to update Ask AI button
 | 
			
		||||
          window.dispatchEvent(new CustomEvent('aiProfileLoaded', { detail: profile }));
 | 
			
		||||
          console.log('=== AI PROFILE FETCH SUCCESS (PUBLIC API) ===');
 | 
			
		||||
          return;
 | 
			
		||||
        } else {
 | 
			
		||||
          console.error('Public API failed with status:', response.status);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.log('Failed to fetch AI profile, using defaults:', error);
 | 
			
		||||
        const fallbackProfile = {
 | 
			
		||||
          did: aiConfig.aiDid,
 | 
			
		||||
          handle: 'ai-assistant',
 | 
			
		||||
          displayName: 'AI Assistant',
 | 
			
		||||
          avatar: null,
 | 
			
		||||
          description: 'AI assistant for this blog'
 | 
			
		||||
        };
 | 
			
		||||
        setAiProfile(fallbackProfile);
 | 
			
		||||
        
 | 
			
		||||
        // Dispatch event even with fallback profile
 | 
			
		||||
        window.dispatchEvent(new CustomEvent('aiProfileLoaded', { detail: fallbackProfile }));
 | 
			
		||||
        console.error('Failed to fetch AI profile:', error);
 | 
			
		||||
        setAiProfile(null);
 | 
			
		||||
      }
 | 
			
		||||
      console.log('=== AI PROFILE FETCH FAILED ===');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    fetchAIProfile();
 | 
			
		||||
@@ -97,9 +98,11 @@ export const AIChat: React.FC<AIChatProps> = ({ user, isEnabled }) => {
 | 
			
		||||
 | 
			
		||||
    // Listen for AI question posts from base.html
 | 
			
		||||
    const handleAIQuestion = async (event: any) => {
 | 
			
		||||
      if (!user || !event.detail || !event.detail.question || isProcessing) return;
 | 
			
		||||
      if (!user || !event.detail || !event.detail.question || isProcessing || !aiProfile) return;
 | 
			
		||||
      
 | 
			
		||||
      console.log('AIChat received question:', event.detail.question);
 | 
			
		||||
      console.log('Current aiProfile state:', aiProfile);
 | 
			
		||||
      
 | 
			
		||||
      setIsProcessing(true);
 | 
			
		||||
      try {
 | 
			
		||||
        await postQuestionAndGenerateResponse(event.detail.question);
 | 
			
		||||
@@ -120,10 +123,10 @@ export const AIChat: React.FC<AIChatProps> = ({ user, isEnabled }) => {
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener('postAIQuestion', handleAIQuestion);
 | 
			
		||||
    };
 | 
			
		||||
  }, [user, isEnabled, isProcessing]);
 | 
			
		||||
  }, [user, isEnabled, isProcessing, aiProfile]);
 | 
			
		||||
 | 
			
		||||
  const postQuestionAndGenerateResponse = async (question: string) => {
 | 
			
		||||
    if (!user || !aiConfig.askAi) return;
 | 
			
		||||
    if (!user || !aiConfig.askAi || !aiProfile) return;
 | 
			
		||||
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    
 | 
			
		||||
@@ -232,6 +235,9 @@ Answer:`;
 | 
			
		||||
      // 5. Save AI response in background
 | 
			
		||||
      const answerRkey = now.toISOString().replace(/[:.]/g, '-') + '-answer';
 | 
			
		||||
      
 | 
			
		||||
      console.log('=== SAVING AI ANSWER ===');
 | 
			
		||||
      console.log('Current aiProfile:', aiProfile);
 | 
			
		||||
      
 | 
			
		||||
      const answerRecord = {
 | 
			
		||||
        $type: appConfig.collections.chat,
 | 
			
		||||
        answer: aiAnswer,
 | 
			
		||||
@@ -239,11 +245,14 @@ Answer:`;
 | 
			
		||||
        url: window.location.href,
 | 
			
		||||
        createdAt: now.toISOString(),
 | 
			
		||||
        author: {
 | 
			
		||||
          did: aiConfig.aiDid,
 | 
			
		||||
          handle: 'AI Assistant',
 | 
			
		||||
          displayName: 'AI Assistant',
 | 
			
		||||
          did: aiProfile.did,
 | 
			
		||||
          handle: aiProfile.handle,
 | 
			
		||||
          displayName: aiProfile.displayName,
 | 
			
		||||
          avatar: aiProfile.avatar,
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      console.log('Answer record to save:', answerRecord);
 | 
			
		||||
 | 
			
		||||
      // Save to ATProto asynchronously (don't wait for it)
 | 
			
		||||
      agent.api.com.atproto.repo.putRecord({
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ export interface AppConfig {
 | 
			
		||||
  aiProvider: string;
 | 
			
		||||
  aiModel: string;
 | 
			
		||||
  aiHost: string;
 | 
			
		||||
  bskyPublicApi: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Generate collection names from host
 | 
			
		||||
@@ -80,13 +81,15 @@ export function getAppConfig(): AppConfig {
 | 
			
		||||
  const aiProvider = import.meta.env.VITE_AI_PROVIDER || 'ollama';
 | 
			
		||||
  const aiModel = import.meta.env.VITE_AI_MODEL || 'gemma2:2b';
 | 
			
		||||
  const aiHost = import.meta.env.VITE_AI_HOST || 'https://ollama.syui.ai';
 | 
			
		||||
  const bskyPublicApi = import.meta.env.VITE_BSKY_PUBLIC_API || 'https://public.api.bsky.app';
 | 
			
		||||
  
 | 
			
		||||
  console.log('App configuration:', {
 | 
			
		||||
    host,
 | 
			
		||||
    adminDid,
 | 
			
		||||
    collections,
 | 
			
		||||
    rkey: rkey || 'none (not on post page)',
 | 
			
		||||
    ai: { enabled: aiEnabled, askAi: aiAskAi, provider: aiProvider, model: aiModel, host: aiHost }
 | 
			
		||||
    ai: { enabled: aiEnabled, askAi: aiAskAi, provider: aiProvider, model: aiModel, host: aiHost },
 | 
			
		||||
    bskyPublicApi
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  return {
 | 
			
		||||
@@ -98,7 +101,8 @@ export function getAppConfig(): AppConfig {
 | 
			
		||||
    aiAskAi,
 | 
			
		||||
    aiProvider,
 | 
			
		||||
    aiModel,
 | 
			
		||||
    aiHost
 | 
			
		||||
    aiHost,
 | 
			
		||||
    bskyPublicApi
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user