import React, { useState, useCallback, useEffect } from 'react'
import mcpService from '../../services/mcpService'
import MCPStatusBanner from '../../components/MCPStatusBanner'
// VRoid Studio integration data
const vroidStudioData = {
connected: true,
version: '1.22.1',
currentModel: {
id: 'robot_assistant_v2',
name: 'Robot Assistant V2',
triangles: 12547,
bones: 89,
textures: 3,
animations: 12
},
models: [
{ id: 'robot_assistant_v1', name: 'Robot Assistant V1', status: 'archived', lastModified: '2024-01-10' },
{ id: 'robot_assistant_v2', name: 'Robot Assistant V2', status: 'active', lastModified: '2024-01-15' },
{ id: 'maintenance_bot', name: 'Maintenance Bot', status: 'draft', lastModified: '2024-01-12' }
],
animations: [
{ id: 'idle', name: 'Idle Animation', duration: 3.2, frames: 96 },
{ id: 'walk', name: 'Walking Cycle', duration: 1.8, frames: 54 },
{ id: 'interact', name: 'Interaction Pose', duration: 2.1, frames: 63 },
{ id: 'maintenance', name: 'Maintenance Routine', duration: 4.5, frames: 135 }
],
rigging: {
status: 'calibrated',
joints: 89,
constraints: 23,
ikChains: 8,
blendShapes: 15
},
export: {
formats: ['VRM', 'FBX', 'GLTF', 'OBJ'],
lastExport: '2024-01-15T14:30:00Z',
target: 'Unity3D'
}
}
export default function VRoidPage() {
console.log('VRoid Studio Integration loading...')
const [vroidData, setVroidData] = useState(vroidStudioData)
const [selectedModel, setSelectedModel] = useState(vroidData.currentModel.id)
const [selectedAnimation, setSelectedAnimation] = useState('')
const [previewMode, setPreviewMode] = useState('model')
const [mcpConnected, setMcpConnected] = useState(false)
const [usingMockData, setUsingMockData] = useState(true)
useEffect(() => {
const checkConnection = async () => {
const healthy = await mcpService.checkHealth('vroid')
setMcpConnected(healthy)
setUsingMockData(!healthy)
}
checkConnection()
const interval = setInterval(checkConnection, 10000)
return () => clearInterval(interval)
}, [])
const exportModel = useCallback(async (format: string) => {
console.log(`Exporting model to ${format}...`)
if (mcpConnected) {
try {
const result = await mcpService.vroidCallTool('export_model', {
model_id: selectedModel,
format: format
})
if (result.success) {
setVroidData(prev => ({
...prev,
export: {
...prev.export,
lastExport: new Date().toISOString(),
target: format === 'VRM' ? 'VRChat' : 'Unity3D'
}
}))
} else {
console.warn(`MCP export failed: ${result.error}`)
}
} catch (error) {
console.error('Error exporting via MCP:', error)
}
} else {
// Mock fallback
setTimeout(() => {
setVroidData(prev => ({
...prev,
export: {
...prev.export,
lastExport: new Date().toISOString(),
target: format === 'VRM' ? 'VRChat' : 'Unity3D'
}
}))
}, 2000)
}
}, [mcpConnected, selectedModel])
const createAnimation = useCallback(() => {
console.log('Creating new animation...')
// Simulate animation creation
const newAnimation = {
id: `anim_${Date.now()}`,
name: 'New Animation',
duration: 2.0,
frames: 60
}
setVroidData(prev => ({
...prev,
animations: [...prev.animations, newAnimation]
}))
}, [])
const calibrateRigging = useCallback(() => {
console.log('Calibrating rigging...')
setVroidData(prev => ({
...prev,
rigging: {
...prev.rigging,
status: 'calibrating'
}
}))
// Simulate calibration
setTimeout(() => {
setVroidData(prev => ({
...prev,
rigging: {
...prev.rigging,
status: 'calibrated'
}
}))
}, 5000)
}, [])
return (
<div style={{
minHeight: '100vh',
backgroundColor: '#0f172a',
color: '#f8fafc',
padding: '24px',
fontFamily: 'system-ui, -apple-system, sans-serif'
}}>
<div style={{ maxWidth: '1400px', margin: '0 auto' }}>
{/* Header */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '32px',
padding: '24px',
backgroundColor: '#1e293b',
borderRadius: '16px',
border: '1px solid #334155'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
<div style={{
width: '64px',
height: '64px',
backgroundColor: '#ec4899',
borderRadius: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '32px'
}}>
π
</div>
<div>
<h1 style={{
fontSize: '32px',
fontWeight: 'bold',
color: '#f8fafc',
marginBottom: '4px'
}}>VRoid Studio Integration</h1>
<p style={{ color: '#94a3b8' }}>Professional 3D avatar creation and animation for robotics interfaces</p>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{
padding: '8px 16px',
backgroundColor: mcpConnected ? '#10b981' : '#dc2626',
color: 'white',
borderRadius: '20px',
fontSize: '12px',
fontWeight: '500'
}}>
π {mcpConnected ? 'MCP Connected' : 'MCP Disconnected'}
</div>
{usingMockData && (
<div style={{
padding: '6px 12px',
backgroundColor: '#f59e0b',
color: 'white',
borderRadius: '12px',
fontSize: '11px',
fontWeight: '500'
}}>
β οΈ MOCK DATA
</div>
)}
<div style={{
padding: '8px 16px',
backgroundColor: '#6b7280',
color: 'white',
borderRadius: '20px',
fontSize: '12px',
fontWeight: '500'
}}>
v{vroidData.version}
</div>
</div>
</div>
{/* Main Content */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 350px',
gap: '24px'
}}>
{/* Left Panel - 3D Preview */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
{/* 3D Model Viewer */}
<div style={{
backgroundColor: '#1e293b',
borderRadius: '12px',
border: '1px solid #334155',
padding: '24px',
height: '500px'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '16px'
}}>
<h3 style={{ fontSize: '18px', fontWeight: '600', color: '#f8fafc' }}>
π 3D Model Preview - {vroidData.currentModel.name}
</h3>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={() => setPreviewMode('model')}
style={{
padding: '6px 12px',
backgroundColor: previewMode === 'model' ? '#3b82f6' : '#6b7280',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '11px',
cursor: 'pointer'
}}
>
π€ Model
</button>
<button
onClick={() => setPreviewMode('rig')}
style={{
padding: '6px 12px',
backgroundColor: previewMode === 'rig' ? '#3b82f6' : '#6b7280',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '11px',
cursor: 'pointer'
}}
>
𦴠Rig
</button>
<button
onClick={() => setPreviewMode('anim')}
style={{
padding: '6px 12px',
backgroundColor: previewMode === 'anim' ? '#3b82f6' : '#6b7280',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '11px',
cursor: 'pointer'
}}
>
π¬ Animation
</button>
</div>
</div>
{/* 3D Preview Area */}
<div style={{
height: '350px',
backgroundColor: '#0f172a',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '2px dashed #475569'
}}>
<div style={{ textAlign: 'center', color: '#94a3b8' }}>
<div style={{ fontSize: '64px', marginBottom: '16px' }}>
{previewMode === 'model' ? 'π€' : previewMode === 'rig' ? 'π¦΄' : 'π¬'}
</div>
<div style={{ fontSize: '18px', fontWeight: '500', marginBottom: '8px' }}>
VRoid Studio 3D Preview
</div>
<div style={{ fontSize: '14px' }}>
{vroidData.currentModel.triangles.toLocaleString()} triangles β’ {vroidData.currentModel.bones} bones β’ {vroidData.currentModel.animations} animations
</div>
<div style={{ fontSize: '12px', marginTop: '8px', color: '#64748b' }}>
Mode: {previewMode} β’ Status: {vroidData.rigging.status}
</div>
</div>
</div>
{/* Animation Controls */}
{previewMode === 'anim' && (
<div style={{ marginTop: '16px' }}>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{vroidData.animations.map(anim => (
<button
key={anim.id}
onClick={() => setSelectedAnimation(anim.id)}
style={{
padding: '6px 12px',
backgroundColor: selectedAnimation === anim.id ? '#10b981' : '#374151',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '11px',
cursor: 'pointer'
}}
>
{anim.name}
</button>
))}
<button
onClick={createAnimation}
style={{
padding: '6px 12px',
backgroundColor: '#8b5cf6',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '11px',
cursor: 'pointer'
}}
>
β New
</button>
</div>
</div>
)}
</div>
{/* Animation Timeline */}
{previewMode === 'anim' && (
<div style={{
backgroundColor: '#1e293b',
borderRadius: '12px',
border: '1px solid #334155',
padding: '24px'
}}>
<h3 style={{ fontSize: '18px', fontWeight: '600', color: '#f8fafc', marginBottom: '16px' }}>
π¬ Animation Timeline
</h3>
{selectedAnimation && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: '14px' }}>Selected Animation</span>
<span style={{ color: '#f8fafc', fontSize: '14px', fontWeight: '500' }}>
{vroidData.animations.find(a => a.id === selectedAnimation)?.name}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: '14px' }}>Duration</span>
<span style={{ color: '#f8fafc', fontSize: '14px' }}>
{vroidData.animations.find(a => a.id === selectedAnimation)?.duration}s
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: '14px' }}>Frames</span>
<span style={{ color: '#f8fafc', fontSize: '14px' }}>
{vroidData.animations.find(a => a.id === selectedAnimation)?.frames}
</span>
</div>
</div>
)}
{!selectedAnimation && (
<div style={{ textAlign: 'center', color: '#94a3b8', padding: '20px' }}>
Select an animation to view timeline
</div>
)}
</div>
)}
</div>
{/* Right Panel - Controls & Settings */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
{/* Launch Controls */}
<LaunchControls
appId="vroid"
appName="VRoid Studio"
onStatusChange={(running) => {
// Update UI based on app status if needed
}}
/>
{/* Model Library */}
<div style={{
backgroundColor: '#1e293b',
borderRadius: '12px',
border: '1px solid #334155',
padding: '24px'
}}>
<h3 style={{ fontSize: '18px', fontWeight: '600', color: '#f8fafc', marginBottom: '16px' }}>
π Model Library
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{vroidData.models.map(model => (
<div
key={model.id}
onClick={() => setSelectedModel(model.id)}
style={{
padding: '12px 16px',
backgroundColor: selectedModel === model.id ? '#3b82f6' : '#0f172a',
borderRadius: '6px',
cursor: 'pointer',
border: '1px solid #334155',
transition: 'all 0.2s'
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<div style={{ fontSize: '14px', fontWeight: '500', color: '#f8fafc' }}>
{model.name}
</div>
<div style={{ fontSize: '12px', color: '#94a3b8' }}>
Modified: {model.lastModified}
</div>
</div>
<div style={{
padding: '4px 8px',
borderRadius: '12px',
fontSize: '10px',
fontWeight: '500',
backgroundColor: model.status === 'active' ? '#10b981' :
model.status === 'draft' ? '#f59e0b' : '#6b7280',
color: 'white'
}}>
{model.status.toUpperCase()}
</div>
</div>
</div>
))}
</div>
<button style={{
width: '100%',
marginTop: '16px',
padding: '12px',
backgroundColor: '#10b981',
color: 'white',
border: 'none',
borderRadius: '6px',
fontWeight: '500',
cursor: 'pointer'
}}>
β Create New Model
</button>
</div>
{/* Rigging & Animation */}
<div style={{
backgroundColor: '#1e293b',
borderRadius: '12px',
border: '1px solid #334155',
padding: '24px'
}}>
<h3 style={{ fontSize: '18px', fontWeight: '600', color: '#f8fafc', marginBottom: '16px' }}>
𦴠Rigging & Animation
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>Rig Status</span>
<span style={{
padding: '4px 8px',
backgroundColor: vroidData.rigging.status === 'calibrated' ? '#10b981' :
vroidData.rigging.status === 'calibrating' ? '#f59e0b' : '#6b7280',
color: 'white',
borderRadius: '12px',
fontSize: '10px',
fontWeight: '500'
}}>
{vroidData.rigging.status.toUpperCase()}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>Joints</span>
<span style={{ color: '#f8fafc', fontSize: '12px' }}>{vroidData.rigging.joints}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>IK Chains</span>
<span style={{ color: '#f8fafc', fontSize: '12px' }}>{vroidData.rigging.ikChains}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>Blend Shapes</span>
<span style={{ color: '#f8fafc', fontSize: '12px' }}>{vroidData.rigging.blendShapes}</span>
</div>
<button
onClick={calibrateRigging}
disabled={vroidData.rigging.status === 'calibrating'}
style={{
marginTop: '8px',
padding: '10px 16px',
backgroundColor: vroidData.rigging.status === 'calibrating' ? '#6b7280' : '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '6px',
fontSize: '12px',
fontWeight: '500',
cursor: vroidData.rigging.status === 'calibrating' ? 'not-allowed' : 'pointer'
}}
>
{vroidData.rigging.status === 'calibrating' ? 'π Calibrating...' : 'π§ Calibrate Rig'}
</button>
</div>
</div>
{/* Export Options */}
<div style={{
backgroundColor: '#1e293b',
borderRadius: '12px',
border: '1px solid #334155',
padding: '24px'
}}>
<h3 style={{ fontSize: '18px', fontWeight: '600', color: '#f8fafc', marginBottom: '16px' }}>
π€ Export Options
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{vroidData.export.formats.map(format => (
<button
key={format}
onClick={() => exportModel(format)}
style={{
padding: '10px 16px',
backgroundColor: '#374151',
color: 'white',
border: 'none',
borderRadius: '6px',
fontSize: '12px',
cursor: 'pointer',
transition: 'background-color 0.2s'
}}
>
π¦ Export as {format}
</button>
))}
</div>
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: '#0f172a', borderRadius: '6px' }}>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>Last Export</div>
<div style={{ fontSize: '12px', color: '#f8fafc' }}>
{new Date(vroidData.export.lastExport).toLocaleString()}
</div>
<div style={{ fontSize: '12px', color: '#94a3b8' }}>
Target: {vroidData.export.target}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}