1
0
Fork 0

Lucide icons and banned tokens gui

This commit is contained in:
Pabloader 2026-03-23 13:14:18 +00:00
parent 9726472a38
commit 6c8857478c
10 changed files with 209 additions and 30 deletions

View File

@ -14,6 +14,7 @@
"ace-builds": "1.36.3", "ace-builds": "1.36.3",
"clsx": "2.1.1", "clsx": "2.1.1",
"delay": "6.0.0", "delay": "6.0.0",
"lucide-preact": "^0.577.0",
"preact": "10.22.0", "preact": "10.22.0",
}, },
"devDependencies": { "devDependencies": {
@ -116,6 +117,8 @@
"lower-case": ["lower-case@1.1.4", "", {}, "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA=="], "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=="], "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=="], "no-case": ["no-case@2.3.2", "", { "dependencies": { "lower-case": "^1.1.1" } }, "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ=="],

View File

@ -18,10 +18,11 @@
"ace-builds": "1.36.3", "ace-builds": "1.36.3",
"clsx": "2.1.1", "clsx": "2.1.1",
"delay": "6.0.0", "delay": "6.0.0",
"lucide-preact": "0.577.0",
"preact": "10.22.0" "preact": "10.22.0"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.10", "@types/bun": "latest",
"@types/html-minifier": "4.0.5", "@types/html-minifier": "4.0.5",
"@types/inquirer": "9.0.7", "@types/inquirer": "9.0.7",
"@types/web-bluetooth": "0.0.21", "@types/web-bluetooth": "0.0.21",

View File

@ -114,3 +114,49 @@
background: var(--accent); background: var(--accent);
color: var(--accent-text); 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);
}

View File

