diff --git a/webapp/_webapp/src/components/tabs.tsx b/webapp/_webapp/src/components/tabs.tsx index d54dcbf2..1ae39151 100644 --- a/webapp/_webapp/src/components/tabs.tsx +++ b/webapp/_webapp/src/components/tabs.tsx @@ -1,6 +1,6 @@ import { cn, Tab, Tabs as NextTabs } from "@heroui/react"; import { Icon } from "@iconify/react"; -import { ReactNode, forwardRef, useImperativeHandle, useCallback } from "react"; +import { ReactNode, forwardRef, useImperativeHandle, useCallback, useRef, useEffect } from "react"; import { useConversationUiStore } from "../stores/conversation/conversation-ui-store"; import { useAuthStore } from "../stores/auth-store"; import { Avatar } from "./avatar"; @@ -22,16 +22,112 @@ type TabProps = { items: TabItem[]; }; +// Constants for width limits +const MIN_TAB_ITEMS_WIDTH = 64; // Minimum width (w-16 = 64px) +const MAX_TAB_ITEMS_WIDTH = 200; // Maximum width +const COLLAPSE_THRESHOLD = 113; // Width threshold to auto-collapse text + export const Tabs = forwardRef(({ items }, ref) => { const { user } = useAuthStore(); - const { activeTab, setActiveTab, sidebarCollapsed } = useConversationUiStore(); + const { + activeTab, + setActiveTab, + sidebarCollapsed, + setSidebarCollapsed, + tabItemsWidth, + setTabItemsWidth + } = useConversationUiStore(); const { hideAvatar } = useSettingStore(); const { minimalistMode } = useSettingStore(); + + const resizeHandleRef = useRef(null); + const isResizingRef = useRef(false); + const tabItemsWidthRef = useRef(tabItemsWidth); + const mouseMoveHandlerRef = useRef<((e: MouseEvent) => void) | null>(null); + const mouseUpHandlerRef = useRef<(() => void) | null>(null); + + // Keep ref in sync with tabItemsWidth + useEffect(() => { + tabItemsWidthRef.current = tabItemsWidth; + }, [tabItemsWidth]); + + // Auto-collapse based on width + useEffect(() => { + const shouldCollapse = tabItemsWidth < COLLAPSE_THRESHOLD; + // Get current state to avoid stale closure + const currentCollapsed = useConversationUiStore.getState().sidebarCollapsed; + // Only update if the state doesn't match the desired state + if (shouldCollapse !== currentCollapsed) { + setSidebarCollapsed(shouldCollapse); + } + }, [tabItemsWidth, setSidebarCollapsed]); // Only depend on tabItemsWidth to avoid loops useImperativeHandle(ref, () => ({ setSelectedTab: setActiveTab, })); + // Cleanup function to reset resize state and remove event listeners + const cleanupResizeState = useCallback(() => { + isResizingRef.current = false; + document.body.style.cursor = "default"; + document.body.style.userSelect = ""; + if (resizeHandleRef.current) { + resizeHandleRef.current.classList.remove("resizing"); + } + // Remove event listeners if they exist + if (mouseMoveHandlerRef.current) { + document.removeEventListener("mousemove", mouseMoveHandlerRef.current); + mouseMoveHandlerRef.current = null; + } + if (mouseUpHandlerRef.current) { + document.removeEventListener("mouseup", mouseUpHandlerRef.current); + mouseUpHandlerRef.current = null; + } + }, []); + + // Cleanup on unmount to prevent leaks if component unmounts during resize + useEffect(() => { + return () => { + if (isResizingRef.current) { + cleanupResizeState(); + } + }; + }, [cleanupResizeState]); + + // Handle resize drag + const handleMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + isResizingRef.current = true; + const startX = e.clientX; + const startWidth = tabItemsWidthRef.current; + + const handleMouseMove = (e: MouseEvent) => { + if (!isResizingRef.current) return; + e.preventDefault(); + const delta = e.clientX - startX; + const newWidth = Math.max(MIN_TAB_ITEMS_WIDTH, Math.min(MAX_TAB_ITEMS_WIDTH, startWidth + delta)); + setTabItemsWidth(newWidth); + }; + + const handleMouseUp = () => { + cleanupResizeState(); + }; + + // Store handlers in refs so they can be cleaned up on unmount + mouseMoveHandlerRef.current = handleMouseMove; + mouseUpHandlerRef.current = handleMouseUp; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + if (resizeHandleRef.current) { + resizeHandleRef.current.classList.add("resizing"); + } + }, [setTabItemsWidth, cleanupResizeState]); + const renderTabItem = useCallback( (item: TabItem) => { const tabTitle = ( @@ -54,10 +150,22 @@ export const Tabs = forwardRef(({ items }, ref) => { [sidebarCollapsed], ); - const width = sidebarCollapsed ? "w-16" : minimalistMode ? "w-[118px]" : "w-[140px]"; return ( <> -
+
+ {/* Resize handle on the right edge */} +
+ {/* Visual indicator line */} +
+
+ {!hideAvatar && } setValue(e.target.value)} /> -
+
current: {value}
diff --git a/webapp/_webapp/src/devtool/app.tsx b/webapp/_webapp/src/devtool/app.tsx index 5a7f0c7f..a2597d65 100644 --- a/webapp/_webapp/src/devtool/app.tsx +++ b/webapp/_webapp/src/devtool/app.tsx @@ -3,33 +3,35 @@ import { useAuthStore } from "../stores/auth-store"; import { useCallback, useEffect, useState } from "react"; import { getCookies } from "../intermediate"; import { TooltipArea } from "./tooltip"; -import { storage } from "../libs/storage"; +import { DevTools } from "../views/devtools"; +import { useDevtoolStore } from "../stores/devtool-store"; const App = () => { const { token, refreshToken, setToken, setRefreshToken } = useAuthStore(); - const [projectId, setProjectId] = useState(storage.getItem("pd.projectId") ?? ""); - const [overleafSession, setOverleafSession] = useState(storage.getItem("pd.auth.overleafSession") ?? ""); - const [gclb, setGclb] = useState(storage.getItem("pd.auth.gclb") ?? ""); - + const [projectId, setProjectId] = useState(localStorage.getItem("pd.projectId") ?? ""); + const [overleafSession, setOverleafSession] = useState(localStorage.getItem("pd.auth.overleafSession") ?? ""); + const [gclb, setGclb] = useState(localStorage.getItem("pd.auth.gclb") ?? ""); + const { showTool } = useDevtoolStore(); + useEffect(() => { getCookies(window.location.hostname).then((cookies) => { - setOverleafSession(cookies.session ?? storage.getItem("pd.auth.overleafSession") ?? ""); - setGclb(cookies.gclb ?? storage.getItem("pd.auth.gclb") ?? ""); + setOverleafSession(cookies.session ?? localStorage.getItem("pd.auth.overleafSession") ?? ""); + setGclb(cookies.gclb ?? localStorage.getItem("pd.auth.gclb") ?? ""); }); }, []); const setProjectId_ = useCallback((projectId: string) => { - storage.setItem("pd.projectId", projectId); + localStorage.setItem("pd.projectId", projectId); setProjectId(projectId); }, []); const setOverleafSession_ = useCallback((overleafSession: string) => { - storage.setItem("pd.auth.overleafSession", overleafSession); + localStorage.setItem("pd.auth.overleafSession", overleafSession); setOverleafSession(overleafSession); }, []); const setGclb_ = useCallback((gclb: string) => { - storage.setItem("pd.auth.gclb", gclb); + localStorage.setItem("pd.auth.gclb", gclb); setGclb(gclb); }, []); @@ -69,9 +71,9 @@ const App = () => { />
-
+
-
+
{JSON.stringify( { projectId, @@ -86,6 +88,9 @@ const App = () => {
+
+ {import.meta.env.DEV && showTool && } +
); }; diff --git a/webapp/_webapp/src/devtool/index.html b/webapp/_webapp/src/devtool/index.html index c4092bb1..4116298e 100644 --- a/webapp/_webapp/src/devtool/index.html +++ b/webapp/_webapp/src/devtool/index.html @@ -9,25 +9,25 @@ Paper Debugger Dev Tools - -
-
-
PaperDebugger Dev Tools
+ +
+
+
+ +
+
+
PaperDebugger Dev Tools
+
+
+ + +
+ + + +
-
-
-
- Vertical Federated Learning (VFL) is a crucial paradigm for training machine learning models on - feature-partitioned, distributed data. However, due to privacy restrictions, few public real-world VFL datasets - exist for algorithm evaluation, and these represent a limited array of feature distributions. Existing benchmarks - often resort to synthetic datasets, derived from arbitrary feature splits from a global set, which only capture a - subset of feature distributions, leading to inadequate algorithm performance assessment. This paper addresses - these shortcomings by introducing two key factors affecting VFL performance - feature importance and feature - correlation - and proposing associated evaluation metrics and dataset splitting methods. Additionally, we - introduce a real VFL dataset to address the deficit in image-image VFL scenarios. Our comprehensive evaluation of - cutting-edge VFL algorithms provides valuable insights for future research in the field. -
diff --git a/webapp/_webapp/src/index.css b/webapp/_webapp/src/index.css index d9ecde7c..88ba8954 100644 --- a/webapp/_webapp/src/index.css +++ b/webapp/_webapp/src/index.css @@ -16,6 +16,23 @@ body { sans-serif; } +/* Overleaf Compatibility Fixes (tricks) */ +img, svg { + display: inline-block; + vertical-align: middle; +} + +/* Block-level media elements */ +video, canvas, audio, iframe, embed, object { + display: block; +} + +.collapse { + visibility: visible !important; +} +/* Overleaf Compatibility Fixes (tricks) */ + + /* Others */ .noselect { -webkit-touch-callout: none; @@ -245,6 +262,8 @@ body { gap: 1rem; @apply bg-gray-100 px-3 pb-3 pt-12; z-index: 888; + position: relative; + flex-shrink: 0; } .pd-app-tab-content { @@ -576,3 +595,90 @@ body { vertical-align: top; text-align: right; } +/* Embed Sidebar Styles */ +#pd-embed-sidebar { + background-color: var(--pd-default-bg); + position: relative; + overflow: hidden; + flex-shrink: 0; + display: flex; + flex-direction: column; +} + +#pd-embed-sidebar .pd-app-container { + border-radius: 0; + border: none; + border-left: 1px solid var(--pd-border-color); + background-color: var(--pd-default-bg); + position: relative; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.pd-embed-resize-handle { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 8px; + cursor: col-resize; + background-color: transparent; + transition: background-color 0.2s, opacity 0.2s; + z-index: 10000; + pointer-events: auto; + display: flex; + align-items: center; + justify-content: center; +} + +.pd-embed-resize-handle:hover { + background-color: rgba(59, 130, 246, 0.1); +} + +.pd-embed-resize-handle:hover .resize-handle-indicator { + opacity: 1 !important; +} + +.resize-handle-indicator { + width: 2px; + height: 40px; + background-color: var(--pd-primary-color, #3b82f6); + border-radius: 1px; + opacity: 0; + transition: opacity 0.2s; +} + +/* Tab Items Resize Handle */ +.pd-tab-items-resize-handle { + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 8px; + cursor: col-resize; + background-color: transparent; + transition: background-color 0.2s, opacity 0.2s; + z-index: 1000; + pointer-events: auto; + display: flex; + align-items: center; + justify-content: center; +} + +.pd-tab-items-resize-handle:hover { + background-color: rgba(59, 130, 246, 0.1); +} + +.pd-tab-items-resize-handle:hover .resize-handle-indicator { + opacity: 1 !important; +} + +.pd-tab-items-resize-handle.resizing { + background-color: rgba(59, 130, 246, 0.2) !important; +} + +.pd-tab-items-resize-handle.resizing .resize-handle-indicator { + opacity: 1 !important; +} \ No newline at end of file diff --git a/webapp/_webapp/src/libs/inline-suggestion.ts b/webapp/_webapp/src/libs/inline-suggestion.ts index 36f09e36..7c218832 100644 --- a/webapp/_webapp/src/libs/inline-suggestion.ts +++ b/webapp/_webapp/src/libs/inline-suggestion.ts @@ -102,33 +102,13 @@ export function debouncePromise any>( // eslint-di }; } -export async function completion(state: EditorState): Promise { +export async function completion(_state: EditorState): Promise { const settings = useSettingStore.getState().settings; if (!settings?.enableCompletion) { return ""; } - const cursor = state.selection.main.head; - const left = state.doc.sliceString(Math.max(0, cursor - 2048), cursor); - logDebug("left", left); - - // const completion = await chatCompletion({ - // languageModel: LanguageModel.OPENAI_GPT4O_MINI, - // messages: [ - // { - // role: "developer", - // content: - // 'You are a senior PhD candidate writing in Overleaf. At [COMPLETE_AT_HERE], write a concise, context-aware sentence (15 words or fewer). The text may include LaTeX code—handle it cleanly but don’t overfocus on it. Avoid the words “ensuring,” “utilizing,” "illustrates", “showcasing,” and “necessitating.” Keep it short, direct, and natural—no need for lengthy or structured phrasing. Replace all transition words and conjunctions in the sentences with the most basic and commonly used ones. Use simple expressions,avoiding complex vocabulary. Ensure the logical connections between sentences are clear. Deletes the conclusion part in the end of the text.', - // }, - // { - // role: "user", - // content: `The paragraph is: ${left}[COMPLETE_AT_HERE]`, - // }, - // ], - // }); - // const responseText = completion.message?.content || ""; - const responseText = "Unsupported Feature"; - return responseText; + return "Unsupported Feature"; } /** diff --git a/webapp/_webapp/src/libs/overleaf-socket.ts b/webapp/_webapp/src/libs/overleaf-socket.ts index 786d1fd5..01d4615e 100644 --- a/webapp/_webapp/src/libs/overleaf-socket.ts +++ b/webapp/_webapp/src/libs/overleaf-socket.ts @@ -195,7 +195,6 @@ export async function postCommentToThread( ): Promise { const currentDomain = window.location.hostname; const threadUrl = `https://${currentDomain}/project/${projectId}/thread/${threadId}/messages`; - // console.log("Posting comment to thread:", threadUrl, comment); if (!comment || comment.length === 0) { throw new Error("Comment is empty"); diff --git a/webapp/_webapp/src/main.tsx b/webapp/_webapp/src/main.tsx index 81dfff05..fa89b021 100644 --- a/webapp/_webapp/src/main.tsx +++ b/webapp/_webapp/src/main.tsx @@ -1,5 +1,5 @@ import { Extension } from "@codemirror/state"; -import { StrictMode, useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { createRoot } from "react-dom/client"; import { OnboardingGuide } from "./components/onboarding-guide"; @@ -13,11 +13,9 @@ import apiclient, { apiclientV2, getEndpointFromLocalStorage } from "./libs/apic import { Providers } from "./providers"; import { useAuthStore } from "./stores/auth-store"; import { useConversationUiStore } from "./stores/conversation/conversation-ui-store"; -import { useDevtoolStore } from "./stores/devtool-store"; import { useSelectionStore } from "./stores/selection-store"; import { useSettingStore } from "./stores/setting-store"; import { MainDrawer } from "./views"; -import { DevTools } from "./views/devtools"; import { usePromptLibraryStore } from "./stores/prompt-library-store"; import { TopMenuButton } from "./components/top-menu-button"; import { Logo } from "./components/logo"; @@ -69,7 +67,6 @@ export const Main = () => { } = useSelectionStore(); const [menuElement, setMenuElement] = useState(null); const { isOpen, setIsOpen } = useConversationUiStore(); - const { showTool: showDevTool } = useDevtoolStore(); const { settings, loadSettings, disableLineWrap } = useSettingStore(); const { login } = useAuthStore(); const { loadPrompts } = usePromptLibraryStore(); @@ -215,7 +212,6 @@ export const Main = () => { {buttonPortal} - {import.meta.env.DEV && showDevTool && } ); @@ -234,22 +230,13 @@ if (!import.meta.env.DEV) { const root = createRoot(div); const adapter = getOverleafAdapter(); + // This block only runs in production (!DEV), so always render without StrictMode root.render( - import.meta.env.DEV ? ( - - - -
- - - - ) : ( - - -
- - - ), + + +
+ + ); googleAnalytics.firePageViewEvent( "unknown", diff --git a/webapp/_webapp/src/query/api.ts b/webapp/_webapp/src/query/api.ts index 586567c2..9f4245e2 100644 --- a/webapp/_webapp/src/query/api.ts +++ b/webapp/_webapp/src/query/api.ts @@ -25,8 +25,6 @@ import { import { GetProjectRequest, GetProjectResponseSchema, - RunProjectPaperScoreRequest, - RunProjectPaperScoreResponseSchema, UpsertProjectRequest, UpsertProjectResponseSchema, GetProjectInstructionsRequest, @@ -195,11 +193,6 @@ export const upsertUserInstructions = async (data: PlainMessage) => { - const response = await apiclient.post(`/projects/${data.projectId}/paper-score`, data); - return fromJson(RunProjectPaperScoreResponseSchema, response); -}; export const getProjectInstructions = async (data: PlainMessage) => { if (!apiclient.hasToken()) { diff --git a/webapp/_webapp/src/query/index.ts b/webapp/_webapp/src/query/index.ts index 7a78ccae..ba397ad4 100644 --- a/webapp/_webapp/src/query/index.ts +++ b/webapp/_webapp/src/query/index.ts @@ -16,7 +16,6 @@ import { listConversations, listPrompts, listSupportedModels, - runProjectPaperScore, updateConversation, updatePrompt, getUserInstructions, @@ -35,22 +34,11 @@ import { import { queryKeys } from "./keys"; import { GetProjectResponse, - RunProjectPaperScoreResponse, GetProjectInstructionsResponse, UpsertProjectInstructionsResponse, } from "../pkg/gen/apiclient/project/v1/project_pb"; import { useAuthStore } from "../stores/auth-store"; -// Deprecated -// export const useGetUserQuery = ( -// opts?: UseQueryOptionsOverride, -// ) => { -// return useQuery({ -// queryKey: queryKeys.users.getUser().queryKey, -// queryFn: getUser, -// ...opts, -// }); -// }; export const useGetProjectQuery = (projectId: string, opts?: UseQueryOptionsOverride) => { return useQuery({ @@ -118,13 +106,6 @@ export const useDeleteConversationMutation = (opts?: UseMutationOptionsOverride< }); }; -export const useRunProjectPaperScoreMutation = (opts?: UseMutationOptionsOverride) => { - return useMutation({ - mutationFn: runProjectPaperScore, - ...opts, - }); -}; - export const useGetConversationQuery = ( conversationId: string, opts?: UseQueryOptionsOverride, diff --git a/webapp/_webapp/src/query/keys.ts b/webapp/_webapp/src/query/keys.ts index e28ef91e..e09bfd7e 100644 --- a/webapp/_webapp/src/query/keys.ts +++ b/webapp/_webapp/src/query/keys.ts @@ -17,12 +17,6 @@ export const queryKeys = createQueryKeyStore({ }, projects: { getProject: (projectId: string) => ["projects", projectId], - runProjectPaperScore: (projectId: string, conversationId: string) => [ - "projects", - "paper-score", - projectId, - conversationId, - ], getProjectInstructions: (projectId: string) => ["projects", projectId, "instructions"], }, comments: { diff --git a/webapp/_webapp/src/stores/auth-store.ts b/webapp/_webapp/src/stores/auth-store.ts index acc92b08..b425c5f9 100644 --- a/webapp/_webapp/src/stores/auth-store.ts +++ b/webapp/_webapp/src/stores/auth-store.ts @@ -87,13 +87,6 @@ export const useAuthStore = create((set, get) => ({ }, initFromStorage: () => { - // const token = storage.getItem(LOCAL_STORAGE_KEY.TOKEN) ?? ""; - // const refreshToken = storage.getItem(LOCAL_STORAGE_KEY.REFRESH_TOKEN) ?? ""; - // console.log("[AuthStore] initFromStorage:", { - // hasToken: !!token, - // tokenLength: token.length, - // hasRefreshToken: !!refreshToken - // }); - // set({ token, refreshToken }); + // Function intentionally left empty - initialization handled elsewhere }, })); diff --git a/webapp/_webapp/src/stores/conversation/conversation-ui-store.ts b/webapp/_webapp/src/stores/conversation/conversation-ui-store.ts index 59c2b697..8728c1fd 100644 --- a/webapp/_webapp/src/stores/conversation/conversation-ui-store.ts +++ b/webapp/_webapp/src/stores/conversation/conversation-ui-store.ts @@ -8,6 +8,7 @@ export const DISPLAY_MODES = [ { key: "floating", label: "Floating" }, { key: "right-fixed", label: "Right Fixed" }, { key: "bottom-fixed", label: "Bottom Fixed" }, + { key: "embed", label: "Embed Sidebar" }, { key: "fullscreen", label: "Full Screen" }, ] as const; export type DisplayMode = (typeof DISPLAY_MODES)[number]["key"]; @@ -42,6 +43,9 @@ interface ConversationUiStore { rightFixedWidth: number; setRightFixedWidth: (rightFixedWidth: number) => void; + embedWidth: number; + setEmbedWidth: (embedWidth: number) => void; + isOpen: boolean; // for the main drawer setIsOpen: (isOpen: boolean) => void; @@ -51,6 +55,9 @@ interface ConversationUiStore { sidebarCollapsed: boolean; setSidebarCollapsed: (sidebarCollapsed: boolean) => void; + tabItemsWidth: number; + setTabItemsWidth: (tabItemsWidth: number) => void; + heightCollapseRequired: boolean; setHeightCollapseRequired: (heightCollapseRequired: boolean) => void; @@ -92,6 +99,9 @@ export const useConversationUiStore = create()( rightFixedWidth: 580, setRightFixedWidth: (rightFixedWidth: number) => set({ rightFixedWidth }), + embedWidth: 480, + setEmbedWidth: (embedWidth: number) => set({ embedWidth }), + isOpen: false, setIsOpen: (isOpen: boolean) => set({ isOpen }), @@ -101,6 +111,9 @@ export const useConversationUiStore = create()( sidebarCollapsed: false, setSidebarCollapsed: (sidebarCollapsed: boolean) => set({ sidebarCollapsed }), + tabItemsWidth: 140, // Default width in pixels + setTabItemsWidth: (tabItemsWidth: number) => set({ tabItemsWidth }), + heightCollapseRequired: false, setHeightCollapseRequired: (heightCollapseRequired: boolean) => set({ heightCollapseRequired }), @@ -111,8 +124,8 @@ export const useConversationUiStore = create()( set({ floatingX: 100, floatingY: 100, - floatingWidth: 620, - floatingHeight: 200, + floatingWidth: 500, + floatingHeight: 500, displayMode: "floating", }); }, diff --git a/webapp/_webapp/src/views/body.tsx b/webapp/_webapp/src/views/body.tsx new file mode 100644 index 00000000..7cd30d20 --- /dev/null +++ b/webapp/_webapp/src/views/body.tsx @@ -0,0 +1,8 @@ +import { useAuthStore } from "../stores/auth-store"; +import { PaperDebugger } from "../paperdebugger"; +import { Login } from "./login"; + +export const Body = () => { + const { isAuthenticated } = useAuthStore(); + return isAuthenticated() ? : ; +}; diff --git a/webapp/_webapp/src/views/devtools/index.tsx b/webapp/_webapp/src/views/devtools/index.tsx index f07f5f4e..1772a411 100644 --- a/webapp/_webapp/src/views/devtools/index.tsx +++ b/webapp/_webapp/src/views/devtools/index.tsx @@ -1,4 +1,3 @@ -import { Rnd } from "react-rnd"; import { useSelectionStore } from "../../stores/selection-store"; import { Button, Input } from "@heroui/react"; import { useStreamingMessageStore } from "../../stores/streaming-message-store"; @@ -231,111 +230,144 @@ export const DevTools = () => { // --- Render --- return ( - -
-

DevTools

- {/* Conversation section */} -
-

- Conversation ( - {isEmptyConversation() ? ( - empty - ) : ( - not empty - )} - ) - -

-
-

Selected Text

- - -
-
-

- Finalized Message ({currentConversation.messages.length}) - -

-
- - - - -
+ + {/* Selected Text */} +
+

Selected Text

+
+ + +
+
+ + {/* Finalized Messages */} +
+
+

+ Finalized Messages ({currentConversation.messages.length}) +

+ +
+
+ + + + + +
+
+ {/* Streaming Message section */} -
-

- Streaming Message -
- ({streamingMessage.parts.length} total, +
+
+

Streaming Message

+ +
+
+ ({streamingMessage.parts.length} total, + {streamingMessage.parts.filter((part) => part.status === MessageEntryStatus.PREPARING).length}{" "} preparing, + + {streamingMessage.parts.filter((part) => part.status === MessageEntryStatus.FINALIZED).length}{" "} finalized, + + {streamingMessage.parts.filter((part) => part.status === MessageEntryStatus.INCOMPLETE).length}{" "} incomplete, - {streamingMessage.parts.filter((part) => part.status === MessageEntryStatus.STALE).length} stale ) -
- -

-
-

Preparing delay (seconds):

+ + + {streamingMessage.parts.filter((part) => part.status === MessageEntryStatus.STALE).length} stale + + ) +
+ + {/* Preparing delay */} +
+ setPreparingDelay(Number(e.target.value) || 0)} />
- - - - - - + + {/* Streaming buttons */} +
+ + + + + + +
-
+
); }; diff --git a/webapp/_webapp/src/views/embed-sidebar.tsx b/webapp/_webapp/src/views/embed-sidebar.tsx new file mode 100644 index 00000000..5d65c106 --- /dev/null +++ b/webapp/_webapp/src/views/embed-sidebar.tsx @@ -0,0 +1,279 @@ +import { useEffect, useState, useRef, useCallback } from "react"; +import { createPortal } from "react-dom"; +import { useConversationUiStore } from "../stores/conversation/conversation-ui-store"; +import { PdAppContainer } from "../components/pd-app-container"; +import { WindowController } from "./window-controller"; +import { Body } from "./body"; + +export const EmbedSidebar = () => { + const [container, setContainer] = useState(null); + const { embedWidth, isOpen, setEmbedWidth } = useConversationUiStore(); + const resizeHandleRef = useRef(null); + const embedWidthRef = useRef(embedWidth); + const isResizingRef = useRef(false); + const originalBodyStyleRef = useRef<{ + display?: string; + flexDirection?: string; + }>({}); + const mouseMoveHandlerRef = useRef<((e: MouseEvent) => void) | null>(null); + const mouseUpHandlerRef = useRef<(() => void) | null>(null); + const originalCursorRef = useRef(""); + const originalUserSelectRef = useRef(""); + + // Keep ref in sync with embedWidth + useEffect(() => { + embedWidthRef.current = embedWidth; + }, [embedWidth]); + + // Cleanup resize handlers and body styles on unmount or when isOpen changes + useEffect(() => { + return () => { + // Clean up any active resize handlers + if (mouseMoveHandlerRef.current) { + document.removeEventListener("mousemove", mouseMoveHandlerRef.current); + mouseMoveHandlerRef.current = null; + } + if (mouseUpHandlerRef.current) { + document.removeEventListener("mouseup", mouseUpHandlerRef.current); + mouseUpHandlerRef.current = null; + } + + // Restore body styles + if (originalCursorRef.current !== "") { + document.body.style.cursor = originalCursorRef.current; + originalCursorRef.current = ""; + } + if (originalUserSelectRef.current !== "") { + document.body.style.userSelect = originalUserSelectRef.current; + originalUserSelectRef.current = ""; + } + + // Reset resizing state + isResizingRef.current = false; + }; + }, [isOpen]); + + // Function to update main content area flex properties + const updateMainContentFlex = useCallback((ideBody: HTMLElement) => { + // Find or create ide-redesign-inner + let ideInner = ideBody.querySelector(".ide-redesign-inner") as HTMLElement | null; + if (!ideInner) { + ideInner = document.createElement("div"); + ideInner.className = "ide-redesign-inner"; + // Move all existing children (except sidebar) into ideInner + const children = Array.from(ideBody.children) as HTMLElement[]; + children.forEach((child) => { + if (child.id !== "pd-embed-sidebar" && !child.classList.contains("ide-redesign-inner")) { + ideInner!.appendChild(child); + } + }); + // Insert ideInner before sidebar (or at the beginning if no sidebar) + const sidebar = ideBody.querySelector("#pd-embed-sidebar"); + if (sidebar) { + ideBody.insertBefore(ideInner, sidebar); + } else { + ideBody.appendChild(ideInner); + } + } + + // Set flex properties for ide-redesign-inner + ideInner.style.flex = "1"; + ideInner.style.minWidth = "0"; + ideInner.style.overflow = "hidden"; + }, []); + + // Update container width when embedWidth changes + useEffect(() => { + if (container) { + container.style.width = `${embedWidth}px`; + } + + // Also update layout when embedWidth changes + if (isOpen) { + const ideBody = document.querySelector(".ide-redesign-body") as HTMLElement | null; + if (ideBody) { + // Re-apply flex layout + updateMainContentFlex(ideBody); + } + } + }, [container, embedWidth, isOpen, updateMainContentFlex]); + + // Handle window resize to ensure layout stays correct + useEffect(() => { + if (!isOpen || !container) return; + + const handleResize = () => { + const ideBody = document.querySelector(".ide-redesign-body") as HTMLElement | null; + if (ideBody) { + // Re-apply flex layout on window resize + updateMainContentFlex(ideBody); + } + }; + + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [isOpen, container, updateMainContentFlex]); + + useEffect(() => { + if (!isOpen) return; + + // Find ide-redesign-body (works in both normal and dev mode) + const ideBody = document.querySelector(".ide-redesign-body") as HTMLElement | null; + + if (!ideBody) { + return; + } + + // Store original styles + originalBodyStyleRef.current = { + display: ideBody.style.display || "", + flexDirection: ideBody.style.flexDirection || "", + }; + + // Ensure ide-redesign-inner exists and has correct flex properties + updateMainContentFlex(ideBody); + + // Create sidebar container + const sidebarDiv = document.createElement("div"); + sidebarDiv.id = "pd-embed-sidebar"; + sidebarDiv.className = "pd-embed-sidebar"; + sidebarDiv.style.width = `${embedWidthRef.current}px`; + sidebarDiv.style.height = "100%"; // Use 100% to match parent height + sidebarDiv.style.display = "flex"; + sidebarDiv.style.flexDirection = "column"; + sidebarDiv.style.borderLeft = "1px solid var(--pd-border-color)"; + sidebarDiv.style.flexShrink = "0"; + sidebarDiv.style.position = "relative"; + sidebarDiv.style.overflow = "hidden"; // Prevent overflow + + // Modify parent container to flex layout + ideBody.style.display = "flex"; + ideBody.style.flexDirection = "row"; + ideBody.style.width = "100%"; + ideBody.style.height = "100vh"; // Ensure full viewport height + ideBody.style.overflow = "hidden"; + + // Append sidebar to ideBody (after ide-redesign-inner) + ideBody.appendChild(sidebarDiv); + setContainer(sidebarDiv); + + return () => { + // Cleanup + const sidebarDiv = document.getElementById("pd-embed-sidebar"); + if (sidebarDiv) { + sidebarDiv.remove(); + } + + // Restore original styles + if (ideBody && originalBodyStyleRef.current) { + ideBody.style.display = originalBodyStyleRef.current.display || ""; + ideBody.style.flexDirection = originalBodyStyleRef.current.flexDirection || ""; + ideBody.style.width = ""; + ideBody.style.height = ""; + ideBody.style.overflow = ""; + + // Restore ide-redesign-inner flex properties + const ideInner = ideBody.querySelector(".ide-redesign-inner") as HTMLElement | null; + if (ideInner) { + ideInner.style.flex = ""; + ideInner.style.minWidth = ""; + ideInner.style.overflow = ""; + } + } + + setContainer(null); + }; + }, [isOpen, updateMainContentFlex]); + + // Handle resize drag + const handleMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // Clean up any existing handlers first + if (mouseMoveHandlerRef.current) { + document.removeEventListener("mousemove", mouseMoveHandlerRef.current); + } + if (mouseUpHandlerRef.current) { + document.removeEventListener("mouseup", mouseUpHandlerRef.current); + } + + isResizingRef.current = true; + const startX = e.clientX; + const startWidth = embedWidthRef.current; + + const handleMouseMove = (e: MouseEvent) => { + if (!isResizingRef.current) return; + e.preventDefault(); + const delta = e.clientX - startX; + const maxWidth = window.innerWidth * 0.8; // 80% of window width + const newWidth = Math.max(300, Math.min(maxWidth, startWidth - delta)); // min 300px, max 80% of window + setEmbedWidth(newWidth); + }; + + const handleMouseUp = () => { + isResizingRef.current = false; + + // Remove event listeners + if (mouseMoveHandlerRef.current) { + document.removeEventListener("mousemove", mouseMoveHandlerRef.current); + mouseMoveHandlerRef.current = null; + } + if (mouseUpHandlerRef.current) { + document.removeEventListener("mouseup", mouseUpHandlerRef.current); + mouseUpHandlerRef.current = null; + } + + // Restore body styles (including clearing inline styles when originally unset) + document.body.style.cursor = originalCursorRef.current; + document.body.style.userSelect = originalUserSelectRef.current; + originalCursorRef.current = ""; + originalUserSelectRef.current = ""; + }; + + // Store handlers in refs for cleanup + mouseMoveHandlerRef.current = handleMouseMove; + mouseUpHandlerRef.current = handleMouseUp; + + // Save original body styles + originalCursorRef.current = document.body.style.cursor || ""; + originalUserSelectRef.current = document.body.style.userSelect || ""; + + // Apply new styles and attach listeners + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }, [setEmbedWidth]); + + if (!container || !isOpen) return null; + + return createPortal( +
+ {/* Resize handle on the left */} +
+ {/* Visual indicator line */} +
+
+ + + + +
, + container, + ); +}; diff --git a/webapp/_webapp/src/views/index.tsx b/webapp/_webapp/src/views/index.tsx index fa963239..c2323b52 100644 --- a/webapp/_webapp/src/views/index.tsx +++ b/webapp/_webapp/src/views/index.tsx @@ -1,129 +1,16 @@ -import { cn, Tooltip } from "@heroui/react"; +import { cn } from "@heroui/react"; import { Rnd } from "react-rnd"; import { useCallback, useState, useEffect, useMemo } from "react"; -import { PaperDebugger } from "../paperdebugger"; import { createPortal } from "react-dom"; import { COLLAPSED_HEIGHT, useConversationUiStore } from "../stores/conversation/conversation-ui-store"; -import { Icon } from "@iconify/react/dist/iconify.js"; import { debounce } from "../libs/helpers"; -import { Login } from "./login"; import { PdAppContainer } from "../components/pd-app-container"; -import { PdAppControlTitleBar } from "../components/pd-app-control-title-bar"; -import { PdAppSmallControlButton } from "../components/pd-app-small-control-button"; -import { useAuthStore } from "../stores/auth-store"; import { useSettingStore } from "../stores/setting-store"; +import { WindowController } from "./window-controller"; +import { Body } from "./body"; +import { EmbedSidebar } from "./embed-sidebar"; -const PositionController = () => { - const { - sidebarCollapsed, - floatingHeight, - bottomFixedHeight, - setHeightCollapseRequired, - setDisplayMode, - displayMode, - } = useConversationUiStore(); - return ( -
- - { - if (floatingHeight < COLLAPSED_HEIGHT) { - setHeightCollapseRequired(true); - } else { - setHeightCollapseRequired(false); - } - setDisplayMode("floating"); - }} - > - - - - - { - if (bottomFixedHeight < COLLAPSED_HEIGHT) { - setHeightCollapseRequired(true); - } else { - setHeightCollapseRequired(false); - } - setDisplayMode("bottom-fixed"); - }} - > - - - - - { - if (window.innerHeight < COLLAPSED_HEIGHT) { - setHeightCollapseRequired(true); - } else { - setHeightCollapseRequired(false); - } - setDisplayMode("right-fixed"); - }} - > - - - -
- ); -}; - -const WindowController = () => { - const { sidebarCollapsed, setSidebarCollapsed, setIsOpen } = useConversationUiStore(); - const CompactHeader = useMemo(() => { - return ( - -
-
- - setIsOpen(false)}> - - - - - - setSidebarCollapsed(!sidebarCollapsed)}> - - - -
-
-
- ); - }, [sidebarCollapsed, setSidebarCollapsed, setIsOpen]); - return CompactHeader; -}; - -const Body = () => { - const { isAuthenticated } = useAuthStore(); - return isAuthenticated() ? : ; -}; export const MainDrawer = () => { const { displayMode, isOpen } = useConversationUiStore(); @@ -149,6 +36,26 @@ export const MainDrawer = () => { return () => window.removeEventListener("resize", handleResize); }, []); + const handleResize = useCallback( + (...args: unknown[]) => { + const [, , /* _e */ /* _dir */ ref] = args; + if (ref && ref instanceof HTMLElement && ref.offsetHeight < COLLAPSED_HEIGHT) { + setHeightCollapseRequired(true); + } else { + setHeightCollapseRequired(false); + } + }, + [setHeightCollapseRequired], + ); + const debouncedHandleResize = useMemo(() => debounce(handleResize, 100), [handleResize]); + + // Handle embed mode separately + if (displayMode === "embed") { + return ; + } + + // Handle other modes with Rnd + // Layout configs for each mode type RndProps = React.ComponentProps; let rndProps: Partial = {}; @@ -194,19 +101,6 @@ export const MainDrawer = () => { }; } - const handleResize = useCallback( - (...args: unknown[]) => { - const [, , /* _e */ /* _dir */ ref] = args; - if (ref && ref instanceof HTMLElement && ref.offsetHeight < COLLAPSED_HEIGHT) { - setHeightCollapseRequired(true); - } else { - setHeightCollapseRequired(false); - } - }, - [setHeightCollapseRequired], - ); - const debouncedHandleResize = useMemo(() => debounce(handleResize, 100), [handleResize]); - return createPortal( {
- {/* */} diff --git a/webapp/_webapp/src/views/window-controller.tsx b/webapp/_webapp/src/views/window-controller.tsx new file mode 100644 index 00000000..ad14082b --- /dev/null +++ b/webapp/_webapp/src/views/window-controller.tsx @@ -0,0 +1,114 @@ +import { useMemo } from "react"; +import { cn, Tooltip } from "@heroui/react"; +import { Icon } from "@iconify/react/dist/iconify.js"; +import { COLLAPSED_HEIGHT, useConversationUiStore } from "../stores/conversation/conversation-ui-store"; +import { PdAppControlTitleBar } from "../components/pd-app-control-title-bar"; +import { PdAppSmallControlButton } from "../components/pd-app-small-control-button"; + +export const WindowController = () => { + const { sidebarCollapsed, setIsOpen } = useConversationUiStore(); + const CompactHeader = useMemo(() => { + return ( + +
+
+ + setIsOpen(false)}> + + + + +
+
+
+ ); + }, [sidebarCollapsed, setIsOpen]); + return CompactHeader; +}; + +const PositionController = () => { + const { + sidebarCollapsed, + floatingHeight, + bottomFixedHeight, + setHeightCollapseRequired, + setDisplayMode, + displayMode, + } = useConversationUiStore(); + return ( +
+ + { + if (floatingHeight < COLLAPSED_HEIGHT) { + setHeightCollapseRequired(true); + } else { + setHeightCollapseRequired(false); + } + setDisplayMode("floating"); + }} + > + + + + + { + if (bottomFixedHeight < COLLAPSED_HEIGHT) { + setHeightCollapseRequired(true); + } else { + setHeightCollapseRequired(false); + } + setDisplayMode("bottom-fixed"); + }} + > + + + + + { + if (window.innerHeight < COLLAPSED_HEIGHT) { + setHeightCollapseRequired(true); + } else { + setHeightCollapseRequired(false); + } + setDisplayMode("right-fixed"); + }} + > + + + + + { + setDisplayMode("embed"); + }} + > + + + +
+ ); +};