增加分栏后可以全屏和还原的功能
This commit is contained in:
@@ -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);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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}",
|
||||||
|
|||||||
@@ -425,6 +425,7 @@
|
|||||||
"ariaClose": "關閉",
|
"ariaClose": "關閉",
|
||||||
"ariaFullPage": "全螢幕開啟",
|
"ariaFullPage": "全螢幕開啟",
|
||||||
"ariaHidePanel": "收起 AI 面板",
|
"ariaHidePanel": "收起 AI 面板",
|
||||||
|
"ariaRestoreSplit": "還原分欄",
|
||||||
"clearCurrent": "清空目前會話",
|
"clearCurrent": "清空目前會話",
|
||||||
"cleared": "已清空目前會話",
|
"cleared": "已清空目前會話",
|
||||||
"clearFailed": "清空失敗:{message}",
|
"clearFailed": "清空失敗:{message}",
|
||||||
|
|||||||
@@ -426,6 +426,7 @@
|
|||||||
"ariaClose": "关闭",
|
"ariaClose": "关闭",
|
||||||
"ariaFullPage": "全屏打开",
|
"ariaFullPage": "全屏打开",
|
||||||
"ariaHidePanel": "收起 AI 面板",
|
"ariaHidePanel": "收起 AI 面板",
|
||||||
|
"ariaRestoreSplit": "还原分栏",
|
||||||
"clearCurrent": "清空当前会话",
|
"clearCurrent": "清空当前会话",
|
||||||
"cleared": "已清空当前会话",
|
"cleared": "已清空当前会话",
|
||||||
"clearFailed": "清空失败:{message}",
|
"clearFailed": "清空失败:{message}",
|
||||||
|
|||||||
@@ -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();
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user