<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>動画生成結果(React UI)</title>
<!-- React & ReactDOM (CDN) -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- Babel Standalone (JSXをブラウザで変換) -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Noto Sans JP', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #fcfcfc;
padding: 48px 20px;
min-height: 100vh;
}
.container {
max-width: 720px;
margin: 0 auto;
}
/* 和風ヘッダー */
.header {
margin-bottom: 40px;
text-align: center;
padding-bottom: 20px;
border-bottom: 0.5px solid #d4d4d4;
}
.header h1 {
font-size: 22px;
font-weight: 500;
color: #2e4057;
margin-bottom: 8px;
letter-spacing: 0.05em;
}
.header p {
font-size: 13px;
color: #8e8e8e;
letter-spacing: 0.02em;
}
/* メインカード */
.main-card {
background: white;
border-radius: 2px;
border: 0.5px solid #e0e0e0;
overflow: hidden;
}
/* 動画セクション */
.video-section {
position: relative;
background: #000;
}
.video-player {
width: 100%;
display: block;
}
.video-overlay {
position: absolute;
top: 16px;
right: 16px;
display: flex;
gap: 8px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border-radius: 2px;
font-size: 11px;
font-weight: 400;
background: rgba(255, 255, 255, 0.95);
color: #2e4057;
border: 0.5px solid rgba(0, 0, 0, 0.1);
letter-spacing: 0.03em;
}
/* 情報セクション */
.info-section {
padding: 32px 28px;
border-bottom: 0.5px solid #e0e0e0;
}
.title {
font-size: 17px;
font-weight: 500;
color: #2e4057;
margin-bottom: 12px;
line-height: 1.7;
letter-spacing: 0.02em;
}
.description {
font-size: 13px;
color: #6e6e6e;
margin-bottom: 24px;
line-height: 1.8;
letter-spacing: 0.02em;
}
.meta-info {
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px;
background: #fafafa;
border-radius: 2px;
border: 0.5px solid #e8e8e8;
}
.meta-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
padding: 8px 0;
border-bottom: 0.5px solid #f0f0f0;
}
.meta-item:last-child {
border-bottom: none;
}
.meta-item .icon {
display: none;
}
.meta-item .label {
font-weight: 400;
color: #8e8e8e;
letter-spacing: 0.03em;
}
.meta-item .value {
font-weight: 400;
color: #2e4057;
letter-spacing: 0.02em;
}
/* QRコードセクション */
.qr-section {
background: white;
padding: 40px 28px;
text-align: center;
}
.qr-title {
font-size: 15px;
font-weight: 400;
color: #2e4057;
margin-bottom: 28px;
letter-spacing: 0.08em;
}
.qr-code-wrapper {
background: white;
border: 0.5px solid #d4d4d4;
border-radius: 2px;
padding: 24px;
display: inline-block;
margin-bottom: 28px;
}
.qr-image {
width: 180px;
height: 180px;
display: block;
}
.url-section {
background: #fafafa;
border: 0.5px solid #e8e8e8;
border-radius: 2px;
padding: 20px;
margin-top: 24px;
}
.url-label {
font-size: 11px;
color: #8e8e8e;
margin-bottom: 10px;
font-weight: 400;
letter-spacing: 0.05em;
}
.url-link {
color: #2e4057;
font-size: 13px;
font-weight: 400;
text-decoration: none;
word-break: break-all;
display: block;
padding: 12px;
background: white;
border: 0.5px solid #d4d4d4;
border-radius: 2px;
letter-spacing: 0.02em;
transition: background 0.2s;
}
.url-link:hover {
background: #fcfcfc;
}
/* アクションボタン */
.actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
flex: 1;
padding: 12px 16px;
border: 0.5px solid #2e4057;
border-radius: 2px;
font-size: 12px;
font-weight: 400;
cursor: pointer;
background: white;
color: #2e4057;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
letter-spacing: 0.03em;
transition: all 0.2s;
}
.btn:hover {
background: #2e4057;
color: white;
}
.btn-primary {
background: #2e4057;
color: white;
}
.btn-primary:hover {
background: #1f2d3d;
}
/* ローディング */
.loading {
text-align: center;
padding: 60px 20px;
color: #2e4057;
}
.spinner {
width: 40px;
height: 40px;
border: 2px solid #e8e8e8;
border-top-color: #2e4057;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* レスポンシブ */
@media (max-width: 600px) {
.header h1 {
font-size: 24px;
}
.qr-image {
width: 180px;
height: 180px;
}
.actions {
flex-direction: column;
}
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
// 言語コード → 表示名のマッピング
const languageNames = {
'ja': '日本語', 'en': 'English', 'zh': '中文', 'zh-TW': '繁體中文',
'ko': '한국어', 'th': 'ไทย', 'es': 'Español', 'it': 'Italiano',
'fr': 'Français', 'de': 'Deutsch', 'ru': 'Русский',
'ms': 'Bahasa Melayu', 'id': 'Bahasa Indonesia',
'vi': 'Tiếng Việt', 'fil': 'Filipino'
};
function App() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
console.log('[React Widget] Component mounted');
// 初期データ取得
const initialData = window.openai?.toolOutput;
console.log('[React Widget] Initial toolOutput:', initialData);
if (initialData && Object.keys(initialData).length > 0) {
console.log('[React Widget] ✅ Initial data found');
setData(initialData);
setLoading(false);
}
// openai:set_globals イベントリスナー
const handleSetGlobals = (event) => {
console.log('[React Widget] 🎉 openai:set_globals event!', event);
const newData = event.detail?.toolOutput || window.openai?.toolOutput;
console.log('[React Widget] New toolOutput:', newData);
if (newData && Object.keys(newData).length > 0) {
console.log('[React Widget] ✅ Data received, updating state');
setData(newData);
setLoading(false);
}
};
window.addEventListener('openai:set_globals', handleSetGlobals);
console.log('[React Widget] ✅ Event listener registered');
return () => {
window.removeEventListener('openai:set_globals', handleSetGlobals);
};
}, []);
// ローディング表示
if (loading || !data) {
return (
<div className="loading">
<div className="spinner"></div>
<p>動画データを読み込み中...</p>
</div>
);
}
const {
qrCode, shortUrl, videoUrl, title, description,
language, durationSeconds, projectId,
useBgm, useSubtitles, useVerticalVideo
} = data;
const langName = languageNames[language] || language || '日本語';
const duration = durationSeconds ? Math.floor(durationSeconds) + '秒' : '不明';
return (
<div className="container">
<div className="header">
<h1>動画が生成されました</h1>
<p>{title || 'AI生成動画'}</p>
</div>
<div className="main-card">
{/* 動画セクション */}
<div className="video-section">
<video className="video-player" controls preload="metadata">
<source src={videoUrl} type="video/mp4" />
お使いのブラウザは動画再生に対応していません。
</video>
{/* バッジオーバーレイ */}
<div className="video-overlay">
<div className="badge language">
🌐 {langName}
</div>
{useBgm && (
<div className="badge bgm">
🎵 BGM
</div>
)}
{useSubtitles && (
<div className="badge subtitles">
📝 字幕
</div>
)}
{useVerticalVideo && (
<div className="badge vertical">
📱 縦動画
</div>
)}
</div>
</div>
{/* 情報セクション */}
<div className="info-section">
<h2 className="title">{title || '生成された動画'}</h2>
{description && (
<p className="description">{description}</p>
)}
<div className="meta-info">
<div className="meta-item">
<span className="label">再生時間</span>
<span className="value">{duration}</span>
</div>
<div className="meta-item">
<span className="label">プロジェクトID</span>
<span className="value">{projectId}</span>
</div>
<div className="meta-item">
<span className="label">BGM</span>
<span className="value">{useBgm ? 'あり' : 'なし'}</span>
</div>
<div className="meta-item">
<span className="label">字幕</span>
<span className="value">{useSubtitles ? 'あり' : 'なし'}</span>
</div>
</div>
</div>
{/* QRコードセクション */}
<div className="qr-section">
<h3 className="qr-title">QRコード</h3>
<div className="qr-code-wrapper">
<img src={qrCode} alt="QR Code" className="qr-image" />
</div>
<div className="url-section">
<div className="url-label">共有用URL</div>
<a href={shortUrl} target="_blank" rel="noopener noreferrer" className="url-link">
{shortUrl}
</a>
<div className="actions">
<a href={qrCode} download={`qrcode_${projectId}.png`} className="btn btn-primary" style={{ textDecoration: 'none' }}>
QRコードをダウンロード
</a>
<a href={shortUrl} target="_blank" rel="noopener noreferrer" className="btn" style={{ textDecoration: 'none' }} onClick={(e) => { e.preventDefault(); navigator.clipboard.writeText(shortUrl).then(() => alert('URLをコピーしました!')).catch(() => alert('コピーに失敗しました')); }}>
URLをコピー
</a>
<a href={shortUrl} target="_blank" rel="noopener noreferrer" className="btn" style={{ textDecoration: 'none' }}>
新しいタブで開く
</a>
</div>
</div>
</div>
</div>
</div>
);
}
// React アプリケーションをマウント
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
console.log('[React Widget] ✅ React app rendered');
</script>
</body>
</html>