diff --git a/src/App.css b/src/App.css index 624192b..f05b2b1 100644 --- a/src/App.css +++ b/src/App.css @@ -166,7 +166,13 @@ input[type="checkbox"] { .margin-0 { margin: 0; } +.margin-lg { + margin: 1rem; +} +.padding-0 { + padding: 0; +} .padding-sm { padding: .25rem; } @@ -176,11 +182,9 @@ input[type="checkbox"] { .padding-lg { padding: 1rem; } -.padding-b-sm { - padding-bottom: .25rem; -} -.padding-b-md { - padding-bottom: .5rem; +.padding-h-lg { + padding-left: 1rem; + padding-right: 1rem; } .padding-b-lg { padding-bottom: 1rem; @@ -209,6 +213,27 @@ input[type="checkbox"] { border: 3px solid canvastext; } +.shadow-sm { + box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.25); +} +.shadow-md { + box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.25); +} +.shadow-lg { + box-shadow: 0 0 1rem rgba(0, 0, 0, 0.25); +} +@media (prefers-color-scheme: dark) { + .shadow-sm { + box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.5); + } + .shadow-md { + box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.5); + } + .shadow-lg { + box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5); + } +} + .font-small { font-size: .75rem; } @@ -225,6 +250,9 @@ input[type="checkbox"] { font-weight: bold; } +.relative { + position: relative; +} .clickable { cursor: pointer; diff --git a/src/App.tsx b/src/App.tsx index 25c9468..c9ac2d7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,7 +19,7 @@ function App(){
© Utrecht University (ICS) Home - +
diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx new file mode 100644 index 0000000..093f0a4 --- /dev/null +++ b/src/components/Dialog.tsx @@ -0,0 +1,48 @@ +import {type ReactNode, type RefObject, useEffect, useRef} from "react"; + +export default function Dialog({ + open, + close, + classname, + children, +}: { + open: boolean; + close: () => void; + classname?: string; + children: ReactNode; +}) { + const ref: RefObject = useRef(null); + + useEffect(() => { + if (open) { + ref.current?.showModal(); + } else { + ref.current?.close(); + } + }, [open]); + + function handleClickOutside(event: React.MouseEvent) { + if (!ref.current) return; + + const dialogDimensions = ref.current.getBoundingClientRect() + if ( + event.clientX < dialogDimensions.left || + event.clientX > dialogDimensions.right || + event.clientY < dialogDimensions.top || + event.clientY > dialogDimensions.bottom + ) { + close(); + } + } + + return ( + + {children} + + ); +} diff --git a/src/components/Icons/Next.tsx b/src/components/Icons/Next.tsx new file mode 100644 index 0000000..1d30937 --- /dev/null +++ b/src/components/Icons/Next.tsx @@ -0,0 +1,5 @@ +export default function Next({ fill }: { fill?: string }) { + return + + ; +} \ No newline at end of file diff --git a/src/components/Icons/Pause.tsx b/src/components/Icons/Pause.tsx new file mode 100644 index 0000000..dcf5e08 --- /dev/null +++ b/src/components/Icons/Pause.tsx @@ -0,0 +1,5 @@ +export default function Pause({ fill }: { fill?: string }) { + return + + ; +} diff --git a/src/components/Icons/Play.tsx b/src/components/Icons/Play.tsx new file mode 100644 index 0000000..3f0fb6b --- /dev/null +++ b/src/components/Icons/Play.tsx @@ -0,0 +1,5 @@ +export default function Play({ fill }: { fill?: string }) { + return + + ; +} diff --git a/src/components/Icons/Redo.tsx b/src/components/Icons/Redo.tsx new file mode 100644 index 0000000..4268fc5 --- /dev/null +++ b/src/components/Icons/Redo.tsx @@ -0,0 +1,5 @@ +export default function Redo({ fill }: { fill?: string }) { + return + + ; +} \ No newline at end of file diff --git a/src/components/Icons/Replay.tsx b/src/components/Icons/Replay.tsx new file mode 100644 index 0000000..057d4b4 --- /dev/null +++ b/src/components/Icons/Replay.tsx @@ -0,0 +1,5 @@ +export default function Replay({ fill }: { fill?: string }) { + return + + ; +} diff --git a/src/components/Logging/Definitions.ts b/src/components/Logging/Definitions.ts new file mode 100644 index 0000000..a870301 --- /dev/null +++ b/src/components/Logging/Definitions.ts @@ -0,0 +1,31 @@ +import type {Cell} from "../../utils/cellStore.ts"; +import type {LogRecord} from "./useLogs.ts"; + +/** + * Zustand store definition for managing user preferences related to logging. + * + * Includes flags for toggling relative timestamps and automatic scroll behavior. + */ +export type LoggingSettings = { + /** Whether to display log timestamps as relative (e.g., "2m 15s ago") instead of absolute. */ + showRelativeTime: boolean; + /** Updates the `showRelativeTime` setting. */ + setShowRelativeTime: (showRelativeTime: boolean) => void; +}; + +/** + * Props for any component that renders a single log message entry. + * + * @param recordCell - A reactive `Cell` containing a single `LogRecord`. + * @param onUpdate - Optional callback triggered when the log entry updates. + */ +export type MessageComponentProps = { + recordCell: Cell, + onUpdate?: () => void, +}; + +/** + * Key used for the experiment filter predicate in the filter map, to exclude experiment logs from the developer logs. + */ +export const EXPERIMENT_FILTER_KEY = "experiment_filter"; +export const EXPERIMENT_LOGGER_NAME = "experiment"; diff --git a/src/components/Logging/Filters.tsx b/src/components/Logging/Filters.tsx index acf30fc..9b4f611 100644 --- a/src/components/Logging/Filters.tsx +++ b/src/components/Logging/Filters.tsx @@ -16,9 +16,8 @@ type Setter = (value: T | ((prev: T) => T)) => void; * Mapping of log level names to their corresponding numeric severity. * Used for comparison in log filtering predicates. */ -const optionMapping = new Map([ +const optionMapping: Map = new Map([ ["ALL", 0], - ["LLM", 9], ["DEBUG", 10], ["INFO", 20], ["WARNING", 30], @@ -96,7 +95,7 @@ function GlobalLevelFilter({ filterPredicates: Map; setFilterPredicates: Setter>; }) { - const selected = filterPredicates.get(GLOBAL_LOG_LEVEL_PREDICATE_KEY)?.value || "ALL"; + const selected = filterPredicates.get(GLOBAL_LOG_LEVEL_PREDICATE_KEY)?.value ?? "ALL"; const setSelected = (selected: string | null) => { if (!selected || !optionMapping.has(selected)) return; diff --git a/src/components/Logging/Logging.module.css b/src/components/Logging/Logging.module.css index c3f2ef3..9b26530 100644 --- a/src/components/Logging/Logging.module.css +++ b/src/components/Logging/Logging.module.css @@ -10,7 +10,6 @@ University within the Software Project course. flex-shrink: 0; box-shadow: 0 0 1rem black; - padding: 1rem 1rem 0 1rem; } .no-numbers { @@ -20,8 +19,6 @@ University within the Software Project course. } .log-container { - margin-bottom: .5rem; - .accented-0, .accented-10 { background-color: color-mix(in oklab, canvas, rgb(159, 159, 159) 35%) } @@ -37,7 +34,7 @@ University within the Software Project course. } .floating-button { - position: fixed; + position: absolute; bottom: 1rem; right: 1rem; box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5); diff --git a/src/components/Logging/Logging.tsx b/src/components/Logging/Logging.tsx index 2e10081..8b1bf71 100644 --- a/src/components/Logging/Logging.tsx +++ b/src/components/Logging/Logging.tsx @@ -1,41 +1,26 @@ // This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) -import {useEffect, useRef, useState} from "react"; -import {create} from "zustand"; - +import {type ComponentType, useEffect, useRef, useState} from "react"; import formatDuration from "../../utils/formatDuration.ts"; import {type LogFilterPredicate, type LogRecord, useLogs} from "./useLogs.ts"; import Filters from "./Filters.tsx"; import {type Cell, useCell} from "../../utils/cellStore.ts"; - import styles from "./Logging.module.css"; - +import { + EXPERIMENT_FILTER_KEY, + EXPERIMENT_LOGGER_NAME, + type LoggingSettings, + type MessageComponentProps +} from "./Definitions.ts"; +import {create} from "zustand"; /** - * Zustand store definition for managing user preferences related to logging. - * - * Includes flags for toggling relative timestamps and automatic scroll behavior. - */ -type LoggingSettings = { - /** Whether to display log timestamps as relative (e.g., "2m 15s ago") instead of absolute. */ - showRelativeTime: boolean; - /** Updates the `showRelativeTime` setting. */ - setShowRelativeTime: (showRelativeTime: boolean) => void; - /** Whether the log view should automatically scroll to the newest entry. */ - scrollToBottom: boolean; - /** Updates the `scrollToBottom` setting. */ - setScrollToBottom: (scrollToBottom: boolean) => void; -}; - -/** - * Global Zustand store for logging UI preferences. + * Local Zustand store for logging UI preferences. */ const useLoggingSettings = create((set) => ({ showRelativeTime: false, setShowRelativeTime: (showRelativeTime: boolean) => set({ showRelativeTime }), - scrollToBottom: true, - setScrollToBottom: (scrollToBottom: boolean) => set({ scrollToBottom }), })); /** @@ -48,13 +33,7 @@ const useLoggingSettings = create((set) => ({ * @param onUpdate - Optional callback triggered when the log entry updates. * @returns A JSX element displaying a formatted log message. */ -function LogMessage({ - recordCell, - onUpdate, -}: { - recordCell: Cell, - onUpdate?: () => void, -}) { +function LogMessage({ recordCell, onUpdate }: MessageComponentProps) { const { showRelativeTime, setShowRelativeTime } = useLoggingSettings(); const record = useCell(recordCell); @@ -72,7 +51,7 @@ function LogMessage({ /** Simplifies the logger name by showing only the last path segment. */ const normalizedName = record.name.split(".").pop() || record.name; - // Notify parent component (e.g. for scroll updates) when this record changes. + // Notify the parent component (e.g., for scroll updates) when this record changes. useEffect(() => { if (onUpdate) onUpdate(); }, [record, onUpdate]); @@ -80,11 +59,10 @@ function LogMessage({ return
{record.levelname} - setShowRelativeTime(!showRelativeTime)} - >{showRelativeTime - ? formatDuration(record.relativeCreated) - : new Date(record.created * 1000).toLocaleTimeString() + setShowRelativeTime(!showRelativeTime)}>{ + showRelativeTime + ? formatDuration(record.relativeCreated) + : new Date(record.created * 1000).toLocaleTimeString() }
@@ -103,12 +81,18 @@ function LogMessage({ * - A floating "Scroll to bottom" button when not at the bottom. * * @param recordCells - Array of reactive log records to display. + * @param MessageComponent - A component to use to render each log message entry. * @returns A scrollable log list component. */ -function LogMessages({ recordCells }: { recordCells: Cell[] }) { +export function LogMessages({ + recordCells, + MessageComponent, +}: { + recordCells: Cell[], + MessageComponent: ComponentType, +}) { const scrollableRef = useRef(null); - const lastElementRef = useRef(null) - const { scrollToBottom, setScrollToBottom } = useLoggingSettings(); + const [scrollToBottom, setScrollToBottom] = useState(true); // Disable auto-scroll if the user manually scrolls. useEffect(() => { @@ -127,30 +111,28 @@ function LogMessages({ recordCells }: { recordCells: Cell[] }) { }, [scrollableRef, setScrollToBottom]); /** - * Scrolls the last log message into view if auto-scroll is enabled, - * or if forced (e.g., user clicks "Scroll to bottom"). + * Scrolls the log messages to the bottom, making the latest messages visible. * * @param force - If true, forces scrolling even if `scrollToBottom` is false. */ - function scrollLastElementIntoView(force = false) { - if ((!scrollToBottom && !force) || !lastElementRef.current) return; - lastElementRef.current.scrollIntoView({ behavior: "smooth" }); + function showBottom(force = false) { + if ((!scrollToBottom && !force) || !scrollableRef.current) return; + scrollableRef.current.scrollTo({top: scrollableRef.current.scrollHeight, left: 0, behavior: "smooth"}); } - return
+ return
    {recordCells.map((recordCell, i) => (
  1. - +
  2. ))} -
{!scrollToBottom &&
- {/* LOGS TODO: add actual logs */} - + {/* LOGS */} + {/* FOOTER */}