Files
log/oauth/src/components/TestUI.jsx
2025-06-19 13:09:37 +09:00

531 lines
14 KiB
JavaScript

import React, { useState } from 'react'
import { env } from '../config/env.js'
import AvatarTestPanel from './AvatarTestPanel.jsx'
import AvatarTest from './AvatarTest.jsx'
export default function TestUI() {
const [activeTab, setActiveTab] = useState('putRecord')
const [accessJwt, setAccessJwt] = useState('')
const [handle, setHandle] = useState('')
const [sessionDid, setSessionDid] = useState('')
const [collection, setCollection] = useState('ai.syui.log')
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [success, setSuccess] = useState(null)
const [showJson, setShowJson] = useState(false)
const [lastRecord, setLastRecord] = useState(null)
const collections = [
'ai.syui.log',
'ai.syui.log.chat',
'ai.syui.log.chat.lang',
'ai.syui.log.chat.comment'
]
const generateDummyData = (collectionType) => {
const timestamp = new Date().toISOString()
const url = 'https://syui.ai/test/dummy'
const basePost = {
url: url,
date: timestamp,
slug: 'dummy-test',
tags: ['test', 'dummy'],
title: 'Test Post',
language: 'ja'
}
const baseAuthor = {
did: sessionDid || null, // Use real session DID if available, otherwise null
handle: handle || 'test.user',
displayName: 'Test User',
avatar: null
}
switch (collectionType) {
case 'ai.syui.log':
return {
$type: collectionType,
url: url,
post: basePost,
text: 'テストコメントです。これはダミーデータです。',
type: 'comment',
author: baseAuthor,
createdAt: timestamp
}
case 'ai.syui.log.chat':
const isQuestion = Math.random() > 0.5
return {
$type: collectionType,
post: basePost,
text: isQuestion ? 'これはテスト用の質問です。' : 'これはテスト用のAI回答です。詳しく説明します。',
type: isQuestion ? 'question' : 'answer',
author: isQuestion ? baseAuthor : {
did: 'did:plc:ai-test',
handle: 'ai.syui.ai',
displayName: 'ai',
avatar: null
},
createdAt: timestamp
}
case 'ai.syui.log.chat.lang':
return {
$type: collectionType,
post: basePost,
text: 'This is a test translation. Hello, this is a dummy English translation of the Japanese post.',
type: 'en',
author: {
did: 'did:plc:ai-test',
handle: 'ai.syui.ai',
displayName: 'ai',
avatar: null
},
createdAt: timestamp
}
case 'ai.syui.log.chat.comment':
return {
$type: collectionType,
post: basePost,
text: 'これはAIによるテストコメントです。記事についての感想や補足情報を提供します。',
author: {
did: 'did:plc:ai-test',
handle: 'ai.syui.ai',
displayName: 'ai',
avatar: null
},
createdAt: timestamp
}
default:
return {}
}
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!accessJwt.trim() || !handle.trim()) {
setError('Access JWT and Handle are required')
return
}
setLoading(true)
setError(null)
setSuccess(null)
try {
const recordData = generateDummyData(collection)
const rkey = `test-${Date.now()}`
const record = {
repo: handle, // Use handle as is, without adding .bsky.social
collection: collection,
rkey: rkey,
record: recordData
}
setLastRecord(record)
// Direct API call with accessJwt
const response = await fetch(`https://${env.pds}/xrpc/com.atproto.repo.putRecord`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessJwt}`
},
body: JSON.stringify(record)
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(`API Error: ${response.status} - ${errorData.message || response.statusText}`)
}
const result = await response.json()
setSuccess(`Record created successfully! URI: ${result.uri}`)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
const handleDelete = async () => {
if (!lastRecord || !accessJwt.trim()) {
setError('No record to delete or missing access JWT')
return
}
setLoading(true)
setError(null)
try {
const deleteData = {
repo: lastRecord.repo,
collection: lastRecord.collection,
rkey: lastRecord.rkey
}
const response = await fetch(`https://${env.pds}/xrpc/com.atproto.repo.deleteRecord`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessJwt}`
},
body: JSON.stringify(deleteData)
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(`Delete Error: ${response.status} - ${errorData.message || response.statusText}`)
}
setSuccess('Record deleted successfully!')
setLastRecord(null)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
return (
<div className="test-ui">
<h2>🧪 Test UI</h2>
{/* Tab Navigation */}
<div className="test-tabs">
<button
onClick={() => setActiveTab('putRecord')}
className={`test-tab ${activeTab === 'putRecord' ? 'active' : ''}`}
>
Manual putRecord
</button>
<button
onClick={() => setActiveTab('avatar')}
className={`test-tab ${activeTab === 'avatar' ? 'active' : ''}`}
>
Avatar System
</button>
</div>
{activeTab === 'putRecord' && (
<div className="test-content">
<p className="description">
OAuth不要のテスト用UIaccessJwtとhandleを直接入力して各collectionにダミーデータを投稿できます
</p>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="access-jwt">Access JWT:</label>
<textarea
id="access-jwt"
value={accessJwt}
onChange={(e) => setAccessJwt(e.target.value)}
placeholder="eyJ... (Access JWT token)"
rows={3}
required
disabled={loading}
/>
</div>
<div className="form-group">
<label htmlFor="handle">Handle:</label>
<input
id="handle"
type="text"
value={handle}
onChange={(e) => setHandle(e.target.value)}
placeholder="user.bsky.social"
required
disabled={loading}
/>
</div>
<div className="form-group">
<label htmlFor="session-did">Session DID (optional):</label>
<input
id="session-did"
type="text"
value={sessionDid}
onChange={(e) => setSessionDid(e.target.value)}
placeholder="did:plc:xxxxx (Leave empty to use test DID)"
disabled={loading}
/>
</div>
<div className="form-group">
<label htmlFor="collection">Collection:</label>
<select
id="collection"
value={collection}
onChange={(e) => setCollection(e.target.value)}
disabled={loading}
>
{collections.map(col => (
<option key={col} value={col}>{col}</option>
))}
</select>
</div>
{error && (
<div className="error-message">
{error}
</div>
)}
{success && (
<div className="success-message">
{success}
</div>
)}
<div className="form-actions">
<button
type="submit"
disabled={loading || !accessJwt.trim() || !handle.trim()}
className="submit-btn"
>
{loading ? '⏳ Creating...' : '📤 Create Record'}
</button>
<button
type="button"
onClick={() => setShowJson(!showJson)}
className="json-btn"
disabled={loading}
>
{showJson ? '🙈 Hide JSON' : '👁️ Show JSON'}
</button>
{lastRecord && (
<button
type="button"
onClick={handleDelete}
className="delete-btn"
disabled={loading}
>
{loading ? '⏳ Deleting...' : '🗑️ Delete Last Record'}
</button>
)}
</div>
</form>
{showJson && (
<div className="json-preview">
<h3>Generated JSON:</h3>
<pre>{JSON.stringify(generateDummyData(collection), null, 2)}</pre>
</div>
)}
{lastRecord && (
<div className="last-record">
<h3>Last Created Record:</h3>
<div className="record-info">
<p><strong>Collection:</strong> {lastRecord.collection}</p>
<p><strong>RKey:</strong> {lastRecord.rkey}</p>
<p><strong>Repo:</strong> {lastRecord.repo}</p>
</div>
</div>
)}
</div>
)}
{activeTab === 'avatar' && (
<div className="test-content">
<AvatarTestPanel />
</div>
)}
<style jsx>{`
.test-ui {
border: 3px solid #ff6b6b;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
background: #fff5f5;
}
.test-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #ddd;
padding-bottom: 10px;
}
.test-tab {
background: #f8f9fa;
border: 1px solid #ddd;
padding: 8px 16px;
border-radius: 4px 4px 0 0;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: #666;
transition: all 0.2s;
}
.test-tab:hover {
background: #e9ecef;
color: #333;
}
.test-tab.active {
background: #ff6b6b;
color: white;
border-color: #ff6b6b;
}
.test-content {
margin-top: 20px;
}
.test-ui h2 {
color: #ff6b6b;
margin-top: 0;
}
.description {
color: #666;
font-style: italic;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
font-family: monospace;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #ff6b6b;
box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.25);
}
.form-group input:disabled,
.form-group textarea:disabled,
.form-group select:disabled {
background: #f8f9fa;
cursor: not-allowed;
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
border: 1px solid #f5c6cb;
}
.success-message {
background: #d4edda;
color: #155724;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
border: 1px solid #c3e6cb;
}
.form-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 20px;
}
.submit-btn {
background: #ff6b6b;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background 0.2s;
}
.submit-btn:hover:not(:disabled) {
background: #ff5252;
}
.submit-btn:disabled {
background: #6c757d;
cursor: not-allowed;
}
.json-btn {
background: #17a2b8;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background 0.2s;
}
.json-btn:hover:not(:disabled) {
background: #138496;
}
.delete-btn {
background: #dc3545;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background 0.2s;
}
.delete-btn:hover:not(:disabled) {
background: #c82333;
}
.json-preview {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
}
.json-preview h3 {
margin-top: 0;
color: #495057;
}
.json-preview pre {
background: #e9ecef;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
margin: 0;
}
.last-record {
margin-top: 20px;
padding: 15px;
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 4px;
}
.last-record h3 {
margin-top: 0;
color: #0066cc;
}
.record-info p {
margin: 5px 0;
font-family: monospace;
font-size: 14px;
}
`}</style>
</div>
)
}