From d9dac5c97f47a36928ed673db80f0d833437b8b4 Mon Sep 17 00:00:00 2001 From: Pabloader Date: Wed, 8 Apr 2026 07:59:40 +0000 Subject: [PATCH] Settings refactor --- .../assets/settings-modal.module.css | 45 +++-- .../components/connection-settings-modal.tsx | 185 ------------------ src/games/storywriter/components/editor.tsx | 8 +- .../chapters.tsx} | 6 +- .../character.tsx} | 8 +- .../location.tsx} | 8 +- .../{lore-editor.tsx => editors/lore.tsx} | 4 +- src/games/storywriter/components/menu.tsx | 10 +- .../storywriter/components/settings-modal.tsx | 145 +++----------- .../components/settings/banned-tokens.tsx | 71 +++++++ .../components/settings/connection.tsx | 138 +++++++++++++ .../settings/system-instruction.tsx | 29 +++ 12 files changed, 320 insertions(+), 337 deletions(-) delete mode 100644 src/games/storywriter/components/connection-settings-modal.tsx rename src/games/storywriter/components/{chapters-editor.tsx => editors/chapters.tsx} (94%) rename src/games/storywriter/components/{character-editor.tsx => editors/character.tsx} (98%) rename src/games/storywriter/components/{location-editor.tsx => editors/location.tsx} (97%) rename src/games/storywriter/components/{lore-editor.tsx => editors/lore.tsx} (98%) create mode 100644 src/games/storywriter/components/settings/banned-tokens.tsx create mode 100644 src/games/storywriter/components/settings/connection.tsx create mode 100644 src/games/storywriter/components/settings/system-instruction.tsx diff --git a/src/games/storywriter/assets/settings-modal.module.css b/src/games/storywriter/assets/settings-modal.module.css index babe6a1..a4d3dae 100644 --- a/src/games/storywriter/assets/settings-modal.module.css +++ b/src/games/storywriter/assets/settings-modal.module.css @@ -15,8 +15,8 @@ background: var(--bg); border-radius: 8px; width: 90%; - max-width: 500px; - max-height: 80vh; + max-width: 720px; + height: 80vh; display: flex; flex-direction: column; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); @@ -56,25 +56,35 @@ } } -.tabs { +.body { display: flex; - border-bottom: 1px solid var(--border); - padding: 0 20px; - gap: 8px; + flex: 1; + overflow: hidden; } -.tab { +.sidebar { + display: flex; + flex-direction: column; + width: 160px; + flex-shrink: 0; + border-right: 1px solid var(--border); + padding: 8px; + gap: 2px; +} + +.menuItem { display: flex; align-items: center; - gap: 6px; - padding: 12px 16px; + width: 100%; + padding: 10px 12px; background: transparent; border: none; - border-bottom: 2px solid transparent; + border-radius: 6px; color: var(--text-muted); cursor: pointer; font-size: 14px; - transition: all 0.2s; + text-align: left; + transition: all 0.15s; &:hover { color: var(--text); @@ -83,7 +93,8 @@ &.active { color: var(--accent); - border-bottom-color: var(--accent); + background: var(--bg-active); + font-weight: 500; } } @@ -91,12 +102,15 @@ flex: 1; overflow-y: auto; padding: 20px; + display: flex; + flex-direction: column; } .form { display: flex; flex-direction: column; gap: 16px; + flex: 1; } .formGroup { @@ -104,6 +118,10 @@ flex-direction: column; } +.formGroupFill { + flex: 1; +} + .label { display: block; margin-bottom: 4px; @@ -122,10 +140,11 @@ } .textarea { - resize: vertical; + resize: none; font-family: inherit; font-size: inherit; line-height: 1.5; + flex: 1; } .footer { diff --git a/src/games/storywriter/components/connection-settings-modal.tsx b/src/games/storywriter/components/connection-settings-modal.tsx deleted file mode 100644 index 5f23328..0000000 --- a/src/games/storywriter/components/connection-settings-modal.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { useMemo, useRef } from "preact/hooks"; - -import { useQuery } from "@common/hooks/useAsyncState"; -import { useInputState } from "@common/hooks/useInputState"; -import { useUpdate } from "@common/hooks/useUpdate"; - -import { useAppState } from "../contexts/state"; -import LLM from "../utils/llm"; -import styles from "../assets/settings-modal.module.css"; -import { X } from "lucide-preact"; - -interface Props { - onClose: () => void; -} - -export const ConnectionSettingsModal = ({ onClose }: Props) => { - const { connection, model, dispatch } = useAppState(); - const [url, setUrl] = useInputState(connection?.url ?? ""); - const [apiKey, setApiKey] = useInputState(connection?.apiKey ?? ""); - const [selectedModel, setSelectedModel] = useInputState(model?.id ?? ""); - const [update, triggerFetch] = useUpdate(); - - const urlRef = useRef(url); - const apiKeyRef = useRef(apiKey); - - urlRef.current = url; - apiKeyRef.current = apiKey; - - const connectionToFetch = useMemo(() => { - const currentUrl = urlRef.current; - const currentApiKey = apiKeyRef.current; - if (!currentUrl || !currentApiKey) return null; - return { url: currentUrl, apiKey: currentApiKey }; - }, [update]); - - const fetchModels = useMemo(() => async (conn: LLM.Connection | null) => { - if (!conn) return []; - const r = await LLM.getModels(conn); - return r.data; - }, []); - - const modelsData = useQuery(fetchModels, connectionToFetch); - - const isLoadingModels = connectionToFetch != null && modelsData == undefined; - const groupedModels = useMemo(() => { - const sorted = (modelsData ?? []).sort((a, b) => { - const aWeight = Number(a.support_tools) * 2 + Number(a.support_thinking); - const bWeight = Number(b.support_tools) * 2 + Number(b.support_thinking); - if (aWeight !== bWeight) { - return bWeight - aWeight; - } - - const aContext = a.max_context ?? 0; - const bContext = b.max_context ?? 0; - if (aContext !== bContext) { - return bContext - aContext; - } - - return a.id.localeCompare(b.id); - }); - - // Group by context size - const groups = Map.groupBy(sorted, m => m.max_context ?? 0); - - // Convert to array sorted by context size (bigger first) - return Array.from(groups.entries()) - .sort((a, b) => b[0] - a[0]) - .map(([context, models]) => ({ context, models })); - }, [modelsData]); - - const handleBlur = () => { - if (url && apiKey) { - triggerFetch(); - } - }; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter' && url && apiKey) { - triggerFetch(); - } - }; - - const handleConfirm = () => { - dispatch({ - type: 'SET_CONNECTION', - connection: connectionToFetch, - }); - const selectedModelInfo = modelsData?.find(m => m.id === selectedModel) ?? null; - dispatch({ - type: 'SET_MODEL', - model: selectedModelInfo, - }); - onClose(); - }; - - const connectionToTest = url && apiKey ? { url, apiKey } : null; - - return ( -
-
e.stopPropagation()}> -
-

Connection Settings

- -
-
-
-
- - -
-
- - -
-
- - {connectionToTest ? ( - isLoadingModels ? ( -

Loading models...

- ) : groupedModels.length > 0 ? ( - - ) : ( -

No models available

- ) - ) : ( -

Enter connection details to load models

- )} -
-
-
-
- - -
-
-
- ); -}; diff --git a/src/games/storywriter/components/editor.tsx b/src/games/storywriter/components/editor.tsx index ecfbe31..37dc1fb 100644 --- a/src/games/storywriter/components/editor.tsx +++ b/src/games/storywriter/components/editor.tsx @@ -4,10 +4,10 @@ import { useAppState, type Tab } from "../contexts/state"; import styles from '../assets/editor.module.css'; import { useMemo, useRef, useEffect } from "preact/hooks"; import clsx from "clsx"; -import { CharacterEditor } from "./character-editor"; -import { LocationEditor } from "./location-editor"; -import { ChaptersEditor } from "./chapters-editor"; -import { LoreEditor } from "./lore-editor"; +import { CharacterEditor } from "./editors/character"; +import { LocationEditor } from "./editors/location"; +import { ChaptersEditor } from "./editors/chapters"; +import { LoreEditor } from "./editors/lore"; import { Menu } from "./menu"; import { ChatPanel } from "./chat-sidebar"; import { useInputCallback } from "@common/hooks/useInputCallback"; diff --git a/src/games/storywriter/components/chapters-editor.tsx b/src/games/storywriter/components/editors/chapters.tsx similarity index 94% rename from src/games/storywriter/components/chapters-editor.tsx rename to src/games/storywriter/components/editors/chapters.tsx index 8ed7056..0b45c09 100644 --- a/src/games/storywriter/components/chapters-editor.tsx +++ b/src/games/storywriter/components/editors/chapters.tsx @@ -1,9 +1,9 @@ import { useMemo } from "preact/hooks"; -import { useAppState } from "../contexts/state"; -import Chapters from "../utils/chapters"; import { ContentEditable } from "@common/components/ContentEditable"; import { highlight } from "@common/highlight"; -import styles from "../assets/chapters-editor.module.css"; +import { useAppState } from "../../contexts/state"; +import Chapters from "../../utils/chapters"; +import styles from "../../assets/chapters-editor.module.css"; export const ChaptersEditor = () => { const { currentWorld, currentStory, dispatch } = useAppState(); diff --git a/src/games/storywriter/components/character-editor.tsx b/src/games/storywriter/components/editors/character.tsx similarity index 98% rename from src/games/storywriter/components/character-editor.tsx rename to src/games/storywriter/components/editors/character.tsx index 9b1fb69..483e447 100644 --- a/src/games/storywriter/components/character-editor.tsx +++ b/src/games/storywriter/components/editors/character.tsx @@ -1,8 +1,8 @@ -import { CharacterRole, useAppState, type Character } from "../contexts/state"; + import { useState } from "preact/hooks"; -import styles from '../assets/character-editor.module.css'; -import LLM from "../utils/llm"; -import { ContentEditable } from "@common/components/ContentEditable"; +import { ContentEditable } from "@common/components/ContentEditable";import { CharacterRole, useAppState, type Character } from "../../contexts/state"; +import styles from '../../assets/character-editor.module.css'; +import LLM from "../../utils/llm"; export const CharacterEditor = () => { const { currentWorld, currentStory, mergedCharacters, dispatch, connection, model } = useAppState(); diff --git a/src/games/storywriter/components/location-editor.tsx b/src/games/storywriter/components/editors/location.tsx similarity index 97% rename from src/games/storywriter/components/location-editor.tsx rename to src/games/storywriter/components/editors/location.tsx index cf5cc23..682f0f3 100644 --- a/src/games/storywriter/components/location-editor.tsx +++ b/src/games/storywriter/components/editors/location.tsx @@ -1,8 +1,8 @@ -import { useAppState, type Location, LocationScale } from "../contexts/state"; -import { useState } from "preact/hooks"; -import styles from '../assets/location-editor.module.css'; -import LLM from "../utils/llm"; import { ContentEditable } from "@common/components/ContentEditable"; +import { useAppState, type Location, LocationScale } from "../../contexts/state"; +import { useState } from "preact/hooks"; +import styles from '../../assets/location-editor.module.css'; +import LLM from "../../utils/llm"; export const LocationEditor = () => { const { currentWorld, currentStory, dispatch, connection, model } = useAppState(); diff --git a/src/games/storywriter/components/lore-editor.tsx b/src/games/storywriter/components/editors/lore.tsx similarity index 98% rename from src/games/storywriter/components/lore-editor.tsx rename to src/games/storywriter/components/editors/lore.tsx index 0452f4e..dc71db3 100644 --- a/src/games/storywriter/components/lore-editor.tsx +++ b/src/games/storywriter/components/editors/lore.tsx @@ -1,7 +1,7 @@ -import { useAppState, type LoreEntry } from "../contexts/state"; -import styles from '../assets/lore-editor.module.css'; import { ContentEditable } from "@common/components/ContentEditable"; import { useState } from "preact/hooks"; +import { useAppState, type LoreEntry } from "../../contexts/state"; +import styles from '../../assets/lore-editor.module.css'; export const LoreEditor = () => { const { currentWorld, currentStory, dispatch } = useAppState(); diff --git a/src/games/storywriter/components/menu.tsx b/src/games/storywriter/components/menu.tsx index a19ccd6..e6f9b4d 100644 --- a/src/games/storywriter/components/menu.tsx +++ b/src/games/storywriter/components/menu.tsx @@ -1,5 +1,4 @@ import clsx from "clsx"; -import { ConnectionSettingsModal } from "./connection-settings-modal"; import { SettingsModal } from "./settings-modal"; import { useAppState } from "../contexts/state"; import { useBool } from "@common/hooks/useBool"; @@ -8,7 +7,7 @@ import type { World, Story } from "../contexts/state"; import { isWorld } from "../contexts/state"; import CharacterCard from "../utils/character-card"; import styles from '../assets/menu.module.css'; -import { Pencil, X, Plus, Plug, Settings, Copy, ChevronRight, ChevronDown, Globe, Download, Upload, MessagesSquare, MessageSquarePlus } from "lucide-preact"; +import { Pencil, X, Plus, Settings, Copy, ChevronRight, ChevronDown, Globe, Download, Upload, MessagesSquare, MessageSquarePlus } from "lucide-preact"; // ─── Inline Rename Input ────────────────────────────────────────────────────── @@ -191,7 +190,6 @@ const WorldItem = ({ export const Menu = () => { const { worlds, currentWorld, currentStory, dispatch } = useAppState(); - const isConnectionSettingsOpen = useBool(false); const isSettingsOpen = useBool(false); const handleCreateWorld = () => { @@ -323,16 +321,10 @@ export const Menu = () => { - {isSettingsOpen.value && ( )} - {isConnectionSettingsOpen.value && ( - - )} ); }; diff --git a/src/games/storywriter/components/settings-modal.tsx b/src/games/storywriter/components/settings-modal.tsx index 2912141..0ad82d4 100644 --- a/src/games/storywriter/components/settings-modal.tsx +++ b/src/games/storywriter/components/settings-modal.tsx @@ -1,61 +1,20 @@ import clsx from "clsx"; -import { useMemo, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; -import { useInputState } from "@common/hooks/useInputState"; - -import { useAppState } from "../contexts/state"; import styles from "../assets/settings-modal.module.css"; import { X } from "lucide-preact"; -import { useInputCallback } from "@common/hooks/useInputCallback"; +import { BannedTokensSettings } from "./settings/banned-tokens"; +import { SystemInstructionSettings } from "./settings/system-instruction"; +import { ConnectionSettings } from "./settings/connection"; interface Props { onClose: () => void; } -type Tab = "banned-tokens" | "system-instruction"; +type Tab = "banned-tokens" | "system-instruction" | "connection"; export const SettingsModal = ({ onClose }: Props) => { - const { bannedTokens, systemInstruction, dispatch } = useAppState(); - const [inputValue, setInputValue] = useInputState(); - const [activeTab, setActiveTab] = useState("banned-tokens"); - - // Save system instruction on every change - const setInstructionValue = useInputCallback((instructionValue) => { - dispatch({ - type: "SET_SYSTEM_INSTRUCTION", - systemInstruction: instructionValue, - }); - }, []); - - const handleAdd = () => { - const trimmed = inputValue.trim(); - if (trimmed && !bannedTokens.includes(trimmed)) { - dispatch({ - type: "SET_BANNED_TOKENS", - tokens: [...bannedTokens, trimmed], - }); - setInputValue(""); - } - }; - - const handleRemove = (token: string) => { - dispatch({ - type: "SET_BANNED_TOKENS", - tokens: bannedTokens.filter((t) => t !== token), - }); - }; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter") { - handleAdd(); - } else if (e.key === "Escape") { - onClose(); - } - }; - - const sortedTokens = [...bannedTokens].sort((a, b) => - a.trim().toLowerCase().localeCompare(b.trim().toLowerCase()) - ); + const [activeTab, setActiveTab] = useState("connection"); return (
@@ -66,72 +25,32 @@ export const SettingsModal = ({ onClose }: Props) => {
-
- - -
-
- {activeTab === "banned-tokens" ? ( - <> -
- - -
-
-
- {sortedTokens.length === 0 ? ( -

No banned tokens

- ) : ( - sortedTokens.map((token) => ( -
- {token} - -
- )) - )} -
- - ) : ( -
-
- -