diff --git a/bun.lock b/bun.lock index f62c77e..58cf289 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ "ace-builds": "1.36.3", "clsx": "2.1.1", "delay": "6.0.0", + "lucide-preact": "^0.577.0", "preact": "10.22.0", }, "devDependencies": { @@ -116,6 +117,8 @@ "lower-case": ["lower-case@1.1.4", "", {}, "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA=="], + "lucide-preact": ["lucide-preact@0.577.0", "", { "peerDependencies": { "preact": "^10.27.2" } }, "sha512-fCY59YQ2OMYWqE1V7k8HwfXyiBMHAfTI1roCOasdc+Cekya7BIObSJ/cil+tVMSbU6siv4uZlaz5twAGmkYqIQ=="], + "mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], "no-case": ["no-case@2.3.2", "", { "dependencies": { "lower-case": "^1.1.1" } }, "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ=="], diff --git a/package.json b/package.json index 0c87c7c..867b173 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,11 @@ "ace-builds": "1.36.3", "clsx": "2.1.1", "delay": "6.0.0", + "lucide-preact": "0.577.0", "preact": "10.22.0" }, "devDependencies": { - "@types/bun": "^1.3.10", + "@types/bun": "latest", "@types/html-minifier": "4.0.5", "@types/inquirer": "9.0.7", "@types/web-bluetooth": "0.0.21", diff --git a/src/games/storywriter/assets/settings-modal.module.css b/src/games/storywriter/assets/settings-modal.module.css index dd660ab..5a4906d 100644 --- a/src/games/storywriter/assets/settings-modal.module.css +++ b/src/games/storywriter/assets/settings-modal.module.css @@ -114,3 +114,49 @@ background: var(--accent); color: var(--accent-text); } + +.inputRow { + display: flex; + gap: 8px; +} + +.inputRow .input { + flex: 1; +} + +.divider { + height: 1px; + background: var(--border); + margin: 16px 0; +} + +.tokenList { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; +} + +.tokenItem { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + background: var(--bg-active); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text); +} + +.tokenRemoveButton { + background: none; + border: none; + cursor: pointer; + padding: 0 2px; + font-size: 14px; + color: var(--text-muted); +} + +.emptyText { + color: var(--text-muted); +} diff --git a/src/games/storywriter/assets/style.css b/src/games/storywriter/assets/style.css index ba18ba0..a73fe5d 100644 --- a/src/games/storywriter/assets/style.css +++ b/src/games/storywriter/assets/style.css @@ -1,27 +1,27 @@ :root { /* Monokai-inspired palette */ - --bg: #272822; - --bg-panel: #1e1f1a; - --bg-hover: #3e3d32; - --bg-active: #49483e; - --border: #3e3d32; - --accent: #f92672; - --accent-alt: #a6e22e; - --text: #f8f8f2; - --text-muted: #75715e; - --text-dim: #cfcfc2; - --yellow: #e6db74; - --orange: #fd971f; - --blue: #66d9ef; - --purple: #ae81ff; + --bg: #272822; + --bg-panel: #1e1f1a; + --bg-hover: #3e3d32; + --bg-active: #49483e; + --border: #3e3d32; + --accent: #f92672; + --accent-alt: #a6e22e; + --text: #f8f8f2; + --text-muted: #75715e; + --text-dim: #cfcfc2; + --yellow: #e6db74; + --orange: #fd971f; + --blue: #66d9ef; + --purple: #ae81ff; - --radius: 4px; - --transition: 0.15s ease; - - --textColor: #DCDCD2; + --radius: 4px; + --transition: 0.15s ease; + + --textColor: #DCDCD2; --italicColor: #AFAFAF; - --quoteColor: #D4E5FF; - --codeBg: #49483e; + --quoteColor: #D4E5FF; + --codeBg: #49483e; } * { @@ -51,9 +51,13 @@ button { font-family: inherit; transition: color var(--transition), background var(--transition); border-radius: var(--radius); + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; &:hover { color: var(--text); background: var(--bg-hover); } -} +} \ No newline at end of file diff --git a/src/games/storywriter/components/banned-tokens-modal.tsx b/src/games/storywriter/components/banned-tokens-modal.tsx new file mode 100644 index 0000000..763ba44 --- /dev/null +++ b/src/games/storywriter/components/banned-tokens-modal.tsx @@ -0,0 +1,98 @@ +import clsx from "clsx"; + +import { useInputState } from "@common/hooks/useInputState"; + +import { useAppState } from "../contexts/state"; +import styles from "../assets/settings-modal.module.css"; +import { X } from "lucide-preact"; + +interface Props { + onClose: () => void; +} + +export const BannedTokensModal = ({ onClose }: Props) => { + const { bannedTokens, dispatch } = useAppState(); + const [inputValue, setInputValue] = useInputState(); + + 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()) + ); + + return ( +
+
e.stopPropagation()}> +
+

Banned Tokens

+ +
+
+
+ + +
+
+
+ {sortedTokens.length === 0 ? ( +

No banned tokens

+ ) : ( + sortedTokens.map((token) => ( +
+ {token} + +
+ )) + )} +
+
+
+ +
+
+
+ ); +}; diff --git a/src/games/storywriter/components/menu-sidebar.tsx b/src/games/storywriter/components/menu-sidebar.tsx index c0ef6f8..63d7370 100644 --- a/src/games/storywriter/components/menu-sidebar.tsx +++ b/src/games/storywriter/components/menu-sidebar.tsx @@ -1,11 +1,13 @@ import clsx from "clsx"; import { Sidebar } from "./sidebar"; import { SettingsModal } from "./settings-modal"; +import { BannedTokensModal } from "./banned-tokens-modal"; import { useAppState } from "../contexts/state"; import { useBool } from "@common/hooks/useBool"; import type { Story } from "../contexts/state"; import styles from '../assets/menu-sidebar.module.css'; import { useState } from "preact/hooks"; +import { Pencil, X, Plus, Settings, Ban } from "lucide-preact"; // ─── Story Item ─────────────────────────────────────────────────────────────── @@ -66,10 +68,10 @@ const StoryItem = ({ story, active, onSelect, onRename, onDelete }: StoryItemPro
@@ -81,6 +83,7 @@ const StoryItem = ({ story, active, onSelect, onRename, onDelete }: StoryItemPro export const MenuSidebar = () => { const { stories, currentStory, dispatch } = useAppState(); const isSettingsOpen = useBool(false); + const isBannedTokensOpen = useBool(false); const handleCreate = () => { dispatch({ type: 'CREATE_STORY', title: 'New Story' }); @@ -106,7 +109,7 @@ export const MenuSidebar = () => {
{stories.map(story => ( @@ -121,11 +124,17 @@ export const MenuSidebar = () => { ))}
+
+ {isBannedTokensOpen.value && ( + + )} {isSettingsOpen.value && ( )} diff --git a/src/games/storywriter/components/settings-modal.tsx b/src/games/storywriter/components/settings-modal.tsx index 572d8ac..c27e484 100644 --- a/src/games/storywriter/components/settings-modal.tsx +++ b/src/games/storywriter/components/settings-modal.tsx @@ -7,6 +7,7 @@ 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; @@ -100,7 +101,7 @@ export const SettingsModal = ({ onClose }: Props) => {

Settings

diff --git a/src/games/storywriter/components/sidebar.tsx b/src/games/storywriter/components/sidebar.tsx index 8239326..c6cb826 100644 --- a/src/games/storywriter/components/sidebar.tsx +++ b/src/games/storywriter/components/sidebar.tsx @@ -2,6 +2,7 @@ import clsx from "clsx"; import type { ComponentChildren } from "preact"; import { useBool } from "@common/hooks/useBool"; import styles from '../assets/sidebar.module.css'; +import { PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from "lucide-preact"; interface Props { side: 'left' | 'right'; @@ -17,10 +18,15 @@ export const Sidebar = ({ side, children, onCollapseChanged }: Props) => { onCollapseChanged?.(!open.value); }; + const isLeft = side === 'left'; + const IconComponent = isLeft + ? (open.value ? PanelLeftClose : PanelLeftOpen) + : (open.value ? PanelRightClose : PanelRightOpen); + return (
{open.value && (
diff --git a/src/games/storywriter/contexts/state.tsx b/src/games/storywriter/contexts/state.tsx index 879c678..52462bb 100644 --- a/src/games/storywriter/contexts/state.tsx +++ b/src/games/storywriter/contexts/state.tsx @@ -29,6 +29,7 @@ interface IState { connection: LLM.Connection | null; model: LLM.ModelInfo | null; enableThinking: boolean; + bannedTokens: string[]; } // ─── Actions ───────────────────────────────────────────────────────────────── @@ -45,7 +46,8 @@ type Action = | { type: 'CLEAR_CHAT'; storyId: string } | { type: 'SET_CONNECTION'; connection: LLM.Connection | null } | { type: 'SET_MODEL'; model: LLM.ModelInfo | null } - | { type: 'SET_ENABLE_THINKING'; enable: boolean }; + | { type: 'SET_ENABLE_THINKING'; enable: boolean } + | { type: 'SET_BANNED_TOKENS'; tokens: string[] }; // ─── Initial State ─────────────────────────────────────────────────────────── @@ -55,6 +57,7 @@ const DEFAULT_STATE: IState = { connection: null, model: null, enableThinking: false, + bannedTokens: [], }; // ─── Reducer ───────────────────────────────────────────────────────────────── @@ -166,6 +169,12 @@ function reducer(state: IState, action: Action): IState { enableThinking: action.enable, }; } + case 'SET_BANNED_TOKENS': { + return { + ...state, + bannedTokens: action.tokens, + }; + } } } @@ -177,6 +186,7 @@ export interface AppState { connection: LLM.Connection | null; model: LLM.ModelInfo | null; enableThinking: boolean; + bannedTokens: string[]; dispatch: (action: Action) => void; } @@ -195,6 +205,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => { connection: state.connection, model: state.model, enableThinking: state.enableThinking, + bannedTokens: state.bannedTokens ?? [], dispatch, }), [state]); diff --git a/src/games/storywriter/utils/prompt.ts b/src/games/storywriter/utils/prompt.ts index ac72621..b861e2e 100644 --- a/src/games/storywriter/utils/prompt.ts +++ b/src/games/storywriter/utils/prompt.ts @@ -22,7 +22,7 @@ namespace Prompt { model: model.id, messages, tools: Tools.getTools(), - // TODO banned_tokens + banned_tokens: state.bannedTokens, enable_thinking: enableThinking, }; }