webui: Add a "Continue" Action for Assistant Message (#16971)
* feat: Add "Continue" action for assistant messages * feat: Continuation logic & prompt improvements * chore: update webui build output * feat: Improve logic for continuing the assistant message * chore: update webui build output * chore: Linting * chore: update webui build output * fix: Remove synthetic prompt logic, use the prefill feature by sending the conversation payload ending with assistant message * chore: update webui build output * feat: Enable "Continue" button based on config & non-reasoning model type * chore: update webui build output * chore: Update packages with `npm audit fix` * fix: Remove redundant error * chore: update webui build output * chore: Update `.gitignore` * fix: Add missing change * feat: Add auto-resizing for Edit Assistant/User Message textareas * chore: update webui build output
This commit is contained in:
parent
07b0e7a5ac
commit
99c53d6558
Binary file not shown.
|
|
@ -25,3 +25,4 @@ vite.config.ts.timestamp-*
|
||||||
|
|
||||||
*storybook.log
|
*storybook.log
|
||||||
storybook-static
|
storybook-static
|
||||||
|
*.code-workspace
|
||||||
|
|
@ -2109,9 +2109,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/kit": {
|
"node_modules/@sveltejs/kit": {
|
||||||
"version": "2.48.4",
|
"version": "2.48.5",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.4.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.5.tgz",
|
||||||
"integrity": "sha512-TGFX1pZUt9qqY20Cv5NyYvy0iLWHf2jXi8s+eCGsig7jQMdwZWKUFMR6TbvFNhfDSUpc1sH/Y5EHv20g3HHA3g==",
|
"integrity": "sha512-/rnwfSWS3qwUSzvHynUTORF9xSJi7PCR9yXkxUOnRrNqyKmCmh3FPHH+E9BbgqxXfTevGXBqgnlh9kMb+9T5XA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -5087,9 +5087,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
class?: string;
|
class?: string;
|
||||||
message: DatabaseMessage;
|
message: DatabaseMessage;
|
||||||
onCopy?: (message: DatabaseMessage) => void;
|
onCopy?: (message: DatabaseMessage) => void;
|
||||||
|
onContinueAssistantMessage?: (message: DatabaseMessage) => void;
|
||||||
onDelete?: (message: DatabaseMessage) => void;
|
onDelete?: (message: DatabaseMessage) => void;
|
||||||
onEditWithBranching?: (message: DatabaseMessage, newContent: string) => void;
|
onEditWithBranching?: (message: DatabaseMessage, newContent: string) => void;
|
||||||
onEditWithReplacement?: (
|
onEditWithReplacement?: (
|
||||||
|
|
@ -17,6 +18,7 @@
|
||||||
newContent: string,
|
newContent: string,
|
||||||
shouldBranch: boolean
|
shouldBranch: boolean
|
||||||
) => void;
|
) => void;
|
||||||
|
onEditUserMessagePreserveResponses?: (message: DatabaseMessage, newContent: string) => void;
|
||||||
onNavigateToSibling?: (siblingId: string) => void;
|
onNavigateToSibling?: (siblingId: string) => void;
|
||||||
onRegenerateWithBranching?: (message: DatabaseMessage) => void;
|
onRegenerateWithBranching?: (message: DatabaseMessage) => void;
|
||||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
siblingInfo?: ChatMessageSiblingInfo | null;
|
||||||
|
|
@ -26,9 +28,11 @@
|
||||||
class: className = '',
|
class: className = '',
|
||||||
message,
|
message,
|
||||||
onCopy,
|
onCopy,
|
||||||
|
onContinueAssistantMessage,
|
||||||
onDelete,
|
onDelete,
|
||||||
onEditWithBranching,
|
onEditWithBranching,
|
||||||
onEditWithReplacement,
|
onEditWithReplacement,
|
||||||
|
onEditUserMessagePreserveResponses,
|
||||||
onNavigateToSibling,
|
onNavigateToSibling,
|
||||||
onRegenerateWithBranching,
|
onRegenerateWithBranching,
|
||||||
siblingInfo = null
|
siblingInfo = null
|
||||||
|
|
@ -133,17 +137,33 @@
|
||||||
onRegenerateWithBranching?.(message);
|
onRegenerateWithBranching?.(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleContinue() {
|
||||||
|
onContinueAssistantMessage?.(message);
|
||||||
|
}
|
||||||
|
|
||||||
function handleSaveEdit() {
|
function handleSaveEdit() {
|
||||||
if (message.role === 'user') {
|
if (message.role === 'user') {
|
||||||
|
// For user messages, trim to avoid accidental whitespace
|
||||||
onEditWithBranching?.(message, editedContent.trim());
|
onEditWithBranching?.(message, editedContent.trim());
|
||||||
} else {
|
} else {
|
||||||
onEditWithReplacement?.(message, editedContent.trim(), shouldBranchAfterEdit);
|
// For assistant messages, preserve exact content including trailing whitespace
|
||||||
|
// This is important for the Continue feature to work properly
|
||||||
|
onEditWithReplacement?.(message, editedContent, shouldBranchAfterEdit);
|
||||||
}
|
}
|
||||||
|
|
||||||
isEditing = false;
|
isEditing = false;
|
||||||
shouldBranchAfterEdit = false;
|
shouldBranchAfterEdit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSaveEditOnly() {
|
||||||
|
if (message.role === 'user') {
|
||||||
|
// For user messages, trim to avoid accidental whitespace
|
||||||
|
onEditUserMessagePreserveResponses?.(message, editedContent.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
isEditing = false;
|
||||||
|
}
|
||||||
|
|
||||||
function handleShowDeleteDialogChange(show: boolean) {
|
function handleShowDeleteDialogChange(show: boolean) {
|
||||||
showDeleteDialog = show;
|
showDeleteDialog = show;
|
||||||
}
|
}
|
||||||
|
|
@ -166,6 +186,7 @@
|
||||||
onEditedContentChange={handleEditedContentChange}
|
onEditedContentChange={handleEditedContentChange}
|
||||||
{onNavigateToSibling}
|
{onNavigateToSibling}
|
||||||
onSaveEdit={handleSaveEdit}
|
onSaveEdit={handleSaveEdit}
|
||||||
|
onSaveEditOnly={handleSaveEditOnly}
|
||||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
||||||
{showDeleteDialog}
|
{showDeleteDialog}
|
||||||
{siblingInfo}
|
{siblingInfo}
|
||||||
|
|
@ -181,6 +202,7 @@
|
||||||
messageContent={message.content}
|
messageContent={message.content}
|
||||||
onCancelEdit={handleCancelEdit}
|
onCancelEdit={handleCancelEdit}
|
||||||
onConfirmDelete={handleConfirmDelete}
|
onConfirmDelete={handleConfirmDelete}
|
||||||
|
onContinue={handleContinue}
|
||||||
onCopy={handleCopy}
|
onCopy={handleCopy}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Edit, Copy, RefreshCw, Trash2 } from '@lucide/svelte';
|
import { Edit, Copy, RefreshCw, Trash2, ArrowRight } from '@lucide/svelte';
|
||||||
import { ActionButton, ConfirmationDialog } from '$lib/components/app';
|
import { ActionButton, ConfirmationDialog } from '$lib/components/app';
|
||||||
import ChatMessageBranchingControls from './ChatMessageBranchingControls.svelte';
|
import ChatMessageBranchingControls from './ChatMessageBranchingControls.svelte';
|
||||||
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
onCopy: () => void;
|
onCopy: () => void;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
onRegenerate?: () => void;
|
onRegenerate?: () => void;
|
||||||
|
onContinue?: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onConfirmDelete: () => void;
|
onConfirmDelete: () => void;
|
||||||
onNavigateToSibling?: (siblingId: string) => void;
|
onNavigateToSibling?: (siblingId: string) => void;
|
||||||
|
|
@ -31,6 +32,7 @@
|
||||||
onCopy,
|
onCopy,
|
||||||
onEdit,
|
onEdit,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
|
onContinue,
|
||||||
onDelete,
|
onDelete,
|
||||||
onNavigateToSibling,
|
onNavigateToSibling,
|
||||||
onShowDeleteDialogChange,
|
onShowDeleteDialogChange,
|
||||||
|
|
@ -69,6 +71,10 @@
|
||||||
<ActionButton icon={RefreshCw} tooltip="Regenerate" onclick={onRegenerate} />
|
<ActionButton icon={RefreshCw} tooltip="Regenerate" onclick={onRegenerate} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if role === 'assistant' && onContinue}
|
||||||
|
<ActionButton icon={ArrowRight} tooltip="Continue" onclick={onContinue} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<ActionButton icon={Trash2} tooltip="Delete" onclick={onDelete} />
|
<ActionButton icon={Trash2} tooltip="Delete" onclick={onDelete} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { ChatMessageThinkingBlock, MarkdownContent } from '$lib/components/app';
|
import { ChatMessageThinkingBlock, MarkdownContent } from '$lib/components/app';
|
||||||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||||
import { isLoading } from '$lib/stores/chat.svelte';
|
import { isLoading } from '$lib/stores/chat.svelte';
|
||||||
|
import autoResizeTextarea from '$lib/utils/autoresize-textarea';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
|
|
@ -39,6 +40,7 @@
|
||||||
onCancelEdit?: () => void;
|
onCancelEdit?: () => void;
|
||||||
onCopy: () => void;
|
onCopy: () => void;
|
||||||
onConfirmDelete: () => void;
|
onConfirmDelete: () => void;
|
||||||
|
onContinue?: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
onEditKeydown?: (event: KeyboardEvent) => void;
|
onEditKeydown?: (event: KeyboardEvent) => void;
|
||||||
|
|
@ -65,6 +67,7 @@
|
||||||
messageContent,
|
messageContent,
|
||||||
onCancelEdit,
|
onCancelEdit,
|
||||||
onConfirmDelete,
|
onConfirmDelete,
|
||||||
|
onContinue,
|
||||||
onCopy,
|
onCopy,
|
||||||
onDelete,
|
onDelete,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
|
@ -107,6 +110,12 @@
|
||||||
void copyToClipboard(model ?? '');
|
void copyToClipboard(model ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (isEditing && textareaElement) {
|
||||||
|
autoResizeTextarea(textareaElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function formatToolCallBadge(toolCall: ApiChatCompletionToolCall, index: number) {
|
function formatToolCallBadge(toolCall: ApiChatCompletionToolCall, index: number) {
|
||||||
const callNumber = index + 1;
|
const callNumber = index + 1;
|
||||||
const functionName = toolCall.function?.name?.trim();
|
const functionName = toolCall.function?.name?.trim();
|
||||||
|
|
@ -190,7 +199,10 @@
|
||||||
bind:value={editedContent}
|
bind:value={editedContent}
|
||||||
class="min-h-[50vh] w-full resize-y rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}"
|
class="min-h-[50vh] w-full resize-y rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}"
|
||||||
onkeydown={onEditKeydown}
|
onkeydown={onEditKeydown}
|
||||||
oninput={(e) => onEditedContentChange?.(e.currentTarget.value)}
|
oninput={(e) => {
|
||||||
|
autoResizeTextarea(e.currentTarget);
|
||||||
|
onEditedContentChange?.(e.currentTarget.value);
|
||||||
|
}}
|
||||||
placeholder="Edit assistant message..."
|
placeholder="Edit assistant message..."
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
|
|
@ -335,6 +347,9 @@
|
||||||
{onCopy}
|
{onCopy}
|
||||||
{onEdit}
|
{onEdit}
|
||||||
{onRegenerate}
|
{onRegenerate}
|
||||||
|
onContinue={currentConfig.enableContinueGeneration && !thinkingContent
|
||||||
|
? onContinue
|
||||||
|
: undefined}
|
||||||
{onDelete}
|
{onDelete}
|
||||||
{onConfirmDelete}
|
{onConfirmDelete}
|
||||||
{onNavigateToSibling}
|
{onNavigateToSibling}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Check, X } from '@lucide/svelte';
|
import { Check, X, Send } from '@lucide/svelte';
|
||||||
import { Card } from '$lib/components/ui/card';
|
import { Card } from '$lib/components/ui/card';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { ChatAttachmentsList, MarkdownContent } from '$lib/components/app';
|
import { ChatAttachmentsList, MarkdownContent } from '$lib/components/app';
|
||||||
import { INPUT_CLASSES } from '$lib/constants/input-classes';
|
import { INPUT_CLASSES } from '$lib/constants/input-classes';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
|
import autoResizeTextarea from '$lib/utils/autoresize-textarea';
|
||||||
import ChatMessageActions from './ChatMessageActions.svelte';
|
import ChatMessageActions from './ChatMessageActions.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -22,6 +23,7 @@
|
||||||
} | null;
|
} | null;
|
||||||
onCancelEdit: () => void;
|
onCancelEdit: () => void;
|
||||||
onSaveEdit: () => void;
|
onSaveEdit: () => void;
|
||||||
|
onSaveEditOnly?: () => void;
|
||||||
onEditKeydown: (event: KeyboardEvent) => void;
|
onEditKeydown: (event: KeyboardEvent) => void;
|
||||||
onEditedContentChange: (content: string) => void;
|
onEditedContentChange: (content: string) => void;
|
||||||
onCopy: () => void;
|
onCopy: () => void;
|
||||||
|
|
@ -43,6 +45,7 @@
|
||||||
deletionInfo,
|
deletionInfo,
|
||||||
onCancelEdit,
|
onCancelEdit,
|
||||||
onSaveEdit,
|
onSaveEdit,
|
||||||
|
onSaveEditOnly,
|
||||||
onEditKeydown,
|
onEditKeydown,
|
||||||
onEditedContentChange,
|
onEditedContentChange,
|
||||||
onCopy,
|
onCopy,
|
||||||
|
|
@ -58,6 +61,12 @@
|
||||||
let messageElement: HTMLElement | undefined = $state();
|
let messageElement: HTMLElement | undefined = $state();
|
||||||
const currentConfig = config();
|
const currentConfig = config();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (isEditing && textareaElement) {
|
||||||
|
autoResizeTextarea(textareaElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!messageElement || !message.content.trim()) return;
|
if (!messageElement || !message.content.trim()) return;
|
||||||
|
|
||||||
|
|
@ -95,20 +104,34 @@
|
||||||
bind:value={editedContent}
|
bind:value={editedContent}
|
||||||
class="min-h-[60px] w-full resize-none rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}"
|
class="min-h-[60px] w-full resize-none rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}"
|
||||||
onkeydown={onEditKeydown}
|
onkeydown={onEditKeydown}
|
||||||
oninput={(e) => onEditedContentChange(e.currentTarget.value)}
|
oninput={(e) => {
|
||||||
|
autoResizeTextarea(e.currentTarget);
|
||||||
|
onEditedContentChange(e.currentTarget.value);
|
||||||
|
}}
|
||||||
placeholder="Edit your message..."
|
placeholder="Edit your message..."
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<div class="mt-2 flex justify-end gap-2">
|
<div class="mt-2 flex justify-end gap-2">
|
||||||
<Button class="h-8 px-3" onclick={onCancelEdit} size="sm" variant="outline">
|
<Button class="h-8 px-3" onclick={onCancelEdit} size="sm" variant="ghost">
|
||||||
<X class="mr-1 h-3 w-3" />
|
<X class="mr-1 h-3 w-3" />
|
||||||
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button class="h-8 px-3" onclick={onSaveEdit} disabled={!editedContent.trim()} size="sm">
|
{#if onSaveEditOnly}
|
||||||
<Check class="mr-1 h-3 w-3" />
|
<Button
|
||||||
|
class="h-8 px-3"
|
||||||
|
onclick={onSaveEditOnly}
|
||||||
|
disabled={!editedContent.trim()}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<Check class="mr-1 h-3 w-3" />
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Button class="h-8 px-3" onclick={onSaveEdit} disabled={!editedContent.trim()} size="sm">
|
||||||
|
<Send class="mr-1 h-3 w-3" />
|
||||||
Send
|
Send
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@
|
||||||
import { DatabaseStore } from '$lib/stores/database';
|
import { DatabaseStore } from '$lib/stores/database';
|
||||||
import {
|
import {
|
||||||
activeConversation,
|
activeConversation,
|
||||||
|
continueAssistantMessage,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
navigateToSibling,
|
|
||||||
editMessageWithBranching,
|
|
||||||
editAssistantMessage,
|
editAssistantMessage,
|
||||||
|
editMessageWithBranching,
|
||||||
|
editUserMessagePreserveResponses,
|
||||||
|
navigateToSibling,
|
||||||
regenerateMessageWithBranching
|
regenerateMessageWithBranching
|
||||||
} from '$lib/stores/chat.svelte';
|
} from '$lib/stores/chat.svelte';
|
||||||
import { getMessageSiblings } from '$lib/utils/branching';
|
import { getMessageSiblings } from '$lib/utils/branching';
|
||||||
|
|
@ -93,6 +95,26 @@
|
||||||
|
|
||||||
refreshAllMessages();
|
refreshAllMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleContinueAssistantMessage(message: DatabaseMessage) {
|
||||||
|
onUserAction?.();
|
||||||
|
|
||||||
|
await continueAssistantMessage(message.id);
|
||||||
|
|
||||||
|
refreshAllMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEditUserMessagePreserveResponses(
|
||||||
|
message: DatabaseMessage,
|
||||||
|
newContent: string
|
||||||
|
) {
|
||||||
|
onUserAction?.();
|
||||||
|
|
||||||
|
await editUserMessagePreserveResponses(message.id, newContent);
|
||||||
|
|
||||||
|
refreshAllMessages();
|
||||||
|
}
|
||||||
|
|
||||||
async function handleDeleteMessage(message: DatabaseMessage) {
|
async function handleDeleteMessage(message: DatabaseMessage) {
|
||||||
await deleteMessage(message.id);
|
await deleteMessage(message.id);
|
||||||
|
|
||||||
|
|
@ -110,7 +132,9 @@
|
||||||
onNavigateToSibling={handleNavigateToSibling}
|
onNavigateToSibling={handleNavigateToSibling}
|
||||||
onEditWithBranching={handleEditWithBranching}
|
onEditWithBranching={handleEditWithBranching}
|
||||||
onEditWithReplacement={handleEditWithReplacement}
|
onEditWithReplacement={handleEditWithReplacement}
|
||||||
|
onEditUserMessagePreserveResponses={handleEditUserMessagePreserveResponses}
|
||||||
onRegenerateWithBranching={handleRegenerateWithBranching}
|
onRegenerateWithBranching={handleRegenerateWithBranching}
|
||||||
|
onContinueAssistantMessage={handleContinueAssistantMessage}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,11 @@
|
||||||
{ value: 'dark', label: 'Dark', icon: Moon }
|
{ value: 'dark', label: 'Dark', icon: Moon }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'pasteLongTextToFileLen',
|
||||||
|
label: 'Paste long text to file length',
|
||||||
|
type: 'input'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'showMessageStats',
|
key: 'showMessageStats',
|
||||||
label: 'Show message generation statistics',
|
label: 'Show message generation statistics',
|
||||||
|
|
@ -68,14 +73,15 @@
|
||||||
type: 'checkbox'
|
type: 'checkbox'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'askForTitleConfirmation',
|
key: 'showModelInfo',
|
||||||
label: 'Ask for confirmation before changing conversation title',
|
label: 'Show model information',
|
||||||
type: 'checkbox'
|
type: 'checkbox'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'pasteLongTextToFileLen',
|
key: 'enableContinueGeneration',
|
||||||
label: 'Paste long text to file length',
|
label: 'Enable "Continue" button',
|
||||||
type: 'input'
|
type: 'checkbox',
|
||||||
|
isExperimental: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'pdfAsImage',
|
key: 'pdfAsImage',
|
||||||
|
|
@ -83,13 +89,13 @@
|
||||||
type: 'checkbox'
|
type: 'checkbox'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'showModelInfo',
|
key: 'renderUserContentAsMarkdown',
|
||||||
label: 'Show model information',
|
label: 'Render user content as Markdown',
|
||||||
type: 'checkbox'
|
type: 'checkbox'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'renderUserContentAsMarkdown',
|
key: 'askForTitleConfirmation',
|
||||||
label: 'Render user content as Markdown',
|
label: 'Ask for confirmation before changing conversation title',
|
||||||
type: 'checkbox'
|
type: 'checkbox'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { RotateCcw } from '@lucide/svelte';
|
import { RotateCcw, FlaskConical } from '@lucide/svelte';
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
|
|
@ -55,8 +55,12 @@
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Label for={field.key} class="text-sm font-medium">
|
<Label for={field.key} class="flex items-center gap-1.5 text-sm font-medium">
|
||||||
{field.label}
|
{field.label}
|
||||||
|
|
||||||
|
{#if field.isExperimental}
|
||||||
|
<FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
{/if}
|
||||||
</Label>
|
</Label>
|
||||||
{#if isCustomRealTime}
|
{#if isCustomRealTime}
|
||||||
<ParameterSourceIndicator />
|
<ParameterSourceIndicator />
|
||||||
|
|
@ -97,8 +101,12 @@
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if field.type === 'textarea'}
|
{:else if field.type === 'textarea'}
|
||||||
<Label for={field.key} class="block text-sm font-medium">
|
<Label for={field.key} class="block flex items-center gap-1.5 text-sm font-medium">
|
||||||
{field.label}
|
{field.label}
|
||||||
|
|
||||||
|
{#if field.isExperimental}
|
||||||
|
<FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
{/if}
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|
@ -129,8 +137,12 @@
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Label for={field.key} class="text-sm font-medium">
|
<Label for={field.key} class="flex items-center gap-1.5 text-sm font-medium">
|
||||||
{field.label}
|
{field.label}
|
||||||
|
|
||||||
|
{#if field.isExperimental}
|
||||||
|
<FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
{/if}
|
||||||
</Label>
|
</Label>
|
||||||
{#if isCustomRealTime}
|
{#if isCustomRealTime}
|
||||||
<ParameterSourceIndicator />
|
<ParameterSourceIndicator />
|
||||||
|
|
@ -214,9 +226,13 @@
|
||||||
for={field.key}
|
for={field.key}
|
||||||
class="cursor-pointer text-sm leading-none font-medium {isDisabled
|
class="cursor-pointer text-sm leading-none font-medium {isDisabled
|
||||||
? 'text-muted-foreground'
|
? 'text-muted-foreground'
|
||||||
: ''}"
|
: ''} flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
{field.label}
|
{field.label}
|
||||||
|
|
||||||
|
{#if field.isExperimental}
|
||||||
|
<FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{#if field.help || SETTING_CONFIG_INFO[field.key]}
|
{#if field.help || SETTING_CONFIG_INFO[field.key]}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean> =
|
||||||
max_tokens: -1,
|
max_tokens: -1,
|
||||||
custom: '', // custom json-stringified object
|
custom: '', // custom json-stringified object
|
||||||
// experimental features
|
// experimental features
|
||||||
pyInterpreterEnabled: false
|
pyInterpreterEnabled: false,
|
||||||
|
enableContinueGeneration: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SETTING_CONFIG_INFO: Record<string, string> = {
|
export const SETTING_CONFIG_INFO: Record<string, string> = {
|
||||||
|
|
@ -96,5 +97,7 @@ export const SETTING_CONFIG_INFO: Record<string, string> = {
|
||||||
modelSelectorEnabled:
|
modelSelectorEnabled:
|
||||||
'Enable the model selector in the chat input to choose the inference model. Sends the associated model field in API requests.',
|
'Enable the model selector in the chat input to choose the inference model. Sends the associated model field in API requests.',
|
||||||
pyInterpreterEnabled:
|
pyInterpreterEnabled:
|
||||||
'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.'
|
'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.',
|
||||||
|
enableContinueGeneration:
|
||||||
|
'Enable "Continue" button for assistant messages. Currently works only with non-reasoning models.'
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -312,7 +312,6 @@ export class ChatService {
|
||||||
let aggregatedContent = '';
|
let aggregatedContent = '';
|
||||||
let fullReasoningContent = '';
|
let fullReasoningContent = '';
|
||||||
let aggregatedToolCalls: ApiChatCompletionToolCall[] = [];
|
let aggregatedToolCalls: ApiChatCompletionToolCall[] = [];
|
||||||
let hasReceivedData = false;
|
|
||||||
let lastTimings: ChatMessageTimings | undefined;
|
let lastTimings: ChatMessageTimings | undefined;
|
||||||
let streamFinished = false;
|
let streamFinished = false;
|
||||||
let modelEmitted = false;
|
let modelEmitted = false;
|
||||||
|
|
@ -352,8 +351,6 @@ export class ChatService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasReceivedData = true;
|
|
||||||
|
|
||||||
if (!abortSignal?.aborted) {
|
if (!abortSignal?.aborted) {
|
||||||
onToolCallChunk?.(serializedToolCalls);
|
onToolCallChunk?.(serializedToolCalls);
|
||||||
}
|
}
|
||||||
|
|
@ -415,7 +412,6 @@ export class ChatService {
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
finalizeOpenToolCallBatch();
|
finalizeOpenToolCallBatch();
|
||||||
hasReceivedData = true;
|
|
||||||
aggregatedContent += content;
|
aggregatedContent += content;
|
||||||
if (!abortSignal?.aborted) {
|
if (!abortSignal?.aborted) {
|
||||||
onChunk?.(content);
|
onChunk?.(content);
|
||||||
|
|
@ -424,7 +420,6 @@ export class ChatService {
|
||||||
|
|
||||||
if (reasoningContent) {
|
if (reasoningContent) {
|
||||||
finalizeOpenToolCallBatch();
|
finalizeOpenToolCallBatch();
|
||||||
hasReceivedData = true;
|
|
||||||
fullReasoningContent += reasoningContent;
|
fullReasoningContent += reasoningContent;
|
||||||
if (!abortSignal?.aborted) {
|
if (!abortSignal?.aborted) {
|
||||||
onReasoningChunk?.(reasoningContent);
|
onReasoningChunk?.(reasoningContent);
|
||||||
|
|
@ -446,15 +441,6 @@ export class ChatService {
|
||||||
if (streamFinished) {
|
if (streamFinished) {
|
||||||
finalizeOpenToolCallBatch();
|
finalizeOpenToolCallBatch();
|
||||||
|
|
||||||
if (
|
|
||||||
!hasReceivedData &&
|
|
||||||
aggregatedContent.length === 0 &&
|
|
||||||
aggregatedToolCalls.length === 0
|
|
||||||
) {
|
|
||||||
const noResponseError = new Error('No response received from server. Please try again.');
|
|
||||||
throw noResponseError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalToolCalls =
|
const finalToolCalls =
|
||||||
aggregatedToolCalls.length > 0 ? JSON.stringify(aggregatedToolCalls) : undefined;
|
aggregatedToolCalls.length > 0 ? JSON.stringify(aggregatedToolCalls) : undefined;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1486,6 +1486,10 @@ class ChatStore {
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ensure currNode points to the edited message to maintain correct path
|
||||||
|
await DatabaseStore.updateCurrentNode(this.activeConversation.id, messageToEdit.id);
|
||||||
|
this.activeConversation.currNode = messageToEdit.id;
|
||||||
|
|
||||||
this.updateMessageAtIndex(messageIndex, {
|
this.updateMessageAtIndex(messageIndex, {
|
||||||
content: newContent,
|
content: newContent,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
|
|
@ -1499,6 +1503,69 @@ class ChatStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits a user message and preserves all responses below
|
||||||
|
* Updates the message content in-place without deleting or regenerating responses
|
||||||
|
*
|
||||||
|
* **Use Case**: When you want to fix a typo or rephrase a question without losing the assistant's response
|
||||||
|
*
|
||||||
|
* **Important Behavior:**
|
||||||
|
* - Does NOT create a branch (unlike editMessageWithBranching)
|
||||||
|
* - Does NOT regenerate assistant responses
|
||||||
|
* - Only updates the user message content in the database
|
||||||
|
* - Preserves the entire conversation tree below the edited message
|
||||||
|
* - Updates conversation title if this is the first user message
|
||||||
|
*
|
||||||
|
* @param messageId - The ID of the user message to edit
|
||||||
|
* @param newContent - The new content for the message
|
||||||
|
*/
|
||||||
|
async editUserMessagePreserveResponses(messageId: string, newContent: string): Promise<void> {
|
||||||
|
if (!this.activeConversation) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const messageIndex = this.findMessageIndex(messageId);
|
||||||
|
if (messageIndex === -1) {
|
||||||
|
console.error('Message not found for editing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageToEdit = this.activeMessages[messageIndex];
|
||||||
|
if (messageToEdit.role !== 'user') {
|
||||||
|
console.error('Only user messages can be edited with this method');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simply update the message content in-place
|
||||||
|
await DatabaseStore.updateMessage(messageId, {
|
||||||
|
content: newContent,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateMessageAtIndex(messageIndex, {
|
||||||
|
content: newContent,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if first user message for title update
|
||||||
|
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id);
|
||||||
|
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
|
||||||
|
const isFirstUserMessage =
|
||||||
|
rootMessage && messageToEdit.parent === rootMessage.id && messageToEdit.role === 'user';
|
||||||
|
|
||||||
|
if (isFirstUserMessage && newContent.trim()) {
|
||||||
|
await this.updateConversationTitleWithConfirmation(
|
||||||
|
this.activeConversation.id,
|
||||||
|
newContent.trim(),
|
||||||
|
this.titleUpdateConfirmationCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateConversationTimestamp();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to edit user message:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edits a message by creating a new branch with the edited content
|
* Edits a message by creating a new branch with the edited content
|
||||||
* @param messageId - The ID of the message to edit
|
* @param messageId - The ID of the message to edit
|
||||||
|
|
@ -1696,6 +1763,200 @@ class ChatStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continues generation for an existing assistant message
|
||||||
|
* @param messageId - The ID of the assistant message to continue
|
||||||
|
*/
|
||||||
|
async continueAssistantMessage(messageId: string): Promise<void> {
|
||||||
|
if (!this.activeConversation || this.isLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const messageIndex = this.findMessageIndex(messageId);
|
||||||
|
if (messageIndex === -1) {
|
||||||
|
console.error('Message not found for continuation');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageToContinue = this.activeMessages[messageIndex];
|
||||||
|
if (messageToContinue.role !== 'assistant') {
|
||||||
|
console.error('Only assistant messages can be continued');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Race condition protection: Check if this specific conversation is already loading
|
||||||
|
// This prevents multiple rapid clicks on "Continue" from creating concurrent operations
|
||||||
|
if (this.isConversationLoading(this.activeConversation.id)) {
|
||||||
|
console.warn('Continuation already in progress for this conversation');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errorDialogState = null;
|
||||||
|
this.setConversationLoading(this.activeConversation.id, true);
|
||||||
|
this.clearConversationStreaming(this.activeConversation.id);
|
||||||
|
|
||||||
|
// IMPORTANT: Fetch the latest content from the database to ensure we have
|
||||||
|
// the most up-to-date content, especially after a stopped generation
|
||||||
|
// This prevents issues where the in-memory state might be stale
|
||||||
|
const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id);
|
||||||
|
const dbMessage = allMessages.find((m) => m.id === messageId);
|
||||||
|
|
||||||
|
if (!dbMessage) {
|
||||||
|
console.error('Message not found in database for continuation');
|
||||||
|
this.setConversationLoading(this.activeConversation.id, false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use content from database as the source of truth
|
||||||
|
const originalContent = dbMessage.content;
|
||||||
|
const originalThinking = dbMessage.thinking || '';
|
||||||
|
|
||||||
|
// Get conversation context up to (but not including) the message to continue
|
||||||
|
const conversationContext = this.activeMessages.slice(0, messageIndex);
|
||||||
|
|
||||||
|
const contextWithContinue = [
|
||||||
|
...conversationContext.map((msg) => {
|
||||||
|
if ('id' in msg && 'convId' in msg && 'timestamp' in msg) {
|
||||||
|
return msg as DatabaseMessage & { extra?: DatabaseMessageExtra[] };
|
||||||
|
}
|
||||||
|
return msg as ApiChatMessageData;
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
role: 'assistant' as const,
|
||||||
|
content: originalContent
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let appendedContent = '';
|
||||||
|
let appendedThinking = '';
|
||||||
|
let hasReceivedContent = false;
|
||||||
|
|
||||||
|
await chatService.sendMessage(
|
||||||
|
contextWithContinue,
|
||||||
|
{
|
||||||
|
...this.getApiOptions(),
|
||||||
|
|
||||||
|
onChunk: (chunk: string) => {
|
||||||
|
hasReceivedContent = true;
|
||||||
|
appendedContent += chunk;
|
||||||
|
// Preserve originalContent exactly as-is, including any trailing whitespace
|
||||||
|
// The concatenation naturally preserves any whitespace at the end of originalContent
|
||||||
|
const fullContent = originalContent + appendedContent;
|
||||||
|
|
||||||
|
this.setConversationStreaming(
|
||||||
|
messageToContinue.convId,
|
||||||
|
fullContent,
|
||||||
|
messageToContinue.id
|
||||||
|
);
|
||||||
|
|
||||||
|
this.updateMessageAtIndex(messageIndex, {
|
||||||
|
content: fullContent
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onReasoningChunk: (reasoningChunk: string) => {
|
||||||
|
hasReceivedContent = true;
|
||||||
|
appendedThinking += reasoningChunk;
|
||||||
|
|
||||||
|
const fullThinking = originalThinking + appendedThinking;
|
||||||
|
|
||||||
|
this.updateMessageAtIndex(messageIndex, {
|
||||||
|
thinking: fullThinking
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onComplete: async (
|
||||||
|
finalContent?: string,
|
||||||
|
reasoningContent?: string,
|
||||||
|
timings?: ChatMessageTimings
|
||||||
|
) => {
|
||||||
|
const fullContent = originalContent + (finalContent || appendedContent);
|
||||||
|
const fullThinking = originalThinking + (reasoningContent || appendedThinking);
|
||||||
|
|
||||||
|
const updateData: {
|
||||||
|
content: string;
|
||||||
|
thinking: string;
|
||||||
|
timestamp: number;
|
||||||
|
timings?: ChatMessageTimings;
|
||||||
|
} = {
|
||||||
|
content: fullContent,
|
||||||
|
thinking: fullThinking,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
timings: timings
|
||||||
|
};
|
||||||
|
|
||||||
|
await DatabaseStore.updateMessage(messageToContinue.id, updateData);
|
||||||
|
|
||||||
|
this.updateMessageAtIndex(messageIndex, updateData);
|
||||||
|
|
||||||
|
this.updateConversationTimestamp();
|
||||||
|
|
||||||
|
this.setConversationLoading(messageToContinue.convId, false);
|
||||||
|
this.clearConversationStreaming(messageToContinue.convId);
|
||||||
|
slotsService.clearConversationState(messageToContinue.convId);
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: async (error: Error) => {
|
||||||
|
if (this.isAbortError(error)) {
|
||||||
|
// User cancelled - save partial continuation if any content was received
|
||||||
|
if (hasReceivedContent && appendedContent) {
|
||||||
|
const partialContent = originalContent + appendedContent;
|
||||||
|
const partialThinking = originalThinking + appendedThinking;
|
||||||
|
|
||||||
|
await DatabaseStore.updateMessage(messageToContinue.id, {
|
||||||
|
content: partialContent,
|
||||||
|
thinking: partialThinking,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateMessageAtIndex(messageIndex, {
|
||||||
|
content: partialContent,
|
||||||
|
thinking: partialThinking,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setConversationLoading(messageToContinue.convId, false);
|
||||||
|
this.clearConversationStreaming(messageToContinue.convId);
|
||||||
|
slotsService.clearConversationState(messageToContinue.convId);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-abort error - rollback to original content
|
||||||
|
console.error('Continue generation error:', error);
|
||||||
|
|
||||||
|
// Rollback: Restore original content in UI
|
||||||
|
this.updateMessageAtIndex(messageIndex, {
|
||||||
|
content: originalContent,
|
||||||
|
thinking: originalThinking
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure database has original content (in case of partial writes)
|
||||||
|
await DatabaseStore.updateMessage(messageToContinue.id, {
|
||||||
|
content: originalContent,
|
||||||
|
thinking: originalThinking
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setConversationLoading(messageToContinue.convId, false);
|
||||||
|
this.clearConversationStreaming(messageToContinue.convId);
|
||||||
|
slotsService.clearConversationState(messageToContinue.convId);
|
||||||
|
|
||||||
|
const dialogType = error.name === 'TimeoutError' ? 'timeout' : 'server';
|
||||||
|
this.showErrorDialog(dialogType, error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messageToContinue.convId
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.isAbortError(error)) return;
|
||||||
|
console.error('Failed to continue message:', error);
|
||||||
|
if (this.activeConversation) {
|
||||||
|
this.setConversationLoading(this.activeConversation.id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public methods for accessing per-conversation states
|
* Public methods for accessing per-conversation states
|
||||||
*/
|
*/
|
||||||
|
|
@ -1743,8 +2004,11 @@ export const refreshActiveMessages = chatStore.refreshActiveMessages.bind(chatSt
|
||||||
export const navigateToSibling = chatStore.navigateToSibling.bind(chatStore);
|
export const navigateToSibling = chatStore.navigateToSibling.bind(chatStore);
|
||||||
export const editAssistantMessage = chatStore.editAssistantMessage.bind(chatStore);
|
export const editAssistantMessage = chatStore.editAssistantMessage.bind(chatStore);
|
||||||
export const editMessageWithBranching = chatStore.editMessageWithBranching.bind(chatStore);
|
export const editMessageWithBranching = chatStore.editMessageWithBranching.bind(chatStore);
|
||||||
|
export const editUserMessagePreserveResponses =
|
||||||
|
chatStore.editUserMessagePreserveResponses.bind(chatStore);
|
||||||
export const regenerateMessageWithBranching =
|
export const regenerateMessageWithBranching =
|
||||||
chatStore.regenerateMessageWithBranching.bind(chatStore);
|
chatStore.regenerateMessageWithBranching.bind(chatStore);
|
||||||
|
export const continueAssistantMessage = chatStore.continueAssistantMessage.bind(chatStore);
|
||||||
export const deleteMessage = chatStore.deleteMessage.bind(chatStore);
|
export const deleteMessage = chatStore.deleteMessage.bind(chatStore);
|
||||||
export const getDeletionInfo = chatStore.getDeletionInfo.bind(chatStore);
|
export const getDeletionInfo = chatStore.getDeletionInfo.bind(chatStore);
|
||||||
export const updateConversationName = chatStore.updateConversationName.bind(chatStore);
|
export const updateConversationName = chatStore.updateConversationName.bind(chatStore);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export interface SettingsFieldConfig {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: 'input' | 'textarea' | 'checkbox' | 'select';
|
type: 'input' | 'textarea' | 'checkbox' | 'select';
|
||||||
|
isExperimental?: boolean;
|
||||||
help?: string;
|
help?: string;
|
||||||
options?: Array<{ value: string; label: string; icon?: typeof import('@lucide/svelte').Icon }>;
|
options?: Array<{ value: string; label: string; icon?: typeof import('@lucide/svelte').Icon }>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue