Lucide icons and banned tokens gui
This commit is contained in:
parent
9726472a38
commit
6c8857478c
3
bun.lock
3
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=="],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
--radius: 4px;
|
||||
--transition: 0.15s ease;
|
||||
|
||||
--textColor: #DCDCD2;
|
||||
--textColor: #DCDCD2;
|
||||
--italicColor: #AFAFAF;
|
||||
--quoteColor: #D4E5FF;
|
||||
--codeBg: #49483e;
|
||||
--quoteColor: #D4E5FF;
|
||||
--codeBg: #49483e;
|
||||
}
|
||||
|
||||
* {
|
||||
|
|
@ -51,6 +51,10 @@ 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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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
|
|||
</button>
|
||||
<div class={styles.actions}>
|
||||
<button class={styles.actionButton} onClick={() => setIsEditing(true)} title="Rename">
|
||||
✎
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<button class={styles.actionButton} onClick={onDelete} title="Delete">
|
||||
×
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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 = () => {
|
|||
<Sidebar side="left">
|
||||
<div class={styles.menu}>
|
||||
<button class={styles.newButton} onClick={handleCreate}>
|
||||
+ New Story
|
||||
<Plus size={16} /> New Story
|
||||
</button>
|
||||
<div class={styles.list}>
|
||||
{stories.map(story => (
|
||||
|
|
@ -121,11 +124,17 @@ export const MenuSidebar = () => {
|
|||
))}
|
||||
</div>
|
||||
<div class={styles.bottomButtons}>
|
||||
<button class={styles.settingsButton} onClick={isBannedTokensOpen.toggle}>
|
||||
<Ban size={16} /> Banned Tokens
|
||||
</button>
|
||||
<button class={styles.settingsButton} onClick={isSettingsOpen.toggle}>
|
||||
⚙ Settings
|
||||
<Settings size={16} /> Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isBannedTokensOpen.value && (
|
||||
<BannedTokensModal onClose={isBannedTokensOpen.toggle} />
|
||||
)}
|
||||
{isSettingsOpen.value && (
|
||||
<SettingsModal onClose={isSettingsOpen.toggle} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
|||
<div class={styles.header}>
|
||||
<h2 class={styles.title}>Settings</h2>
|
||||
<button class={styles.closeButton} onClick={onClose}>
|
||||
×
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div class={styles.content}>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div class={clsx(styles.sidebar, open.value ? styles.open : styles.closed)} data-side={side}>
|
||||
<button class={styles.toggle} onClick={handleToggle}>
|
||||
{side === 'left' ? (open.value ? '◀' : '▶') : (open.value ? '▶' : '◀')}
|
||||
<IconComponent size={16} />
|
||||
</button>
|
||||
{open.value && (
|
||||
<div class={styles.content}>
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ namespace Prompt {
|
|||
model: model.id,
|
||||
messages,
|
||||
tools: Tools.getTools(),
|
||||
// TODO banned_tokens
|
||||
banned_tokens: state.bannedTokens,
|
||||
enable_thinking: enableThinking,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue