diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2102ca142a0bdd91d73f49cd43666d6181713b39 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM node:18-alpine + +# Use the existing node user (usually UID 1000) +# Set up environment variables for the node user +ENV HOME=/home/node \ + PATH=/home/node/.local/bin:$PATH + +# Create and set up app directory owned by node user +# Go to user's home directory first to ensure it exists +WORKDIR $HOME +RUN mkdir -p $HOME/app && \ + chown -R node:node $HOME/app && \ + chmod -R 755 $HOME/app # Set initial permissions +WORKDIR $HOME/app + +# Switch to the node user +USER node + +# Copy package files (owned by node) +COPY --chown=node:node package*.json ./ + +# Install dependencies +RUN npm install + +# Copy the entire viewer directory (owned by node) +COPY --chown=node:node . . + +# Build the application +RUN npm run build + +# Expose port +EXPOSE 7860 + +# Start the application +CMD ["npm", "run", "preview", "--", "--port", "7860", "--host"] diff --git a/README.md b/README.md index e7b4c5152ca2f9f0ec158fa100b8e00aca3f12cd..083efe0f56d6974914d6e57ddb1fb175d68b8a91 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,84 @@ --- title: LeLab -emoji: 🌍 -colorFrom: pink -colorTo: purple +emoji: ⚡ +colorFrom: yellow +colorTo: red sdk: docker +app_port: 7860 pinned: false -license: mit short_description: Simple Interface to use LeRobot --- -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# Welcome to your Lovable project + +## Project info + +**URL**: https://lovable.dev/projects/58aee4a4-2f51-49a3-a4d9-56d3d66140b4 + +## How can I edit this code? + +There are several ways of editing your application. + +**Use Lovable** + +Simply visit the [Lovable Project](https://lovable.dev/projects/58aee4a4-2f51-49a3-a4d9-56d3d66140b4) and start prompting. + +Changes made via Lovable will be committed automatically to this repo. + +**Use your preferred IDE** + +If you want to work locally using your own IDE, you can clone this repo and push changes. Pushed changes will also be reflected in Lovable. + +The only requirement is having Node.js & npm installed - [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating) + +Follow these steps: + +```sh +# Step 1: Clone the repository using the project's Git URL. +git clone + +# Step 2: Navigate to the project directory. +cd + +# Step 3: Install the necessary dependencies. +npm i + +# Step 4: Start the development server with auto-reloading and an instant preview. +npm run dev +``` + +**Edit a file directly in GitHub** + +- Navigate to the desired file(s). +- Click the "Edit" button (pencil icon) at the top right of the file view. +- Make your changes and commit the changes. + +**Use GitHub Codespaces** + +- Navigate to the main page of your repository. +- Click on the "Code" button (green button) near the top right. +- Select the "Codespaces" tab. +- Click on "New codespace" to launch a new Codespace environment. +- Edit files directly within the Codespace and commit and push your changes once you're done. + +## What technologies are used for this project? + +This project is built with: + +- Vite +- TypeScript +- React +- shadcn-ui +- Tailwind CSS + +## How can I deploy this project? + +Simply open [Lovable](https://lovable.dev/projects/58aee4a4-2f51-49a3-a4d9-56d3d66140b4) and click on Share -> Publish. + +## Can I connect a custom domain to my Lovable project? + +Yes, you can! + +To connect a domain, navigate to Project > Settings > Domains and click Connect Domain. + +Read more here: [Setting up a custom domain](https://docs.lovable.dev/tips-tricks/custom-domain#step-by-step-guide) diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..b702660b4dc6db8e51738fda2a789e20b861a08b --- /dev/null +++ b/bun.lockb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9ef82613bb5c109eaa4b79a4432e742b40c028b8841047e5af1bd2941e15d91 +size 228980 diff --git a/components.json b/components.json new file mode 100644 index 0000000000000000000000000000000000000000..f29e3f1610677762f3d97aaefc9ce40a9d410948 --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..e67846f70fbbe40407fc84875913595ab31c4a47 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,29 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "@typescript-eslint/no-unused-vars": "off", + }, + } +); diff --git a/index.html b/index.html new file mode 100644 index 0000000000000000000000000000000000000000..cd01319749496c9c51cedb191b328208b3053b9f --- /dev/null +++ b/index.html @@ -0,0 +1,24 @@ + + + + + + robot-insight-control-panel + + + + + + + + + + + + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..d8b5178b4b101a99c3ae4fc58f8e090bf18f6940 --- /dev/null +++ b/package.json @@ -0,0 +1,88 @@ +{ + "name": "vite_react_shadcn_ts", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "build:dev": "vite build --mode development", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "^3.9.0", + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.1", + "@radix-ui/react-aspect-ratio": "^1.1.0", + "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-collapsible": "^1.1.0", + "@radix-ui/react-context-menu": "^2.2.1", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-hover-card": "^1.1.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-menubar": "^1.1.1", + "@radix-ui/react-navigation-menu": "^1.2.0", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-radio-group": "^1.2.0", + "@radix-ui/react-scroll-area": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-toggle-group": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", + "@react-three/drei": "^9.122.0", + "@react-three/fiber": "^8.18.0", + "@tanstack/react-query": "^5.56.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.3.0", + "input-otp": "^1.2.4", + "jszip": "^3.10.1", + "lucide-react": "^0.462.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", + "react-resizable-panels": "^2.1.3", + "react-router-dom": "^6.26.2", + "recharts": "^2.12.7", + "sonner": "^1.5.0", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7", + "three": "^0.177.0", + "urdf-loader": "^0.12.6", + "vaul": "^0.9.3", + "zod": "^3.23.8" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@tailwindcss/typography": "^0.5.15", + "@types/node": "^22.5.5", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "autoprefixer": "^10.4.20", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "lovable-tagger": "^1.1.7", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.11", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite": "^5.4.1" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..2e7af2b7f1a6f391da1631d93968a9d487ba977d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..dd5a12627d36db7eb9c19fa2f931ff1509f0323e Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/lovable-uploads/5e648747-34b7-4d8f-93fd-4dbd00aeeefc.png b/public/lovable-uploads/5e648747-34b7-4d8f-93fd-4dbd00aeeefc.png new file mode 100644 index 0000000000000000000000000000000000000000..39595804e566c6c8298b4dfbb31bf7f314c2e562 Binary files /dev/null and b/public/lovable-uploads/5e648747-34b7-4d8f-93fd-4dbd00aeeefc.png differ diff --git a/public/placeholder.svg b/public/placeholder.svg new file mode 100644 index 0000000000000000000000000000000000000000..e763910b27fdd9ac872f56baede51bc839402347 --- /dev/null +++ b/public/placeholder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000000000000000000000000000000000000..6018e701fc7dd0317cda9eceea390524322e8a05 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,14 @@ +User-agent: Googlebot +Allow: / + +User-agent: Bingbot +Allow: / + +User-agent: Twitterbot +Allow: / + +User-agent: facebookexternalhit +Allow: / + +User-agent: * +Allow: / diff --git a/public/so-101-urdf/CMakeLists.txt b/public/so-101-urdf/CMakeLists.txt new file mode 100755 index 0000000000000000000000000000000000000000..e33efd9b8bbbec3086795d7fb9f8fc8bcb1c2bd4 --- /dev/null +++ b/public/so-101-urdf/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.10.2) + +project(so_arm_description) + +find_package(ament_cmake REQUIRED) +find_package(urdf REQUIRED) + +# Install the mesh files from SO101/assets +install( + DIRECTORY + SO101/assets/ + DESTINATION + share/${PROJECT_NAME}/meshes + FILES_MATCHING PATTERN "*.stl" +) + +# Install URDF files +install( + DIRECTORY + urdf/ + DESTINATION + share/${PROJECT_NAME}/urdf + FILES_MATCHING PATTERN "*.urdf" +) + +# Install other directories +install( + DIRECTORY + meshes + config + launch + DESTINATION + share/${PROJECT_NAME} + OPTIONAL +) + +ament_package() + diff --git a/public/so-101-urdf/README.md b/public/so-101-urdf/README.md new file mode 100644 index 0000000000000000000000000000000000000000..dacc2193dbaf13bc1584bc208e28bf9ca32e9cbe --- /dev/null +++ b/public/so-101-urdf/README.md @@ -0,0 +1,41 @@ +# SO-ARM ROS2 URDF Package + +A complete ROS2 package for the SO-ARM101 robotic arm with URDF description. + +## 📋 Overview + +This package provides a complete ROS2 implementation for the SO-ARM101 robotic arm, including: +- URDF robot description with visual and collision meshes +- RViz visualization with pre-configured displays +- Launch files for easy robot visualization +- Integration with MoveIt for motion planning +- Joint state publishers for interactive control + +## 🎯 Original Source +https://github.com/TheRobotStudio/SO-ARM100/tree/main/Simulation/SO101 + + +## 🚀 Key Improvements Made + +### 1. **Complete ROS2 Package Structure** +- ✅ Proper `package.xml` with all necessary dependencies +- ✅ CMakeLists.txt for ROS2 build system +- ✅ Organized directory structure following ROS2 conventions + +### 2. **Enhanced Visualization** +- ✅ Fixed mesh file paths for proper package integration + + +### Build Instructions +1. Clone this repository into your ROS2 workspace: + ```bash + cd ~/your_ros2_ws/src + git clone so_arm_description + ``` + +2. Build the package: + ```bash + cd ~/your_ros2_ws + colcon build --packages-select so_arm_description + source install/setup.bash + ``` \ No newline at end of file diff --git a/public/so-101-urdf/config/joint_names_so_arm_urdf.yaml b/public/so-101-urdf/config/joint_names_so_arm_urdf.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1f0c831755649001a8ba1a88e81e162b944a710c --- /dev/null +++ b/public/so-101-urdf/config/joint_names_so_arm_urdf.yaml @@ -0,0 +1 @@ +controller_joint_names: ['', 'Rotation', 'Pitch', 'Elbow', 'Wrist_Pitch', 'Wrist_Roll', 'Jaw', ] diff --git a/public/so-101-urdf/joints_properties.xml b/public/so-101-urdf/joints_properties.xml new file mode 100644 index 0000000000000000000000000000000000000000..0e2b2a5f6f2b16755821d1cd90d38884891b0e97 --- /dev/null +++ b/public/so-101-urdf/joints_properties.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/public/so-101-urdf/meshes/base_motor_holder_so101_v1.stl b/public/so-101-urdf/meshes/base_motor_holder_so101_v1.stl new file mode 100644 index 0000000000000000000000000000000000000000..ac9c38076fe1036517faf0bccadea5de9dce0097 --- /dev/null +++ b/public/so-101-urdf/meshes/base_motor_holder_so101_v1.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cd2f241037ea377af1191fffe0dd9d9006beea6dcc48543660ed41647072424 +size 1877084 diff --git a/public/so-101-urdf/meshes/base_so101_v2.stl b/public/so-101-urdf/meshes/base_so101_v2.stl new file mode 100644 index 0000000000000000000000000000000000000000..503d30be06a91e401ba8d46ebb7e650866229550 --- /dev/null +++ b/public/so-101-urdf/meshes/base_so101_v2.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb12b7026575e1f70ccc7240051f9d943553bf34e5128537de6cd86fae33924d +size 471584 diff --git a/public/so-101-urdf/meshes/motor_holder_so101_base_v1.stl b/public/so-101-urdf/meshes/motor_holder_so101_base_v1.stl new file mode 100644 index 0000000000000000000000000000000000000000..f8e3d75c027f28bb672f830ec6e0795567c1b7c9 --- /dev/null +++ b/public/so-101-urdf/meshes/motor_holder_so101_base_v1.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31242ae6fb59d8b15c66617b88ad8e9bded62d57c35d11c0c43a70d2f4caa95b +size 1129384 diff --git a/public/so-101-urdf/meshes/motor_holder_so101_wrist_v1.stl b/public/so-101-urdf/meshes/motor_holder_so101_wrist_v1.stl new file mode 100644 index 0000000000000000000000000000000000000000..e55b7194683c6ac301504c6f59137362f0ebd13e --- /dev/null +++ b/public/so-101-urdf/meshes/motor_holder_so101_wrist_v1.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:887f92e6013cb64ea3a1ab8675e92da1e0beacfd5e001f972523540545e08011 +size 1052184 diff --git a/public/so-101-urdf/meshes/moving_jaw_so101_v1.stl b/public/so-101-urdf/meshes/moving_jaw_so101_v1.stl new file mode 100644 index 0000000000000000000000000000000000000000..eb17d253df8a84a88472ecc7f859d3b8b4d78884 --- /dev/null +++ b/public/so-101-urdf/meshes/moving_jaw_so101_v1.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:785a9dded2f474bc1d869e0d3dae398a3dcd9c0c345640040472210d2861fa9d +size 1413584 diff --git a/public/so-101-urdf/meshes/rotation_pitch_so101_v1.stl b/public/so-101-urdf/meshes/rotation_pitch_so101_v1.stl new file mode 100644 index 0000000000000000000000000000000000000000..b536cb4100c1f204f8a9d9b182acdc4a3afbc66c --- /dev/null +++ b/public/so-101-urdf/meshes/rotation_pitch_so101_v1.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9be900cc2a2bf718102841ef82ef8d2873842427648092c8ed2ca1e2ef4ffa34 +size 883684 diff --git a/public/so-101-urdf/meshes/sts3215_03a_no_horn_v1.stl b/public/so-101-urdf/meshes/sts3215_03a_no_horn_v1.stl new file mode 100644 index 0000000000000000000000000000000000000000..18e9335673f6d46ea8fd0a03a791516203eb6f4c --- /dev/null +++ b/public/so-101-urdf/meshes/sts3215_03a_no_horn_v1.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75ef3781b752e4065891aea855e34dc161a38a549549cd0970cedd07eae6f887 +size 865884 diff --git a/public/so-101-urdf/meshes/sts3215_03a_v1.stl b/public/so-101-urdf/meshes/sts3215_03a_v1.stl new file mode 100644 index 0000000000000000000000000000000000000000..a14c57b9033b82f1daa38f45e7e3c91343702df4 --- /dev/null +++ b/public/so-101-urdf/meshes/sts3215_03a_v1.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a37c871fb502483ab96c256baf457d36f2e97afc9205313d9c5ab275ef941cd0 +size 954084 diff --git a/public/so-101-urdf/meshes/under_arm_so101_v1.stl b/public/so-101-urdf/meshes/under_arm_so101_v1.stl new file mode 100644 index 0000000000000000000000000000000000000000..47b611ef939e452f791ae749756f717317922cfd --- /dev/null +++ b/public/so-101-urdf/meshes/under_arm_so101_v1.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d01d1f2de365651dcad9d6669e94ff87ff7652b5bb2d10752a66a456a86dbc71 +size 1975884 diff --git a/public/so-101-urdf/meshes/upper_arm_so101_v1.stl b/public/so-101-urdf/meshes/upper_arm_so101_v1.stl new file mode 100644 index 0000000000000000000000000000000000000000..8832740f9540065e6006907a9a826b01f96cd122 --- /dev/null +++ b/public/so-101-urdf/meshes/upper_arm_so101_v1.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:475056e03a17e71919b82fd88ab9a0b898ab50164f2a7943652a6b2941bb2d4f +size 1303484 diff --git a/public/so-101-urdf/meshes/waveshare_mounting_plate_so101_v2.stl b/public/so-101-urdf/meshes/waveshare_mounting_plate_so101_v2.stl new file mode 100644 index 0000000000000000000000000000000000000000..e0d90d5b6554ab8ca91928fa6521a675089beb12 --- /dev/null +++ b/public/so-101-urdf/meshes/waveshare_mounting_plate_so101_v2.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e197e24005a07d01bbc06a8c42311664eaeda415bf859f68fa247884d0f1a6e9 +size 62784 diff --git a/public/so-101-urdf/meshes/wrist_roll_follower_so101_v1.stl b/public/so-101-urdf/meshes/wrist_roll_follower_so101_v1.stl new file mode 100644 index 0000000000000000000000000000000000000000..9a5fa8fe2d7d8e59cd4a30d4dba0ca337513ab4a --- /dev/null +++ b/public/so-101-urdf/meshes/wrist_roll_follower_so101_v1.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b17b410a12d64ec39554abc3e8054d8a97384b2dc4a8d95a5ecb2a93670f5f4 +size 1439884 diff --git a/public/so-101-urdf/meshes/wrist_roll_pitch_so101_v2.stl b/public/so-101-urdf/meshes/wrist_roll_pitch_so101_v2.stl new file mode 100644 index 0000000000000000000000000000000000000000..2f531712f88ec01d09824ee8e27c791a4616516f --- /dev/null +++ b/public/so-101-urdf/meshes/wrist_roll_pitch_so101_v2.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c7ec5525b4d8b9e397a30ab4bb0037156a5d5f38a4adf2c7d943d6c56eda5ae +size 2699784 diff --git a/public/so-101-urdf/package.xml b/public/so-101-urdf/package.xml new file mode 100644 index 0000000000000000000000000000000000000000..ff52e481f026862b7094d3746579bc8a7ad9672d --- /dev/null +++ b/public/so-101-urdf/package.xml @@ -0,0 +1,26 @@ + + + + so_arm_description + 1.0.0 + SO-ARM101 URDF Resources + + LycheeAI + + LycheeAI + + BSD + + ament_cmake + + urdf + robot_state_publisher + joint_state_publisher + joint_state_publisher_gui + rviz2 + xacro + + + ament_cmake + + \ No newline at end of file diff --git a/public/so-101-urdf/urdf/so101_new_calib.urdf b/public/so-101-urdf/urdf/so101_new_calib.urdf new file mode 100644 index 0000000000000000000000000000000000000000..557c565bd3d227be739bc81de64d2f9ff3f80699 --- /dev/null +++ b/public/so-101-urdf/urdf/so101_new_calib.urdf @@ -0,0 +1,435 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + transmission_interface/SimpleTransmission + + hardware_interface/PositionJointInterface + + + hardware_interface/PositionJointInterface + 1 + + + + + + + + + + + + + + transmission_interface/SimpleTransmission + + hardware_interface/PositionJointInterface + + + hardware_interface/PositionJointInterface + 1 + + + + + + + + + + + + + + transmission_interface/SimpleTransmission + + hardware_interface/PositionJointInterface + + + hardware_interface/PositionJointInterface + 1 + + + + + + + + + + + + + + transmission_interface/SimpleTransmission + + hardware_interface/PositionJointInterface + + + hardware_interface/PositionJointInterface + 1 + + + + + + + + + + + + + + transmission_interface/SimpleTransmission + + hardware_interface/PositionJointInterface + + + hardware_interface/PositionJointInterface + 1 + + + + + + + + + + + + + + transmission_interface/SimpleTransmission + + hardware_interface/PositionJointInterface + + + hardware_interface/PositionJointInterface + 1 + + + + \ No newline at end of file diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40b878db5b1c97fc77049537a71bb2e249abe5dc --- /dev/null +++ b/src/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000000000000000000000000000000000000..b9d355df2a5956b526c004531b7b0ffe412461e0 --- /dev/null +++ b/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ea561c8cb3d1db7b0da5bfb69ca57c7f4653a715 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,40 @@ +import { Toaster } from "@/components/ui/toaster"; +import { Toaster as Sonner } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Index from "./pages/Index"; +import NotFound from "./pages/NotFound"; +import Landing from "./pages/Landing"; +import TeleoperationPage from "./pages/Teleoperation"; +import Recording from "./pages/Recording"; +import Calibration from "./pages/Calibration"; +import Training from "./pages/Training"; +import { UrdfProvider } from "./contexts/UrdfContext"; + +const queryClient = new QueryClient(); + +const App = () => ( + + + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + + + + +); + +export default App; diff --git a/src/components/UrdfProcessorInitializer.tsx b/src/components/UrdfProcessorInitializer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..29fec7193ab0aa64fa934379cfc2c07c0f0c40e0 --- /dev/null +++ b/src/components/UrdfProcessorInitializer.tsx @@ -0,0 +1,40 @@ +import React, { useEffect, useMemo } from "react"; +import { useUrdf } from "@/hooks/useUrdf"; + +/** + * Component that only handles initializing the URDF processor + * This component doesn't render anything visible, just initializes the processor + */ +const UrdfProcessorInitializer: React.FC = () => { + const { registerUrdfProcessor } = useUrdf(); + + // Create the URDF processor + const urdfProcessor = useMemo( + () => ({ + loadUrdf: (urdfPath: string) => { + console.log("📂 URDF path set:", urdfPath); + // This will be handled by the actual viewer component + return urdfPath; + }, + setUrlModifierFunc: (func: (url: string) => string) => { + console.log("🔗 URL modifier function set"); + return func; + }, + getPackage: () => { + return ""; + }, + }), + [] + ); + + // Register the URDF processor with the context + useEffect(() => { + console.log("🔧 Registering URDF processor"); + registerUrdfProcessor(urdfProcessor); + }, [registerUrdfProcessor, urdfProcessor]); + + // This component doesn't render anything + return null; +}; + +export default UrdfProcessorInitializer; diff --git a/src/components/UrdfViewer.tsx b/src/components/UrdfViewer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..99c21dd90e69f4703c0479e7c048481fbc2a4936 --- /dev/null +++ b/src/components/UrdfViewer.tsx @@ -0,0 +1,236 @@ +import React, { + useEffect, + useRef, + useState, + useMemo, + useCallback, +} from "react"; +import { cn } from "@/lib/utils"; + +import URDFManipulator from "urdf-loader/src/urdf-manipulator-element.js"; +import { useUrdf } from "@/hooks/useUrdf"; +import { useRealTimeJoints } from "@/hooks/useRealTimeJoints"; +import { + createUrdfViewer, + setupMeshLoader, + setupJointHighlighting, + setupModelLoading, + URDFViewerElement, +} from "@/lib/urdfViewerHelpers"; + +// Register the URDFManipulator as a custom element if it hasn't been already +if (typeof window !== "undefined" && !customElements.get("urdf-viewer")) { + customElements.define("urdf-viewer", URDFManipulator); +} + +// Extend the interface for the URDF viewer element to include background property +interface UrdfViewerElement extends HTMLElement { + background?: string; + setJointValue?: (jointName: string, value: number) => void; +} + +const UrdfViewer: React.FC = () => { + const containerRef = useRef(null); + const [highlightedJoint, setHighlightedJoint] = useState(null); + const { registerUrdfProcessor, alternativeUrdfModels, isDefaultModel } = + useUrdf(); + + // Add state for animation control + useState(isDefaultModel); + const cleanupAnimationRef = useRef<(() => void) | null>(null); + const viewerRef = useRef(null); + const hasInitializedRef = useRef(false); + + // Real-time joint updates via WebSocket + const { isConnected: isWebSocketConnected } = useRealTimeJoints({ + viewerRef, + enabled: isDefaultModel, // Only enable WebSocket for default model + }); + + // Add state for custom URDF path + const [customUrdfPath, setCustomUrdfPath] = useState(null); + const [urlModifierFunc, setUrlModifierFunc] = useState< + ((url: string) => string) | null + >(null); + + const packageRef = useRef(""); + + // Implement UrdfProcessor interface for drag and drop + const urdfProcessor = useMemo( + () => ({ + loadUrdf: (urdfPath: string) => { + setCustomUrdfPath(urdfPath); + }, + setUrlModifierFunc: (func: (url: string) => string) => { + setUrlModifierFunc(() => func); + }, + getPackage: () => { + return packageRef.current; + }, + }), + [] + ); + + // Register the URDF processor with the global drag and drop context + useEffect(() => { + registerUrdfProcessor(urdfProcessor); + }, [registerUrdfProcessor, urdfProcessor]); + + // Create URL modifier function for default model + const defaultUrlModifier = useCallback((url: string) => { + console.log(`🔗 defaultUrlModifier called with: ${url}`); + + // Handle various package:// URL formats for the default SO-101 model + if (url.startsWith("package://so_arm_description/meshes/")) { + const modifiedUrl = url.replace( + "package://so_arm_description/meshes/", + "/so-101-urdf/meshes/" + ); + console.log(`🔗 Modified URL (package): ${modifiedUrl}`); + return modifiedUrl; + } + + // Handle case where package path might be partially resolved + if (url.includes("so_arm_description/meshes/")) { + const modifiedUrl = url.replace( + /.*so_arm_description\/meshes\//, + "/so-101-urdf/meshes/" + ); + console.log(`🔗 Modified URL (partial): ${modifiedUrl}`); + return modifiedUrl; + } + + // Handle the specific problematic path pattern we're seeing in logs + if (url.includes("/so-101-urdf/so_arm_description/meshes/")) { + const modifiedUrl = url.replace( + "/so-101-urdf/so_arm_description/meshes/", + "/so-101-urdf/meshes/" + ); + console.log(`🔗 Modified URL (problematic path): ${modifiedUrl}`); + return modifiedUrl; + } + + // Handle relative paths that might need mesh folder prefix + if ( + url.endsWith(".stl") && + !url.startsWith("/") && + !url.startsWith("http") + ) { + const modifiedUrl = `/so-101-urdf/meshes/${url}`; + console.log(`🔗 Modified URL (relative): ${modifiedUrl}`); + return modifiedUrl; + } + + console.log(`🔗 Unmodified URL: ${url}`); + return url; + }, []); + + // Main effect to create and setup the viewer only once + useEffect(() => { + if (!containerRef.current) return; + + // Create and configure the URDF viewer element + const viewer = createUrdfViewer(containerRef.current, true); + viewerRef.current = viewer; // Store reference to the viewer + + // Setup mesh loading function with appropriate URL modifier + const activeUrlModifier = isDefaultModel + ? defaultUrlModifier + : urlModifierFunc; + setupMeshLoader(viewer, activeUrlModifier); + + // Determine which URDF to load - fixed path to match the actual available file + const urdfPath = isDefaultModel + ? "/so-101-urdf/urdf/so101_new_calib.urdf" + : customUrdfPath || ""; + + // Set the package path for the default model + if (isDefaultModel) { + packageRef.current = "/"; // Set to root so we can handle full path resolution in URL modifier + } + + // Setup model loading if a path is available + let cleanupModelLoading = () => {}; + if (urdfPath) { + cleanupModelLoading = setupModelLoading( + viewer, + urdfPath, + packageRef.current, + setCustomUrdfPath, + alternativeUrdfModels + ); + } + + // Setup joint highlighting + const cleanupJointHighlighting = setupJointHighlighting( + viewer, + setHighlightedJoint + ); + + // Setup animation event handler for the default model or when hasAnimation is true + const onModelProcessed = () => { + hasInitializedRef.current = true; + if ("setJointValue" in viewer) { + // Clear any existing animation + if (cleanupAnimationRef.current) { + cleanupAnimationRef.current(); + cleanupAnimationRef.current = null; + } + } + }; + + viewer.addEventListener("urdf-processed", onModelProcessed); + + // Return cleanup function + return () => { + if (cleanupAnimationRef.current) { + cleanupAnimationRef.current(); + cleanupAnimationRef.current = null; + } + hasInitializedRef.current = false; + cleanupJointHighlighting(); + cleanupModelLoading(); + viewer.removeEventListener("urdf-processed", onModelProcessed); + }; + }, [isDefaultModel, customUrdfPath, urlModifierFunc, defaultUrlModifier]); + + return ( +
+
+ + {/* Joint highlight indicator */} + {highlightedJoint && ( +
+ Joint: {highlightedJoint} +
+ )} + + {/* WebSocket connection status */} + {isDefaultModel && ( +
+
+
+ {isWebSocketConnected ? "Live Robot Data" : "Disconnected"} +
+
+ )} +
+ ); +}; + +export default UrdfViewer; diff --git a/src/components/control/CommandBar.tsx b/src/components/control/CommandBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ea5032d6d0330981e08f39b4ce460da6d1918024 --- /dev/null +++ b/src/components/control/CommandBar.tsx @@ -0,0 +1,81 @@ + +import React from 'react'; +import { Mic, MicOff, Send, Camera } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; + +interface CommandBarProps { + command: string; + setCommand: (command: string) => void; + handleSendCommand: () => void; + isVoiceActive: boolean; + setIsVoiceActive: (isActive: boolean) => void; + showCamera: boolean; + setShowCamera: (show: boolean) => void; + handleEndSession: () => void; +} + +const CommandBar: React.FC = ({ + command, + setCommand, + handleSendCommand, + isVoiceActive, + setIsVoiceActive, + showCamera, + setShowCamera, + handleEndSession +}) => { + return ( +
+
+ setCommand(e.target.value)} + placeholder="Tell the robot what to do..." + className="flex-1 bg-gray-800 border-gray-600 text-white placeholder-gray-400 text-lg py-3" + onKeyPress={(e) => e.key === 'Enter' && handleSendCommand()} + /> + +
+ +
+
+ + + + + +
+
+
+ ); +}; + +export default CommandBar; diff --git a/src/components/control/MetricsPanel.tsx b/src/components/control/MetricsPanel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2485cfa9b79a0bb0d4d20129df4d82f26b4bac96 --- /dev/null +++ b/src/components/control/MetricsPanel.tsx @@ -0,0 +1,190 @@ + +import React, { useEffect, useRef } from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; +import { Camera, MicOff } from 'lucide-react'; + +interface MetricsPanelProps { + activeTab: 'SENSORS' | 'MOTORS'; + setActiveTab: (tab: 'SENSORS' | 'MOTORS') => void; + sensorData: any[]; + motorData: any[]; + hasPermissions: boolean; + streamRef: React.RefObject; + isVoiceActive: boolean; + micLevel: number; +} + +const MetricsPanel: React.FC = ({ + activeTab, + setActiveTab, + sensorData, + motorData, + hasPermissions, + streamRef, + isVoiceActive, + micLevel, +}) => { + const sensorVideoRef = useRef(null); + + useEffect(() => { + if (activeTab === 'SENSORS' && hasPermissions && sensorVideoRef.current && streamRef.current) { + if (sensorVideoRef.current.srcObject !== streamRef.current) { + sensorVideoRef.current.srcObject = streamRef.current; + } + } + }, [activeTab, hasPermissions, streamRef]); + + return ( +
+
+ {/* Tab Headers */} +
+ + +
+ + {/* Chart Content */} +
+ {activeTab === 'SENSORS' && ( +
+ {/* Webcam Feed */} +
+

Live Camera Feed

+ {hasPermissions ? ( +
+
+ ) : ( +
+
+ +

Camera permission not granted.

+
+
+ )} +
+ + {/* Mic Detection & Other Sensors */} +
+
+

Voice Activity

+ {hasPermissions ? ( +
+
+ {[...Array(15)].map((_, i) => { + const barIsActive = isVoiceActive && i < (micLevel / 120 * 15); + return ( +
+ ); + })} +
+

+ {isVoiceActive ? "Voice commands active" : "Voice commands muted"} +

+
+ ) : ( +
+
+ +

Microphone permission not granted.

+
+
+ )} +
+ + {/* Sensor Charts */} + {['sensor3', 'sensor4'].map((sensor, index) => ( +
+

Sensor {index + 3}

+ + + + + + + + + +
+ ))} +
+
+ )} + + {activeTab === 'MOTORS' && ( +
+ {['motor1', 'motor2', 'motor3', 'motor4', 'motor5', 'motor6'].map((motor, index) => ( +
+

Motor {index + 1}

+ + + + + + + + + +
+ ))} +
+ )} +
+
+
+ ); +}; + +export default MetricsPanel; diff --git a/src/components/control/RobotArm.tsx b/src/components/control/RobotArm.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1defa06aeaf2f4e8e83f38e02c2a7342ed4e5d5e --- /dev/null +++ b/src/components/control/RobotArm.tsx @@ -0,0 +1,40 @@ + +import React from 'react'; + +const RobotArm = () => { + return ( + + {/* Base */} + + + + + + {/* First joint */} + + + + + + {/* Second segment */} + + + + + + {/* Third segment */} + + + + + + {/* End effector */} + + + + + + ); +}; + +export default RobotArm; diff --git a/src/components/control/VisualizerPanel.tsx b/src/components/control/VisualizerPanel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1350a8efbcf83e3bb7e51fe35dacbcf5ecd7c0b4 --- /dev/null +++ b/src/components/control/VisualizerPanel.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft } from "lucide-react"; +import { cn } from "@/lib/utils"; +import UrdfViewer from "../UrdfViewer"; +import UrdfProcessorInitializer from "../UrdfProcessorInitializer"; + +interface VisualizerPanelProps { + onGoBack: () => void; + className?: string; +} + +const VisualizerPanel: React.FC = ({ + onGoBack, + className, +}) => { + return ( +
+
+
+
+ LiveLab Logo +

LiveLab

+
+ +
+
+ {/* + + + + + */} + + +
+
+ +
+ {[1, 2, 3, 4].map((cam) => ( +
+ Camera {cam} +
+ ))} +
+
+ ); +}; + +export default VisualizerPanel; diff --git a/src/components/test/WebSocketTest.tsx b/src/components/test/WebSocketTest.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6500714834334dedae6df749b43be67af3e3e88a --- /dev/null +++ b/src/components/test/WebSocketTest.tsx @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; + +interface JointData { + type: "joint_update"; + joints: Record; + timestamp: number; +} + +const WebSocketTest: React.FC = () => { + const [isConnected, setIsConnected] = useState(false); + const [lastMessage, setLastMessage] = useState(null); + const [connectionStatus, setConnectionStatus] = + useState("Disconnected"); + const [ws, setWs] = useState(null); + + const connect = () => { + // First test server health + fetch("http://localhost:8000/health") + .then((response) => response.json()) + .then((data) => { + console.log("Server health:", data); + + // Now try WebSocket connection + const websocket = new WebSocket("ws://localhost:8000/ws/joint-data"); + + websocket.onopen = () => { + console.log("WebSocket connected"); + setIsConnected(true); + setConnectionStatus("Connected"); + setWs(websocket); + }; + + websocket.onmessage = (event) => { + try { + const data: JointData = JSON.parse(event.data); + setLastMessage(data); + console.log("Received joint data:", data); + } catch (error) { + console.error("Error parsing message:", error); + } + }; + + websocket.onclose = (event) => { + console.log("WebSocket closed:", event.code, event.reason); + setIsConnected(false); + setConnectionStatus(`Closed (${event.code})`); + setWs(null); + }; + + websocket.onerror = (error) => { + console.error("WebSocket error:", error); + setConnectionStatus("Error"); + }; + }) + .catch((error) => { + console.error("Server health check failed:", error); + setConnectionStatus("Server unreachable"); + }); + }; + + const disconnect = () => { + if (ws) { + ws.close(); + } + }; + + useEffect(() => { + return () => { + if (ws) { + ws.close(); + } + }; + }, [ws]); + + return ( +
+

WebSocket Connection Test

+ +
+
+
+ Status: {connectionStatus} +
+ +
+ + +
+ + {lastMessage && ( +
+

Last Joint Data:

+
+
+ Timestamp:{" "} + {new Date(lastMessage.timestamp * 1000).toLocaleTimeString()} +
+
Joints:
+ {Object.entries(lastMessage.joints).map(([joint, value]) => ( +
+ {joint}: {value.toFixed(4)} rad ( + {((value * 180) / Math.PI).toFixed(2)}°) +
+ ))} +
+
+ )} + +
+
Expected URL: ws://localhost:8000/ws/joint-data
+
Make sure your FastAPI server is running!
+
+
+
+ ); +}; + +export default WebSocketTest; diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e6a723d06574ee5cec8b00759b98f3fbe1ac7cc9 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8722561cf6bda62d62f9a0c67730aefda971873a --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000000000000000000000000000000000000..41fa7e0561a3fdb5f986c1213a35e563de740e96 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/ui/aspect-ratio.tsx b/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c4abbf37f217c715a0eaade7f45ac78600df419f --- /dev/null +++ b/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..991f56ecb117e96284bf0f6cad3b14ea2fdf5264 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f000e3ef5176395b067dfc3f3e1256a80c450015 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000000000000000000000000000000000000..71a5c325cdce2e6898d11cfeb4f2fdd458e3e2da --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>