Think parsing & styles
This commit is contained in:
parent
d32f675db8
commit
268e5cf5ea
|
|
@ -43,6 +43,12 @@
|
||||||
background: var(--bg-hover);
|
background: var(--bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message[data-role="tool"] .content {
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.role {
|
.role {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
@ -50,6 +56,17 @@
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reasoningContent {
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.5;
|
||||||
|
border-left: 2px solid currentColor;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|
@ -57,6 +74,22 @@
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolCalls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolBadge {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: var(--bg-active);
|
||||||
|
color: var(--text-dim);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|
@ -83,6 +116,21 @@
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggleContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
@ -128,7 +176,7 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin-top: 8px;
|
margin: 8px 0 28px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
--textColor: #DCDCD2;
|
--textColor: #DCDCD2;
|
||||||
--italicColor: #AFAFAF;
|
--italicColor: #AFAFAF;
|
||||||
--quoteColor: #D4E5FF;
|
--quoteColor: #D4E5FF;
|
||||||
|
--codeBg: #49483e;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useInputState } from "@common/hooks/useInputState";
|
||||||
import { Sidebar } from "./sidebar";
|
import { Sidebar } from "./sidebar";
|
||||||
import { useAppState, type ChatMessage } from "../contexts/state";
|
import { useAppState, type ChatMessage } from "../contexts/state";
|
||||||
import styles from '../assets/chat-sidebar.module.css';
|
import styles from '../assets/chat-sidebar.module.css';
|
||||||
|
|
@ -10,9 +11,11 @@ import clsx from "clsx";
|
||||||
|
|
||||||
export const ChatSidebar = () => {
|
export const ChatSidebar = () => {
|
||||||
const appState = useAppState();
|
const appState = useAppState();
|
||||||
const { currentStory, dispatch, connection, model } = appState;
|
const { currentStory, dispatch, connection, model, enableThinking } = appState;
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useInputState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isCollapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const messagesRef = useRef<HTMLDivElement>(null);
|
const messagesRef = useRef<HTMLDivElement>(null);
|
||||||
const abortControllerRef = useRef<AbortController>(new AbortController());
|
const abortControllerRef = useRef<AbortController>(new AbortController());
|
||||||
|
|
@ -26,6 +29,12 @@ export const ChatSidebar = () => {
|
||||||
}
|
}
|
||||||
}, [currentStory?.chatMessages.length]);
|
}, [currentStory?.chatMessages.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messagesRef.current) {
|
||||||
|
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [isCollapsed]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
abortControllerRef.current?.abort();
|
abortControllerRef.current?.abort();
|
||||||
|
|
@ -50,7 +59,8 @@ export const ChatSidebar = () => {
|
||||||
message: {
|
message: {
|
||||||
id: assistantMessageId,
|
id: assistantMessageId,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: 'Generating...',
|
content: '',
|
||||||
|
reasoning_content: 'Generating...',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -64,6 +74,7 @@ export const ChatSidebar = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let accumulatedContent = '';
|
let accumulatedContent = '';
|
||||||
|
let accumulatedReasoning = '';
|
||||||
let tool_calls: LLM.ToolCall[] | undefined;
|
let tool_calls: LLM.ToolCall[] | undefined;
|
||||||
|
|
||||||
for await (const chunk of LLM.generateStream(connection, request)) {
|
for await (const chunk of LLM.generateStream(connection, request)) {
|
||||||
|
|
@ -76,6 +87,12 @@ export const ChatSidebar = () => {
|
||||||
const content = delta?.content;
|
const content = delta?.content;
|
||||||
if (content) {
|
if (content) {
|
||||||
accumulatedContent += content;
|
accumulatedContent += content;
|
||||||
|
}
|
||||||
|
const reasoningContent = delta?.reasoning_content;
|
||||||
|
if (reasoningContent) {
|
||||||
|
accumulatedReasoning += reasoningContent;
|
||||||
|
}
|
||||||
|
if (content || reasoningContent) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'ADD_CHAT_MESSAGE',
|
type: 'ADD_CHAT_MESSAGE',
|
||||||
storyId: currentStory.id,
|
storyId: currentStory.id,
|
||||||
|
|
@ -83,6 +100,7 @@ export const ChatSidebar = () => {
|
||||||
id: assistantMessageId,
|
id: assistantMessageId,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: accumulatedContent,
|
content: accumulatedContent,
|
||||||
|
reasoning_content: accumulatedReasoning,
|
||||||
tool_calls,
|
tool_calls,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -95,6 +113,7 @@ export const ChatSidebar = () => {
|
||||||
id: assistantMessageId,
|
id: assistantMessageId,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: accumulatedContent,
|
content: accumulatedContent,
|
||||||
|
reasoning_content: accumulatedReasoning,
|
||||||
tool_calls,
|
tool_calls,
|
||||||
};
|
};
|
||||||
dispatch({
|
dispatch({
|
||||||
|
|
@ -171,7 +190,7 @@ export const ChatSidebar = () => {
|
||||||
const isDisabled = !currentStory || !connection || !model || isLoading;
|
const isDisabled = !currentStory || !connection || !model || isLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar side="right">
|
<Sidebar side="right" onCollapseChanged={setCollapsed}>
|
||||||
<div class={styles.chat}>
|
<div class={styles.chat}>
|
||||||
{!currentStory ? (
|
{!currentStory ? (
|
||||||
<div class={styles.placeholder}>
|
<div class={styles.placeholder}>
|
||||||
|
|
@ -190,10 +209,27 @@ export const ChatSidebar = () => {
|
||||||
{currentStory.chatMessages.map((message) => (
|
{currentStory.chatMessages.map((message) => (
|
||||||
<div key={message.id} class={styles.message} data-role={message.role}>
|
<div key={message.id} class={styles.message} data-role={message.role}>
|
||||||
<div class={styles.role}>{message.role}</div>
|
<div class={styles.role}>{message.role}</div>
|
||||||
|
|
||||||
|
{message.role === 'assistant' && message.reasoning_content && (
|
||||||
|
<div class={styles.reasoningContent}>
|
||||||
|
{message.reasoning_content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={styles.content}
|
class={styles.content}
|
||||||
dangerouslySetInnerHTML={{ __html: highlight(message.content) }}
|
dangerouslySetInnerHTML={{ __html: highlight(message.content, false).trim() }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{message.role === 'assistant' && message.tool_calls && (
|
||||||
|
<div class={styles.toolCalls}>
|
||||||
|
{message.tool_calls.map((tool) => (
|
||||||
|
<span key={tool.id} class={styles.toolBadge}>
|
||||||
|
{tool.function.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{error && (
|
{error && (
|
||||||
|
|
@ -209,10 +245,24 @@ export const ChatSidebar = () => {
|
||||||
)}
|
)}
|
||||||
{currentStory && (
|
{currentStory && (
|
||||||
<div class={styles.inputContainer}>
|
<div class={styles.inputContainer}>
|
||||||
|
{model?.support_thinking &&
|
||||||
|
<label class={styles.toggleContainer}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={enableThinking}
|
||||||
|
onChange={(e) => dispatch({
|
||||||
|
type: 'SET_ENABLE_THINKING',
|
||||||
|
enable: (e.target as HTMLInputElement).checked,
|
||||||
|
})}
|
||||||
|
disabled={isDisabled}
|
||||||
|
/>
|
||||||
|
<span>Enable thinking</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
<textarea
|
<textarea
|
||||||
class={styles.input}
|
class={styles.input}
|
||||||
value={input}
|
value={input}
|
||||||
onInput={(e) => setInput((e.target as HTMLTextAreaElement).value)}
|
onInput={setInput}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={isDisabled ? 'Connect to an LLM server to chat' : 'Type a message...'}
|
placeholder={isDisabled ? 'Connect to an LLM server to chat' : 'Type a message...'}
|
||||||
rows={3}
|
rows={3}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export const SettingsModal = ({ onClose }: Props) => {
|
||||||
const { connection, model, dispatch } = useAppState();
|
const { connection, model, dispatch } = useAppState();
|
||||||
const [url, setUrl] = useInputState(connection?.url ?? "");
|
const [url, setUrl] = useInputState(connection?.url ?? "");
|
||||||
const [apiKey, setApiKey] = useInputState(connection?.apiKey ?? "");
|
const [apiKey, setApiKey] = useInputState(connection?.apiKey ?? "");
|
||||||
const [selectedModel, setSelectedModel] = useInputState(model ?? "");
|
const [selectedModel, setSelectedModel] = useInputState(model?.id ?? "");
|
||||||
const [update, triggerFetch] = useUpdate();
|
const [update, triggerFetch] = useUpdate();
|
||||||
|
|
||||||
const urlRef = useRef(url);
|
const urlRef = useRef(url);
|
||||||
|
|
@ -43,17 +43,18 @@ export const SettingsModal = ({ onClose }: Props) => {
|
||||||
const isLoadingModels = connectionToFetch != null && modelsData == undefined;
|
const isLoadingModels = connectionToFetch != null && modelsData == undefined;
|
||||||
const groupedModels = useMemo(() => {
|
const groupedModels = useMemo(() => {
|
||||||
const sorted = (modelsData ?? []).sort((a, b) => {
|
const sorted = (modelsData ?? []).sort((a, b) => {
|
||||||
// Sort by tool support first (true before false)
|
const aWeight = Number(a.support_tools) * 2 + Number(a.support_thinking);
|
||||||
if (a.support_tools !== b.support_tools) {
|
const bWeight = Number(b.support_tools) * 2 + Number(b.support_thinking);
|
||||||
return a.support_tools ? -1 : 1;
|
if (aWeight !== bWeight) {
|
||||||
|
return bWeight - aWeight;
|
||||||
}
|
}
|
||||||
// Then by max context (bigger first, undefined treated as 0)
|
|
||||||
const aContext = a.max_context ?? 0;
|
const aContext = a.max_context ?? 0;
|
||||||
const bContext = b.max_context ?? 0;
|
const bContext = b.max_context ?? 0;
|
||||||
if (aContext !== bContext) {
|
if (aContext !== bContext) {
|
||||||
return bContext - aContext;
|
return bContext - aContext;
|
||||||
}
|
}
|
||||||
// Then by name (alphabetically)
|
|
||||||
return a.id.localeCompare(b.id);
|
return a.id.localeCompare(b.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -83,9 +84,10 @@ export const SettingsModal = ({ onClose }: Props) => {
|
||||||
type: 'SET_CONNECTION',
|
type: 'SET_CONNECTION',
|
||||||
connection: connectionToFetch,
|
connection: connectionToFetch,
|
||||||
});
|
});
|
||||||
|
const selectedModelInfo = modelsData?.find(m => m.id === selectedModel) ?? null;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_MODEL',
|
type: 'SET_MODEL',
|
||||||
model: selectedModel || null,
|
model: selectedModelInfo,
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
@ -153,7 +155,7 @@ export const SettingsModal = ({ onClose }: Props) => {
|
||||||
<optgroup key={context} label={`${context} context`}>
|
<optgroup key={context} label={`${context} context`}>
|
||||||
{models.map(m => (
|
{models.map(m => (
|
||||||
<option key={m.id} value={m.id}>
|
<option key={m.id} value={m.id}>
|
||||||
{m.support_tools ? '🔧 ' : ''}{m.id} {m.max_length ? `(len: ${m.max_length})` : ''}
|
{m.support_tools ? '🔨' : ''}{m.support_thinking ? '🧠' : ''}{m.id} {m.max_length ? `(len: ${m.max_length})` : ''}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,20 @@ import styles from '../assets/sidebar.module.css';
|
||||||
interface Props {
|
interface Props {
|
||||||
side: 'left' | 'right';
|
side: 'left' | 'right';
|
||||||
children?: ComponentChildren;
|
children?: ComponentChildren;
|
||||||
|
onCollapseChanged?: (collapsed: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar = ({ side, children }: Props) => {
|
export const Sidebar = ({ side, children, onCollapseChanged }: Props) => {
|
||||||
const open = useBool(true);
|
const open = useBool(true);
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
open.toggle();
|
||||||
|
onCollapseChanged?.(!open.value);
|
||||||
|
};
|
||||||
|
|
||||||
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={open.toggle}>
|
<button class={styles.toggle} onClick={handleToggle}>
|
||||||
{side === 'left' ? (open.value ? '◀' : '▶') : (open.value ? '▶' : '◀')}
|
{side === 'left' ? (open.value ? '◀' : '▶') : (open.value ? '▶' : '◀')}
|
||||||
</button>
|
</button>
|
||||||
{open.value && (
|
{open.value && (
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ interface IState {
|
||||||
stories: Story[];
|
stories: Story[];
|
||||||
currentStoryId: string | null;
|
currentStoryId: string | null;
|
||||||
connection: LLM.Connection | null;
|
connection: LLM.Connection | null;
|
||||||
model: string | null;
|
model: LLM.ModelInfo | null;
|
||||||
|
enableThinking: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Actions ─────────────────────────────────────────────────────────────────
|
// ─── Actions ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -32,12 +33,14 @@ type Action =
|
||||||
| { type: 'CREATE_STORY'; title: string }
|
| { type: 'CREATE_STORY'; title: string }
|
||||||
| { type: 'RENAME_STORY'; id: string; title: string }
|
| { type: 'RENAME_STORY'; id: string; title: string }
|
||||||
| { type: 'EDIT_STORY'; id: string; text: string }
|
| { type: 'EDIT_STORY'; id: string; text: string }
|
||||||
|
| { type: 'APPEND_TO_STORY'; id: string; text: string }
|
||||||
| { type: 'DELETE_STORY'; id: string }
|
| { type: 'DELETE_STORY'; id: string }
|
||||||
| { type: 'SELECT_STORY'; id: string }
|
| { type: 'SELECT_STORY'; id: string }
|
||||||
| { type: 'ADD_CHAT_MESSAGE'; storyId: string; message: ChatMessage }
|
| { type: 'ADD_CHAT_MESSAGE'; storyId: string; message: ChatMessage }
|
||||||
| { 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: string | null };
|
| { type: 'SET_MODEL'; model: LLM.ModelInfo | null }
|
||||||
|
| { type: 'SET_ENABLE_THINKING'; enable: boolean };
|
||||||
|
|
||||||
// ─── Initial State ───────────────────────────────────────────────────────────
|
// ─── Initial State ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -46,6 +49,7 @@ const DEFAULT_STATE: IState = {
|
||||||
currentStoryId: null,
|
currentStoryId: null,
|
||||||
connection: null,
|
connection: null,
|
||||||
model: null,
|
model: null,
|
||||||
|
enableThinking: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Reducer ─────────────────────────────────────────────────────────────────
|
// ─── Reducer ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -121,6 +125,14 @@ function reducer(state: IState, action: Action): IState {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'APPEND_TO_STORY': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
stories: state.stories.map(s =>
|
||||||
|
s.id === action.id ? { ...s, text: s.text + action.text } : s
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
case 'SET_CONNECTION': {
|
case 'SET_CONNECTION': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
@ -133,6 +145,12 @@ function reducer(state: IState, action: Action): IState {
|
||||||
model: action.model,
|
model: action.model,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'SET_ENABLE_THINKING': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
enableThinking: action.enable,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,7 +160,8 @@ export interface AppState {
|
||||||
stories: Story[];
|
stories: Story[];
|
||||||
currentStory: Story | null;
|
currentStory: Story | null;
|
||||||
connection: LLM.Connection | null;
|
connection: LLM.Connection | null;
|
||||||
model: string | null;
|
model: LLM.ModelInfo | null;
|
||||||
|
enableThinking: boolean;
|
||||||
dispatch: (action: Action) => void;
|
dispatch: (action: Action) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,6 +179,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
|
||||||
currentStory: state.stories.find(s => s.id === state.currentStoryId) ?? null,
|
currentStory: state.stories.find(s => s.id === state.currentStoryId) ?? null,
|
||||||
connection: state.connection,
|
connection: state.connection,
|
||||||
model: state.model,
|
model: state.model,
|
||||||
|
enableThinking: state.enableThinking,
|
||||||
dispatch,
|
dispatch,
|
||||||
}), [state]);
|
}), [state]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,60 @@
|
||||||
export const highlight = (message: string): string => {
|
export const highlight = (message: string, keepMarkup = true): string => {
|
||||||
const replaceRegex = /(\*\*?|")/ig;
|
let resultHTML = '';
|
||||||
|
const replaceRegex = /(\*\*?|"|```|`)/ig;
|
||||||
const splitToken = '___SPLIT_AWOORWA___';
|
const splitToken = '___SPLIT_AWOORWA___';
|
||||||
|
|
||||||
const preparedMessage = message.replace(replaceRegex, `${splitToken}$1${splitToken}`);
|
const preparedMessage = message.replace(replaceRegex, `${splitToken}$1${splitToken}`);
|
||||||
const parts = preparedMessage.split(splitToken);
|
const parts = preparedMessage.split(splitToken);
|
||||||
|
|
||||||
const stack: string[] = [];
|
const stack: string[] = [];
|
||||||
|
let inCodeBlock = false;
|
||||||
let resultHTML = '';
|
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
const isClose = stack.at(-1) === part;
|
const isClose = stack.at(-1) === part;
|
||||||
|
const keepPart = keepMarkup || part === '"';
|
||||||
|
|
||||||
|
if (inCodeBlock) {
|
||||||
|
if (part === '```' && isClose) {
|
||||||
|
inCodeBlock = false;
|
||||||
|
stack.pop();
|
||||||
|
resultHTML += `${keepPart ? part : ''}</span>`;
|
||||||
|
} else {
|
||||||
|
resultHTML += part;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (isClose) {
|
if (isClose) {
|
||||||
stack.pop();
|
stack.pop();
|
||||||
if (part === '*' || part === '**' || part === '"') {
|
if (part === '*' || part === '**' || part === '"' || part === '`' || part === '```') {
|
||||||
resultHTML += `${part}</span>`;
|
resultHTML += `${keepPart ? part : ''}</span>`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (part === '*') {
|
if (part === '*') {
|
||||||
stack.push(part);
|
stack.push(part);
|
||||||
resultHTML += `<span style="font-style:italic;color:var(--italicColor)">`;
|
resultHTML += `<span style="font-style:italic;color:var(--italicColor)">${keepPart ? part : ''}`;
|
||||||
} else if (part === '**') {
|
} else if (part === '**') {
|
||||||
stack.push(part);
|
stack.push(part);
|
||||||
resultHTML += `<span style="font-weight:bold">`;
|
resultHTML += `<span style="font-weight:bold">${keepPart ? part : ''}`;
|
||||||
} else if (part === '"') {
|
} else if (part === '"') {
|
||||||
stack.push(part);
|
stack.push(part);
|
||||||
resultHTML += `<span style="color:var(--quoteColor)">`;
|
resultHTML += `<span style="color:var(--quoteColor)">"`;
|
||||||
}
|
} else if (part === '```') {
|
||||||
|
stack.push(part);
|
||||||
|
inCodeBlock = true;
|
||||||
|
resultHTML += `<span style="font-family:monospace;background:var(--codeBg);padding:0.5em;display:block;border-radius:var(--radius)">`;
|
||||||
|
} else if (part === '`') {
|
||||||
|
stack.push(part);
|
||||||
|
resultHTML += `<span style="font-family:monospace;background:var(--codeBg);padding:0.1em 0.3em;border-radius:0.2em">`;
|
||||||
|
} else {
|
||||||
resultHTML += part;
|
resultHTML += part;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (stack.length) {
|
while (stack.length) {
|
||||||
const part = stack.pop();
|
const part = stack.pop();
|
||||||
if (part === '*' || part === '**' || part === '"') {
|
if (part === '*' || part === '**' || part === '"' || part === '`' || part === '```') {
|
||||||
resultHTML += `</span>`;
|
resultHTML += `</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ namespace LLM {
|
||||||
interface ChatMessageAssistant {
|
interface ChatMessageAssistant {
|
||||||
role: 'assistant';
|
role: 'assistant';
|
||||||
content: string;
|
content: string;
|
||||||
|
reasoning_content?: string;
|
||||||
tool_calls?: ToolCall[];
|
tool_calls?: ToolCall[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,11 +79,7 @@ namespace LLM {
|
||||||
function: {
|
function: {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
parameters: {
|
parameters: ToolObjectParameter;
|
||||||
type: 'object';
|
|
||||||
properties: Record<string, ToolParameter>;
|
|
||||||
required?: string[];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,6 +94,7 @@ namespace LLM {
|
||||||
top_p?: number;
|
top_p?: number;
|
||||||
frequency_penalty?: number;
|
frequency_penalty?: number;
|
||||||
presence_penalty?: number;
|
presence_penalty?: number;
|
||||||
|
enable_thinking?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatCompletionChoice {
|
export interface ChatCompletionChoice {
|
||||||
|
|
@ -121,7 +119,12 @@ namespace LLM {
|
||||||
|
|
||||||
export interface ChatCompletionChunkChoice {
|
export interface ChatCompletionChunkChoice {
|
||||||
index: number;
|
index: number;
|
||||||
delta: { role?: string; content?: string; tool_calls?: ToolCall[] };
|
delta: {
|
||||||
|
role?: string;
|
||||||
|
content?: string;
|
||||||
|
reasoning_content?: string;
|
||||||
|
tool_calls?: ToolCall[];
|
||||||
|
};
|
||||||
finish_reason: 'stop' | 'tool_calls' | null;
|
finish_reason: 'stop' | 'tool_calls' | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,6 +142,8 @@ namespace LLM {
|
||||||
created: number;
|
created: number;
|
||||||
owned_by: string;
|
owned_by: string;
|
||||||
support_tools: boolean;
|
support_tools: boolean;
|
||||||
|
support_infill: boolean;
|
||||||
|
support_thinking: boolean;
|
||||||
max_context?: number;
|
max_context?: number;
|
||||||
max_length?: number;
|
max_length?: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Tools } from "./tools";
|
||||||
|
|
||||||
namespace Prompt {
|
namespace Prompt {
|
||||||
export function compilePrompt(state: AppState, newMessages: LLM.ChatMessage[] = []): LLM.ChatCompletionRequest | null {
|
export function compilePrompt(state: AppState, newMessages: LLM.ChatMessage[] = []): LLM.ChatCompletionRequest | null {
|
||||||
const { currentStory, model } = state;
|
const { currentStory, model, enableThinking } = state;
|
||||||
|
|
||||||
if (!currentStory || !model) {
|
if (!currentStory || !model) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -19,10 +19,11 @@ namespace Prompt {
|
||||||
messages.push(...newMessages);
|
messages.push(...newMessages);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model,
|
model: model.id,
|
||||||
messages,
|
messages,
|
||||||
tools: Tools.getTools(),
|
tools: Tools.getTools(),
|
||||||
// TODO banned_tokens
|
// TODO banned_tokens
|
||||||
|
enable_thinking: enableThinking,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { formatError } from "@common/errors";
|
import { formatError } from "@common/errors";
|
||||||
import type { AppState } from "../contexts/state";
|
import type { AppState } from "../contexts/state";
|
||||||
import LLM from "./llm";
|
import type LLM from "./llm";
|
||||||
|
|
||||||
export namespace Tools {
|
export namespace Tools {
|
||||||
interface Tool {
|
interface Tool {
|
||||||
|
|
@ -10,20 +10,35 @@ export namespace Tools {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOOLS: Record<string, Tool> = {
|
const TOOLS: Record<string, Tool> = {
|
||||||
'test': {
|
'append_to_story': {
|
||||||
handler: async (args) => (
|
handler: async (args, appState) => {
|
||||||
`Test successful, received: ${JSON.stringify(args)}`
|
if (!args || typeof args !== 'object' || !('text' in args)) {
|
||||||
),
|
return 'Error: Missing required argument "text"';
|
||||||
description: 'A simple test function',
|
}
|
||||||
|
const { text } = args as { text: string };
|
||||||
|
if (typeof text !== 'string') {
|
||||||
|
return 'Error: Argument "text" must be a string';
|
||||||
|
}
|
||||||
|
if (!appState.currentStory) {
|
||||||
|
return 'Error: No story selected';
|
||||||
|
}
|
||||||
|
appState.dispatch({
|
||||||
|
type: 'APPEND_TO_STORY',
|
||||||
|
id: appState.currentStory.id,
|
||||||
|
text
|
||||||
|
});
|
||||||
|
return 'Text appended successfully';
|
||||||
|
},
|
||||||
|
description: 'Append text to the current story',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
message: {
|
text: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The test message',
|
description: 'The text to append to the story',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['message'],
|
required: ['text'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -57,6 +72,9 @@ export namespace Tools {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await handler(args, appState);
|
const result = await handler(args, appState);
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
return JSON.stringify(result);
|
return JSON.stringify(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return formatError(err, 'Error executing tool');
|
return formatError(err, 'Error executing tool');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue