From f5f015726082cea4b8f0bfdc7260f39aaddb3b57 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Sun, 14 Jul 2024 23:13:48 +0530 Subject: [PATCH] fix: scroll issue playground --- src/assets/tailwind.css | 60 ++++++++++ .../Option/Playground/Playground.tsx | 1 + .../Option/Playground/PlaygroundChat.tsx | 32 ++++-- src/entries/background.ts | 40 ++++--- src/hooks/useScrollAnchor.tsx | 106 ------------------ src/hooks/useSmartScroll.tsx | 35 ++++++ src/public/_locales/en/messages.json | 3 + src/services/app.ts | 18 +++ 8 files changed, 165 insertions(+), 130 deletions(-) delete mode 100644 src/hooks/useScrollAnchor.tsx create mode 100644 src/hooks/useSmartScroll.tsx diff --git a/src/assets/tailwind.css b/src/assets/tailwind.css index e6703c6..ee4b772 100644 --- a/src/assets/tailwind.css +++ b/src/assets/tailwind.css @@ -65,3 +65,63 @@ animation: gradient-border 3s infinite; border-radius: 10px; } + +/* Hide scrollbar by default */ +.custom-scrollbar { + scrollbar-width: none; + -ms-overflow-style: none; +} + +.custom-scrollbar::-webkit-scrollbar { + display: none; +} + +/* Show scrollbar on hover */ +.custom-scrollbar:hover { + scrollbar-width: thin; + -ms-overflow-style: auto; +} + +.custom-scrollbar:hover::-webkit-scrollbar { + display: block; + width: 8px; +} + +/* Custom scrollbar styles for light theme */ +.custom-scrollbar:hover::-webkit-scrollbar-track { + @apply bg-gray-50; + border-radius: 4px; +} + +.custom-scrollbar:hover::-webkit-scrollbar-thumb { + @apply bg-gray-300; + border-radius: 4px; + transition: background 0.2s ease; +} + +.custom-scrollbar:hover::-webkit-scrollbar-thumb:hover { + @apply bg-gray-400; +} + +/* Custom scrollbar styles for dark theme */ +.dark .custom-scrollbar:hover::-webkit-scrollbar-track { + background-color: #262626; +} + +.dark .custom-scrollbar:hover::-webkit-scrollbar-thumb { + background-color: #404040; +} + +.dark .custom-scrollbar:hover::-webkit-scrollbar-thumb:hover { + background-color: #525252; +} + +/* For Firefox */ +.custom-scrollbar { + scrollbar-color: theme('colors.gray.300') theme('colors.gray.50'); + scrollbar-width: thin; +} + +.dark .custom-scrollbar { + scrollbar-color: #404040 #262626; +} diff --git a/src/components/Option/Playground/Playground.tsx b/src/components/Option/Playground/Playground.tsx index a18b258..bc52b59 100644 --- a/src/components/Option/Playground/Playground.tsx +++ b/src/components/Option/Playground/Playground.tsx @@ -78,6 +78,7 @@ export const Playground = () => { dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800 z-10" : "" } bg-white dark:bg-[#171717]`}> +
diff --git a/src/components/Option/Playground/PlaygroundChat.tsx b/src/components/Option/Playground/PlaygroundChat.tsx index c8e90da..e34c594 100644 --- a/src/components/Option/Playground/PlaygroundChat.tsx +++ b/src/components/Option/Playground/PlaygroundChat.tsx @@ -3,6 +3,8 @@ import { useMessageOption } from "~/hooks/useMessageOption" import { PlaygroundEmpty } from "./PlaygroundEmpty" import { PlaygroundMessage } from "~/components/Common/Playground/Message" import { MessageSourcePopup } from "@/components/Common/Playground/MessageSourcePopup" +import { useSmartScroll } from "~/hooks/useSmartScroll" +import { ChevronDown } from "lucide-react" export const PlaygroundChat = () => { const { @@ -13,24 +15,24 @@ export const PlaygroundChat = () => { editMessage, ttsEnabled } = useMessageOption() - const divRef = React.useRef(null) const [isSourceOpen, setIsSourceOpen] = React.useState(false) const [source, setSource] = React.useState(null) - React.useEffect(() => { - if (divRef.current) { - divRef.current.scrollIntoView({ behavior: "smooth" }) - } - }) + + const { containerRef, isAtBottom, scrollToBottom } = useSmartScroll( + messages, + streaming + ) + return ( <> - {" "} -
+
{messages.length === 0 && (
)} - {/* {messages.length > 0 &&
} */} {messages.map((message, index) => ( { /> ))} {messages.length > 0 && ( -
+
)} -
+ {!isAtBottom && ( +
+ +
+ )} { return ((completed / total) * 100).toFixed(0) + "%" } @@ -75,11 +76,12 @@ const streamDownload = async (url: string, model: string) => { clearBadge() }, 5000) } + export default defineBackground({ main() { browser.runtime.onMessage.addListener(async (message) => { if (message.type === "sidepanel") { - browser.sidebarAction.open() + await browser.sidebarAction.open() } else if (message.type === "pull_model") { const ollamaURL = await getOllamaURL() @@ -100,7 +102,7 @@ export default defineBackground({ if (import.meta.env.BROWSER === "chrome") { chrome.action.onClicked.addListener((tab) => { - browser.tabs.create({ url: browser.runtime.getURL("/options.html") }) + chrome.tabs.create({ url: chrome.runtime.getURL("/options.html") }) }) } else { browser.browserAction.onClicked.addListener((tab) => { @@ -109,23 +111,31 @@ export default defineBackground({ }) } + const contextMenuTitle = { + webUi: browser.i18n.getMessage("openOptionToChat"), + sidePanel: browser.i18n.getMessage("openSidePanelToChat") + } + + const contextMenuId = { + webUi: "open-web-ui-pa", + sidePanel: "open-side-panel-pa" + } + browser.contextMenus.create({ - id: "open-side-panel-pa", - title: browser.i18n.getMessage("openSidePanelToChat"), + id: contextMenuId["sidePanel"], + title: contextMenuTitle["sidePanel"], contexts: ["all"] }) if (import.meta.env.BROWSER === "chrome") { browser.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === "open-side-panel-pa") { - chrome.tabs.query( - { active: true, currentWindow: true }, - async (tabs) => { - const tab = tabs[0] - chrome.sidePanel.open({ - tabId: tab.id! - }) - } - ) + chrome.sidePanel.open({ + tabId: tab.id! + }) + } else if (info.menuItemId === "open-web-ui-pa") { + browser.tabs.create({ + url: browser.runtime.getURL("/options.html") + }) } }) @@ -152,6 +162,10 @@ export default defineBackground({ browser.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === "open-side-panel-pa") { browser.sidebarAction.toggle() + } else if (info.menuItemId === "open-web-ui-pa") { + browser.tabs.create({ + url: browser.runtime.getURL("/options.html") + }) } }) diff --git a/src/hooks/useScrollAnchor.tsx b/src/hooks/useScrollAnchor.tsx deleted file mode 100644 index c2d72a2..0000000 --- a/src/hooks/useScrollAnchor.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react" -import { useMessageOption } from "./useMessageOption" - -export const useScrollAnchor = () => { - const { isProcessing, messages } = useMessageOption() - - const [isAtTop, setIsAtTop] = useState(false) - const [isAtBottom, setIsAtBottom] = useState(true) - const [userScrolled, setUserScrolled] = useState(false) - const [isOverflowing, setIsOverflowing] = useState(false) - - const messagesStartRef = useRef(null) - const messagesEndRef = useRef(null) - const containerRef = useRef(null) - const isAutoScrolling = useRef(false) - - console.log(`isAtTop: ${isAtTop}, isAtBottom: ${isAtBottom}, userScrolled: ${userScrolled}, isOverflowing: ${isOverflowing}`) - - useEffect(() => { - if (!isProcessing && userScrolled) { - console.log("userScrolled") - setUserScrolled(false) - } - }, [isProcessing]) - - useEffect(() => { - if (isProcessing && !userScrolled) { - scrollToBottom() - } - }, [messages]) - - useEffect(() => { - const container = containerRef.current - if (!container) return - - const topObserver = new IntersectionObserver( - ([entry]) => { - setIsAtTop(entry.isIntersecting) - }, - { threshold: 1 } - ) - - const bottomObserver = new IntersectionObserver( - ([entry]) => { - setIsAtBottom(entry.isIntersecting) - if (entry.isIntersecting) { - setUserScrolled(false) - } else if (!isAutoScrolling.current) { - setUserScrolled(true) - } - }, - { threshold: 1 } - ) - - if (messagesStartRef.current) { - topObserver.observe(messagesStartRef.current) - } - - if (messagesEndRef.current) { - bottomObserver.observe(messagesEndRef.current) - } - - const resizeObserver = new ResizeObserver(() => { - setIsOverflowing(container.scrollHeight > container.clientHeight) - }) - - resizeObserver.observe(container) - - return () => { - topObserver.disconnect() - bottomObserver.disconnect() - resizeObserver.disconnect() - } - }, []) - - const scrollToTop = useCallback(() => { - if (messagesStartRef.current) { - messagesStartRef.current.scrollIntoView({ behavior: "smooth" }) - } - }, []) - - const scrollToBottom = useCallback(() => { - isAutoScrolling.current = true - - setTimeout(() => { - if (messagesEndRef.current) { - messagesEndRef.current.scrollIntoView({ behavior: "smooth" }) - } - - isAutoScrolling.current = false - }, 100) - }, []) - - return { - messagesStartRef, - messagesEndRef, - containerRef, - isAtTop, - isAtBottom, - userScrolled, - isOverflowing, - scrollToTop, - scrollToBottom, - setIsAtBottom - } -} \ No newline at end of file diff --git a/src/hooks/useSmartScroll.tsx b/src/hooks/useSmartScroll.tsx new file mode 100644 index 0000000..cad0ce4 --- /dev/null +++ b/src/hooks/useSmartScroll.tsx @@ -0,0 +1,35 @@ +import { useRef, useEffect, useState } from 'react'; + +export const useSmartScroll = (messages: any[], streaming: boolean) => { + const containerRef = useRef(null); + const [isAtBottom, setIsAtBottom] = useState(true); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = container; + setIsAtBottom(scrollHeight - scrollTop - clientHeight < 50); + }; + + container.addEventListener('scroll', handleScroll); + return () => container.removeEventListener('scroll', handleScroll); + }, []); + + useEffect(() => { + if (isAtBottom && containerRef.current) { + const scrollOptions: ScrollIntoViewOptions = streaming + ? { behavior: 'smooth', block: 'end' } + : { behavior: 'auto', block: 'end' }; + containerRef.current.lastElementChild?.scrollIntoView(scrollOptions); + } + }, [messages, streaming, isAtBottom]); + + const scrollToBottom = () => { + containerRef.current?.lastElementChild?.scrollIntoView({ behavior: 'smooth', block: 'end' }); + }; + + + return { containerRef, isAtBottom, scrollToBottom }; +}; diff --git a/src/public/_locales/en/messages.json b/src/public/_locales/en/messages.json index 6e3f66b..a574d20 100644 --- a/src/public/_locales/en/messages.json +++ b/src/public/_locales/en/messages.json @@ -7,5 +7,8 @@ }, "openSidePanelToChat": { "message": "Open Copilot to Chat" + }, + "openOptionToChat": { + "message": "Open Web UI to Chat" } } \ No newline at end of file diff --git a/src/services/app.ts b/src/services/app.ts index ca16437..c442bd9 100644 --- a/src/services/app.ts +++ b/src/services/app.ts @@ -80,3 +80,21 @@ export const getCustomOllamaHeaders = async (): Promise< return headerMap } + +export const getOpenOnIconClick = async (): Promise => { + const openOnIconClick = await storage.get("openOnIconClick"); + return openOnIconClick || "webUI"; +}; + +export const setOpenOnIconClick = async (option: "webUI" | "sidePanel"): Promise => { + await storage.set("openOnIconClick", option); +}; + +export const getOpenOnRightClick = async (): Promise => { + const openOnRightClick = await storage.get("openOnRightClick"); + return openOnRightClick || "sidePanel"; +}; + +export const setOpenOnRightClick = async (option: "webUI" | "sidePanel"): Promise => { + await storage.set("openOnRightClick", option); +};