1
0
Fork 0

Full tab chat on mobile

This commit is contained in:
Pabloader 2026-04-09 14:03:41 +00:00
parent ffaa137d08
commit 3d94a298b8
4 changed files with 37 additions and 10 deletions

View File

@ -0,0 +1,14 @@
import { useEffect, useState } from "preact/hooks";
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
useEffect(() => {
const mq = window.matchMedia(query);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, [query]);
return matches;
}

View File

@ -0,0 +1 @@
export const MOBILE_BREAKPOINT = 1000;

View File

@ -1,9 +1,11 @@
import { ContentEditable } from "@common/components/ContentEditable"; import { ContentEditable } from "@common/components/ContentEditable";
import { highlight } from "@common/highlight"; import { highlight } from "@common/highlight";
import { useInputState } from "@common/hooks/useInputState"; import { useInputState } from "@common/hooks/useInputState";
import { useMediaQuery } from "@common/hooks/useMediaQuery";
import clsx from "clsx"; import clsx from "clsx";
import { Check, ChevronsRight, Edit2, GitFork, RefreshCw, Sparkles, Trash2, X } from "lucide-preact"; import { Check, ChevronsRight, Edit2, GitFork, RefreshCw, Sparkles, Trash2, X } from "lucide-preact";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"; import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { MOBILE_BREAKPOINT } from '../assets/breakpoints';
import styles from '../assets/chat-sidebar.module.css'; import styles from '../assets/chat-sidebar.module.css';
import sidebarStyles from '../assets/sidebar.module.css'; import sidebarStyles from '../assets/sidebar.module.css';
import { useAppState, type ChatMessage } from "../contexts/state"; import { useAppState, type ChatMessage } from "../contexts/state";
@ -635,9 +637,10 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
export const ChatSidebar = () => { export const ChatSidebar = () => {
const { currentWorld, chatOpen, dispatch } = useAppState(); const { currentWorld, chatOpen, dispatch } = useAppState();
const isMobile = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT}px)`);
// In chat-only worlds, chat is a full editor tab — no sidebar needed // In chat-only worlds or on mobile, chat is a full editor tab — no sidebar needed
if (currentWorld?.chatOnly) return null; if (currentWorld?.chatOnly || isMobile) return null;
return ( return (
<Sidebar <Sidebar

View File

@ -1,7 +1,9 @@
import { highlight } from "@common/highlight"; import { highlight } from "@common/highlight";
import { useMediaQuery } from "@common/hooks/useMediaQuery";
import clsx from "clsx"; import clsx from "clsx";
import { BookMarked, BookOpen, BrainCircuit, Code, FileText, Globe, Layers, List, MapPin, MessageSquare, MessagesSquare, Users, type LucideIcon } from "lucide-preact"; import { BookMarked, BookOpen, BrainCircuit, Code, FileText, Globe, Layers, List, MapPin, MessageSquare, MessagesSquare, Users, type LucideIcon } from "lucide-preact";
import { useEffect, useMemo, useRef } from "preact/hooks"; import { useEffect, useMemo, useRef } from "preact/hooks";
import { MOBILE_BREAKPOINT } from '../assets/breakpoints';
import styles from '../assets/editor.module.css'; import styles from '../assets/editor.module.css';
import { useAppState, type Tab } from "../contexts/state"; import { useAppState, type Tab } from "../contexts/state";
import Prompt from "../utils/prompt"; import Prompt from "../utils/prompt";
@ -36,10 +38,11 @@ const WORLD_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[
{ id: "system", label: "System", icon: BrainCircuit }, { id: "system", label: "System", icon: BrainCircuit },
]; ];
const CHAT_TAB = { id: "chat", label: "Chat", icon: MessageSquare } as const;
// Tabs for a chat session within a chat-only world // Tabs for a chat session within a chat-only world
const CHAT_STORY_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [ const CHAT_STORY_TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [
{ id: "menu", label: "Menu", icon: List }, { id: "menu", label: "Menu", icon: List },
{ id: "chat", label: "Chat", icon: MessageSquare }, CHAT_TAB,
{ id: "scratchpad", label: "Scratchpad", icon: FileText, right: true }, { id: "scratchpad", label: "Scratchpad", icon: FileText, right: true },
{ id: "prompt", label: "Prompt", icon: Code }, { id: "prompt", label: "Prompt", icon: Code },
]; ];
@ -88,14 +91,20 @@ export const Editor = () => {
return () => cancelAnimationFrame(raf); return () => cancelAnimationFrame(raf);
}, [currentStory?.id, currentWorld?.id, currentTab]); }, [currentStory?.id, currentWorld?.id, currentTab]);
const isMobile = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT}px)`);
const hasSelection = currentWorld !== null; const hasSelection = currentWorld !== null;
const isChatOnly = currentWorld?.chatOnly ?? false; const isChatOnly = currentWorld?.chatOnly ?? false;
const tabs = currentStory const tabs = useMemo(() => {
? (isChatOnly ? CHAT_STORY_TABS : STORY_TABS) if (currentStory) {
: currentWorld if (isChatOnly) return CHAT_STORY_TABS;
? (isChatOnly ? CHAT_WORLD_TABS : WORLD_TABS) if (isMobile) return [...STORY_TABS, { ...CHAT_TAB, right: true }];
: [{ id: "menu" as Tab, label: "Menu", icon: List }]; return STORY_TABS;
}
if (currentWorld) return isChatOnly ? CHAT_WORLD_TABS : WORLD_TABS;
return [{ id: "menu" as Tab, label: "Menu", icon: List }];
}, [currentStory, currentWorld, isChatOnly, isMobile]);
// Title bar: use MessagesSquare icon for chat-only worlds // Title bar: use MessagesSquare icon for chat-only worlds
const WorldIcon = isChatOnly ? MessagesSquare : Globe; const WorldIcon = isChatOnly ? MessagesSquare : Globe;
@ -124,7 +133,7 @@ export const Editor = () => {
class={clsx(styles.promptPreview, currentTab !== "prompt" && styles.tabHidden)} class={clsx(styles.promptPreview, currentTab !== "prompt" && styles.tabHidden)}
dangerouslySetInnerHTML={{ __html: Prompt.substituteVars(appState, promptPreview) }} dangerouslySetInnerHTML={{ __html: Prompt.substituteVars(appState, promptPreview) }}
/> />
{isChatOnly && <ChatPanel visible={currentTab === "chat"} />} {(isChatOnly || isMobile) && <ChatPanel visible={currentTab === "chat"} />}
</>)} </>)}
{(currentStory || currentWorld) && (<> {(currentStory || currentWorld) && (<>
<LoreEditor visible={currentTab === "lore"} /> <LoreEditor visible={currentTab === "lore"} />
@ -145,7 +154,7 @@ export const Editor = () => {
<span class={styles.tabLabel}>{tab.label}</span> <span class={styles.tabLabel}>{tab.label}</span>
</button> </button>
))} ))}
{currentStory && !isChatOnly && ( {currentStory && !isChatOnly && !isMobile && (
<button <button
class={clsx(styles.tab, styles.tabRight, chatOpen && styles.active)} class={clsx(styles.tab, styles.tabRight, chatOpen && styles.active)}
onClick={() => dispatch({ type: 'SET_CHAT_OPEN', open: !chatOpen })} onClick={() => dispatch({ type: 'SET_CHAT_OPEN', open: !chatOpen })}