handleResizeMouseDown(e, "w")}
+ />
+
handleResizeMouseDown(e, "e")}
+ />
+ >
+ )}
+
+ );
+}
diff --git a/src/components/SerialConsole.tsx b/src/components/SerialConsole.tsx
new file mode 100644
index 0000000..bb3f906
--- /dev/null
+++ b/src/components/SerialConsole.tsx
@@ -0,0 +1,316 @@
+import { useState, useCallback, useRef, useEffect, useMemo } from "react";
+import {
+ IconTerminal,
+ IconSend,
+ IconFilter,
+ IconClearAll,
+ IconX,
+ IconSettings,
+} from "@tabler/icons-react";
+import { useSerialPort } from "../hooks/useSerialPort";
+import type { SerialPortConfig } from "../hooks/useSerialPort";
+
+interface SerialConsoleProps {
+ consoleId: string;
+ onClose?: () => void;
+ isSnapped?: boolean;
+ onSnapOut?: () => void;
+ existingPort?: SerialPort;
+}
+
+export function SerialConsole({
+ consoleId,
+ onClose,
+ isSnapped = false,
+ onSnapOut,
+ existingPort,
+}: SerialConsoleProps) {
+ const {
+ isConnected,
+ isConnecting,
+ error,
+ connect,
+ connectWithPort,
+ disconnect,
+ sendData,
+ receivedData,
+ clearData,
+ } = useSerialPort();
+
+ const [showConfig, setShowConfig] = useState(!isConnected && !existingPort);
+ const [baudRate, setBaudRate] = useState("115200");
+ const [inputText, setInputText] = useState("");
+ const [filterRegex, setFilterRegex] = useState("");
+ const [replacePattern, setReplacePattern] = useState("");
+ const [replaceWith, setReplaceWith] = useState("");
+ const [showFilters, setShowFilters] = useState(false);
+
+ const terminalRef = useRef
(null);
+
+ // Auto-connect if we have an existing port
+ useEffect(() => {
+ if (existingPort && !isConnected) {
+ connectWithPort(existingPort, { baudRate: parseInt(baudRate, 10) });
+ }
+ }, [existingPort, isConnected, connectWithPort, baudRate]);
+
+ // Auto-scroll to bottom when new data arrives
+ useEffect(() => {
+ if (terminalRef.current) {
+ terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
+ }
+ }, [receivedData]);
+
+ const handleConnect = useCallback(async () => {
+ const config: SerialPortConfig = {
+ baudRate: parseInt(baudRate, 10),
+ };
+ await connect(config);
+ setShowConfig(false);
+ }, [connect, baudRate]);
+
+ const handleDisconnect = useCallback(async () => {
+ await disconnect();
+ setShowConfig(true);
+ }, [disconnect]);
+
+ const handleSend = useCallback(async () => {
+ if (!inputText.trim()) return;
+ try {
+ await sendData(inputText + "\n");
+ setInputText("");
+ } catch (err) {
+ console.error("Failed to send:", err);
+ }
+ }, [inputText, sendData]);
+
+ const handleKeyPress = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ },
+ [handleSend],
+ );
+
+ // Apply filtering and replacement
+ const processedData = useMemo(() => {
+ let lines = receivedData.split("\n");
+
+ // Apply regex filter
+ if (filterRegex.trim()) {
+ try {
+ const regex = new RegExp(filterRegex, "i");
+ lines = lines.filter((line) => regex.test(line));
+ } catch {
+ // Invalid regex, skip filtering
+ }
+ }
+
+ // Apply sed-style replacement
+ if (replacePattern.trim() && replaceWith !== undefined) {
+ try {
+ const regex = new RegExp(replacePattern, "g");
+ lines = lines.map((line) => line.replace(regex, replaceWith));
+ } catch {
+ // Invalid regex, skip replacement
+ }
+ }
+
+ return lines.join("\n");
+ }, [receivedData, filterRegex, replacePattern, replaceWith]);
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Serial Console {consoleId.replace(/^console-/, "#")}
+
+ {isConnected && (
+
+ )}
+
+
+
+
+ {isSnapped && onSnapOut && (
+
+ )}
+ {onClose && (
+
+ )}
+
+
+
+ {/* Filters Panel */}
+ {showFilters && (
+
+ )}
+
+ {/* Configuration Panel */}
+ {showConfig && (
+
+
+
+
+
+
+
+ Serial Port Configuration
+
+
+
+
+
+
+
+
+
+
+
+ {onClose && (
+
+ )}
+
+
+ {error && (
+
+ )}
+
+
+
+ )}
+
+ {/* Terminal Display */}
+ {!showConfig && isConnected && (
+ <>
+
+ {processedData}
+
+
+ {/* Input Area */}
+
+ setInputText(e.target.value)}
+ onKeyPress={handleKeyPress}
+ placeholder="Type message and press Enter..."
+ className="flex-1 input-field text-sm"
+ />
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/src/components/SplashScreen.tsx b/src/components/SplashScreen.tsx
index 541186d..dbd9685 100644
--- a/src/components/SplashScreen.tsx
+++ b/src/components/SplashScreen.tsx
@@ -1,11 +1,12 @@
import { motion } from "framer-motion";
import { IconBluetooth, IconUsb, IconDeviceDesktop } from "@tabler/icons-react";
-import { useMemo } from "react";
+import { useMemo, useCallback } from "react";
import DyaLogo from "../assets/dya.svg?react";
import type { ConnectionMethod } from "./DeviceConnection";
interface SplashScreenProps {
onConnect: (method: ConnectionMethod) => void;
+ onConnectWithFallback?: (method: ConnectionMethod) => Promise;
isConnecting: boolean;
error: string | null;
}
@@ -21,6 +22,7 @@ function DisabledSlash({ color }: { color: string }) {
export function SplashScreen({
onConnect,
+ onConnectWithFallback,
isConnecting,
error,
}: SplashScreenProps) {
@@ -28,6 +30,14 @@ export function SplashScreen({
const isSerialAvailable = useMemo(() => "serial" in navigator, []);
const isBLEAvailable = useMemo(() => "bluetooth" in navigator, []);
+ const handleSerialConnect = useCallback(async () => {
+ if (onConnectWithFallback) {
+ await onConnectWithFallback("serial");
+ } else {
+ onConnect("serial");
+ }
+ }, [onConnect, onConnectWithFallback]);
+
return (