增加分栏后可以全屏和还原的功能

This commit is contained in:
allen2fuc
2026-05-13 17:46:05 +08:00
parent b3d3f0edcb
commit db4d8e5090
6 changed files with 67 additions and 11 deletions
+14 -4
View File
@@ -8,7 +8,7 @@
============================================================ */ ============================================================ */
import { useDevice } from "@/contexts/DeviceContext"; import { useDevice } from "@/contexts/DeviceContext";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useAIDrawer } from "@/contexts/AIDrawerContext"; import { readAndClearAiSplitRestorePath, useAIDrawer } from "@/contexts/AIDrawerContext";
import { useIsLgUp } from "@/hooks/useIsLgUp"; import { useIsLgUp } from "@/hooks/useIsLgUp";
import localeZh from "@/locales/data-zh.json"; import localeZh from "@/locales/data-zh.json";
import localeZhHK from "@/locales/data-zh-HK.json"; import localeZhHK from "@/locales/data-zh-HK.json";
@@ -42,7 +42,7 @@ const NAV_ITEMS: NavItem[] = [
]; ];
export default function BottomNav() { export default function BottomNav() {
const [location] = useLocation(); const [location, setLocation] = useLocation();
const isLgUp = useIsLgUp(); const isLgUp = useIsLgUp();
const { open: aiDrawerOpen, setOpen: setAiDrawerOpen } = useAIDrawer(); const { open: aiDrawerOpen, setOpen: setAiDrawerOpen } = useAIDrawer();
const { deviceState } = useDevice(); const { deviceState } = useDevice();
@@ -160,13 +160,23 @@ export default function BottomNav() {
tabIndex={0} tabIndex={0}
className="cursor-pointer select-none" className="cursor-pointer select-none"
onClick={() => { onClick={() => {
if (location === "/ai") return; if (location === "/ai") {
const path = readAndClearAiSplitRestorePath();
setLocation(path);
setAiDrawerOpen(true);
return;
}
setAiDrawerOpen((o) => !o); setAiDrawerOpen((o) => !o);
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key !== "Enter" && e.key !== " ") return; if (e.key !== "Enter" && e.key !== " ") return;
e.preventDefault(); e.preventDefault();
if (location === "/ai") return; if (location === "/ai") {
const path = readAndClearAiSplitRestorePath();
setLocation(path);
setAiDrawerOpen(true);
return;
}
setAiDrawerOpen((o) => !o); setAiDrawerOpen((o) => !o);
}} }}
> >
+14
View File
@@ -10,6 +10,20 @@ import {
} from "react"; } from "react";
const SPLIT_STORAGE_KEY = "luxsin-ai-split-pct"; 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_MIN = 28;
const SPLIT_MAX = 72; const SPLIT_MAX = 72;
+1
View File
@@ -609,6 +609,7 @@
"ariaClose": "Close", "ariaClose": "Close",
"ariaFullPage": "Open full screen", "ariaFullPage": "Open full screen",
"ariaHidePanel": "Hide AI panel", "ariaHidePanel": "Hide AI panel",
"ariaRestoreSplit": "Restore split view",
"clearCurrent": "Clear this chat", "clearCurrent": "Clear this chat",
"cleared": "Current chat cleared", "cleared": "Current chat cleared",
"clearFailed": "Clear failed: {message}", "clearFailed": "Clear failed: {message}",
+1
View File
@@ -425,6 +425,7 @@
"ariaClose": "關閉", "ariaClose": "關閉",
"ariaFullPage": "全螢幕開啟", "ariaFullPage": "全螢幕開啟",
"ariaHidePanel": "收起 AI 面板", "ariaHidePanel": "收起 AI 面板",
"ariaRestoreSplit": "還原分欄",
"clearCurrent": "清空目前會話", "clearCurrent": "清空目前會話",
"cleared": "已清空目前會話", "cleared": "已清空目前會話",
"clearFailed": "清空失敗:{message}", "clearFailed": "清空失敗:{message}",
+1
View File
@@ -426,6 +426,7 @@
"ariaClose": "关闭", "ariaClose": "关闭",
"ariaFullPage": "全屏打开", "ariaFullPage": "全屏打开",
"ariaHidePanel": "收起 AI 面板", "ariaHidePanel": "收起 AI 面板",
"ariaRestoreSplit": "还原分栏",
"clearCurrent": "清空当前会话", "clearCurrent": "清空当前会话",
"cleared": "已清空当前会话", "cleared": "已清空当前会话",
"clearFailed": "清空失败:{message}", "clearFailed": "清空失败:{message}",
+36 -7
View File
@@ -9,6 +9,12 @@
import BottomNav from "@/components/BottomNav"; import BottomNav from "@/components/BottomNav";
import ConnectScreen from "@/components/ConnectScreen"; import ConnectScreen from "@/components/ConnectScreen";
import { useDevice } from "@/contexts/DeviceContext"; import { useDevice } from "@/contexts/DeviceContext";
import {
AI_SPLIT_RESTORE_PATH_KEY,
readAndClearAiSplitRestorePath,
useAIDrawer,
} from "@/contexts/AIDrawerContext";
import { useIsLgUp } from "@/hooks/useIsLgUp";
import { import {
ChatRead, ChatRead,
MessageRead, MessageRead,
@@ -45,6 +51,7 @@ import {
Trash2, Trash2,
Maximize2, Maximize2,
ChevronsRight, ChevronsRight,
ChevronsLeft,
} from "lucide-react"; } from "lucide-react";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -165,12 +172,10 @@ function moveLastOptimizeAfterAssistantReply(items: UIMessage[]): UIMessage[] {
} }
let endAsst = firstAsst; let endAsst = firstAsst;
while ( while (endAsst + 1 < items.length) {
endAsst + 1 < items.length && const m = items[endAsst + 1];
items[endAsst + 1].kind === "chat" && if (m.kind === "chat" && m.role === "assistant") endAsst++;
items[endAsst + 1].role === "assistant" else break;
) {
endAsst++;
} }
if (lastOptIdx === endAsst + 1) return items; if (lastOptIdx === endAsst + 1) return items;
@@ -199,7 +204,9 @@ export interface AIPageProps {
export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) { export default function AIPage({ variant = "page", onClose }: AIPageProps = {}) {
const isEmbedded = variant === "split"; const isEmbedded = variant === "split";
const [, setLocation] = useLocation(); const [routePath, setLocation] = useLocation();
const isLgUp = useIsLgUp();
const { setOpen: setAiPanelOpen } = useAIDrawer();
const { isConnected, deviceState, api } = useDevice(); const { isConnected, deviceState, api } = useDevice();
const mac = deviceState?.mac ?? ""; const mac = deviceState?.mac ?? "";
const language = deviceState?.language ?? 2; const language = deviceState?.language ?? 2;
@@ -207,6 +214,12 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {})
const aiText = useMemo(() => resolveAILocale(language), [language]); const aiText = useMemo(() => resolveAILocale(language), [language]);
const restoreDesktopSplit = useCallback(() => {
const path = readAndClearAiSplitRestorePath();
setLocation(path);
setAiPanelOpen(true);
}, [setLocation, setAiPanelOpen]);
const [chatId, setChatId] = useState<string | null>(null); const [chatId, setChatId] = useState<string | null>(null);
const [messages, setMessages] = useState<UIMessage[]>([]); const [messages, setMessages] = useState<UIMessage[]>([]);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
@@ -692,6 +705,17 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {})
{/* ── Header ── */} {/* ── Header ── */}
<div className="page-header flex-shrink-0"> <div className="page-header flex-shrink-0">
<div className="flex items-center gap-2 mr-auto"> <div className="flex items-center gap-2 mr-auto">
{!isEmbedded && isLgUp && (
<button
type="button"
onClick={restoreDesktopSplit}
className="mr-0.5 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full text-white/70 transition-colors active:text-white"
style={{ background: "rgba(44,44,46,0.6)" }}
aria-label={aiText.ariaRestoreSplit}
>
<ChevronsLeft size={18} />
</button>
)}
{isEmbedded && onClose && ( {isEmbedded && onClose && (
<> <>
<button <button
@@ -706,6 +730,11 @@ export default function AIPage({ variant = "page", onClose }: AIPageProps = {})
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
try {
sessionStorage.setItem(AI_SPLIT_RESTORE_PATH_KEY, routePath);
} catch {
/* ignore */
}
setLocation("/ai"); setLocation("/ai");
onClose(); onClose();
}} }}