```typescript // src/main.tsx createRoot(document.getElementById('root')!).render( , ); // src/App.tsx export type ActiveModule = 'dashboard' | 'projects' | 'ai' | 'workspace' | 'templates' | 'admin'; export default function App() { const isAuthenticated = useStore((state) => state.isAuthenticated); const activeModule = useStore((state) => state.activeModule); const setActiveModule = useStore((state) => state.setActiveModule); const setProjects = useStore((state) => state.setProjects); const setCurrentUser = useStore((state) => state.setCurrentUser); const logout = useStore((state) => state.logout); const currentUser = useStore((state) => state.currentUser); useEffect(() => { if (!isAuthenticated) return; Promise.all([getCurrentUser(), getProjects()]) .then(([user, projects]) => { setCurrentUser(user); setProjects(projects); }) .catch((err) => { if (err?.response?.status === 401) logout(); }); }, [isAuthenticated]); if (!isAuthenticated) return ; return (
{activeModule === 'dashboard' && } {activeModule === 'projects' && setActiveModule('workspace')} />} {activeModule === 'ai' && setActiveModule('workspace')} />} {activeModule === 'workspace' && setActiveModule('ai')} />} {activeModule === 'templates' && } {activeModule === 'admin' && currentUser?.role === 'admin' && }
); } // src/store/useStore.ts export interface Project { id: string; name: string; description?: string; status: 'pending' | 'parsing' | 'ready' | 'error'; fps?: string; frames?: number; thumbnail_url?: string; video_path?: string; source_type?: string; original_fps?: number; parse_fps?: number; } export type AiModelId = | 'sam2.1_hiera_tiny' | 'sam2.1_hiera_small' | 'sam2.1_hiera_base_plus' | 'sam2.1_hiera_large'; export interface Frame { id: string; projectId: string; index: number; url: string; width: number; height: number; timestampMs?: number; sourceFrameNumber?: number; } export interface Mask { id: string; frameId: string; annotationId?: string; templateId?: string; classId?: string; className?: string; classMaskId?: number; saveStatus?: 'draft' | 'saved' | 'dirty' | 'saving' | 'error'; pathData: string; label: string; color: string; segmentation?: number[][]; points?: number[][]; bbox?: [number, number, number, number]; metadata?: Record; } export interface TemplateClass { id: string; name: string; color: string; zIndex: number; maskId?: number; description?: string; } export interface Template { id: string; name: string; description?: string; color?: string; z_index?: number; classes: TemplateClass[]; rules?: TemplateRule[]; } export interface UserProfile { id: number; username: string; role: string; is_active?: number; } export const useStore = create((set) => ({ isAuthenticated: Boolean(localStorage.getItem('token')), token: localStorage.getItem('token'), currentUser: null, login: (token, user = null) => { localStorage.setItem('token', token); set({ isAuthenticated: true, token, currentUser: user }); }, logout: () => { localStorage.removeItem('token'); set({ isAuthenticated: false, token: null, currentUser: null, currentProject: null, projects: [], templates: [], frames: [], annotations: [], masks: [], selectedMaskIds: [], activeTemplateId: null, activeClassId: null, activeClass: null, }); }, projects: [], currentProject: null, setProjects: (projects) => set({ projects }), setCurrentProject: (currentProject) => set({ currentProject }), updateProject: (project) => set((state) => ({ projects: state.projects.map((item) => (item.id === project.id ? project : item)), })), activeModule: 'dashboard', activeTool: 'move', aiModel: 'sam2.1_hiera_tiny', frames: [], currentFrameIndex: 0, annotations: [], masks: [], selectedMaskIds: [], maskHistory: [], maskFuture: [], setActiveModule: (activeModule) => set({ activeModule }), setActiveTool: (activeTool) => set({ activeTool }), setFrames: (frames) => set({ frames }), setCurrentFrame: (currentFrameIndex) => set({ currentFrameIndex }), addMask: (mask) => set((state) => ({ masks: [...state.masks, mask], maskHistory: [...state.maskHistory, state.masks], maskFuture: [], })), updateMask: (id, updates) => set((state) => ({ masks: state.masks.map((mask) => (mask.id === id ? { ...mask, ...updates } : mask)), maskHistory: [...state.maskHistory, state.masks], maskFuture: [], })), setMasks: (masks) => set((state) => ({ masks, maskHistory: [...state.maskHistory, state.masks], maskFuture: [], })), setSelectedMaskIds: (selectedMaskIds) => set({ selectedMaskIds }), undoMasks: () => set((state) => { const previous = state.maskHistory[state.maskHistory.length - 1]; if (!previous) return state; return { masks: previous, maskHistory: state.maskHistory.slice(0, -1), maskFuture: [state.masks, ...state.maskFuture], }; }), redoMasks: () => set((state) => { const next = state.maskFuture[0]; if (!next) return state; return { masks: next, maskHistory: [...state.maskHistory, state.masks], maskFuture: state.maskFuture.slice(1), }; }), templates: [], activeTemplateId: null, activeClassId: null, activeClass: null, setTemplates: (templates) => set({ templates }), setActiveTemplateId: (activeTemplateId) => set({ activeTemplateId }), setActiveClassId: (activeClassId) => set({ activeClassId }), setActiveClass: (activeClass) => set({ activeClass }), })); // src/lib/config.ts function trimTrailingSlash(value: string): string { return value.replace(/\/+$/, ''); } function inferApiBaseUrl(): string { const envUrl = import.meta.env.VITE_API_BASE_URL; if (envUrl) return trimTrailingSlash(envUrl); if (typeof window !== 'undefined' && window.location.hostname) { return `${window.location.protocol}//${window.location.hostname}:8000`; } return 'http://192.168.3.11:8000'; } export const API_BASE_URL = inferApiBaseUrl(); function inferWsProgressUrl(): string { const envUrl = import.meta.env.VITE_WS_PROGRESS_URL; if (envUrl) return envUrl; const url = new URL('/ws/progress', API_BASE_URL); url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; return url.toString(); } export const WS_PROGRESS_URL = inferWsProgressUrl(); // src/lib/api.ts const apiClient = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json' }, timeout: 30000, }); apiClient.interceptors.request.use((config) => { const token = localStorage.getItem('token'); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); apiClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { localStorage.removeItem('token'); if (!error.config?.url?.includes('/api/auth/login')) window.location.reload(); } return Promise.reject(error); }, ); export async function login(username: string, password: string) { const response = await apiClient.post('/api/auth/login', { username, password }); return response.data; } export async function getCurrentUser() { const response = await apiClient.get('/api/auth/me'); return response.data; } export async function getProjects() { const response = await apiClient.get('/api/projects'); return response.data.map(mapProject); } export async function createProject(payload: { name: string; description?: string; parse_fps?: number }) { const response = await apiClient.post('/api/projects', payload); return mapProject(response.data); } export async function updateProject(id: string, payload: Partial) { const response = await apiClient.patch(`/api/projects/${id}`, payload); return mapProject(response.data); } export async function copyProject(id: string, payload: { mode: 'reset' | 'full'; name?: string }) { const response = await apiClient.post(`/api/projects/${id}/copy`, payload); return mapProject(response.data); } export async function deleteProject(id: string) { await apiClient.delete(`/api/projects/${id}`); } export async function uploadMedia(projectId: string, file: File, onProgress?: (progress: UploadProgress) => void) { const formData = new FormData(); formData.append('file', file); formData.append('project_id', projectId); const response = await apiClient.post('/api/media/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (event) => onProgress?.({ loaded: event.loaded, total: event.total }), }); return response.data; } export async function uploadDicomBatch(files: File[], onProgress?: (progress: UploadProgress) => void) { const formData = new FormData(); files.forEach((file) => formData.append('files', file)); const response = await apiClient.post('/api/media/upload/dicom', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (event) => onProgress?.({ loaded: event.loaded, total: event.total }), }); return response.data; } export async function parseMedia(projectId: string, options: { parse_fps?: number; max_frames?: number }) { const response = await apiClient.post('/api/media/parse', { project_id: Number(projectId), ...options }); return response.data; } export async function getFrames(projectId: string): Promise { const response = await apiClient.get(`/api/projects/${projectId}/frames`); return response.data.map(mapFrame); } export async function getTemplates(): Promise { const response = await apiClient.get('/api/templates'); return response.data.map(mapTemplate); } export async function createTemplate(payload: Partial