feat: initial ui redesign from demo

This commit is contained in:
LukeGus
2026-05-11 01:23:11 -05:00
parent eaa758effe
commit 33dcde0827
349 changed files with 23984 additions and 31634 deletions
@@ -0,0 +1,73 @@
import React, { useEffect, useRef } from "react";
import { cn } from "@/lib/utils.ts";
interface CommandAutocompleteProps {
suggestions: string[];
selectedIndex: number;
onSelect: (command: string) => void;
position: { top: number; left: number };
visible: boolean;
}
export function CommandAutocomplete({
suggestions,
selectedIndex,
onSelect,
position,
visible,
}: CommandAutocompleteProps) {
const containerRef = useRef<HTMLDivElement>(null);
const selectedRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (selectedRef.current && containerRef.current) {
selectedRef.current.scrollIntoView({
block: "nearest",
behavior: "smooth",
});
}
}, [selectedIndex]);
if (!visible || suggestions.length === 0) {
return null;
}
const footerHeight = 32;
const maxSuggestionsHeight = 240 - footerHeight;
return (
<div
ref={containerRef}
className="fixed z-[9999] bg-canvas border border-edge rounded-md shadow-lg min-w-[200px] max-w-[600px] flex flex-col"
style={{
top: `${position.top}px`,
left: `${position.left}px`,
maxHeight: "240px",
}}
>
<div
className="overflow-y-auto thin-scrollbar"
style={{ maxHeight: `${maxSuggestionsHeight}px` }}
>
{suggestions.map((suggestion, index) => (
<div
key={index}
ref={index === selectedIndex ? selectedRef : null}
className={cn(
"px-3 py-1.5 text-sm font-mono cursor-pointer transition-colors",
"hover:bg-hover",
index === selectedIndex && "bg-surface text-muted-foreground",
)}
onClick={() => onSelect(suggestion)}
onMouseEnter={() => {}}
>
{suggestion}
</div>
))}
</div>
<div className="px-3 py-1 text-xs text-muted-foreground border-t border-edge bg-canvas/50 shrink-0">
Tab/Enter to complete to navigate Esc to close
</div>
</div>
);
}
@@ -0,0 +1,85 @@
import React, {
createContext,
useContext,
useState,
useCallback,
ReactNode,
} from "react";
interface CommandHistoryContextType {
commandHistory: string[];
isLoading: boolean;
setCommandHistory: (history: string[]) => void;
setIsLoading: (loading: boolean) => void;
onSelectCommand?: (command: string) => void;
setOnSelectCommand: (callback: (command: string) => void) => void;
onDeleteCommand?: (command: string) => void;
setOnDeleteCommand: (callback: (command: string) => void) => void;
openCommandHistory: () => void;
setOpenCommandHistory: (callback: () => void) => void;
}
const CommandHistoryContext = createContext<
CommandHistoryContextType | undefined
>(undefined);
export function CommandHistoryProvider({ children }: { children: ReactNode }) {
const [commandHistory, setCommandHistory] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [onSelectCommand, setOnSelectCommand] = useState<
((command: string) => void) | undefined
>(undefined);
const [onDeleteCommand, setOnDeleteCommand] = useState<
((command: string) => void) | undefined
>(undefined);
const [openCommandHistory, setOpenCommandHistory] = useState<
(() => void) | undefined
>(() => () => {});
const handleSetOnSelectCommand = useCallback(
(callback: (command: string) => void) => {
setOnSelectCommand(() => callback);
},
[],
);
const handleSetOnDeleteCommand = useCallback(
(callback: (command: string) => void) => {
setOnDeleteCommand(() => callback);
},
[],
);
const handleSetOpenCommandHistory = useCallback((callback: () => void) => {
setOpenCommandHistory(() => callback);
}, []);
return (
<CommandHistoryContext.Provider
value={{
commandHistory,
isLoading,
setCommandHistory,
setIsLoading,
onSelectCommand,
setOnSelectCommand: handleSetOnSelectCommand,
onDeleteCommand,
setOnDeleteCommand: handleSetOnDeleteCommand,
openCommandHistory: openCommandHistory || (() => {}),
setOpenCommandHistory: handleSetOpenCommandHistory,
}}
>
{children}
</CommandHistoryContext.Provider>
);
}
export function useCommandHistory() {
const context = useContext(CommandHistoryContext);
if (context === undefined) {
throw new Error(
"useCommandHistory must be used within a CommandHistoryProvider",
);
}
return context;
}
@@ -0,0 +1,126 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { getCommandHistory, saveCommandToHistory } from "@/main-axios.ts";
interface UseCommandHistoryOptions {
hostId?: number;
enabled?: boolean;
}
interface CommandHistoryResult {
suggestions: string[];
getSuggestions: (input: string) => string[];
saveCommand: (command: string) => Promise<void>;
clearSuggestions: () => void;
isLoading: boolean;
}
export function useCommandHistory({
hostId,
enabled = true,
}: UseCommandHistoryOptions): CommandHistoryResult {
const [commandHistory, setCommandHistory] = useState<string[]>([]);
const [suggestions, setSuggestions] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const historyCache = useRef<Map<number, string[]>>(new Map());
useEffect(() => {
if (!enabled || !hostId) {
setCommandHistory([]);
setSuggestions([]);
return;
}
const cached = historyCache.current.get(hostId);
if (cached) {
setCommandHistory(cached);
return;
}
const fetchHistory = async () => {
setIsLoading(true);
try {
const history = await getCommandHistory(hostId);
setCommandHistory(history);
historyCache.current.set(hostId, history);
} catch (error) {
console.error("Failed to fetch command history:", error);
setCommandHistory([]);
} finally {
setIsLoading(false);
}
};
fetchHistory();
}, [hostId, enabled]);
const getSuggestions = useCallback(
(input: string): string[] => {
if (!input || input.trim().length === 0) {
return [];
}
const trimmedInput = input.trim();
const matches = commandHistory.filter((cmd) =>
cmd.startsWith(trimmedInput),
);
const filtered = matches
.filter((cmd) => cmd !== trimmedInput)
.slice(0, 10);
setSuggestions(filtered);
return filtered;
},
[commandHistory],
);
const saveCommand = useCallback(
async (command: string) => {
if (!enabled || !hostId || !command || command.trim().length === 0) {
return;
}
const trimmedCommand = command.trim();
if (commandHistory.length > 0 && commandHistory[0] === trimmedCommand) {
return;
}
try {
await saveCommandToHistory(hostId, trimmedCommand);
setCommandHistory((prev) => {
const newHistory = [
trimmedCommand,
...prev.filter((c) => c !== trimmedCommand),
];
const limited = newHistory.slice(0, 500);
historyCache.current.set(hostId, limited);
return limited;
});
} catch (error) {
console.error("Failed to save command to history:", error);
setCommandHistory((prev) => {
const newHistory = [
trimmedCommand,
...prev.filter((c) => c !== trimmedCommand),
];
return newHistory.slice(0, 500);
});
}
},
[enabled, hostId, commandHistory],
);
const clearSuggestions = useCallback(() => {
setSuggestions([]);
}, []);
return {
suggestions,
getSuggestions,
saveCommand,
clearSuggestions,
isLoading,
};
}
@@ -0,0 +1,130 @@
import { useRef, useCallback } from "react";
import { saveCommandToHistory } from "@/main-axios.ts";
const SENSITIVE_PATTERNS = [
/\bpassw(or)?d\b/i,
/\bsecret\b/i,
/\btoken\b/i,
/\bapi.?key\b/i,
/\bPASS(WORD)?=/i,
/\bAWS_SECRET/i,
/\bmysql\b.*-p/i,
/\bsudo\s+-S\b/,
/\bhtpasswd\b/i,
/\bsshpass\b/i,
/\bcurl\b.*-u\s/i,
/\bexport\b.*(?:PASSWORD|SECRET|TOKEN|KEY)=/i,
];
interface UseCommandTrackerOptions {
hostId?: number;
enabled?: boolean;
onCommandExecuted?: (command: string) => void;
}
interface CommandTrackerResult {
trackInput: (data: string) => void;
getCurrentCommand: () => string;
clearCurrentCommand: () => void;
updateCurrentCommand: (command: string) => void;
}
export function useCommandTracker({
hostId,
enabled = true,
onCommandExecuted,
}: UseCommandTrackerOptions): CommandTrackerResult {
const currentCommandRef = useRef<string>("");
const isInEscapeSequenceRef = useRef<boolean>(false);
const trackInput = useCallback(
(data: string) => {
if (!enabled || !hostId) {
return;
}
for (let i = 0; i < data.length; i++) {
const char = data[i];
const charCode = char.charCodeAt(0);
if (charCode === 27) {
isInEscapeSequenceRef.current = true;
continue;
}
if (isInEscapeSequenceRef.current) {
if (
(charCode >= 65 && charCode <= 90) ||
(charCode >= 97 && charCode <= 122) ||
charCode === 126
) {
isInEscapeSequenceRef.current = false;
}
continue;
}
if (charCode === 13 || charCode === 10) {
const command = currentCommandRef.current.trim();
if (command.length > 0) {
const isSensitive = SENSITIVE_PATTERNS.some((p) => p.test(command));
if (!isSensitive) {
saveCommandToHistory(hostId, command).catch((error) => {
console.error("Failed to save command to history:", error);
});
}
if (onCommandExecuted) {
onCommandExecuted(command);
}
}
currentCommandRef.current = "";
continue;
}
if (charCode === 8 || charCode === 127) {
if (currentCommandRef.current.length > 0) {
currentCommandRef.current = currentCommandRef.current.slice(0, -1);
}
continue;
}
if (charCode === 3 || charCode === 4) {
currentCommandRef.current = "";
continue;
}
if (charCode === 21) {
currentCommandRef.current = "";
continue;
}
if (charCode >= 32 && charCode <= 126) {
currentCommandRef.current += char;
}
}
},
[enabled, hostId, onCommandExecuted],
);
const getCurrentCommand = useCallback(() => {
return currentCommandRef.current;
}, []);
const clearCurrentCommand = useCallback(() => {
currentCommandRef.current = "";
}, []);
const updateCurrentCommand = useCallback((command: string) => {
currentCommandRef.current = command;
}, []);
return {
trackInput,
getCurrentCommand,
clearCurrentCommand,
updateCurrentCommand,
};
}