From 9e1a50c3beadfdfed40838869ccb9fdf723c8d65 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:48:26 -0500 Subject: [PATCH] Clean up copy output (#14705) * Remove extra spacing for next/prev carousel buttons * Clarify ollama genai docs * Clean up copied gpu info output * Clean up copied gpu info output * Better display when manually copying/pasting log data --- web/src/components/overlay/GPUInfoDialog.tsx | 10 +-- web/src/pages/Logs.tsx | 86 +++++++++++++++++++- web/src/pages/System.tsx | 2 + 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/web/src/components/overlay/GPUInfoDialog.tsx b/web/src/components/overlay/GPUInfoDialog.tsx index 957d1e681..3821579e2 100644 --- a/web/src/components/overlay/GPUInfoDialog.tsx +++ b/web/src/components/overlay/GPUInfoDialog.tsx @@ -10,6 +10,7 @@ import ActivityIndicator from "../indicators/activity-indicator"; import { GpuInfo, Nvinfo, Vainfo } from "@/types/stats"; import { Button } from "../ui/button"; import copy from "copy-to-clipboard"; +import { toast } from "sonner"; type GPUInfoDialogProps = { showGpuInfo: boolean; @@ -30,12 +31,11 @@ export default function GPUInfoDialog({ const onCopyInfo = async () => { copy( - JSON.stringify(gpuType == "vainfo" ? vainfo : nvinfo).replace( - /[\\\s]+/gi, - "", - ), + JSON.stringify(gpuType == "vainfo" ? vainfo : nvinfo) + .replace(/\\t/g, "\t") + .replace(/\\n/g, "\n"), ); - setShowGpuInfo(false); + toast.success("Copied GPU info to clipboard."); }; if (gpuType == "vainfo") { diff --git a/web/src/pages/Logs.tsx b/web/src/pages/Logs.tsx index 8178e9e90..949fffb8a 100644 --- a/web/src/pages/Logs.tsx +++ b/web/src/pages/Logs.tsx @@ -300,6 +300,84 @@ function Logs() { }, ); + useEffect(() => { + const handleCopy = (e: ClipboardEvent) => { + e.preventDefault(); + if (!contentRef.current) return; + + const selection = window.getSelection(); + if (!selection) return; + + const range = selection.getRangeAt(0); + const fragment = range.cloneContents(); + + const extractLogData = (element: Element) => { + const severity = + element.querySelector(".log-severity")?.textContent?.trim() || ""; + const dateStamp = + element.querySelector(".log-timestamp")?.textContent?.trim() || ""; + const section = + element.querySelector(".log-section")?.textContent?.trim() || ""; + const content = + element.querySelector(".log-content")?.textContent?.trim() || ""; + + return { severity, dateStamp, section, content }; + }; + + let copyData: { + severity: string; + dateStamp: string; + section: string; + content: string; + }[] = []; + + if (fragment.querySelectorAll(".grid").length > 0) { + // Multiple grid elements + copyData = Array.from(fragment.querySelectorAll(".grid")).map( + extractLogData, + ); + } else { + // Try to find the closest grid element or use the first child element + const gridElement = + fragment.querySelector(".grid") || (fragment.firstChild as Element); + + if (gridElement) { + const data = extractLogData(gridElement); + if (data.severity || data.dateStamp || data.section || data.content) { + copyData.push(data); + } + } + } + + if (copyData.length === 0) return; // No valid data to copy + + // Calculate maximum widths for each column + const maxWidths = { + severity: Math.max(...copyData.map((d) => d.severity.length)), + dateStamp: Math.max(...copyData.map((d) => d.dateStamp.length)), + section: Math.max(...copyData.map((d) => d.section.length)), + }; + + const pad = (str: string, length: number) => str.padEnd(length, " "); + + // Create the formatted copy text + const copyText = copyData + .map( + (d) => + `${pad(d.severity, maxWidths.severity)} | ${pad(d.dateStamp, maxWidths.dateStamp)} | ${pad(d.section, maxWidths.section)} | ${d.content}`, + ) + .join("\n"); + + e.clipboardData?.setData("text/plain", copyText); + }; + + const content = contentRef.current; + content?.addEventListener("copy", handleCopy); + return () => { + content?.removeEventListener("copy", handleCopy); + }; + }, []); + return (