@ -1,27 +1,27 @@
:root { :root {
/* Monokai-inspired palette */ /* Monokai-inspired palette */
--bg: #272822; --bg: #272822;
--bg-panel: #1e1f1a; --bg-panel: #1e1f1a;
--bg-hover: #3e3d32; --bg-hover: #3e3d32;
--bg-active: #49483e; --bg-active: #49483e;
--border: #3e3d32; --border: #3e3d32;
--accent: #f92672; --accent: #f92672;
--accent-alt: #a6e22e; --accent-alt: #a6e22e;
--text: #f8f8f2; --text: #f8f8f2;
--text-muted: #75715e; --text-muted: #75715e;
--text-dim: #cfcfc2; --text-dim: #cfcfc2;
--yellow: #e6db74; --yellow: #e6db74;
--orange: #fd971f; --orange: #fd971f;
--blue: #66d9ef; --blue: #66d9ef;
--purple: #ae81ff; --purple: #ae81ff;
--radius: 4px; --radius: 4px;
--transition: 0.15s ease; --transition: 0.15s ease;
--textColor: #DCDCD2; --textColor: #DCDCD2;
--italicColor: #AFAFAF; --italicColor: #AFAFAF;
--quoteColor: #D4E5FF; --quoteColor: #D4E5FF;
--codeBg: #49483e; --codeBg: #49483e;
} }
* { * {
@ -51,6 +51,10 @@ button {
font-family: inherit; font-family: inherit;
transition: color var(--transition), background var(--transition); transition: color var(--transition), background var(--transition);
border-radius: var(--radius); border-radius: var(--radius);
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
&:hover { &:hover {
color: var(--text); color: var(--text);

View File

@ -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 (
<div class={styles.overlay} onClick={onClose}>
<div class={styles.modal} onClick={(e) => e.stopPropagation()}>
<div class={styles.header}>
<h2 class={styles.title}>Banned Tokens</h2>
<button class={styles.closeButton} onClick={onClose}>
<X size={20} />
</button>
</div>
<div class={styles.content}>
<div class={styles.inputRow}>
<input
type="text"
value={inputValue}
onInput={setInputValue}
onKeyDown={handleKeyDown}
placeholder="Token to ban"
class={styles.input}
autoFocus
/>
<button onClick={handleAdd} class={clsx(styles.button, styles.buttonPrimary)}>
Add
</button>
</div>
<div class={styles.divider} />
<div class={styles.tokenList}>
{sortedTokens.length === 0 ? (
<p class={styles.emptyText}>No banned tokens</p>
) : (
sortedTokens.map((token) => (
<div key={token} class={styles.tokenItem}>
<span>{token}</span>
<button
onClick={() => handleRemove(token)}
class={styles.tokenRemoveButton}
>
<X size={12} />
</button>
</div>
))
)}
</div>
</div>
<div class={styles.footer}>
<button onClick={onClose} class={clsx(styles.button, styles.buttonSecondary)}>
Done
</button>
</div>
</div>
</div>
);
};

View File

@ -1,11 +1,13 @@
import clsx from "clsx"; import clsx from "clsx";
import { Sidebar } from "./sidebar"; import { Sidebar } from "./sidebar";
import { SettingsModal } from "./settings-modal"; import { SettingsModal } from "./settings-modal";
import { BannedTokensModal } from "./banned-tokens-modal";
import { useAppState } from "../contexts/state"; import { useAppState } from "../contexts/state";
import { useBool } from "@common/hooks/useBool"; import { useBool } from "@common/hooks/useBool";
import type { Story } from "../contexts/state"; import type { Story } from "../contexts/state";
import styles from '../assets/menu-sidebar.module.css'; import styles from '../assets/menu-sidebar.module.css';
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { Pencil, X, Plus, Settings, Ban } from "lucide-preact";
// ─── Story Item ─────────────────────────────────────────────────────────────── // ─── Story Item ───────────────────────────────────────────────────────────────
@ -66,10 +68,10 @@ const StoryItem = ({ story, active, onSelect, onRename, onDelete }: StoryItemPro
</button> </button>
<div class={styles.actions}> <div class={styles.actions}>
<button class={styles.actionButton} onClick={() => setIsEditing(true)} title="Rename"> <button class={styles.actionButton} onClick={() => setIsEditing(true)} title="Rename">
<Pencil size={14} />
</button> </button>
<button class={styles.actionButton} onClick={onDelete} title="Delete"> <button class={styles.actionButton} onClick={onDelete} title="Delete">
× <X size={14} />
</button> </button>
</div> </div>
</div> </div>
@ -81,6 +83,7 @@ const StoryItem = ({ story, active, onSelect, onRename, onDelete }: StoryItemPro
export const MenuSidebar = () => { export const MenuSidebar = () => {
const { stories, currentStory, dispatch } = useAppState(); const { stories, currentStory, dispatch } = useAppState();
const isSettingsOpen = useBool(false); const isSettingsOpen = useBool(false);
const isBannedTokensOpen = useBool(false);
const handleCreate = () => { const handleCreate = () => {
dispatch({ type: 'CREATE_STORY', title: 'New Story' }); dispatch({ type: 'CREATE_STORY', title: 'New Story' });
@ -106,7 +109,7 @@ export const MenuSidebar = () => {
<Sidebar side="left"> <Sidebar side="left">
<div class={styles.menu}> <div class={styles.menu}>
<button class={styles.newButton} onClick={handleCreate}> <button class={styles.newButton} onClick={handleCreate}>
+ New Story <Plus size={16} /> New Story
</button> </button>
<div class={styles.list}> <div class={styles.list}>
{stories.map(story => ( {stories.map(story => (
@ -121,11 +124,17 @@ export const MenuSidebar = () => {
))} ))}
</div> </div>
<div class={styles.bottomButtons}> <div class={styles.bottomButtons}>
<button class={styles.settingsButton} onClick={isBannedTokensOpen.toggle}>
<Ban size={16} /> Banned Tokens
</button>
<button class={styles.settingsButton} onClick={isSettingsOpen.toggle}> <button class={styles.settingsButton} onClick={isSettingsOpen.toggle}>
Settings <Settings size={16} /> Settings
</button> </button>
</div> </div>
</div> </div>
{isBannedTokensOpen.value && (
<BannedTokensModal onClose={isBannedTokensOpen.toggle} />
)}
{isSettingsOpen.value && ( {isSettingsOpen.value && (
<SettingsModal onClose={isSettingsOpen.toggle} /> <SettingsModal onClose={isSettingsOpen.toggle} />
)} )}

View File

@ -7,6 +7,7 @@ import { useUpdate } from "@common/hooks/useUpdate";
import { useAppState } from "../contexts/state"; import { useAppState } from "../contexts/state";
import LLM from "../utils/llm"; import LLM from "../utils/llm";
import styles from "../assets/settings-modal.module.css"; import styles from "../assets/settings-modal.module.css";
import { X } from "lucide-preact";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
@ -100,7 +101,7 @@ export const SettingsModal = ({ onClose }: Props) => {
<div class={styles.header}> <div class={styles.header}>
<h2 class={styles.title}>Settings</h2> <h2 class={styles.title}>Settings</h2>
<button class={styles.closeButton} onClick={onClose}> <button class={styles.closeButton} onClick={onClose}>
× <X size={20} />
</button> </button>
</div> </div>
<div class={styles.content}> <div class={styles.content}>

View File

@ -2,6 +2,7 @@ import clsx from "clsx";
import type { ComponentChildren } from "preact"; import type { ComponentChildren } from "preact";
import { useBool } from "@common/hooks/useBool"; import { useBool } from "@common/hooks/useBool";
import styles from '../assets/sidebar.module.css'; import styles from '../assets/sidebar.module.css';
import { PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from "lucide-preact";
interface Props { interface Props {
side: 'left' | 'right'; side: 'left' | 'right';
@ -17,10 +18,15 @@ export const Sidebar = ({ side, children, onCollapseChanged }: Props) => {
onCollapseChanged?.(!open.value); onCollapseChanged?.(!open.value);
}; };
const isLeft = side === 'left';
const IconComponent = isLeft
? (open.value ? PanelLeftClose : PanelLeftOpen)
: (open.value ? PanelRightClose : PanelRightOpen);
return ( return (
<div class={clsx(styles.sidebar, open.value ? styles.open : styles.closed)} data-side={side}> <div class={clsx(styles.sidebar, open.value ? styles.open : styles.closed)} data-side={side}>
<button class={styles.toggle} onClick={handleToggle}> <button class={styles.toggle} onClick={handleToggle}>
{side === 'left' ? (open.value ? '◀' : '▶') : (open.value ? '▶' : '◀')} <IconComponent size={16} />
</button> </button>
{open.value && ( {open.value && (
<div class={styles.content}> <div class={styles.content}>

View File

@ -29,6 +29,7 @@ interface IState {
connection: LLM.Connection | null; connection: LLM.Connection | null;
model: LLM.ModelInfo | null; model: LLM.ModelInfo | null;
enableThinking: boolean; enableThinking: boolean;
bannedTokens: string[];
} }
// ─── Actions ───────────────────────────────────────────────────────────────── // ─── Actions ─────────────────────────────────────────────────────────────────
@ -45,7 +46,8 @@ type Action =
| { type: 'CLEAR_CHAT'; storyId: string } | { type: 'CLEAR_CHAT'; storyId: string }
| { type: 'SET_CONNECTION'; connection: LLM.Connection | null } | { type: 'SET_CONNECTION'; connection: LLM.Connection | null }
| { type: 'SET_MODEL'; model: LLM.ModelInfo | 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 ─────────────────────────────────────────────────────────── // ─── Initial State ───────────────────────────────────────────────────────────
@ -55,6 +57,7 @@ const DEFAULT_STATE: IState = {
connection: null, connection: null,
model: null, model: null,
enableThinking: false, enableThinking: false,
bannedTokens: [],
}; };
// ─── Reducer ───────────────────────────────────────────────────────────────── // ─── Reducer ─────────────────────────────────────────────────────────────────
@ -166,6 +169,12 @@ function reducer(state: IState, action: Action): IState {
enableThinking: action.enable, enableThinking: action.enable,
}; };
} }
case 'SET_BANNED_TOKENS': {
return {
...state,
bannedTokens: action.tokens,
};
}
} }
} }
@ -177,6 +186,7 @@ export interface AppState {
connection: LLM.Connection | null; connection: LLM.Connection | null;
model: LLM.ModelInfo | null; model: LLM.ModelInfo | null;
enableThinking: boolean; enableThinking: boolean;
bannedTokens: string[];
dispatch: (action: Action) => void; dispatch: (action: Action) => void;
} }
@ -195,6 +205,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
connection: state.connection, connection: state.connection,
model: state.model, model: state.model,
enableThinking: state.enableThinking, enableThinking: state.enableThinking,
bannedTokens: state.bannedTokens ?? [],
dispatch, dispatch,
}), [state]); }), [state]);

View File

@ -22,7 +22,7 @@ namespace Prompt {
model: model.id, model: model.id,
messages, messages,
tools: Tools.getTools(), tools: Tools.getTools(),
// TODO banned_tokens banned_tokens: state.bannedTokens,
enable_thinking: enableThinking, enable_thinking: enableThinking,
}; };
} }