mirror of
https://github.com/alphaXiv/OpenResearch.git
synced 2026-11-11 09:59:34 +08:00
feat: adapt dashboard workspace for mobile (#818)
Supersedes #382. Rebuild mobile navigation and full-width workspace panes. Assisted-by: gpt-6.1-sol Assisted-by: gpt-6.1-terra Assisted-by: gpt-6-astra
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>OpenResearch</title>
|
||||
<script>
|
||||
|
||||
+30
-13
@@ -480,10 +480,13 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
const workspaceWide = windowWidth >= WORKSPACE_CARD_MIN_WIDTH;
|
||||
const workspaceCardVisible = mainView === "chat" && !panelOpen && workspaceWide;
|
||||
const [railOpen, setRailOpen] = useState(true);
|
||||
const [mobileRailOpen, setMobileRailOpen] = useState(false);
|
||||
const mobile = windowWidth < 768;
|
||||
useEffect(() => { setMobileRailOpen(false); }, [mobile, location.href]);
|
||||
const [preferencesLoaded, setPreferencesLoaded] = useState(false);
|
||||
const railTemporarilyHidden = mainView === "chat" && panelOpen && !sidebarFitsWithPanel(windowWidth);
|
||||
const railVisible = railOpen && !railTemporarilyHidden;
|
||||
const stackPanel = mainView === "chat" && panelOpen && windowWidth < PANEL_MIN_WIDTH * 2;
|
||||
const railVisible = mobile ? mobileRailOpen : railOpen && !railTemporarilyHidden;
|
||||
const stackPanel = !mobile && mainView === "chat" && panelOpen && windowWidth < PANEL_MIN_WIDTH * 2;
|
||||
const visiblePanelWidth = Math.min(panelWidth, panelMaxWidth(windowWidth, railVisible));
|
||||
const bodyRef = useRef<HTMLDivElement>(null);
|
||||
const focusAfterRailChange = useRef<"rail" | "show" | Element | null>(null);
|
||||
@@ -1651,6 +1654,12 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
const codeTab = requestedCodeTab
|
||||
? (codeTabs.find((tab) => sameCodeTab(tab, requestedCodeTab)) ?? null)
|
||||
: null;
|
||||
let activeWorkspaceView: "files" | "artifacts" | "experiments" | "terminal" | null = null;
|
||||
if (panelOpen) {
|
||||
if (expTab || codeTab) activeWorkspaceView = "experiments";
|
||||
else if (fileTab) activeWorkspaceView = fileTab.source === "artifacts" ? "artifacts" : "files";
|
||||
else if (rightTab === "files" || rightTab === "artifacts" || rightTab === "experiments" || rightTab === "terminal") activeWorkspaceView = rightTab;
|
||||
}
|
||||
const contentTabByKey = new Map<string, ContentTab>();
|
||||
for (const tab of [...expTabs, ...fileTabs, ...planTabs, ...subagentTabs, ...sideTabs, ...codeTabs]) {
|
||||
contentTabByKey.set(rightTabKey(tab), tab);
|
||||
@@ -1808,7 +1817,8 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
<RailHeader
|
||||
onCollapse={() => {
|
||||
focusAfterRailChange.current = "show";
|
||||
setRailOpen(false);
|
||||
if (mobile) setMobileRailOpen(false);
|
||||
else setRailOpen(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -1818,7 +1828,8 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
{projectId && <div className="absolute start-[calc(5.15rem/min(1,var(--desktop-page-zoom,1)))] top-[calc(-2px/min(1,var(--desktop-page-zoom,1)))] z-10 hidden h-[calc(44px/min(1,var(--desktop-page-zoom,1)))] items-center mac-titlebar:flex">
|
||||
<Tooltip interactive content={railVisible ? m.header_hide_sidebar() : m.chat_panel_show_sidebar()} className="rounded-md">
|
||||
<IconButton aria-label={railVisible ? m.header_hide_sidebar() : m.chat_panel_show_sidebar()} aria-expanded={railVisible} onClick={() => {
|
||||
if (railVisible) setRailOpen(false);
|
||||
if (mobile) setMobileRailOpen(!mobileRailOpen);
|
||||
else if (railVisible) setRailOpen(false);
|
||||
else {
|
||||
focusAfterRailChange.current = "rail";
|
||||
setRailOpen(true);
|
||||
@@ -1833,7 +1844,7 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
{runtime.kind === "local" && <UpdateBanner status={updateStatus} hasSidebar={Boolean(projectId)} />}
|
||||
<DesktopAppBanner />
|
||||
{workspaceError && <div role="alert" className="flex items-center gap-2 px-4 mac-titlebar:ps-titlebar-sidebar-controls mac-titlebar:min-h-titlebar win-titlebar:pe-win-controls py-2 text-subtext"><span>{workspaceError}</span><Button onClick={retryWorkspace}>{m.app_retry()}</Button></div>}
|
||||
<div ref={bodyRef} className={`app-body workspace-body relative flex flex-1 min-h-0 ${stackPanel ? "flex-col [&>.chat-pane]:overflow-y-auto [&_.chat-empty]:min-h-0 [&_.chat-empty]:overflow-y-auto [&_.chat-empty]:justify-start [&_.chat-empty]:p-0" : ""} ${workspaceCardVisible ? "workspace-card-visible" : ""}`}>
|
||||
<div ref={bodyRef} className={`app-body workspace-body relative flex flex-1 min-h-0 ${stackPanel ? "flex-col [&>.chat-pane]:overflow-y-auto [&_.chat-empty]:min-h-0 [&_.chat-empty]:overflow-y-auto [&_.chat-empty]:justify-start [&_.chat-empty]:p-0" : ""} ${workspaceCardVisible ? "workspace-card-visible" : ""} ${mainView === "chat" && workspaceReady && !conversationError ? "has-workspace-tools" : ""}`}>
|
||||
{projectId && (
|
||||
<ChatPanel
|
||||
projectId={projectId}
|
||||
@@ -1843,14 +1854,20 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
onRetryContent={retryConversation}
|
||||
railHeader={railHeader}
|
||||
railOpen={railVisible}
|
||||
mobileRail={mobile}
|
||||
onCloseMobileRail={() => setMobileRailOpen(false)}
|
||||
contentInert={mobile && panelOpen && mainView === "chat"}
|
||||
onShowRail={() => {
|
||||
focusAfterRailChange.current = "rail";
|
||||
setRailOpen(true);
|
||||
if (railTemporarilyHidden) closePanel();
|
||||
if (mobile) setMobileRailOpen(true);
|
||||
else {
|
||||
focusAfterRailChange.current = "rail";
|
||||
setRailOpen(true);
|
||||
if (railTemporarilyHidden) closePanel();
|
||||
}
|
||||
}}
|
||||
onNewProject={() => setNewProjectOpen(true)}
|
||||
onNewProject={() => { setMobileRailOpen(false); setNewProjectOpen(true); }}
|
||||
mainView={mainView}
|
||||
onSelectMainView={selectMainView}
|
||||
onSelectMainView={(view) => { setMobileRailOpen(false); selectMainView(view); }}
|
||||
onOpenFile={openChatFile}
|
||||
onOpenRun={openRunLogs}
|
||||
runExperimentName={runExperimentName}
|
||||
@@ -1863,7 +1880,7 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
composerFocusNonce={composerFocusNonce}
|
||||
runtime={runtime}
|
||||
activeSessionId={activeSessionId}
|
||||
onActiveSessionChange={onActiveSessionChange}
|
||||
onActiveSessionChange={(...args) => { setMobileRailOpen(false); onActiveSessionChange(...args); }}
|
||||
preferredAgent={uiState.preferredAgent}
|
||||
onPreferredAgentChange={persistPreferredAgent}
|
||||
preferredAutonomy={uiState.preferredAutonomy ?? DEFAULT_AUTONOMY}
|
||||
@@ -1891,7 +1908,7 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
runs={runs}
|
||||
onOpenExperiment={(id, runId) => openExperimentTab(id, "overview", "preview", runId)}
|
||||
rightOffset={panelOpen && !stackPanel ? visiblePanelWidth + 14 : undefined}
|
||||
activeView={panelOpen && (rightTab === "files" || rightTab === "artifacts" || rightTab === "experiments" || rightTab === "terminal") ? rightTab : null}
|
||||
activeView={activeWorkspaceView}
|
||||
projectId={activeProject.id}
|
||||
onCompute={() => selectMainView("compute")}
|
||||
sessionId={activeSessionId}
|
||||
@@ -1909,7 +1926,7 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
{mainView === "chat" && panelOpen && (
|
||||
<aside
|
||||
className={`right-pane relative shrink-0 min-w-0 flex flex-col bg-background [&.max]:fixed [&.max]:inset-0 [&.max]:z-60 [&.max]:border-s-0 overflow-hidden ${stackPanel ? "border-t border-t-border" : "border-s border-s-border"} ${panelMax ? "max" : ""}`}
|
||||
style={panelMax ? undefined : stackPanel ? { width: "100%", height: "50%" } : { width: visiblePanelWidth }}
|
||||
style={mobile || panelMax ? undefined : stackPanel ? { width: "100%", height: "50%" } : { width: visiblePanelWidth }}
|
||||
data-onboarding="experiments"
|
||||
>
|
||||
{!stackPanel && <div
|
||||
|
||||
@@ -449,3 +449,58 @@
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
html, body, #root { height: 100dvh; }
|
||||
.app { padding-top: env(safe-area-inset-top); }
|
||||
.workspace-body.has-workspace-tools { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }
|
||||
.workspace-body > .chat-pane .chat-header { padding-inline: 8px; }
|
||||
.chat-heading-row { grid-template-columns: 44px minmax(0, 1fr) 0; }
|
||||
.chat-heading-row > span > .icon-btn, .mac-titlebar-spacer > span > .icon-btn { width: 44px; height: 44px; }
|
||||
.workspace-tools { display: none; }
|
||||
.workspace-mobile-nav {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
z-index: 65;
|
||||
min-height: 64px;
|
||||
padding: 4px 8px calc(4px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--base);
|
||||
}
|
||||
.workspace-mobile-nav > * { flex: 1 1 0; min-width: 0; }
|
||||
.workspace-mobile-nav > button, .workspace-mobile-nav > div > button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-height: 56px;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--subtext);
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.workspace-mobile-nav button span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
|
||||
.workspace-mobile-nav button:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
|
||||
.workspace-mobile-nav button[aria-pressed="true"] { background: var(--surface); color: var(--text); }
|
||||
.mobile-rail-dialog { width: min(320px, calc(100vw - 40px)); padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); background: var(--base); }
|
||||
.mobile-rail-dialog .session-rail { width: 100%; height: 100%; }
|
||||
.session-rail .rail-nav-item { min-height: 44px; }
|
||||
.workspace-body .right-pane, .workspace-body .right-pane.max {
|
||||
position: fixed;
|
||||
inset: env(safe-area-inset-top) 0 calc(64px + env(safe-area-inset-bottom));
|
||||
width: 100%;
|
||||
border: 0;
|
||||
z-index: 60;
|
||||
}
|
||||
.right-pane .panel-resizer, .right-pane .panel-controls > :first-child { display: none; }
|
||||
.right-pane > .tabs { min-height: 52px; }
|
||||
.right-pane .panel-controls > span > button { width: 44px; height: 44px; }
|
||||
.chat-empty { padding: 20px 16px; overflow-y: auto; }
|
||||
.chat-empty h2 { font-size: 28px; line-height: 1.2; }
|
||||
.chat-thread, .settings-view-scroll, .home { scrollbar-gutter: auto; }
|
||||
.composer-actions { flex-wrap: wrap; }
|
||||
input, textarea, select { font-size: 16px; }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Dialog } from "@base-ui/react/dialog";
|
||||
import { ComposerProjectPicker } from "./ComposerProjectPicker";
|
||||
import { ProjectInfoCard } from "./ProjectInfoCard";
|
||||
import { SessionHoverCard } from "./SessionHoverCard";
|
||||
@@ -4419,6 +4420,7 @@ function SessionRow({
|
||||
anchor={anchor}
|
||||
cardId={cardId}
|
||||
cardRef={cardRef}
|
||||
container={ref.current?.closest(".mobile-rail-dialog") ?? document.body}
|
||||
onEnter={() => { pointerInside.current = true; window.clearTimeout(closeTimer.current); }}
|
||||
onLeave={() => { cardWarmUntil.current = Date.now() + 500; pointerInside.current = false; scheduleClose(); }}
|
||||
onOpenExperiment={(projectId, experimentId) => { closeCard(); onOpenExperiment(projectId, experimentId); }}
|
||||
@@ -4534,6 +4536,9 @@ export function ChatPanel({
|
||||
onRetryContent,
|
||||
railHeader,
|
||||
railOpen,
|
||||
mobileRail = false,
|
||||
onCloseMobileRail,
|
||||
contentInert = false,
|
||||
onShowRail,
|
||||
onNewProject,
|
||||
mainView,
|
||||
@@ -4569,6 +4574,9 @@ export function ChatPanel({
|
||||
railHeader?: React.ReactNode;
|
||||
/** Whether the agents rail is showing (collapsed via its own header icon). */
|
||||
railOpen: boolean;
|
||||
mobileRail?: boolean;
|
||||
onCloseMobileRail?: () => void;
|
||||
contentInert?: boolean;
|
||||
/** Reopen the rail (from the chat header's sidebar icon). */
|
||||
onShowRail: () => void;
|
||||
onNewProject?: () => void;
|
||||
@@ -4671,15 +4679,15 @@ export function ChatPanel({
|
||||
const [focusedSidebarRow, setFocusedSidebarRow] = useState<string | null>(null);
|
||||
const [chatLimits, setChatLimits] = useState<Record<string, number>>({});
|
||||
const sidebarExpanded = sidebarExpandedAll || Object.keys(chatLimits).length > 0;
|
||||
useLayoutEffect(() => {
|
||||
const body = railBodyRef.current;
|
||||
const setRailBody = useCallback((body: HTMLDivElement | null) => {
|
||||
railBodyRef.current = body;
|
||||
if (!body || embedded) return;
|
||||
const resize = () => setRailHeight(body.clientHeight);
|
||||
const observer = new ResizeObserver(resize);
|
||||
observer.observe(body);
|
||||
resize();
|
||||
return () => observer.disconnect();
|
||||
}, [railOpen, embedded, mainView]);
|
||||
return () => { observer.disconnect(); railBodyRef.current = null; };
|
||||
}, [embedded]);
|
||||
const recentActivity = new Map(projectActivity.map((activity) => [activity.projectId, activity.lastActivityAt]));
|
||||
const sortedProjects = [...sidebarProjects].sort((a, b) => Number(pinnedProjects.includes(b.id)) - Number(pinnedProjects.includes(a.id)) || Math.max(recentActivity.get(b.id) ?? 0, b.updatedAt) - Math.max(recentActivity.get(a.id) ?? 0, a.updatedAt));
|
||||
const candidateLimit = sidebarGrouping === "list" || sidebarExpanded ? sortedProjects.length : Math.max(1, Math.ceil(railHeight / 36));
|
||||
@@ -6765,7 +6773,7 @@ export function ChatPanel({
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex-1 min-h-0">
|
||||
<div ref={railBodyRef} tabIndex={-1} className={`rail-body h-full overflow-x-hidden ${sidebarScrollable ? "overflow-y-auto" : "overflow-y-hidden"}`}>
|
||||
<div ref={setRailBody} tabIndex={-1} className={`rail-body h-full overflow-x-hidden ${sidebarScrollable ? "overflow-y-auto" : "overflow-y-hidden"}`}>
|
||||
<div className="relative" style={{ height: (sidebarScrollable ? sidebarVirtualizer.getTotalSize() : defaultRowsHeight) + (showProjectMore ? 38 : 0) + (sidebarExpandedAll ? 46 : 0) }}>
|
||||
{(sidebarScrollable
|
||||
? sidebarVirtualItems.map((item) => ({ row: sidebarRows[item.index], top: item.start, index: item.index }))
|
||||
@@ -6902,10 +6910,19 @@ export function ChatPanel({
|
||||
);
|
||||
|
||||
const headerClass = `chat-header flex items-center gap-2 py-0 px-4 bg-background shrink-0 h-12 mac-titlebar:h-titlebar relative z-4 w-full max-w-readable my-0 mx-auto [&::after]:content-[''] [&::after]:absolute [&::after]:top-full [&::after]:start-0 [&::after]:end-0 [&::after]:h-6 [&::after]:bg-[linear-gradient(to_bottom,_var(--base),_transparent)] [&::after]:pointer-events-none`;
|
||||
const railReopen = !railOpen && !embedded && (
|
||||
const displayedRail = mobileRail ? (
|
||||
<Dialog.Root open={railOpen} onOpenChange={(open) => { if (!open) onCloseMobileRail?.(); }}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Backdrop className="fixed inset-0 z-70 bg-modal-backdrop" />
|
||||
<Dialog.Popup aria-modal="true" aria-label={m.chat_panel_show_sidebar()} className="mobile-rail-dialog fixed inset-y-0 start-0 z-80 outline-none">{rail}</Dialog.Popup>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
) : railOpen ? rail : null;
|
||||
const railReopen = (!railOpen || mobileRail) && !embedded && (
|
||||
<Tooltip interactive content={m.chat_panel_show_sidebar()} className="rounded-md mac-titlebar:invisible">
|
||||
<IconButton
|
||||
aria-label={m.chat_panel_show_sidebar()}
|
||||
aria-expanded={railOpen}
|
||||
onClick={onShowRail}
|
||||
className="mac-titlebar:ms-titlebar-inset"
|
||||
>
|
||||
@@ -6917,9 +6934,9 @@ export function ChatPanel({
|
||||
if (mainView !== "chat") {
|
||||
return (
|
||||
<>
|
||||
{railOpen && rail}
|
||||
<section className="chat-pane flex-1 min-w-0 flex flex-col bg-background min-h-0">
|
||||
{!railOpen && <div className="mac-titlebar-spacer flex h-12 mac-titlebar:h-titlebar shrink-0 items-center">{railReopen}</div>}
|
||||
{displayedRail}
|
||||
<section inert={contentInert} className="chat-pane flex-1 min-w-0 flex flex-col bg-background min-h-0">
|
||||
{(!railOpen || mobileRail) && <div className="mac-titlebar-spacer flex h-12 mac-titlebar:h-titlebar shrink-0 items-center">{railReopen}</div>}
|
||||
{/* Keyed per view so no tab inherits the chat thread's or another tab's scroll offset. */}
|
||||
<div key={mainView} className="settings-view-scroll flex-1 min-h-0 overflow-y-auto [scrollbar-gutter:stable_both-edges]">
|
||||
{contentError ? <div role="alert" className="p-6 text-subtext">{contentError}<Button onClick={onRetryContent}>{m.app_retry()}</Button></div>
|
||||
@@ -6932,11 +6949,11 @@ export function ChatPanel({
|
||||
|
||||
return (
|
||||
<>
|
||||
{railOpen && rail}
|
||||
<section data-side-chat={embedded || undefined} className="chat-pane flex-1 min-w-0 flex flex-col bg-background min-h-0">
|
||||
{displayedRail}
|
||||
<section inert={contentInert} data-side-chat={embedded || undefined} className="chat-pane flex-1 min-w-0 flex flex-col bg-background min-h-0">
|
||||
{/* Header — session title on the left, end-pane view switchers on the
|
||||
right, fading into the chat below (sessions live in the rail). */}
|
||||
<div className={railOpen || embedded ? "contents" : "grid shrink-0 grid-cols-[2rem_minmax(0,1fr)_2rem] mac-titlebar:grid-cols-titlebar-chat items-center"}>
|
||||
<div className={railOpen && !mobileRail || embedded ? "contents" : "chat-heading-row grid shrink-0 grid-cols-[2rem_minmax(0,1fr)_2rem] mac-titlebar:grid-cols-titlebar-chat items-center"}>
|
||||
{railReopen}
|
||||
<div className={headerClass}>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
|
||||
@@ -70,7 +70,7 @@ function ExperimentRow({ row, sessionProjectId, tabbable, onFocus, onOpen }: {
|
||||
);
|
||||
}
|
||||
|
||||
export function SessionHoverCard({ session, modelName, projectName, harnessLogo, anchor, cardId, cardRef, onEnter, onLeave, onOpenExperiment, onReturnToRow, onContinue }: {
|
||||
export function SessionHoverCard({ session, modelName, projectName, harnessLogo, anchor, cardId, cardRef, container, onEnter, onLeave, onOpenExperiment, onReturnToRow, onContinue }: {
|
||||
session: ChatSession;
|
||||
modelName: string | null;
|
||||
projectName?: string;
|
||||
@@ -78,6 +78,7 @@ export function SessionHoverCard({ session, modelName, projectName, harnessLogo,
|
||||
anchor: DOMRect;
|
||||
cardId: string;
|
||||
cardRef: React.RefObject<HTMLDivElement | null>;
|
||||
container: Element;
|
||||
onEnter: () => void;
|
||||
onLeave: () => void;
|
||||
onOpenExperiment: (projectId: string, experimentId: string) => void;
|
||||
@@ -190,6 +191,6 @@ export function SessionHoverCard({ session, modelName, projectName, harnessLogo,
|
||||
</div>
|
||||
</div>}
|
||||
</div>,
|
||||
document.body,
|
||||
container,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,16 @@ export function WorkspaceTools({ expanded, experiments, runs, onOpenExperiment,
|
||||
{ id: "terminal", label: m.workspace_terminal(), Icon: Terminal, onClick: onTerminal },
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<nav aria-label={m.workspace_tools_heading()} className="workspace-mobile-nav hidden">
|
||||
{items.map(({ id, label, Icon, onClick }) => (
|
||||
<button key={id} type="button" aria-pressed={activeView === id} onClick={onClick}>
|
||||
<Icon size={20} aria-hidden="true" />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
))}
|
||||
{onSideChat && sessionId && <SideChatPicker parentId={sessionId} sessions={sessions} onSelect={onSelectSideChat} onCreate={onSideChat} mobile />}
|
||||
</nav>
|
||||
<div className="workspace-tools absolute end-3.5 top-2 mac-titlebar:top-1.5 z-30" style={{ insetInlineEnd: rightOffset }}>
|
||||
{!expanded ? (
|
||||
<nav aria-label={m.workspace_tools_heading()} className="flex items-center justify-end gap-1">
|
||||
@@ -103,11 +113,13 @@ export function WorkspaceTools({ expanded, experiments, runs, onOpenExperiment,
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SideChatPicker({ parentId, sessions, onSelect, onCreate, expanded = false }: {
|
||||
function SideChatPicker({ parentId, sessions, onSelect, onCreate, expanded = false, mobile = false }: {
|
||||
expanded?: boolean;
|
||||
mobile?: boolean;
|
||||
parentId: string;
|
||||
sessions: ChatSession[];
|
||||
onSelect: (sessionId: string) => void;
|
||||
@@ -127,7 +139,11 @@ function SideChatPicker({ parentId, sessions, onSelect, onCreate, expanded = fal
|
||||
useLayoutEffect(() => {
|
||||
if (!open) return;
|
||||
const position = () => {
|
||||
if (panelRef.current) panelRef.current.style.maxHeight = `${Math.max(0, Math.min(384, window.innerHeight - panelRef.current.getBoundingClientRect().top - 8))}px`;
|
||||
if (!panelRef.current) return;
|
||||
const available = mobile
|
||||
? (triggerRef.current?.getBoundingClientRect().top ?? 0) - 12
|
||||
: window.innerHeight - panelRef.current.getBoundingClientRect().top - 8;
|
||||
panelRef.current.style.maxHeight = `${Math.max(0, Math.min(384, available))}px`;
|
||||
};
|
||||
position();
|
||||
window.addEventListener("resize", position);
|
||||
@@ -136,7 +152,7 @@ function SideChatPicker({ parentId, sessions, onSelect, onCreate, expanded = fal
|
||||
window.removeEventListener("resize", position);
|
||||
window.removeEventListener("scroll", position, true);
|
||||
};
|
||||
}, [open]);
|
||||
}, [open, mobile]);
|
||||
|
||||
const openPicker = () => {
|
||||
clearTimeout(closeTimer.current);
|
||||
@@ -183,7 +199,12 @@ function SideChatPicker({ parentId, sessions, onSelect, onCreate, expanded = fal
|
||||
onBlur={(event) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
|
||||
}}>
|
||||
{expanded ? (
|
||||
{mobile ? (
|
||||
<button {...triggerProps} className="w-full">
|
||||
<MessagesSquare size={20} aria-hidden="true" />
|
||||
<span>{m.side_chat_open()}</span>
|
||||
</button>
|
||||
) : expanded ? (
|
||||
<MenuItem {...triggerProps} active={open} className="min-h-7 py-1 text-text">
|
||||
<span className="flex items-center gap-4"><MessagesSquare size={16} />{m.side_chat_open()}</span>
|
||||
</MenuItem>
|
||||
@@ -194,7 +215,7 @@ function SideChatPicker({ parentId, sessions, onSelect, onCreate, expanded = fal
|
||||
)}
|
||||
{open && (
|
||||
<div ref={panelRef} role="dialog" aria-label={m.side_chat_list({ count: children.length })}
|
||||
className="absolute end-0 top-[calc(100%_+_4px)] z-50 flex w-80 max-w-[calc(100vw-1rem)] flex-col overflow-hidden rounded-lg border border-border bg-background p-1.5 shadow-menu">
|
||||
className={`absolute end-0 ${mobile ? "bottom-[calc(100%_+_4px)]" : "top-[calc(100%_+_4px)]"} z-50 flex w-80 max-w-[calc(100vw-1rem)] flex-col overflow-hidden rounded-lg border border-border bg-background p-1.5 shadow-menu`}>
|
||||
<div className="flex shrink-0 items-center justify-between gap-2 px-2">
|
||||
<span className="text-sm font-medium text-text">{m.side_chat_list({ count: children.length })}</span>
|
||||
{children.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user