tuanchris 2 tygodni temu
rodzic
commit
275211ee73
5 zmienionych plików z 249 dodań i 15 usunięć
  1. 11 0
      frontend/.mcp.json
  2. 69 0
      frontend/package-lock.json
  3. 1 0
      frontend/package.json
  4. 131 0
      frontend/src/components/ShinyText.tsx
  5. 37 15
      main.py

+ 11 - 0
frontend/.mcp.json

@@ -0,0 +1,11 @@
+{
+  "mcpServers": {
+    "shadcn": {
+      "command": "npx",
+      "args": [
+        "shadcn@latest",
+        "mcp"
+      ]
+    }
+  }
+}

+ 69 - 0
frontend/package-lock.json

@@ -26,6 +26,7 @@
         "@radix-ui/react-tooltip": "^1.2.8",
         "@tailwindcss/postcss": "^4.1.18",
         "@tanstack/react-query": "^5.90.16",
+        "motion": "^12.27.1",
         "next-themes": "^0.4.6",
         "react": "^19.2.0",
         "react-color": "^2.19.3",
@@ -5729,6 +5730,33 @@
         "url": "https://github.com/sponsors/rawify"
       }
     },
+    "node_modules/framer-motion": {
+      "version": "12.27.1",
+      "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.27.1.tgz",
+      "integrity": "sha512-cEAqO69kcZt3gL0TGua8WTgRQfv4J57nqt1zxHtLKwYhAwA0x9kDS/JbMa1hJbwkGY74AGJKvZ9pX/IqWZtZWQ==",
+      "license": "MIT",
+      "dependencies": {
+        "motion-dom": "^12.27.1",
+        "motion-utils": "^12.24.10",
+        "tslib": "^2.4.0"
+      },
+      "peerDependencies": {
+        "@emotion/is-prop-valid": "*",
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/is-prop-valid": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/fresh": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
@@ -7036,6 +7064,47 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/motion": {
+      "version": "12.27.1",
+      "resolved": "https://registry.npmjs.org/motion/-/motion-12.27.1.tgz",
+      "integrity": "sha512-FAZTPDm1LccBdWSL46WLnEdTSHmdVx+fdWK8f61qBQn67MmFefXLXlrwy94rK2DDsd9A50Gj8H+LYCgQ/cQlFg==",
+      "license": "MIT",
+      "dependencies": {
+        "framer-motion": "^12.27.1",
+        "tslib": "^2.4.0"
+      },
+      "peerDependencies": {
+        "@emotion/is-prop-valid": "*",
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/is-prop-valid": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/motion-dom": {
+      "version": "12.27.1",
+      "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.27.1.tgz",
+      "integrity": "sha512-V/53DA2nBqKl9O2PMJleSUb/G0dsMMeZplZwgIQf5+X0bxIu7Q1cTv6DrjvTTGYRm3+7Y5wMlRZ1wT61boU/bQ==",
+      "license": "MIT",
+      "dependencies": {
+        "motion-utils": "^12.24.10"
+      }
+    },
+    "node_modules/motion-utils": {
+      "version": "12.24.10",
+      "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.24.10.tgz",
+      "integrity": "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==",
+      "license": "MIT"
+    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

+ 1 - 0
frontend/package.json

@@ -28,6 +28,7 @@
     "@radix-ui/react-tooltip": "^1.2.8",
     "@tailwindcss/postcss": "^4.1.18",
     "@tanstack/react-query": "^5.90.16",
+    "motion": "^12.27.1",
     "next-themes": "^0.4.6",
     "react": "^19.2.0",
     "react-color": "^2.19.3",

+ 131 - 0
frontend/src/components/ShinyText.tsx

@@ -0,0 +1,131 @@
+import React, { useState, useCallback, useEffect, useRef } from 'react';
+import { motion, useMotionValue, useAnimationFrame, useTransform } from 'motion/react';
+
+interface ShinyTextProps {
+  text: string;
+  disabled?: boolean;
+  speed?: number;
+  className?: string;
+  color?: string;
+  shineColor?: string;
+  spread?: number;
+  yoyo?: boolean;
+  pauseOnHover?: boolean;
+  direction?: 'left' | 'right';
+  delay?: number;
+}
+
+const ShinyText: React.FC<ShinyTextProps> = ({
+  text,
+  disabled = false,
+  speed = 2,
+  className = '',
+  color = '#b5b5b5',
+  shineColor = '#ffffff',
+  spread = 120,
+  yoyo = false,
+  pauseOnHover = false,
+  direction = 'left',
+  delay = 0
+}) => {
+  const [isPaused, setIsPaused] = useState(false);
+  const progress = useMotionValue(0);
+  const elapsedRef = useRef(0);
+  const lastTimeRef = useRef<number | null>(null);
+  const directionRef = useRef(direction === 'left' ? 1 : -1);
+
+  const animationDuration = speed * 1000;
+  const delayDuration = delay * 1000;
+
+  useAnimationFrame(time => {
+    if (disabled || isPaused) {
+      lastTimeRef.current = null;
+      return;
+    }
+
+    if (lastTimeRef.current === null) {
+      lastTimeRef.current = time;
+      return;
+    }
+
+    const deltaTime = time - lastTimeRef.current;
+    lastTimeRef.current = time;
+
+    elapsedRef.current += deltaTime;
+
+    // Animation goes from 0 to 100
+    if (yoyo) {
+      const cycleDuration = animationDuration + delayDuration;
+      const fullCycle = cycleDuration * 2;
+      const cycleTime = elapsedRef.current % fullCycle;
+
+      if (cycleTime < animationDuration) {
+        // Forward animation: 0 -> 100
+        const p = (cycleTime / animationDuration) * 100;
+        progress.set(directionRef.current === 1 ? p : 100 - p);
+      } else if (cycleTime < cycleDuration) {
+        // Delay at end
+        progress.set(directionRef.current === 1 ? 100 : 0);
+      } else if (cycleTime < cycleDuration + animationDuration) {
+        // Reverse animation: 100 -> 0
+        const reverseTime = cycleTime - cycleDuration;
+        const p = 100 - (reverseTime / animationDuration) * 100;
+        progress.set(directionRef.current === 1 ? p : 100 - p);
+      } else {
+        // Delay at start
+        progress.set(directionRef.current === 1 ? 0 : 100);
+      }
+    } else {
+      const cycleDuration = animationDuration + delayDuration;
+      const cycleTime = elapsedRef.current % cycleDuration;
+
+      if (cycleTime < animationDuration) {
+        // Animation phase: 0 -> 100
+        const p = (cycleTime / animationDuration) * 100;
+        progress.set(directionRef.current === 1 ? p : 100 - p);
+      } else {
+        // Delay phase - hold at end (shine off-screen)
+        progress.set(directionRef.current === 1 ? 100 : 0);
+      }
+    }
+  });
+
+  useEffect(() => {
+    directionRef.current = direction === 'left' ? 1 : -1;
+    elapsedRef.current = 0;
+    progress.set(0);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [direction]);
+
+  // Transform: p=0 -> 150% (shine off right), p=100 -> -50% (shine off left)
+  const backgroundPosition = useTransform(progress, p => `${150 - p * 2}% center`);
+
+  const handleMouseEnter = useCallback(() => {
+    if (pauseOnHover) setIsPaused(true);
+  }, [pauseOnHover]);
+
+  const handleMouseLeave = useCallback(() => {
+    if (pauseOnHover) setIsPaused(false);
+  }, [pauseOnHover]);
+
+  const gradientStyle: React.CSSProperties = {
+    backgroundImage: `linear-gradient(${spread}deg, ${color} 0%, ${color} 35%, ${shineColor} 50%, ${color} 65%, ${color} 100%)`,
+    backgroundSize: '200% auto',
+    WebkitBackgroundClip: 'text',
+    backgroundClip: 'text',
+    WebkitTextFillColor: 'transparent'
+  };
+
+  return (
+    <motion.span
+      className={`inline-block ${className}`}
+      style={{ ...gradientStyle, backgroundPosition }}
+      onMouseEnter={handleMouseEnter}
+      onMouseLeave={handleMouseLeave}
+    >
+      {text}
+    </motion.span>
+  );
+};
+
+export default ShinyText;

+ 37 - 15
main.py

@@ -1254,29 +1254,51 @@ async def debug_serial_send(request: DebugSerialCommand):
             await asyncio.to_thread(ser.write, command.encode())
             await asyncio.to_thread(ser.flush)
 
-            # Read response lines with timeout
+            # Read response with timeout - use read() for more reliable data capture
             responses = []
             start_time = time.time()
-            original_timeout = ser.timeout
-            ser.timeout = 0.1  # Short timeout for reading
+            buffer = ""
+
+            # Small delay to let response arrive
+            await asyncio.sleep(0.05)
 
             while time.time() - start_time < request.timeout:
                 try:
-                    line = await asyncio.to_thread(ser.readline)
-                    if line:
-                        decoded = line.decode('utf-8', errors='replace').strip()
-                        if decoded:
-                            responses.append(decoded)
-                            # Check for ok/error to know command completed
-                            if decoded.lower() in ['ok', 'error'] or decoded.lower().startswith('error:'):
-                                break
+                    # Read all available bytes
+                    waiting = ser.in_waiting
+                    if waiting > 0:
+                        data = await asyncio.to_thread(ser.read, waiting)
+                        if data:
+                            buffer += data.decode('utf-8', errors='replace')
+
+                            # Process complete lines from buffer
+                            while '\n' in buffer:
+                                line, buffer = buffer.split('\n', 1)
+                                line = line.strip()
+                                if line:
+                                    responses.append(line)
+                                    # Check for ok/error to know command completed
+                                    if line.lower() in ['ok', 'error'] or line.lower().startswith('error:'):
+                                        # Give a tiny bit more time for any trailing data
+                                        await asyncio.sleep(0.02)
+                                        # Read any remaining data
+                                        if ser.in_waiting > 0:
+                                            extra = await asyncio.to_thread(ser.read, ser.in_waiting)
+                                            if extra:
+                                                for extra_line in extra.decode('utf-8', errors='replace').strip().split('\n'):
+                                                    if extra_line.strip():
+                                                        responses.append(extra_line.strip())
+                                        break
                     else:
-                        # No data, small delay
-                        await asyncio.sleep(0.05)
-                except:
+                        # No data waiting, small delay
+                        await asyncio.sleep(0.02)
+                except Exception as read_error:
+                    logger.warning(f"Read error: {read_error}")
                     break
 
-            ser.timeout = original_timeout
+            # Add any remaining buffer content
+            if buffer.strip():
+                responses.append(buffer.strip())
 
             return {
                 "success": True,