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);
+};