Mobile adaptation
This commit is contained in:
parent
d8b1893739
commit
5453b0513f
|
|
@ -5,6 +5,7 @@
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,13 @@
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabRight {
|
.tabRight {
|
||||||
|
|
@ -81,7 +88,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 2px solid transparent;
|
border-top: 2px solid transparent;
|
||||||
|
|
@ -101,3 +112,21 @@
|
||||||
border-top-color: var(--accent);
|
border-top-color: var(--accent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.tabLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 0 16px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,38 @@
|
||||||
min-width: 32px;
|
min-width: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.closed[data-controlled] {
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleMobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.toggleMobile {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.mobileOverlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100dvh;
|
||||||
|
z-index: 100;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileOverlay.open {
|
||||||
|
width: 100dvw;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.toggle {
|
.toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Sidebar } from "./sidebar";
|
||||||
import { useAppState, type ChatMessage } from "../contexts/state";
|
import { useAppState, type ChatMessage } from "../contexts/state";
|
||||||
import { useChapterSummarization } from "../utils/useChapterSummarization";
|
import { useChapterSummarization } from "../utils/useChapterSummarization";
|
||||||
import styles from '../assets/chat-sidebar.module.css';
|
import styles from '../assets/chat-sidebar.module.css';
|
||||||
|
import sidebarStyles from '../assets/sidebar.module.css';
|
||||||
import { useState, useRef, useEffect, useMemo, useCallback } from "preact/hooks";
|
import { useState, useRef, useEffect, useMemo, useCallback } from "preact/hooks";
|
||||||
import LLM from "../utils/llm";
|
import LLM from "../utils/llm";
|
||||||
import Prompt from "../utils/prompt";
|
import Prompt from "../utils/prompt";
|
||||||
|
|
@ -43,11 +44,10 @@ const RoleHeader = ({ message, chatMessages }: RoleHeaderProps) => {
|
||||||
|
|
||||||
export const ChatSidebar = () => {
|
export const ChatSidebar = () => {
|
||||||
const appState = useAppState();
|
const appState = useAppState();
|
||||||
const { currentStory, dispatch, connection, model, enableThinking } = appState;
|
const { currentStory, dispatch, connection, model, enableThinking, chatOpen } = appState;
|
||||||
const { summarizeAll, isSummarizing } = useChapterSummarization();
|
const { summarizeAll, isSummarizing } = useChapterSummarization();
|
||||||
const [input, setInput] = useInputState('');
|
const [input, setInput] = useInputState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isCollapsed, setCollapsed] = useState(false);
|
|
||||||
const [tokenCount, setTokenCount] = useState<{ taken: number; total: number } | null>(null);
|
const [tokenCount, setTokenCount] = useState<{ taken: number; total: number } | null>(null);
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -76,7 +76,7 @@ export const ChatSidebar = () => {
|
||||||
if (messagesRef.current) {
|
if (messagesRef.current) {
|
||||||
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
|
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
}, [isCollapsed]);
|
}, [chatOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -301,7 +301,7 @@ export const ChatSidebar = () => {
|
||||||
const isDisabled = !currentStory || !connection || !model || isLoading;
|
const isDisabled = !currentStory || !connection || !model || isLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar side="right" onCollapseChanged={setCollapsed}>
|
<Sidebar side="right" open={chatOpen} onToggle={() => dispatch({ type: 'SET_CHAT_OPEN', open: !chatOpen })} class={sidebarStyles.mobileOverlay}>
|
||||||
<div class={styles.chat}>
|
<div class={styles.chat}>
|
||||||
{!currentStory ? (
|
{!currentStory ? (
|
||||||
<div class={styles.placeholder}>
|
<div class={styles.placeholder}>
|
||||||
|
|
|
||||||
|
|
@ -11,21 +11,22 @@ import { LoreEditor } from "./lore-editor";
|
||||||
import { Menu } from "./menu";
|
import { Menu } from "./menu";
|
||||||
import { useInputCallback } from "@common/hooks/useInputCallback";
|
import { useInputCallback } from "@common/hooks/useInputCallback";
|
||||||
import Prompt from "../utils/prompt";
|
import Prompt from "../utils/prompt";
|
||||||
|
import { BookOpen, List, Users, MapPin, BookMarked, FileText, Code, Layers, MessageSquare, type LucideIcon } from "lucide-preact";
|
||||||
|
|
||||||
const TABS: { id: Tab; label: string; right?: boolean }[] = [
|
const TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [
|
||||||
{ id: "menu", label: "Menu" },
|
{ id: "menu", label: "Menu", icon: List },
|
||||||
{ id: "story", label: "Story" },
|
{ id: "story", label: "Story", icon: BookOpen },
|
||||||
{ id: "chapters", label: "Chapters" },
|
{ id: "chapters", label: "Chapters", icon: Layers },
|
||||||
{ id: "lore", label: "Lore" },
|
{ id: "lore", label: "Lore", icon: BookMarked },
|
||||||
{ id: "characters", label: "Characters" },
|
{ id: "characters", label: "Characters", icon: Users },
|
||||||
{ id: "locations", label: "Locations" },
|
{ id: "locations", label: "Locations", icon: MapPin },
|
||||||
{ id: "scratchpad", label: "Scratchpad", right: true },
|
{ id: "scratchpad", label: "Scratchpad", icon: FileText, right: true },
|
||||||
{ id: "prompt", label: "Prompt" },
|
{ id: "prompt", label: "Prompt", icon: Code },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Editor = () => {
|
export const Editor = () => {
|
||||||
const appState = useAppState();
|
const appState = useAppState();
|
||||||
const { currentStory, currentTab, dispatch } = appState;
|
const { currentStory, currentTab, chatOpen, dispatch } = appState;
|
||||||
|
|
||||||
const handleInput = useInputCallback((text: string) => {
|
const handleInput = useInputCallback((text: string) => {
|
||||||
if (!currentStory) return;
|
if (!currentStory) return;
|
||||||
|
|
@ -135,10 +136,20 @@ export const Editor = () => {
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
class={clsx(styles.tab, currentTab === tab.id && styles.active, tab.right && styles.tabRight)}
|
class={clsx(styles.tab, currentTab === tab.id && styles.active, tab.right && styles.tabRight)}
|
||||||
onClick={() => handleTabChange(tab.id)}
|
onClick={() => handleTabChange(tab.id)}
|
||||||
|
title={tab.label}
|
||||||
>
|
>
|
||||||
{tab.label}
|
<tab.icon size={15} />
|
||||||
|
<span class={styles.tabLabel}>{tab.label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
<button
|
||||||
|
class={clsx(styles.tab, styles.tabRight, chatOpen && styles.active)}
|
||||||
|
onClick={() => dispatch({ type: 'SET_CHAT_OPEN', open: !chatOpen })}
|
||||||
|
title="Chat"
|
||||||
|
>
|
||||||
|
<MessageSquare size={15} />
|
||||||
|
<span class={styles.tabLabel}>Chat</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,28 +7,37 @@ import { PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from "
|
||||||
interface Props {
|
interface Props {
|
||||||
side: 'left' | 'right';
|
side: 'left' | 'right';
|
||||||
children?: ComponentChildren;
|
children?: ComponentChildren;
|
||||||
|
open?: boolean;
|
||||||
|
onToggle?: () => void;
|
||||||
onCollapseChanged?: (collapsed: boolean) => void;
|
onCollapseChanged?: (collapsed: boolean) => void;
|
||||||
|
class?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar = ({ side, children, onCollapseChanged }: Props) => {
|
export const Sidebar = ({ side, children, open: controlledOpen, onToggle, onCollapseChanged, class: className }: Props) => {
|
||||||
const open = useBool(true);
|
const internalOpen = useBool(true);
|
||||||
|
const isControlled = controlledOpen !== undefined;
|
||||||
|
const isOpen = isControlled ? controlledOpen : internalOpen.value;
|
||||||
|
|
||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
open.toggle();
|
if (isControlled) {
|
||||||
onCollapseChanged?.(!open.value);
|
onToggle?.();
|
||||||
|
} else {
|
||||||
|
internalOpen.toggle();
|
||||||
|
}
|
||||||
|
onCollapseChanged?.(!isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLeft = side === 'left';
|
const isLeft = side === 'left';
|
||||||
const IconComponent = isLeft
|
const IconComponent = isLeft
|
||||||
? (open.value ? PanelLeftClose : PanelLeftOpen)
|
? (isOpen ? PanelLeftClose : PanelLeftOpen)
|
||||||
: (open.value ? PanelRightClose : PanelRightOpen);
|
: (isOpen ? PanelRightClose : PanelRightOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.sidebar, open.value ? styles.open : styles.closed)} data-side={side}>
|
<div class={clsx(styles.sidebar, isOpen ? styles.open : styles.closed, className)} data-side={side} data-controlled={isControlled || undefined}>
|
||||||
<button class={styles.toggle} onClick={handleToggle}>
|
<button class={clsx(styles.toggle, isControlled && styles.toggleMobile)} onClick={handleToggle}>
|
||||||
<IconComponent size={16} />
|
<IconComponent size={16} />
|
||||||
</button>
|
</button>
|
||||||
{open.value && (
|
{isOpen && (
|
||||||
<div class={styles.content}>
|
<div class={styles.content}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ interface IState {
|
||||||
stories: Story[];
|
stories: Story[];
|
||||||
currentStoryId: string | null;
|
currentStoryId: string | null;
|
||||||
currentTab: Tab;
|
currentTab: Tab;
|
||||||
|
chatOpen: boolean;
|
||||||
connection: LLM.Connection | null;
|
connection: LLM.Connection | null;
|
||||||
model: LLM.ModelInfo | null;
|
model: LLM.ModelInfo | null;
|
||||||
enableThinking: boolean;
|
enableThinking: boolean;
|
||||||
|
|
@ -103,6 +104,7 @@ type Action =
|
||||||
| { type: 'REORDER_LORE_ENTRIES'; storyId: string; entryIds: string[] }
|
| { type: 'REORDER_LORE_ENTRIES'; storyId: string; entryIds: string[] }
|
||||||
| { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string }
|
| { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string }
|
||||||
| { type: 'SET_CURRENT_TAB'; tab: Tab }
|
| { type: 'SET_CURRENT_TAB'; tab: Tab }
|
||||||
|
| { type: 'SET_CHAT_OPEN'; open: boolean }
|
||||||
| { type: 'DELETE_STORY'; id: string }
|
| { type: 'DELETE_STORY'; id: string }
|
||||||
| { type: 'SELECT_STORY'; id: string }
|
| { type: 'SELECT_STORY'; id: string }
|
||||||
| { type: 'DUPLICATE_STORY'; id: string }
|
| { type: 'DUPLICATE_STORY'; id: string }
|
||||||
|
|
@ -130,6 +132,7 @@ const DEFAULT_STATE: IState = {
|
||||||
stories: [],
|
stories: [],
|
||||||
currentStoryId: null,
|
currentStoryId: null,
|
||||||
currentTab: 'menu',
|
currentTab: 'menu',
|
||||||
|
chatOpen: false,
|
||||||
connection: null,
|
connection: null,
|
||||||
model: null,
|
model: null,
|
||||||
enableThinking: false,
|
enableThinking: false,
|
||||||
|
|
@ -263,6 +266,9 @@ function reducer(state: IState, action: Action): IState {
|
||||||
case 'SET_CURRENT_TAB': {
|
case 'SET_CURRENT_TAB': {
|
||||||
return { ...state, currentTab: action.tab };
|
return { ...state, currentTab: action.tab };
|
||||||
}
|
}
|
||||||
|
case 'SET_CHAT_OPEN': {
|
||||||
|
return { ...state, chatOpen: action.open };
|
||||||
|
}
|
||||||
case 'DELETE_STORY': {
|
case 'DELETE_STORY': {
|
||||||
const remaining = state.stories.filter(s => s.id !== action.id);
|
const remaining = state.stories.filter(s => s.id !== action.id);
|
||||||
const deletingCurrent = state.currentStoryId === action.id;
|
const deletingCurrent = state.currentStoryId === action.id;
|
||||||
|
|
@ -539,6 +545,7 @@ export interface AppState {
|
||||||
stories: Story[];
|
stories: Story[];
|
||||||
currentStory: Story | null;
|
currentStory: Story | null;
|
||||||
currentTab: Tab;
|
currentTab: Tab;
|
||||||
|
chatOpen: boolean;
|
||||||
connection: LLM.Connection | null;
|
connection: LLM.Connection | null;
|
||||||
model: LLM.ModelInfo | null;
|
model: LLM.ModelInfo | null;
|
||||||
enableThinking: boolean;
|
enableThinking: boolean;
|
||||||
|
|
@ -560,6 +567,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
|
||||||
stories: state.stories,
|
stories: state.stories,
|
||||||
currentStory: state.stories.find(s => s.id === state.currentStoryId) ?? null,
|
currentStory: state.stories.find(s => s.id === state.currentStoryId) ?? null,
|
||||||
currentTab: state.currentTab,
|
currentTab: state.currentTab,
|
||||||
|
chatOpen: state.chatOpen,
|
||||||
connection: state.connection,
|
connection: state.connection,
|
||||||
model: state.model,
|
model: state.model,
|
||||||
enableThinking: state.enableThinking,
|
enableThinking: state.enableThinking,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue