(webui) FEATURE: Enable adding or injecting System Message into chat (#19556)
* feat: Enable adding System Prompt per-chat * fix: Save draft message in Chat Form when adding System Prompt from new chat view * fix: Proper system message deletion logic * chore: Formatting * chore: update webui build output
This commit is contained in:
parent
ff599039a9
commit
4d688f9ebb
Binary file not shown.
|
|
@ -27,11 +27,13 @@
|
|||
interface Props {
|
||||
class?: string;
|
||||
disabled?: boolean;
|
||||
initialMessage?: string;
|
||||
isLoading?: boolean;
|
||||
onFileRemove?: (fileId: string) => void;
|
||||
onFileUpload?: (files: File[]) => void;
|
||||
onSend?: (message: string, files?: ChatUploadedFile[]) => Promise<boolean>;
|
||||
onStop?: () => void;
|
||||
onSystemPromptAdd?: (draft: { message: string; files: ChatUploadedFile[] }) => void;
|
||||
showHelperText?: boolean;
|
||||
uploadedFiles?: ChatUploadedFile[];
|
||||
}
|
||||
|
|
@ -39,11 +41,13 @@
|
|||
let {
|
||||
class: className,
|
||||
disabled = false,
|
||||
initialMessage = '',
|
||||
isLoading = false,
|
||||
onFileRemove,
|
||||
onFileUpload,
|
||||
onSend,
|
||||
onStop,
|
||||
onSystemPromptAdd,
|
||||
showHelperText = true,
|
||||
uploadedFiles = $bindable([])
|
||||
}: Props = $props();
|
||||
|
|
@ -53,15 +57,28 @@
|
|||
let currentConfig = $derived(config());
|
||||
let fileInputRef: ChatFormFileInputInvisible | undefined = $state(undefined);
|
||||
let isRecording = $state(false);
|
||||
let message = $state('');
|
||||
let message = $state(initialMessage);
|
||||
let pasteLongTextToFileLength = $derived.by(() => {
|
||||
const n = Number(currentConfig.pasteLongTextToFileLen);
|
||||
return Number.isNaN(n) ? Number(SETTING_CONFIG_DEFAULT.pasteLongTextToFileLen) : n;
|
||||
});
|
||||
let previousIsLoading = $state(isLoading);
|
||||
let previousInitialMessage = $state(initialMessage);
|
||||
let recordingSupported = $state(false);
|
||||
let textareaRef: ChatFormTextarea | undefined = $state(undefined);
|
||||
|
||||
// Sync message when initialMessage prop changes (e.g., after draft restoration)
|
||||
$effect(() => {
|
||||
if (initialMessage !== previousInitialMessage) {
|
||||
message = initialMessage;
|
||||
previousInitialMessage = initialMessage;
|
||||
}
|
||||
});
|
||||
|
||||
function handleSystemPromptClick() {
|
||||
onSystemPromptAdd?.({ message, files: uploadedFiles });
|
||||
}
|
||||
|
||||
// Check if model is selected (in ROUTER mode)
|
||||
let conversationModel = $derived(
|
||||
chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
|
||||
|
|
@ -308,6 +325,7 @@
|
|||
onFileUpload={handleFileUpload}
|
||||
onMicClick={handleMicClick}
|
||||
onStop={handleStop}
|
||||
onSystemPromptClick={handleSystemPromptClick}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { Paperclip } from '@lucide/svelte';
|
||||
import { MessageSquare } from '@lucide/svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
|
|
@ -11,6 +12,7 @@
|
|||
hasAudioModality?: boolean;
|
||||
hasVisionModality?: boolean;
|
||||
onFileUpload?: () => void;
|
||||
onSystemPromptClick?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -18,7 +20,8 @@
|
|||
disabled = false,
|
||||
hasAudioModality = false,
|
||||
hasVisionModality = false,
|
||||
onFileUpload
|
||||
onFileUpload,
|
||||
onSystemPromptClick
|
||||
}: Props = $props();
|
||||
|
||||
const fileUploadTooltipText = $derived.by(() => {
|
||||
|
|
@ -118,6 +121,23 @@
|
|||
</Tooltip.Content>
|
||||
{/if}
|
||||
</Tooltip.Root>
|
||||
<DropdownMenu.Separator />
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger class="w-full">
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={() => onSystemPromptClick?.()}
|
||||
>
|
||||
<MessageSquare class="h-4 w-4" />
|
||||
|
||||
<span>System Prompt</span>
|
||||
</DropdownMenu.Item>
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Add a custom system message for this conversation</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
onFileUpload?: () => void;
|
||||
onMicClick?: () => void;
|
||||
onStop?: () => void;
|
||||
onSystemPromptClick?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -39,7 +40,8 @@
|
|||
uploadedFiles = [],
|
||||
onFileUpload,
|
||||
onMicClick,
|
||||
onStop
|
||||
onStop,
|
||||
onSystemPromptClick
|
||||
}: Props = $props();
|
||||
|
||||
let currentConfig = $derived(config());
|
||||
|
|
@ -170,6 +172,7 @@
|
|||
{hasAudioModality}
|
||||
{hasVisionModality}
|
||||
{onFileUpload}
|
||||
{onSystemPromptClick}
|
||||
/>
|
||||
|
||||
<ModelsSelector
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
chatStore,
|
||||
pendingEditMessageId,
|
||||
clearPendingEditMessageId,
|
||||
removeSystemPromptPlaceholder
|
||||
} from '$lib/stores/chat.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { DatabaseService } from '$lib/services';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants/ui';
|
||||
import { copyToClipboard, isIMEComposing, formatMessageForClipboard } from '$lib/utils';
|
||||
import ChatMessageAssistant from './ChatMessageAssistant.svelte';
|
||||
import ChatMessageUser from './ChatMessageUser.svelte';
|
||||
|
|
@ -92,8 +101,30 @@
|
|||
return null;
|
||||
});
|
||||
|
||||
function handleCancelEdit() {
|
||||
// Auto-start edit mode if this message is the pending edit target
|
||||
$effect(() => {
|
||||
const pendingId = pendingEditMessageId();
|
||||
|
||||
if (pendingId && pendingId === message.id && !isEditing) {
|
||||
handleEdit();
|
||||
clearPendingEditMessageId();
|
||||
}
|
||||
});
|
||||
|
||||
async function handleCancelEdit() {
|
||||
isEditing = false;
|
||||
|
||||
// If canceling a new system message with placeholder content, remove it without deleting children
|
||||
if (message.role === 'system') {
|
||||
const conversationDeleted = await removeSystemPromptPlaceholder(message.id);
|
||||
|
||||
if (conversationDeleted) {
|
||||
goto('/');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
editedContent = message.content;
|
||||
editedExtras = message.extra ? [...message.extra] : [];
|
||||
editedUploadedFiles = [];
|
||||
|
|
@ -114,8 +145,17 @@
|
|||
onCopy?.(message);
|
||||
}
|
||||
|
||||
function handleConfirmDelete() {
|
||||
onDelete?.(message);
|
||||
async function handleConfirmDelete() {
|
||||
if (message.role === 'system') {
|
||||
const conversationDeleted = await removeSystemPromptPlaceholder(message.id);
|
||||
|
||||
if (conversationDeleted) {
|
||||
goto('/');
|
||||
}
|
||||
} else {
|
||||
onDelete?.(message);
|
||||
}
|
||||
|
||||
showDeleteDialog = false;
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +166,12 @@
|
|||
|
||||
function handleEdit() {
|
||||
isEditing = true;
|
||||
editedContent = message.content;
|
||||
// Clear placeholder content for system messages
|
||||
editedContent =
|
||||
message.role === 'system' && message.content === SYSTEM_MESSAGE_PLACEHOLDER
|
||||
? ''
|
||||
: message.content;
|
||||
textareaElement?.focus();
|
||||
editedExtras = message.extra ? [...message.extra] : [];
|
||||
editedUploadedFiles = [];
|
||||
|
||||
|
|
@ -166,7 +211,26 @@
|
|||
}
|
||||
|
||||
async function handleSaveEdit() {
|
||||
if (message.role === 'user' || message.role === 'system') {
|
||||
if (message.role === 'system') {
|
||||
// System messages: update in place without branching
|
||||
const newContent = editedContent.trim();
|
||||
|
||||
// If content is empty or still the placeholder, remove without deleting children
|
||||
if (!newContent) {
|
||||
const conversationDeleted = await removeSystemPromptPlaceholder(message.id);
|
||||
isEditing = false;
|
||||
if (conversationDeleted) {
|
||||
goto('/');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await DatabaseService.updateMessage(message.id, { content: newContent });
|
||||
const index = conversationsStore.findMessageIndex(message.id);
|
||||
if (index !== -1) {
|
||||
conversationsStore.updateMessageAtIndex(index, { content: newContent });
|
||||
}
|
||||
} else if (message.role === 'user') {
|
||||
const finalExtras = await getMergedExtras();
|
||||
onEditWithBranching?.(message, editedContent.trim(), finalExtras);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@
|
|||
|
||||
<Button class="h-8 px-3" onclick={onSaveEdit} disabled={!editedContent.trim()} size="sm">
|
||||
<Check class="mr-1 h-3 w-3" />
|
||||
Send
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@
|
|||
|
||||
let emptyFileNames = $state<string[]>([]);
|
||||
|
||||
let initialMessage = $state('');
|
||||
|
||||
let isEmpty = $derived(
|
||||
showCenteredEmpty && !activeConversation() && activeMessages().length === 0 && !isLoading()
|
||||
);
|
||||
|
|
@ -221,6 +223,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function handleSystemPromptAdd(draft: { message: string; files: ChatUploadedFile[] }) {
|
||||
if (draft.message || draft.files.length > 0) {
|
||||
chatStore.savePendingDraft(draft.message, draft.files);
|
||||
}
|
||||
|
||||
await chatStore.addSystemPrompt();
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (disableAutoScroll || !chatScrollContainer) return;
|
||||
|
||||
|
|
@ -343,6 +353,12 @@
|
|||
if (!disableAutoScroll) {
|
||||
setTimeout(() => scrollChatToBottom('instant'), INITIAL_SCROLL_DELAY);
|
||||
}
|
||||
|
||||
const pendingDraft = chatStore.consumePendingDraft();
|
||||
if (pendingDraft) {
|
||||
initialMessage = pendingDraft.message;
|
||||
uploadedFiles = pendingDraft.files;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
|
|
@ -428,11 +444,13 @@
|
|||
<div class="conversation-chat-form pointer-events-auto rounded-t-3xl pb-4">
|
||||
<ChatForm
|
||||
disabled={hasPropsError || isEditing()}
|
||||
{initialMessage}
|
||||
isLoading={isCurrentConversationLoading}
|
||||
onFileRemove={handleFileRemove}
|
||||
onFileUpload={handleFileUpload}
|
||||
onSend={handleSendMessage}
|
||||
onStop={() => chatStore.stopGeneration()}
|
||||
onSystemPromptAdd={handleSystemPromptAdd}
|
||||
showHelperText={false}
|
||||
bind:uploadedFiles
|
||||
/>
|
||||
|
|
@ -486,11 +504,13 @@
|
|||
<div in:fly={{ y: 10, duration: 250, delay: hasPropsError ? 0 : 300 }}>
|
||||
<ChatForm
|
||||
disabled={hasPropsError}
|
||||
{initialMessage}
|
||||
isLoading={isCurrentConversationLoading}
|
||||
onFileRemove={handleFileRemove}
|
||||
onFileUpload={handleFileUpload}
|
||||
onSend={handleSendMessage}
|
||||
onStop={() => chatStore.stopGeneration()}
|
||||
onSystemPromptAdd={handleSystemPromptAdd}
|
||||
showHelperText={true}
|
||||
bind:uploadedFiles
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from '$lib/utils';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
import { DEFAULT_CONTEXT } from '$lib/constants/default-context';
|
||||
import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants/ui';
|
||||
|
||||
/**
|
||||
* chatStore - Active AI interaction and streaming state management
|
||||
|
|
@ -76,6 +77,10 @@ class ChatStore {
|
|||
private isStreamingActive = $state(false);
|
||||
private isEditModeActive = $state(false);
|
||||
private addFilesHandler: ((files: File[]) => void) | null = $state(null);
|
||||
pendingEditMessageId = $state<string | null>(null);
|
||||
// Draft preservation for navigation (e.g., when adding system prompt from welcome page)
|
||||
private _pendingDraftMessage = $state<string>('');
|
||||
private _pendingDraftFiles = $state<ChatUploadedFile[]>([]);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Loading State
|
||||
|
|
@ -455,6 +460,166 @@ class ChatStore {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a system message at the top of a conversation and triggers edit mode.
|
||||
* The system message is inserted between root and the first message of the active branch.
|
||||
* Creates a new conversation if one doesn't exist.
|
||||
*/
|
||||
async addSystemPrompt(): Promise<void> {
|
||||
let activeConv = conversationsStore.activeConversation;
|
||||
|
||||
// Create conversation if needed
|
||||
if (!activeConv) {
|
||||
await conversationsStore.createConversation();
|
||||
activeConv = conversationsStore.activeConversation;
|
||||
}
|
||||
if (!activeConv) return;
|
||||
|
||||
try {
|
||||
// Get all messages to find the root
|
||||
const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
|
||||
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
|
||||
let rootId: string;
|
||||
|
||||
// Create root message if it doesn't exist
|
||||
if (!rootMessage) {
|
||||
rootId = await DatabaseService.createRootMessage(activeConv.id);
|
||||
} else {
|
||||
rootId = rootMessage.id;
|
||||
}
|
||||
|
||||
// Check if there's already a system message as root's child
|
||||
const existingSystemMessage = allMessages.find(
|
||||
(m) => m.role === 'system' && m.parent === rootId
|
||||
);
|
||||
|
||||
if (existingSystemMessage) {
|
||||
// If system message exists, just trigger edit mode on it
|
||||
this.pendingEditMessageId = existingSystemMessage.id;
|
||||
|
||||
// Make sure it's in active messages at the beginning
|
||||
if (!conversationsStore.activeMessages.some((m) => m.id === existingSystemMessage.id)) {
|
||||
conversationsStore.activeMessages.unshift(existingSystemMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the first message of the active branch (child of root that's in activeMessages)
|
||||
const activeMessages = conversationsStore.activeMessages;
|
||||
const firstActiveMessage = activeMessages.find((m) => m.parent === rootId);
|
||||
|
||||
// Create new system message with placeholder content (will be edited by user)
|
||||
const systemMessage = await DatabaseService.createSystemMessage(
|
||||
activeConv.id,
|
||||
SYSTEM_MESSAGE_PLACEHOLDER,
|
||||
rootId
|
||||
);
|
||||
|
||||
// If there's a first message in the active branch, re-parent it to the system message
|
||||
if (firstActiveMessage) {
|
||||
// Update the first message's parent to be the system message
|
||||
await DatabaseService.updateMessage(firstActiveMessage.id, {
|
||||
parent: systemMessage.id
|
||||
});
|
||||
|
||||
// Update the system message's children to include the first message
|
||||
await DatabaseService.updateMessage(systemMessage.id, {
|
||||
children: [firstActiveMessage.id]
|
||||
});
|
||||
|
||||
// Remove first message from root's children
|
||||
const updatedRootChildren = rootMessage
|
||||
? rootMessage.children.filter((id: string) => id !== firstActiveMessage.id)
|
||||
: [];
|
||||
// Note: system message was already added to root's children by createSystemMessage
|
||||
await DatabaseService.updateMessage(rootId, {
|
||||
children: [
|
||||
...updatedRootChildren.filter((id: string) => id !== systemMessage.id),
|
||||
systemMessage.id
|
||||
]
|
||||
});
|
||||
|
||||
// Update local state
|
||||
const firstMsgIndex = conversationsStore.findMessageIndex(firstActiveMessage.id);
|
||||
if (firstMsgIndex !== -1) {
|
||||
conversationsStore.updateMessageAtIndex(firstMsgIndex, { parent: systemMessage.id });
|
||||
}
|
||||
}
|
||||
|
||||
// Add system message to active messages at the beginning
|
||||
conversationsStore.activeMessages.unshift(systemMessage);
|
||||
|
||||
// Set pending edit message ID to trigger edit mode
|
||||
this.pendingEditMessageId = systemMessage.id;
|
||||
|
||||
conversationsStore.updateConversationTimestamp();
|
||||
} catch (error) {
|
||||
console.error('Failed to add system prompt:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a system message placeholder without deleting its children.
|
||||
* Re-parents children back to the root message.
|
||||
* If this is a new empty conversation (only root + system placeholder), deletes the entire conversation.
|
||||
* @returns true if the entire conversation was deleted, false otherwise
|
||||
*/
|
||||
async removeSystemPromptPlaceholder(messageId: string): Promise<boolean> {
|
||||
const activeConv = conversationsStore.activeConversation;
|
||||
if (!activeConv) return false;
|
||||
|
||||
try {
|
||||
const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
|
||||
const systemMessage = allMessages.find((m) => m.id === messageId);
|
||||
if (!systemMessage || systemMessage.role !== 'system') return false;
|
||||
|
||||
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
|
||||
if (!rootMessage) return false;
|
||||
|
||||
// Check if this is a new empty conversation (only root + system placeholder)
|
||||
const isEmptyConversation = allMessages.length === 2 && systemMessage.children.length === 0;
|
||||
|
||||
if (isEmptyConversation) {
|
||||
// Delete the entire conversation
|
||||
await conversationsStore.deleteConversation(activeConv.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Re-parent system message's children to root
|
||||
for (const childId of systemMessage.children) {
|
||||
await DatabaseService.updateMessage(childId, { parent: rootMessage.id });
|
||||
|
||||
// Update local state
|
||||
const childIndex = conversationsStore.findMessageIndex(childId);
|
||||
if (childIndex !== -1) {
|
||||
conversationsStore.updateMessageAtIndex(childIndex, { parent: rootMessage.id });
|
||||
}
|
||||
}
|
||||
|
||||
// Update root's children: remove system message, add system's children
|
||||
const newRootChildren = [
|
||||
...rootMessage.children.filter((id: string) => id !== messageId),
|
||||
...systemMessage.children
|
||||
];
|
||||
await DatabaseService.updateMessage(rootMessage.id, { children: newRootChildren });
|
||||
|
||||
// Delete the system message (without cascade)
|
||||
await DatabaseService.deleteMessage(messageId);
|
||||
|
||||
// Remove from active messages
|
||||
const systemIndex = conversationsStore.findMessageIndex(messageId);
|
||||
if (systemIndex !== -1) {
|
||||
conversationsStore.activeMessages.splice(systemIndex, 1);
|
||||
}
|
||||
|
||||
conversationsStore.updateConversationTimestamp();
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Failed to remove system prompt placeholder:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async createAssistantMessage(parentId?: string): Promise<DatabaseMessage | null> {
|
||||
const activeConv = conversationsStore.activeConversation;
|
||||
if (!activeConv) return null;
|
||||
|
|
@ -916,6 +1081,28 @@ class ChatStore {
|
|||
if (!activeConv)
|
||||
return { totalCount: 0, userMessages: 0, assistantMessages: 0, messageTypes: [] };
|
||||
const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
|
||||
const messageToDelete = allMessages.find((m) => m.id === messageId);
|
||||
|
||||
// For system messages, don't count descendants as they will be preserved (reparented to root)
|
||||
if (messageToDelete?.role === 'system') {
|
||||
const messagesToDelete = allMessages.filter((m) => m.id === messageId);
|
||||
let userMessages = 0,
|
||||
assistantMessages = 0;
|
||||
const messageTypes: string[] = [];
|
||||
|
||||
for (const msg of messagesToDelete) {
|
||||
if (msg.role === 'user') {
|
||||
userMessages++;
|
||||
if (!messageTypes.includes('user message')) messageTypes.push('user message');
|
||||
} else if (msg.role === 'assistant') {
|
||||
assistantMessages++;
|
||||
if (!messageTypes.includes('assistant response')) messageTypes.push('assistant response');
|
||||
}
|
||||
}
|
||||
|
||||
return { totalCount: 1, userMessages, assistantMessages, messageTypes };
|
||||
}
|
||||
|
||||
const descendants = findDescendantMessages(allMessages, messageId);
|
||||
const allToDelete = [messageId, ...descendants];
|
||||
const messagesToDelete = allMessages.filter((m) => allToDelete.includes(m.id));
|
||||
|
|
@ -1381,6 +1568,31 @@ class ChatStore {
|
|||
return this.addFilesHandler;
|
||||
}
|
||||
|
||||
savePendingDraft(message: string, files: ChatUploadedFile[]): void {
|
||||
this._pendingDraftMessage = message;
|
||||
this._pendingDraftFiles = [...files];
|
||||
}
|
||||
|
||||
consumePendingDraft(): { message: string; files: ChatUploadedFile[] } | null {
|
||||
if (!this._pendingDraftMessage && this._pendingDraftFiles.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const draft = {
|
||||
message: this._pendingDraftMessage,
|
||||
files: [...this._pendingDraftFiles]
|
||||
};
|
||||
|
||||
this._pendingDraftMessage = '';
|
||||
this._pendingDraftFiles = [];
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
hasPendingDraft(): boolean {
|
||||
return Boolean(this._pendingDraftMessage) || this._pendingDraftFiles.length > 0;
|
||||
}
|
||||
|
||||
public getAllLoadingChats(): string[] {
|
||||
return Array.from(this.chatLoadingStates.keys());
|
||||
}
|
||||
|
|
@ -1485,3 +1697,7 @@ export const isEditing = () => chatStore.isEditing();
|
|||
export const isLoading = () => chatStore.isLoading;
|
||||
export const setEditModeActive = (handler: (files: File[]) => void) =>
|
||||
chatStore.setEditModeActive(handler);
|
||||
export const pendingEditMessageId = () => chatStore.pendingEditMessageId;
|
||||
export const clearPendingEditMessageId = () => (chatStore.pendingEditMessageId = null);
|
||||
export const removeSystemPromptPlaceholder = (messageId: string) =>
|
||||
chatStore.removeSystemPromptPlaceholder(messageId);
|
||||
|
|
|
|||
Loading…
Reference in New Issue