diff --git a/client/src/components/BottomNav.tsx b/client/src/components/BottomNav.tsx index 32de789..36d5333 100644 --- a/client/src/components/BottomNav.tsx +++ b/client/src/components/BottomNav.tsx @@ -8,7 +8,7 @@ ============================================================ */ import { useDevice } from "@/contexts/DeviceContext"; import { cn } from "@/lib/utils"; -import { useAIDrawer } from "@/contexts/AIDrawerContext"; +import { readAndClearAiSplitRestorePath, useAIDrawer } from "@/contexts/AIDrawerContext"; import { useIsLgUp } from "@/hooks/useIsLgUp"; import localeZh from "@/locales/data-zh.json"; import localeZhHK from "@/locales/data-zh-HK.json"; @@ -42,7 +42,7 @@ const NAV_ITEMS: NavItem[] = [ ]; export default function BottomNav() { - const [location] = useLocation(); + const [location, setLocation] = useLocation(); const isLgUp = useIsLgUp(); const { open: aiDrawerOpen, setOpen: setAiDrawerOpen } = useAIDrawer(); const { deviceState } = useDevice(); @@ -160,13 +160,23 @@ export default function BottomNav() { tabIndex={0} className="cursor-pointer select-none" onClick={() => { - if (location === "/ai") return; + if (location === "/ai") { + const path = readAndClearAiSplitRestorePath(); + setLocation(path); + setAiDrawerOpen(true); + return; + } setAiDrawerOpen((o) => !o); }} onKeyDown={(e) => { if (e.key !== "Enter" && e.key !== " ") return; e.preventDefault(); - if (location === "/ai") return; + if (location === "/ai") { + const path = readAndClearAiSplitRestorePath(); + setLocation(path); + setAiDrawerOpen(true); + return; + } setAiDrawerOpen((o) => !o); }} > diff --git a/client/src/contexts/AIDrawerContext.tsx b/client/src/contexts/AIDrawerContext.tsx index 1ef8ffa..d0d416b 100644 --- a/client/src/contexts/AIDrawerContext.tsx +++ b/client/src/contexts/AIDrawerContext.tsx @@ -10,6 +10,20 @@ import { } from "react"; const SPLIT_STORAGE_KEY = "luxsin-ai-split-pct"; + +/** sessionStorage: left-pane route before jumping from split to full-screen `#/ai` (desktop). */ +export const AI_SPLIT_RESTORE_PATH_KEY = "luxsin_ai_restore_path"; + +export function readAndClearAiSplitRestorePath(): string { + try { + const s = sessionStorage.getItem(AI_SPLIT_RESTORE_PATH_KEY); + sessionStorage.removeItem(AI_SPLIT_RESTORE_PATH_KEY); + if (s && typeof s === "string" && s.startsWith("/") && !s.includes("..")) return s; + } catch { + /* ignore */ + } + return "/"; +} const SPLIT_MIN = 28; const SPLIT_MAX = 72; diff --git a/client/src/locales/data-en.json b/client/src/locales/data-en.json index 616f76c..08f5b6f 100644 --- a/client/src/locales/data-en.json +++ b/client/src/locales/data-en.json @@ -609,6 +609,7 @@ "ariaClose": "Close", "ariaFullPage": "Open full screen", "ariaHidePanel": "Hide AI panel", + "ariaRestoreSplit": "Restore split view", "clearCurrent": "Clear this chat", "cleared": "Current chat cleared", "clearFailed": "Clear failed: {message}", diff --git a/client/src/locales/data-zh-HK.json b/client/src/locales/data-zh-HK.json index 80665da..ddcb92b 100644 --- a/client/src/locales/data-zh-HK.json +++ b/client/src/locales/data-zh-HK.json @@ -425,6 +425,7 @@ "ariaClose": "關閉", "ariaFullPage": "全螢幕開啟", "ariaHidePanel": "收起 AI 面板", + "ariaRestoreSplit": "還原分欄", "clearCurrent": "清空目前會話", "cleared": "已清空目前會話", "clearFailed": "清空失敗:{message}", diff --git a/client/src/locales/data-zh.json b/client/src/locales/data-zh.json index 0d0e223..02fce64 100644 --- a/client/src/locales/data-zh.json +++ b/client/src/locales/data-zh.json @@ -426,6 +426,7 @@ "ariaClose": "关闭", "ariaFullPage": "全屏打开", "ariaHidePanel": "收起 AI 面板", + "ariaRestoreSplit": "还原分栏", "clearCurrent": "清空当前会话", "cleared": "已清空当前会话", "clearFailed": "清空失败:{message}", diff --git a/client/src/pages/AIPage.tsx b/client/src/pages/AIPage.tsx index 88b7577..04ace33 100644 --- a/client/src/pages/AIPage.tsx +++ b/client/src/pages/AIPage.tsx @@ -9,6 +9,12 @@ import BottomNav from "@/components/BottomNav"; import ConnectScreen from "@/components/ConnectScreen"; import { useDevice } from "@/contexts/DeviceContext"; +import { + AI_SPLIT_RESTORE_PATH_KEY, + readAndClearAiSplitRestorePath, + useAIDrawer, +} from "@/contexts/AIDrawerContext"; +import { useIsLgUp } from "@/hooks/useIsLgUp"; import { ChatRead, MessageRead, @@ -45,6 +51,7 @@ import { Trash2, Maximize2, ChevronsRight, + ChevronsLeft, } from "lucide-react"; import { useLocation } from "wouter"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -165,12 +172,10 @@ function moveLastOptimizeAfterAssistantReply(items: UIMessage[]): UIMessage[] { } let endAsst = firstAsst; - while ( - endAsst + 1 < items.length && - items[endAsst + 1].kind === "chat" && - items[endAsst + 1].role === "assistant" - ) { - endAsst++; + while (endAsst + 1 < items.length) { + const m = items[endAsst + 1]; + if (m.kind === "chat" && m.role === "assistant") endAsst++; + else break; } if (lastOptIdx === endAsst + 1) return items; @@ -199,7 +204,9 @@ export interface AIPageProps { export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) { const isEmbedded = variant === "split"; - const [, setLocation] = useLocation(); + const [routePath, setLocation] = useLocation(); + const isLgUp = useIsLgUp(); + const { setOpen: setAiPanelOpen } = useAIDrawer(); const { isConnected, deviceState, api } = useDevice(); const mac = deviceState?.mac ?? ""; const language = deviceState?.language ?? 2; @@ -207,6 +214,12 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) const aiText = useMemo(() => resolveAILocale(language), [language]); + const restoreDesktopSplit = useCallback(() => { + const path = readAndClearAiSplitRestorePath(); + setLocation(path); + setAiPanelOpen(true); + }, [setLocation, setAiPanelOpen]); + const [chatId, setChatId] = useState(null); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); @@ -692,6 +705,17 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) {/* ── Header ── */}
+ {!isEmbedded && isLgUp && ( + + )} {isEmbedded && onClose && ( <>