feat: add LMS web client

This commit is contained in:
2026-07-15 13:41:00 +09:00
commit 05ff2644e8
308 changed files with 487923 additions and 0 deletions
@@ -0,0 +1,164 @@
import { useEffect, useMemo, useState } from 'react'
import type { EnvironmentProfile } from '../platforms/types'
import type { RuntimeImageArtifact } from './types'
import type { VisualizationStack } from './WorkspaceVisualizationPanel'
type NoticeTone = 'info' | 'warning' | 'error'
type UseWorkspaceVisualizationOptions = {
profiles: EnvironmentProfile[]
clonePathPreview: string
rosDomainId: string
onTitlebarNotice: (title: string, tone?: NoticeTone, addToList?: boolean) => void
}
type UseWorkspaceVisualizationResult = {
visualizationStacks: VisualizationStack[]
visualizationStack: VisualizationStack
setVisualizationStack: (stack: VisualizationStack) => void
visualizationRunning: boolean
visualizationBusy: boolean
visualizationViewerUrl: string
activeEnvironmentProfile: EnvironmentProfile | null
activeRuntimeImages: RuntimeImageArtifact[]
refreshVisualizationStatus: () => Promise<void>
startVisualizationBridge: () => Promise<void>
stopVisualizationBridge: () => Promise<void>
}
function formatVisualizationStackLabel(stack: VisualizationStack | null | undefined): string {
if (stack === 'nav2') return 'Nav2'
if (stack === 'slam') return 'SLAM'
return 'Autoware'
}
export function useWorkspaceVisualization({
profiles,
clonePathPreview,
rosDomainId,
onTitlebarNotice
}: UseWorkspaceVisualizationOptions): UseWorkspaceVisualizationResult {
const [visualizationStack, setVisualizationStack] = useState<VisualizationStack>('autoware')
const [visualizationRunning, setVisualizationRunning] = useState(false)
const [, setVisualizationStatusMessage] = useState('대기')
const [visualizationBusy, setVisualizationBusy] = useState(false)
const visualizationStacks = useMemo(() => (
profiles
.map((profile) => profile.profileType.toLowerCase())
.filter((stack): stack is VisualizationStack => stack === 'autoware' || stack === 'nav2' || stack === 'slam')
), [profiles])
const activeEnvironmentProfile = profiles.find((profile) => profile.profileType.toLowerCase() === visualizationStack) ?? profiles[0] ?? null
const activeEnvironmentConfig = activeEnvironmentProfile?.config ?? {}
const activeRuntimeImages = Array.isArray(activeEnvironmentConfig.runtimeImages)
? activeEnvironmentConfig.runtimeImages.filter((image): image is RuntimeImageArtifact => (
typeof image === 'object' && image !== null && typeof (image as RuntimeImageArtifact).image === 'string'
))
: []
const activeEnvironmentAgent = typeof activeEnvironmentConfig.agent === 'object' && activeEnvironmentConfig.agent !== null
? activeEnvironmentConfig.agent as Record<string, unknown>
: null
const activeEnvironmentAgentUrl = typeof activeEnvironmentAgent?.url === 'string'
? activeEnvironmentAgent.url.replace(/\/+$/, '')
: ''
const visualizationRemote = activeEnvironmentConfig.target === 'remote-jetson' && activeEnvironmentAgentUrl
const visualizationRemoteBridgeHttpUrl = (() => {
if (!visualizationRemote) return ''
try {
const url = new URL(activeEnvironmentAgentUrl)
url.port = '8765'
return url.toString().replace(/\/+$/, '')
} catch {
return ''
}
})()
const visualizationBridgeHttpUrl = visualizationRemoteBridgeHttpUrl || 'http://localhost:8765'
const visualizationBridgeUrl = visualizationRemote
? visualizationBridgeHttpUrl.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:')
: 'ws://localhost:8765'
const visualizationBackendUrl = visualizationBridgeHttpUrl
const visualizationViewerUrl = visualizationStack === 'slam'
? `/slam-viewer/index.html?directBridge=1&bridgeWs=${encodeURIComponent(`${visualizationBridgeUrl}/ws/state`)}&bridgeHttp=${encodeURIComponent(visualizationBridgeHttpUrl)}`
: `/autoware-viewer/index.html?source=ros&stack=${visualizationStack}&bridge=${encodeURIComponent(visualizationBridgeUrl)}&backend=${encodeURIComponent(visualizationBackendUrl)}`
useEffect(() => {
const profileType = profiles[0]?.profileType?.toLowerCase()
if (profileType === 'slam' || profileType === 'nav2' || profileType === 'autoware') {
setVisualizationStack(profileType)
}
}, [profiles.map((profile) => profile.id).join('|')])
const refreshVisualizationStatus = async (): Promise<void> => {
const status = await window.localWorkspace.visualization.status().catch((error) => ({
ok: false,
running: false,
stack: null,
message: error instanceof Error ? error.message : String(error)
}))
setVisualizationRunning(Boolean(status.running))
if (status.stack) setVisualizationStack(status.stack)
setVisualizationStatusMessage(status.running
? formatVisualizationStackLabel(status.stack)
: status.message
? status.message
: '대기')
}
const startVisualizationBridge = async (): Promise<void> => {
if (visualizationBusy) return
if (!activeEnvironmentProfile) {
setVisualizationStatusMessage('지원 안함')
onTitlebarNotice('해당 주차는 시각화를 지원하지 않습니다', 'warning')
return
}
setVisualizationBusy(true)
setVisualizationStatusMessage('시작 중')
try {
const status = await window.localWorkspace.visualization.start(visualizationStack, clonePathPreview, rosDomainId, activeEnvironmentProfile.config)
if (!status.ok) throw new Error(status.message ?? '시작 실패')
setVisualizationRunning(Boolean(status.running))
if (status.stack) setVisualizationStack(status.stack)
setVisualizationStatusMessage(formatVisualizationStackLabel(status.stack))
onTitlebarNotice('화면이 시작되었습니다', 'info')
} catch (error) {
setVisualizationRunning(false)
setVisualizationStatusMessage(error instanceof Error ? error.message : '시작 실패')
onTitlebarNotice('화면 시작에 실패했습니다', 'error')
} finally {
setVisualizationBusy(false)
}
}
const stopVisualizationBridge = async (): Promise<void> => {
if (visualizationBusy) return
setVisualizationBusy(true)
setVisualizationStatusMessage('종료 중')
try {
const status = await window.localWorkspace.visualization.stop(activeEnvironmentProfile?.config)
if (!status.ok) throw new Error(status.message ?? '종료 실패')
setVisualizationRunning(false)
setVisualizationStatusMessage('대기')
onTitlebarNotice('화면이 종료되었습니다', 'info')
} catch (error) {
setVisualizationStatusMessage(error instanceof Error ? error.message : '종료 실패')
onTitlebarNotice('화면 종료에 실패했습니다', 'error')
} finally {
setVisualizationBusy(false)
}
}
return {
visualizationStacks,
visualizationStack,
setVisualizationStack,
visualizationRunning,
visualizationBusy,
visualizationViewerUrl,
activeEnvironmentProfile,
activeRuntimeImages,
refreshVisualizationStatus,
startVisualizationBridge,
stopVisualizationBridge
}
}