mirror of
https://github.com/LostRuins/koboldcpp.git
synced 2026-09-18 16:55:14 +02:00
Merge commit '4ef9301e4de5ea8f5ebf20b107cf30de0070bf2a' into concedo_experimental
# Conflicts: # .github/labeler.yml # docs/multimodal.md # embd_res/ggml-vocab-gemma-4.gguf # embd_res/ggml-vocab-gemma-4.gguf.inp # embd_res/ggml-vocab-gemma-4.gguf.out # ggml/src/ggml-sycl/fattn-tile.cpp # ggml/src/ggml-sycl/fattn-tile.hpp # ggml/src/ggml-sycl/fattn-vec.hpp # ggml/src/ggml-sycl/fattn.cpp # ggml/src/ggml-sycl/ggml-sycl.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-f16-f16.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-f16-q4_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-f16-q4_1.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-f16-q5_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-f16-q5_1.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-f16-q8_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q4_0-f16.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q4_0-q4_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q4_0-q4_1.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q4_0-q5_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q4_0-q5_1.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q4_0-q8_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q4_1-f16.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q4_1-q4_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q4_1-q4_1.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q4_1-q5_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q4_1-q5_1.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q4_1-q8_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q5_0-f16.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q5_0-q4_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q5_0-q4_1.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q5_0-q5_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q5_0-q5_1.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q5_0-q8_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q5_1-f16.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q5_1-q4_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q5_1-q4_1.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q5_1-q5_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q5_1-q5_1.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q5_1-q8_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q8_0-f16.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q8_0-q4_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q8_0-q4_1.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q8_0-q5_0.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q8_0-q5_1.cpp # ggml/src/ggml-sycl/template-instances/fattn-vec-instance-q8_0-q8_0.cpp # tests/CMakeLists.txt # tests/test-jinja.cpp # tools/mtmd/CMakeLists.txt
This commit is contained in:
@@ -294,11 +294,16 @@
|
||||
}
|
||||
|
||||
if (event.key === KeyboardKey.ENTER && !event.shiftKey && !isIMEComposing(event)) {
|
||||
event.preventDefault();
|
||||
const isModifier = event.ctrlKey || event.metaKey;
|
||||
const sendOnEnter = currentConfig.sendOnEnter !== false;
|
||||
|
||||
if (!canSubmit || disabled || isLoading || hasLoadingAttachments) return;
|
||||
if (sendOnEnter || isModifier) {
|
||||
event.preventDefault();
|
||||
|
||||
onSubmit?.();
|
||||
if (!canSubmit || disabled || isLoading || hasLoadingAttachments) return;
|
||||
|
||||
onSubmit?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -62,10 +62,14 @@
|
||||
chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
|
||||
);
|
||||
|
||||
let lastSyncedConversationModel: string | null = null;
|
||||
|
||||
$effect(() => {
|
||||
if (conversationModel) {
|
||||
if (conversationModel && conversationModel !== lastSyncedConversationModel) {
|
||||
lastSyncedConversationModel = conversationModel;
|
||||
modelsStore.selectModelByName(conversationModel);
|
||||
} else if (isRouter && !modelsStore.selectedModelId && modelsStore.loadedModelIds.length > 0) {
|
||||
lastSyncedConversationModel = null;
|
||||
// auto-select the first loaded model only when nothing is selected yet
|
||||
const first = modelOptions().find((m) => modelsStore.loadedModelIds.includes(m.model));
|
||||
if (first) modelsStore.selectModelById(first.id);
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
let { class: className = '', show = true }: Props = $props();
|
||||
|
||||
let sendOnEnter = $derived(config().sendOnEnter !== false);
|
||||
let modKey = browser && /Mac|iPhone|iPad|iPod/.test(navigator.platform) ? 'Cmd' : 'Ctrl';
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div class="mt-6 items-center justify-center {className} hidden md:flex">
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Press <kbd class="rounded bg-muted px-1 py-0.5 font-mono text-xs">Enter</kbd> to send,
|
||||
<kbd class="rounded bg-muted px-1 py-0.5 font-mono text-xs">Shift + Enter</kbd> for new line
|
||||
</p>
|
||||
{#if sendOnEnter}
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Press <kbd class="rounded bg-muted px-1 py-0.5 font-mono text-xs">Enter</kbd> to send,
|
||||
<kbd class="rounded bg-muted px-1 py-0.5 font-mono text-xs">Shift + Enter</kbd> for new line
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Press <kbd class="rounded bg-muted px-1 py-0.5 font-mono text-xs">{modKey} + Enter</kbd> to send,
|
||||
<kbd class="rounded bg-muted px-1 py-0.5 font-mono text-xs">Enter</kbd> for new line
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -64,6 +64,11 @@
|
||||
label: 'Paste long text to file length',
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.SEND_ON_ENTER,
|
||||
label: 'Send message on Enter',
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
|
||||
label: 'Copy text attachments as plain text',
|
||||
@@ -291,14 +296,19 @@
|
||||
title: SETTINGS_SECTION_TITLES.DEVELOPER,
|
||||
icon: Code,
|
||||
fields: [
|
||||
{
|
||||
key: SETTINGS_KEYS.PRE_ENCODE_CONVERSATION,
|
||||
label: 'Pre-fill KV cache after response',
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.DISABLE_REASONING_PARSING,
|
||||
label: 'Disable reasoning content parsing',
|
||||
label: 'Disable server-side thinking extraction',
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.EXCLUDE_REASONING_FROM_CONTEXT,
|
||||
label: 'Exclude reasoning from context',
|
||||
label: 'Strip thinking from message history',
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@ export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean |
|
||||
renderUserContentAsMarkdown: false,
|
||||
alwaysShowSidebarOnDesktop: false,
|
||||
autoShowSidebarOnNewChat: true,
|
||||
sendOnEnter: true,
|
||||
autoMicOnEmpty: false,
|
||||
fullHeightCodeBlocks: false,
|
||||
showRawModelNames: false,
|
||||
@@ -56,6 +57,7 @@ export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean |
|
||||
dry_penalty_last_n: undefined,
|
||||
max_tokens: undefined,
|
||||
custom: '', // custom json-stringified object
|
||||
preEncodeConversation: false,
|
||||
// experimental features
|
||||
pyInterpreterEnabled: false,
|
||||
enableContinueGeneration: true
|
||||
@@ -106,9 +108,9 @@ export const SETTING_CONFIG_INFO: Record<string, string> = {
|
||||
custom: 'Custom JSON parameters to send to the API. Must be valid JSON format.',
|
||||
showThoughtInProgress: 'Expand thought process by default when generating messages.',
|
||||
disableReasoningParsing:
|
||||
'Send reasoning_format=none to prevent server-side extraction of reasoning tokens into separate field',
|
||||
'Send reasoning_format=none so the server returns thinking tokens inline instead of extracting them into a separate field.',
|
||||
excludeReasoningFromContext:
|
||||
'Strip reasoning content from previous messages before sending to the model. When unchecked, reasoning is sent back via the reasoning_content field so the model can see its own chain-of-thought across turns.',
|
||||
'Strip thinking from previous messages before sending. When off, thinking is sent back via the reasoning_content field so the model sees its own chain-of-thought across turns.',
|
||||
showRawOutputSwitch:
|
||||
'Show toggle button to display messages as plain text instead of Markdown-formatted content',
|
||||
keepStatsVisible: 'Keep processing statistics visible after generation finishes.',
|
||||
@@ -125,6 +127,8 @@ export const SETTING_CONFIG_INFO: Record<string, string> = {
|
||||
'Always keep the sidebar visible on desktop instead of auto-hiding it.',
|
||||
autoShowSidebarOnNewChat:
|
||||
'Automatically show sidebar when starting a new chat. Disable to keep the sidebar hidden until you click on it.',
|
||||
sendOnEnter:
|
||||
'Use Enter to send messages and Shift + Enter for new lines. When disabled, use Ctrl/Cmd + Enter.',
|
||||
autoMicOnEmpty:
|
||||
'Automatically show microphone button instead of send button when textarea is empty for models with audio modality support.',
|
||||
fullHeightCodeBlocks:
|
||||
@@ -143,6 +147,8 @@ export const SETTING_CONFIG_INFO: Record<string, string> = {
|
||||
'Automatically expand tool call details while executing and keep them expanded after completion.',
|
||||
pyInterpreterEnabled:
|
||||
'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.',
|
||||
preEncodeConversation:
|
||||
'After each response, re-submit the conversation to pre-fill the server KV cache. Makes the next turn faster since the prompt is already encoded while you read the response.',
|
||||
enableContinueGeneration:
|
||||
'Enable "Continue" button for assistant messages. Currently works only with non-reasoning models.'
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ export const SETTINGS_KEYS = {
|
||||
SYSTEM_MESSAGE: 'systemMessage',
|
||||
PASTE_LONG_TEXT_TO_FILE_LEN: 'pasteLongTextToFileLen',
|
||||
COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT: 'copyTextAttachmentsAsPlainText',
|
||||
SEND_ON_ENTER: 'sendOnEnter',
|
||||
ENABLE_CONTINUE_GENERATION: 'enableContinueGeneration',
|
||||
PDF_AS_IMAGE: 'pdfAsImage',
|
||||
ASK_FOR_TITLE_CONFIRMATION: 'askForTitleConfirmation',
|
||||
@@ -52,6 +53,8 @@ export const SETTINGS_KEYS = {
|
||||
ALWAYS_SHOW_AGENTIC_TURNS: 'alwaysShowAgenticTurns',
|
||||
AGENTIC_MAX_TOOL_PREVIEW_LINES: 'agenticMaxToolPreviewLines',
|
||||
SHOW_TOOL_CALL_IN_PROGRESS: 'showToolCallInProgress',
|
||||
// Performance
|
||||
PRE_ENCODE_CONVERSATION: 'preEncodeConversation',
|
||||
// Developer
|
||||
DISABLE_REASONING_PARSING: 'disableReasoningParsing',
|
||||
EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext',
|
||||
|
||||
@@ -4,7 +4,8 @@ import { isAbortError } from '$lib/utils/abort';
|
||||
import {
|
||||
ATTACHMENT_LABEL_PDF_FILE,
|
||||
ATTACHMENT_LABEL_MCP_PROMPT,
|
||||
ATTACHMENT_LABEL_MCP_RESOURCE
|
||||
ATTACHMENT_LABEL_MCP_RESOURCE,
|
||||
LEGACY_AGENTIC_REGEX
|
||||
} from '$lib/constants';
|
||||
import {
|
||||
AttachmentType,
|
||||
@@ -279,6 +280,107 @@ export class ChatService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether all server slots are currently idle (not processing any requests).
|
||||
* Queries the /slots endpoint (requires --slots flag on the server).
|
||||
* Returns true if all slots are idle, false if any is processing.
|
||||
* If the endpoint is unavailable or errors out, returns true (best-effort fallback).
|
||||
*
|
||||
* @param signal - Optional AbortSignal to cancel the request if needed
|
||||
* @param model - Optional model name to check slots for (required in ROUTER mode)
|
||||
* @returns {Promise<boolean>} Promise that resolves to true if all slots are idle, false if any is processing
|
||||
*/
|
||||
static async areAllSlotsIdle(model?: string | null, signal?: AbortSignal): Promise<boolean> {
|
||||
try {
|
||||
const url = model ? `./slots?model=${encodeURIComponent(model)}` : './slots';
|
||||
const res = await fetch(url, { signal });
|
||||
if (!res.ok) return true;
|
||||
|
||||
const slots: { is_processing: boolean }[] = await res.json();
|
||||
return slots.every((s) => !s.is_processing);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a fire-and-forget request to pre-encode the conversation in the server's KV cache.
|
||||
* After a response completes, this re-submits the full conversation
|
||||
* using n_predict=0 and stream=false so the server processes the prompt without generating tokens.
|
||||
* This warms the cache for the next turn, making it faster.
|
||||
*
|
||||
* When excludeReasoningFromContext is true, reasoning content is stripped from the messages
|
||||
* to match what sendMessage would send on the next turn (avoiding cache misses).
|
||||
* When false, reasoning_content is preserved so the cached prompt matches the next request.
|
||||
*
|
||||
* @param messages - The full conversation including the latest assistant response
|
||||
* @param model - Optional model name (required in ROUTER mode)
|
||||
* @param excludeReasoning - Whether to strip reasoning content (should match excludeReasoningFromContext setting)
|
||||
* @param signal - Optional AbortSignal to cancel the pre-encode request
|
||||
*/
|
||||
static async preEncode(
|
||||
messages: ApiChatMessageData[] | (DatabaseMessage & { extra?: DatabaseMessageExtra[] })[],
|
||||
model?: string | null,
|
||||
excludeReasoning?: boolean,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> {
|
||||
const normalizedMessages: ApiChatMessageData[] = messages
|
||||
.map((msg) => {
|
||||
if ('id' in msg && 'convId' in msg && 'timestamp' in msg) {
|
||||
return ChatService.convertDbMessageToApiChatMessageData(
|
||||
msg as DatabaseMessage & { extra?: DatabaseMessageExtra[] }
|
||||
);
|
||||
}
|
||||
|
||||
return msg as ApiChatMessageData;
|
||||
})
|
||||
.filter((msg) => {
|
||||
if (msg.role === MessageRole.SYSTEM) {
|
||||
const content = typeof msg.content === 'string' ? msg.content : '';
|
||||
|
||||
return content.trim().length > 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const requestBody: Record<string, unknown> = {
|
||||
messages: normalizedMessages.map((msg: ApiChatMessageData) => {
|
||||
const mapped: Record<string, unknown> = {
|
||||
role: msg.role,
|
||||
content: excludeReasoning ? ChatService.stripReasoningContent(msg.content) : msg.content,
|
||||
tool_calls: msg.tool_calls,
|
||||
tool_call_id: msg.tool_call_id
|
||||
};
|
||||
|
||||
if (!excludeReasoning && msg.reasoning_content) {
|
||||
mapped.reasoning_content = msg.reasoning_content;
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}),
|
||||
stream: false,
|
||||
n_predict: 0
|
||||
};
|
||||
|
||||
if (model) {
|
||||
requestBody.model = model;
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch(`./v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: getJsonHeaders(),
|
||||
body: JSON.stringify(requestBody),
|
||||
signal
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isAbortError(error)) {
|
||||
console.warn('[ChatService] Pre-encode request failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
@@ -803,6 +905,28 @@ export class ChatService {
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Strips legacy inline reasoning content tags from message content.
|
||||
* Handles both plain string content and multipart content arrays.
|
||||
*/
|
||||
private static stripReasoningContent(
|
||||
content: string | ApiChatMessageContentPart[]
|
||||
): string | ApiChatMessageContentPart[] {
|
||||
const stripFromString = (text: string): string =>
|
||||
text.replace(LEGACY_AGENTIC_REGEX.REASONING_BLOCK, '').trim();
|
||||
|
||||
if (typeof content === 'string') {
|
||||
return stripFromString(content);
|
||||
}
|
||||
|
||||
return content.map((part) => {
|
||||
if (part.type === ContentPartType.TEXT && part.text) {
|
||||
return { ...part, text: stripFromString(part.text) };
|
||||
}
|
||||
return part;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses error response and creates appropriate error with context information
|
||||
* @param response - HTTP response object
|
||||
|
||||
@@ -239,6 +239,12 @@ export const SYNCABLE_PARAMETERS: SyncableParameter[] = [
|
||||
serverKey: 'excludeReasoningFromContext',
|
||||
type: SyncableParameterType.BOOLEAN,
|
||||
canSync: true
|
||||
},
|
||||
{
|
||||
key: 'sendOnEnter',
|
||||
serverKey: 'sendOnEnter',
|
||||
type: SyncableParameterType.BOOLEAN,
|
||||
canSync: true
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ class ChatStore {
|
||||
chatLoadingStates = new SvelteMap<string, boolean>();
|
||||
chatStreamingStates = new SvelteMap<string, { response: string; messageId: string }>();
|
||||
private abortControllers = new SvelteMap<string, AbortController>();
|
||||
private preEncodeAbortController: AbortController | null = null;
|
||||
private processingStates = new SvelteMap<string, ApiProcessingState | null>();
|
||||
private conversationStateTimestamps = new SvelteMap<string, ConversationStateEntry>();
|
||||
private activeConversationId = $state<string | null>(null);
|
||||
@@ -462,6 +463,9 @@ class ChatStore {
|
||||
const activeConv = conversationsStore.activeConversation;
|
||||
if (activeConv && this.isChatLoadingInternal(activeConv.id)) return;
|
||||
|
||||
// Cancel any in-flight pre-encode request
|
||||
this.cancelPreEncode();
|
||||
|
||||
// Consume MCP resource attachments - converts them to extras and clears the live store
|
||||
const resourceExtras = mcpStore.consumeResourceAttachmentsAsExtras();
|
||||
const allExtras = resourceExtras.length > 0 ? [...(extras || []), ...resourceExtras] : extras;
|
||||
@@ -724,6 +728,16 @@ class ChatStore {
|
||||
|
||||
if (onComplete) onComplete(streamedContent);
|
||||
if (isRouterMode()) modelsStore.fetchRouterModels().catch(console.error);
|
||||
// Pre-encode conversation in KV cache for faster next turn
|
||||
if (config().preEncodeConversation) {
|
||||
this.triggerPreEncode(
|
||||
allMessages,
|
||||
assistantMessage,
|
||||
streamedContent,
|
||||
effectiveModel,
|
||||
!!config().excludeReasoningFromContext
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
this.setStreamingActive(false);
|
||||
@@ -911,6 +925,7 @@ class ChatStore {
|
||||
async regenerateMessage(messageId: string): Promise<void> {
|
||||
const activeConv = conversationsStore.activeConversation;
|
||||
if (!activeConv || this.isChatLoadingInternal(activeConv.id)) return;
|
||||
this.cancelPreEncode();
|
||||
const result = this.getMessageByIdWithRole(messageId, MessageRole.ASSISTANT);
|
||||
if (!result) return;
|
||||
const { index: messageIndex } = result;
|
||||
@@ -940,6 +955,7 @@ class ChatStore {
|
||||
async regenerateMessageWithBranching(messageId: string, modelOverride?: string): Promise<void> {
|
||||
const activeConv = conversationsStore.activeConversation;
|
||||
if (!activeConv || this.isChatLoadingInternal(activeConv.id)) return;
|
||||
this.cancelPreEncode();
|
||||
try {
|
||||
const idx = conversationsStore.findMessageIndex(messageId);
|
||||
if (idx === -1) return;
|
||||
@@ -1616,6 +1632,42 @@ class ChatStore {
|
||||
|
||||
return apiOptions;
|
||||
}
|
||||
|
||||
private cancelPreEncode(): void {
|
||||
if (this.preEncodeAbortController) {
|
||||
this.preEncodeAbortController.abort();
|
||||
this.preEncodeAbortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async triggerPreEncode(
|
||||
allMessages: DatabaseMessage[],
|
||||
assistantMessage: DatabaseMessage,
|
||||
assistantContent: string,
|
||||
model?: string | null,
|
||||
excludeReasoning?: boolean
|
||||
): Promise<void> {
|
||||
this.cancelPreEncode();
|
||||
this.preEncodeAbortController = new AbortController();
|
||||
|
||||
const signal = this.preEncodeAbortController.signal;
|
||||
|
||||
try {
|
||||
const allIdle = await ChatService.areAllSlotsIdle(model, signal);
|
||||
if (!allIdle || signal.aborted) return;
|
||||
|
||||
const messagesWithAssistant: DatabaseMessage[] = [
|
||||
...allMessages,
|
||||
{ ...assistantMessage, content: assistantContent }
|
||||
];
|
||||
|
||||
await ChatService.preEncode(messagesWithAssistant, model, excludeReasoning, signal);
|
||||
} catch (err) {
|
||||
if (!isAbortError(err)) {
|
||||
console.warn('[ChatStore] Pre-encode failed:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const chatStore = new ChatStore();
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
SETTING_CONFIG_DEFAULT,
|
||||
USER_OVERRIDES_LOCALSTORAGE_KEY
|
||||
} from '$lib/constants';
|
||||
import { IsMobile } from '$lib/hooks/is-mobile.svelte';
|
||||
import { ParameterSyncService } from '$lib/services/parameter-sync.service';
|
||||
import { serverStore } from '$lib/stores/server.svelte';
|
||||
import {
|
||||
@@ -122,6 +123,13 @@ class SettingsStore {
|
||||
...savedVal
|
||||
};
|
||||
|
||||
// Default sendOnEnter to false on mobile when the user has no saved preference
|
||||
if (!('sendOnEnter' in savedVal)) {
|
||||
if (new IsMobile().current) {
|
||||
this.config.sendOnEnter = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load user overrides
|
||||
const savedOverrides = JSON.parse(
|
||||
localStorage.getItem(USER_OVERRIDES_LOCALSTORAGE_KEY) || '[]'
|
||||
|
||||
@@ -77,6 +77,11 @@
|
||||
!modelsStore.isModelLoaded(modelsStore.selectedModelName)
|
||||
) {
|
||||
modelsStore.clearSelection();
|
||||
|
||||
const first = modelOptions().find((m) => modelsStore.loadedModelIds.includes(m.model));
|
||||
if (first) {
|
||||
await modelsStore.selectModelById(first.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle URL params only if we have ?q= or ?model= or ?new_chat=true
|
||||
|
||||
Reference in New Issue
Block a user