ui: Clean up contexts, remove prop drilling from Chat Form Actions (#26951)
* refactor: Remove dead context for Chat Settings and create a new one for Chat Messages Actions * refactor: Contexts & types
This commit is contained in:
Vendored
+1
-1
@@ -32,8 +32,8 @@ import type {
|
|||||||
ApiRouterModelsStatusResponse,
|
ApiRouterModelsStatusResponse,
|
||||||
ApiRouterModelsUnloadRequest,
|
ApiRouterModelsUnloadRequest,
|
||||||
ApiRouterModelsUnloadResponse,
|
ApiRouterModelsUnloadResponse,
|
||||||
// Chat types
|
|
||||||
ChatAttachmentDisplayItem,
|
ChatAttachmentDisplayItem,
|
||||||
|
// Chat types
|
||||||
ChatMessagePromptProgress,
|
ChatMessagePromptProgress,
|
||||||
ChatMessageSiblingInfo,
|
ChatMessageSiblingInfo,
|
||||||
ChatMessageTimings,
|
ChatMessageTimings,
|
||||||
|
|||||||
+22
-38
@@ -15,37 +15,16 @@
|
|||||||
ICON_CLASS_DEFAULT,
|
ICON_CLASS_DEFAULT,
|
||||||
TOOLTIP_DELAY_DURATION
|
TOOLTIP_DELAY_DURATION
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
|
import { getChatFormActionsContext } from '$lib/contexts';
|
||||||
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
disabled?: boolean;
|
|
||||||
hasAudioModality?: boolean;
|
|
||||||
hasVideoModality?: boolean;
|
|
||||||
hasVisionModality?: boolean;
|
|
||||||
hasMcpPromptsSupport?: boolean;
|
|
||||||
hasMcpResourcesSupport?: boolean;
|
|
||||||
onFileUpload?: () => void;
|
|
||||||
onSystemPromptClick?: () => void;
|
|
||||||
onMcpPromptClick?: () => void;
|
|
||||||
onMcpSettingsClick?: () => void;
|
|
||||||
onMcpResourcesClick?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { class: className = '' }: Props = $props();
|
||||||
class: className = '',
|
|
||||||
disabled = false,
|
const chatFormActions = getChatFormActionsContext();
|
||||||
hasAudioModality = false,
|
|
||||||
hasMcpPromptsSupport = false,
|
|
||||||
hasMcpResourcesSupport = false,
|
|
||||||
hasVideoModality = false,
|
|
||||||
hasVisionModality = false,
|
|
||||||
onFileUpload,
|
|
||||||
onMcpPromptClick,
|
|
||||||
onMcpResourcesClick,
|
|
||||||
onMcpSettingsClick,
|
|
||||||
onSystemPromptClick
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let dropdownOpen = $state(false);
|
let dropdownOpen = $state(false);
|
||||||
// The system message action moves focus to the message editor, so the menu
|
// The system message action moves focus to the message editor, so the menu
|
||||||
@@ -54,18 +33,23 @@
|
|||||||
|
|
||||||
function handleMcpSettingsClick() {
|
function handleMcpSettingsClick() {
|
||||||
dropdownOpen = false;
|
dropdownOpen = false;
|
||||||
onMcpSettingsClick?.();
|
chatFormActions.onMcpSettingsClick?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachmentMenu = useAttachmentMenu(
|
const attachmentMenu = useAttachmentMenu(
|
||||||
() => ({
|
() => ({
|
||||||
hasAudioModality,
|
hasAudioModality: chatFormActions.hasAudioModality,
|
||||||
hasMcpPromptsSupport,
|
hasMcpPromptsSupport: chatFormActions.hasMcpPromptsSupport,
|
||||||
hasMcpResourcesSupport,
|
hasMcpResourcesSupport: chatFormActions.hasMcpResourcesSupport,
|
||||||
hasVideoModality,
|
hasVideoModality: chatFormActions.hasVideoModality,
|
||||||
hasVisionModality
|
hasVisionModality: chatFormActions.hasVisionModality
|
||||||
|
}),
|
||||||
|
() => ({
|
||||||
|
onFileUpload: chatFormActions.onFileUpload,
|
||||||
|
onMcpPromptClick: chatFormActions.onMcpPromptClick,
|
||||||
|
onMcpResourcesClick: chatFormActions.onMcpResourcesClick,
|
||||||
|
onSystemPromptClick: chatFormActions.onSystemPromptClick
|
||||||
}),
|
}),
|
||||||
() => ({ onFileUpload, onMcpPromptClick, onMcpResourcesClick, onSystemPromptClick }),
|
|
||||||
() => {
|
() => {
|
||||||
dropdownOpen = false;
|
dropdownOpen = false;
|
||||||
}
|
}
|
||||||
@@ -85,7 +69,7 @@
|
|||||||
buttonVariants({ variant: 'secondary' }),
|
buttonVariants({ variant: 'secondary' }),
|
||||||
'file-upload-button h-8 w-8 cursor-pointer rounded-full p-0'
|
'file-upload-button h-8 w-8 cursor-pointer rounded-full p-0'
|
||||||
)}
|
)}
|
||||||
{disabled}
|
disabled={chatFormActions.disabled}
|
||||||
>
|
>
|
||||||
<span class="sr-only">{ATTACHMENT_TOOLTIP_TEXT}</span>
|
<span class="sr-only">{ATTACHMENT_TOOLTIP_TEXT}</span>
|
||||||
|
|
||||||
@@ -162,7 +146,7 @@
|
|||||||
class="flex cursor-pointer items-center gap-2"
|
class="flex cursor-pointer items-center gap-2"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
suppressCloseAutoFocus = true;
|
suppressCloseAutoFocus = true;
|
||||||
onSystemPromptClick?.();
|
chatFormActions.onSystemPromptClick?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MessageSquare class={ICON_CLASS_DEFAULT} />
|
<MessageSquare class={ICON_CLASS_DEFAULT} />
|
||||||
@@ -174,12 +158,12 @@
|
|||||||
|
|
||||||
<ChatFormActionAddMcpServersSubmenu onMcpSettingsClick={handleMcpSettingsClick} />
|
<ChatFormActionAddMcpServersSubmenu onMcpSettingsClick={handleMcpSettingsClick} />
|
||||||
|
|
||||||
{#if hasMcpPromptsSupport}
|
{#if chatFormActions.hasMcpPromptsSupport}
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex cursor-pointer items-center gap-2"
|
class="flex cursor-pointer items-center gap-2"
|
||||||
onclick={onMcpPromptClick}
|
onclick={chatFormActions.onMcpPromptClick}
|
||||||
>
|
>
|
||||||
<Zap class={ICON_CLASS_DEFAULT} />
|
<Zap class={ICON_CLASS_DEFAULT} />
|
||||||
|
|
||||||
@@ -187,10 +171,10 @@
|
|||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if hasMcpResourcesSupport}
|
{#if chatFormActions.hasMcpResourcesSupport}
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex cursor-pointer items-center gap-2"
|
class="flex cursor-pointer items-center gap-2"
|
||||||
onclick={onMcpResourcesClick}
|
onclick={chatFormActions.onMcpResourcesClick}
|
||||||
>
|
>
|
||||||
<FolderOpen class={ICON_CLASS_DEFAULT} />
|
<FolderOpen class={ICON_CLASS_DEFAULT} />
|
||||||
|
|
||||||
|
|||||||
+18
-33
@@ -19,6 +19,7 @@
|
|||||||
ICON_CLASS_DEFAULT,
|
ICON_CLASS_DEFAULT,
|
||||||
TOOLTIP_DELAY_DURATION
|
TOOLTIP_DELAY_DURATION
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
|
import { getChatFormActionsContext } from '$lib/contexts';
|
||||||
import { HealthCheckStatus } from '$lib/enums';
|
import { HealthCheckStatus } from '$lib/enums';
|
||||||
import { AttachmentAction } from '$lib/enums/attachment.enums';
|
import { AttachmentAction } from '$lib/enums/attachment.enums';
|
||||||
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
||||||
@@ -29,33 +30,12 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
disabled?: boolean;
|
|
||||||
hasAudioModality?: boolean;
|
|
||||||
hasVideoModality?: boolean;
|
|
||||||
hasVisionModality?: boolean;
|
|
||||||
hasMcpPromptsSupport?: boolean;
|
|
||||||
hasMcpResourcesSupport?: boolean;
|
|
||||||
onFileUpload?: () => void;
|
|
||||||
onSystemPromptClick?: () => void;
|
|
||||||
onMcpPromptClick?: () => void;
|
|
||||||
onMcpResourcesClick?: () => void;
|
|
||||||
trigger: Snippet<[{ disabled: boolean; onclick?: () => void }]>;
|
trigger: Snippet<[{ disabled: boolean; onclick?: () => void }]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { class: className = '', trigger }: Props = $props();
|
||||||
class: className = '',
|
|
||||||
disabled = false,
|
const chatFormActions = getChatFormActionsContext();
|
||||||
hasAudioModality = false,
|
|
||||||
hasMcpPromptsSupport = false,
|
|
||||||
hasMcpResourcesSupport = false,
|
|
||||||
hasVideoModality = false,
|
|
||||||
hasVisionModality = false,
|
|
||||||
onFileUpload,
|
|
||||||
onMcpPromptClick,
|
|
||||||
onMcpResourcesClick,
|
|
||||||
onSystemPromptClick,
|
|
||||||
trigger
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let sheetOpen = $state(false);
|
let sheetOpen = $state(false);
|
||||||
let reasoningExpanded = $state(false);
|
let reasoningExpanded = $state(false);
|
||||||
@@ -65,13 +45,18 @@
|
|||||||
|
|
||||||
const attachmentMenu = useAttachmentMenu(
|
const attachmentMenu = useAttachmentMenu(
|
||||||
() => ({
|
() => ({
|
||||||
hasAudioModality,
|
hasAudioModality: chatFormActions.hasAudioModality,
|
||||||
hasMcpPromptsSupport,
|
hasMcpPromptsSupport: chatFormActions.hasMcpPromptsSupport,
|
||||||
hasMcpResourcesSupport,
|
hasMcpResourcesSupport: chatFormActions.hasMcpResourcesSupport,
|
||||||
hasVideoModality,
|
hasVideoModality: chatFormActions.hasVideoModality,
|
||||||
hasVisionModality
|
hasVisionModality: chatFormActions.hasVisionModality
|
||||||
|
}),
|
||||||
|
() => ({
|
||||||
|
onFileUpload: chatFormActions.onFileUpload,
|
||||||
|
onMcpPromptClick: chatFormActions.onMcpPromptClick,
|
||||||
|
onMcpResourcesClick: chatFormActions.onMcpResourcesClick,
|
||||||
|
onSystemPromptClick: chatFormActions.onSystemPromptClick
|
||||||
}),
|
}),
|
||||||
() => ({ onFileUpload, onMcpPromptClick, onMcpResourcesClick, onSystemPromptClick }),
|
|
||||||
() => {
|
() => {
|
||||||
sheetOpen = false;
|
sheetOpen = false;
|
||||||
}
|
}
|
||||||
@@ -91,7 +76,7 @@
|
|||||||
|
|
||||||
<div class="flex items-center gap-1 {className}">
|
<div class="flex items-center gap-1 {className}">
|
||||||
<Sheet.Root bind:open={sheetOpen}>
|
<Sheet.Root bind:open={sheetOpen}>
|
||||||
{@render trigger({ disabled, onclick: () => (sheetOpen = true) })}
|
{@render trigger({ disabled: chatFormActions.disabled, onclick: () => (sheetOpen = true) })}
|
||||||
|
|
||||||
<Sheet.Content side="bottom" class="max-h-[85vh] gap-0 overflow-y-auto">
|
<Sheet.Content side="bottom" class="max-h-[85vh] gap-0 overflow-y-auto">
|
||||||
<Sheet.Header>
|
<Sheet.Header>
|
||||||
@@ -350,7 +335,7 @@
|
|||||||
<span>System Message</span>
|
<span>System Message</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if hasMcpPromptsSupport}
|
{#if chatFormActions.hasMcpPromptsSupport}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={sheetItemClass}
|
class={sheetItemClass}
|
||||||
@@ -362,7 +347,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if hasMcpResourcesSupport}
|
{#if chatFormActions.hasMcpResourcesSupport}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={sheetItemClass}
|
class={sheetItemClass}
|
||||||
|
|||||||
+2
-53
@@ -3,65 +3,14 @@
|
|||||||
import ChatFormActionAddDropdown from './ChatFormActionAddDropdown.svelte';
|
import ChatFormActionAddDropdown from './ChatFormActionAddDropdown.svelte';
|
||||||
import ChatFormActionAddSheet from './ChatFormActionAddSheet.svelte';
|
import ChatFormActionAddSheet from './ChatFormActionAddSheet.svelte';
|
||||||
import { isMobile } from '$lib/stores';
|
import { isMobile } from '$lib/stores';
|
||||||
|
|
||||||
interface Props {
|
|
||||||
disabled?: boolean;
|
|
||||||
hasAudioModality?: boolean;
|
|
||||||
hasVideoModality?: boolean;
|
|
||||||
hasMcpPromptsSupport?: boolean;
|
|
||||||
hasMcpResourcesSupport?: boolean;
|
|
||||||
hasVisionModality?: boolean;
|
|
||||||
onFileUpload?: () => void;
|
|
||||||
onMcpPromptClick?: () => void;
|
|
||||||
onMcpResourcesClick?: () => void;
|
|
||||||
onMcpSettingsClick?: () => void;
|
|
||||||
onSystemPromptClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
disabled = false,
|
|
||||||
hasAudioModality = false,
|
|
||||||
hasMcpPromptsSupport = false,
|
|
||||||
hasMcpResourcesSupport = false,
|
|
||||||
hasVideoModality = false,
|
|
||||||
hasVisionModality = false,
|
|
||||||
onFileUpload,
|
|
||||||
onMcpPromptClick,
|
|
||||||
onMcpResourcesClick,
|
|
||||||
onMcpSettingsClick,
|
|
||||||
onSystemPromptClick
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isMobile.current}
|
{#if isMobile.current}
|
||||||
<ChatFormActionAddSheet
|
<ChatFormActionAddSheet>
|
||||||
{disabled}
|
|
||||||
{hasAudioModality}
|
|
||||||
{hasVideoModality}
|
|
||||||
{hasVisionModality}
|
|
||||||
{hasMcpPromptsSupport}
|
|
||||||
{hasMcpResourcesSupport}
|
|
||||||
{onFileUpload}
|
|
||||||
{onSystemPromptClick}
|
|
||||||
{onMcpPromptClick}
|
|
||||||
{onMcpResourcesClick}
|
|
||||||
>
|
|
||||||
{#snippet trigger({ disabled, onclick })}
|
{#snippet trigger({ disabled, onclick })}
|
||||||
<ChatFormActionAddButton {disabled} {onclick} />
|
<ChatFormActionAddButton {disabled} {onclick} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ChatFormActionAddSheet>
|
</ChatFormActionAddSheet>
|
||||||
{:else}
|
{:else}
|
||||||
<ChatFormActionAddDropdown
|
<ChatFormActionAddDropdown />
|
||||||
{disabled}
|
|
||||||
{hasAudioModality}
|
|
||||||
{hasVideoModality}
|
|
||||||
{hasVisionModality}
|
|
||||||
{hasMcpPromptsSupport}
|
|
||||||
{hasMcpResourcesSupport}
|
|
||||||
{onFileUpload}
|
|
||||||
{onMcpPromptClick}
|
|
||||||
{onMcpResourcesClick}
|
|
||||||
{onMcpSettingsClick}
|
|
||||||
{onSystemPromptClick}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
+38
-13
@@ -11,6 +11,7 @@
|
|||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { ICON_CLASS_DEFAULT, ROUTES } from '$lib/constants';
|
import { ICON_CLASS_DEFAULT, ROUTES } from '$lib/constants';
|
||||||
|
import { setChatFormActionsContext } from '$lib/contexts';
|
||||||
import { FileTypeCategory, MessageRole } from '$lib/enums';
|
import { FileTypeCategory, MessageRole } from '$lib/enums';
|
||||||
import { ChatService } from '$lib/services';
|
import { ChatService } from '$lib/services';
|
||||||
import { chatStore, conversationsStore, mcpStore, settingsStore } from '$lib/stores';
|
import { chatStore, conversationsStore, mcpStore, settingsStore } from '$lib/stores';
|
||||||
@@ -132,6 +133,42 @@
|
|||||||
|
|
||||||
return livePromptTokens > 0 || liveOutputTokens > 0;
|
return livePromptTokens > 0 || liveOutputTokens > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setChatFormActionsContext({
|
||||||
|
get disabled() {
|
||||||
|
return disabled;
|
||||||
|
},
|
||||||
|
get hasAudioModality() {
|
||||||
|
return hasAudioModality;
|
||||||
|
},
|
||||||
|
get hasMcpPromptsSupport() {
|
||||||
|
return hasMcpPromptsSupport;
|
||||||
|
},
|
||||||
|
get hasMcpResourcesSupport() {
|
||||||
|
return hasMcpResourcesSupport;
|
||||||
|
},
|
||||||
|
get hasVideoModality() {
|
||||||
|
return hasVideoModality;
|
||||||
|
},
|
||||||
|
get hasVisionModality() {
|
||||||
|
return hasVisionModality;
|
||||||
|
},
|
||||||
|
get onFileUpload() {
|
||||||
|
return onFileUpload;
|
||||||
|
},
|
||||||
|
get onMcpPromptClick() {
|
||||||
|
return onMcpPromptClick;
|
||||||
|
},
|
||||||
|
get onMcpResourcesClick() {
|
||||||
|
return onMcpResourcesClick;
|
||||||
|
},
|
||||||
|
get onMcpSettingsClick() {
|
||||||
|
return () => goto(ROUTES.MCP_SERVERS);
|
||||||
|
},
|
||||||
|
get onSystemPromptClick() {
|
||||||
|
return onSystemPromptClick;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -140,19 +177,7 @@
|
|||||||
>
|
>
|
||||||
{#if showAddButton}
|
{#if showAddButton}
|
||||||
<div class="mr-auto flex items-center gap-2">
|
<div class="mr-auto flex items-center gap-2">
|
||||||
<ChatFormActionsAdd
|
<ChatFormActionsAdd />
|
||||||
{disabled}
|
|
||||||
{hasAudioModality}
|
|
||||||
{hasVideoModality}
|
|
||||||
{hasVisionModality}
|
|
||||||
{hasMcpPromptsSupport}
|
|
||||||
{hasMcpResourcesSupport}
|
|
||||||
{onFileUpload}
|
|
||||||
{onSystemPromptClick}
|
|
||||||
{onMcpPromptClick}
|
|
||||||
{onMcpResourcesClick}
|
|
||||||
onMcpSettingsClick={() => goto(ROUTES.MCP_SERVERS)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,21 @@
|
|||||||
ChatMessageUser
|
ChatMessageUser
|
||||||
} from '$lib/components/app/chat';
|
} from '$lib/components/app/chat';
|
||||||
import { REASONING_TAGS, ROUTES, SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
|
import { REASONING_TAGS, ROUTES, SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
|
||||||
import { getChatActionsContext, setMessageEditContext } from '$lib/contexts';
|
import { setChatMessageActionsContext, setChatMessageEditContext } from '$lib/contexts';
|
||||||
import { AgenticSectionType, AttachmentType, MessageRole } from '$lib/enums';
|
import { AgenticSectionType, AttachmentType, MessageRole } from '$lib/enums';
|
||||||
import { DatabaseService } from '$lib/services/database.service';
|
import { DatabaseService } from '$lib/services/database.service';
|
||||||
import { chatStore, conversationsStore, isMobile } from '$lib/stores';
|
import { chatStore, conversationsStore, isMobile } from '$lib/stores';
|
||||||
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
import type {
|
||||||
|
ChatMessageActions,
|
||||||
|
ChatMessageDeletionInfo,
|
||||||
|
DatabaseMessageExtraMcpPrompt
|
||||||
|
} from '$lib/types';
|
||||||
import { deriveAgenticSections } from '$lib/utils';
|
import { deriveAgenticSections } from '$lib/utils';
|
||||||
import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
|
import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
chatActions: ChatMessageActions;
|
||||||
message: DatabaseMessage;
|
message: DatabaseMessage;
|
||||||
toolMessages?: DatabaseMessage[];
|
toolMessages?: DatabaseMessage[];
|
||||||
isLastAssistantMessage?: boolean;
|
isLastAssistantMessage?: boolean;
|
||||||
@@ -27,6 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
chatActions,
|
||||||
class: className = '',
|
class: className = '',
|
||||||
isLastAssistantMessage = false,
|
isLastAssistantMessage = false,
|
||||||
isLastUserMessage = false,
|
isLastUserMessage = false,
|
||||||
@@ -36,14 +42,7 @@
|
|||||||
toolMessages = []
|
toolMessages = []
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const chatActions = getChatActionsContext();
|
let deletionInfo = $state<ChatMessageDeletionInfo | null>(null);
|
||||||
|
|
||||||
let deletionInfo = $state<{
|
|
||||||
totalCount: number;
|
|
||||||
userMessages: number;
|
|
||||||
assistantMessages: number;
|
|
||||||
messageTypes: string[];
|
|
||||||
} | null>(null);
|
|
||||||
// The system message placeholder must never surface as editable content; keeping
|
// The system message placeholder must never surface as editable content; keeping
|
||||||
// it in the derived (not just in handleEdit) guards against prop invalidation
|
// it in the derived (not just in handleEdit) guards against prop invalidation
|
||||||
// reverting the override while editing
|
// reverting the override while editing
|
||||||
@@ -112,7 +111,7 @@
|
|||||||
let showSaveOnlyOption = $derived(message.role === MessageRole.USER);
|
let showSaveOnlyOption = $derived(message.role === MessageRole.USER);
|
||||||
let showBranchAfterEditOption = $derived(message.role === MessageRole.ASSISTANT);
|
let showBranchAfterEditOption = $derived(message.role === MessageRole.ASSISTANT);
|
||||||
|
|
||||||
setMessageEditContext({
|
setChatMessageEditContext({
|
||||||
cancel: handleCancelEdit,
|
cancel: handleCancelEdit,
|
||||||
get editedContent() {
|
get editedContent() {
|
||||||
return editedContent;
|
return editedContent;
|
||||||
@@ -166,6 +165,30 @@
|
|||||||
startEdit: handleEdit
|
startEdit: handleEdit
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setChatMessageActionsContext({
|
||||||
|
confirmDelete: handleConfirmDelete,
|
||||||
|
copy: handleCopy,
|
||||||
|
get deletionInfo() {
|
||||||
|
return deletionInfo;
|
||||||
|
},
|
||||||
|
get forkConversation() {
|
||||||
|
const isForkableUser = message.role === MessageRole.USER && !mcpPromptExtra;
|
||||||
|
|
||||||
|
return isForkableUser || message.role === MessageRole.ASSISTANT
|
||||||
|
? handleForkConversation
|
||||||
|
: undefined;
|
||||||
|
},
|
||||||
|
navigateToSibling: handleNavigateToSibling,
|
||||||
|
requestDelete: handleDelete,
|
||||||
|
setShowDeleteDialog: handleShowDeleteDialogChange,
|
||||||
|
get showDeleteDialog() {
|
||||||
|
return showDeleteDialog;
|
||||||
|
},
|
||||||
|
get siblingInfo() {
|
||||||
|
return siblingInfo;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mcpPromptExtra = $derived.by(() => {
|
let mcpPromptExtra = $derived.by(() => {
|
||||||
if (message.role !== MessageRole.USER) return null;
|
if (message.role !== MessageRole.USER) return null;
|
||||||
|
|
||||||
@@ -360,73 +383,22 @@
|
|||||||
|
|
||||||
<div class="chat-message" class:chat-message--synthetic={isSynthetic}>
|
<div class="chat-message" class:chat-message--synthetic={isSynthetic}>
|
||||||
{#if message.role === MessageRole.SYSTEM}
|
{#if message.role === MessageRole.SYSTEM}
|
||||||
<ChatMessageSystem
|
<ChatMessageSystem bind:textareaElement class={className} {message} />
|
||||||
bind:textareaElement
|
|
||||||
class={className}
|
|
||||||
{deletionInfo}
|
|
||||||
{message}
|
|
||||||
onConfirmDelete={handleConfirmDelete}
|
|
||||||
onCopy={handleCopy}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
onEdit={handleEdit}
|
|
||||||
onNavigateToSibling={handleNavigateToSibling}
|
|
||||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
|
||||||
{showDeleteDialog}
|
|
||||||
{siblingInfo}
|
|
||||||
/>
|
|
||||||
{:else if mcpPromptExtra}
|
{:else if mcpPromptExtra}
|
||||||
<ChatMessageMcpPrompt
|
<ChatMessageMcpPrompt class={className} {message} mcpPrompt={mcpPromptExtra} />
|
||||||
class={className}
|
|
||||||
{deletionInfo}
|
|
||||||
{message}
|
|
||||||
mcpPrompt={mcpPromptExtra}
|
|
||||||
onConfirmDelete={handleConfirmDelete}
|
|
||||||
onCopy={handleCopy}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
onEdit={handleEdit}
|
|
||||||
onNavigateToSibling={handleNavigateToSibling}
|
|
||||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
|
||||||
{showDeleteDialog}
|
|
||||||
{siblingInfo}
|
|
||||||
/>
|
|
||||||
{:else if isSynthetic}
|
{:else if isSynthetic}
|
||||||
<ChatMessageSynthetic {message} class={className} />
|
<ChatMessageSynthetic {message} class={className} />
|
||||||
{:else if message.role === MessageRole.USER}
|
{:else if message.role === MessageRole.USER}
|
||||||
<ChatMessageUser
|
<ChatMessageUser class={className} {isLastUserMessage} {message} {nextAssistantMessage} />
|
||||||
class={className}
|
|
||||||
{deletionInfo}
|
|
||||||
{isLastUserMessage}
|
|
||||||
{message}
|
|
||||||
{nextAssistantMessage}
|
|
||||||
onConfirmDelete={handleConfirmDelete}
|
|
||||||
onCopy={handleCopy}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
onEdit={handleEdit}
|
|
||||||
onForkConversation={handleForkConversation}
|
|
||||||
onNavigateToSibling={handleNavigateToSibling}
|
|
||||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
|
||||||
{showDeleteDialog}
|
|
||||||
{siblingInfo}
|
|
||||||
/>
|
|
||||||
{:else}
|
{:else}
|
||||||
<ChatMessageAssistant
|
<ChatMessageAssistant
|
||||||
bind:textareaElement
|
bind:textareaElement
|
||||||
class={className}
|
class={className}
|
||||||
{deletionInfo}
|
|
||||||
{isLastAssistantMessage}
|
{isLastAssistantMessage}
|
||||||
{message}
|
{message}
|
||||||
{toolMessages}
|
{toolMessages}
|
||||||
onConfirmDelete={handleConfirmDelete}
|
|
||||||
onContinue={handleContinue}
|
onContinue={handleContinue}
|
||||||
onCopy={handleCopy}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
onEdit={handleEdit}
|
|
||||||
onForkConversation={handleForkConversation}
|
|
||||||
onNavigateToSibling={handleNavigateToSibling}
|
|
||||||
onRegenerate={handleRegenerate}
|
onRegenerate={handleRegenerate}
|
||||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
|
||||||
{showDeleteDialog}
|
|
||||||
{siblingInfo}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-37
@@ -8,7 +8,7 @@
|
|||||||
ChatMessageAssistantStatistics,
|
ChatMessageAssistantStatistics,
|
||||||
ChatMessageEditForm
|
ChatMessageEditForm
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import { getMessageEditContext } from '$lib/contexts';
|
import { getChatMessageEditContext } from '$lib/contexts';
|
||||||
import { MessageRole } from '$lib/enums';
|
import { MessageRole } from '$lib/enums';
|
||||||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||||
import { chatStore, modelsStore, serverStore, settingsStore } from '$lib/stores';
|
import { chatStore, modelsStore, serverStore, settingsStore } from '$lib/stores';
|
||||||
@@ -17,51 +17,26 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
deletionInfo: {
|
|
||||||
totalCount: number;
|
|
||||||
userMessages: number;
|
|
||||||
assistantMessages: number;
|
|
||||||
messageTypes: string[];
|
|
||||||
} | null;
|
|
||||||
isLastAssistantMessage?: boolean;
|
isLastAssistantMessage?: boolean;
|
||||||
message: DatabaseMessage;
|
message: DatabaseMessage;
|
||||||
toolMessages?: DatabaseMessage[];
|
toolMessages?: DatabaseMessage[];
|
||||||
onCopy: () => void;
|
|
||||||
onConfirmDelete: () => void;
|
|
||||||
onContinue?: () => void;
|
onContinue?: () => void;
|
||||||
onDelete: () => void;
|
|
||||||
onEdit?: () => void;
|
|
||||||
onForkConversation?: (options: { name: string; includeAttachments: boolean }) => void;
|
|
||||||
onNavigateToSibling?: (siblingId: string) => void;
|
|
||||||
onRegenerate: (modelOverride?: string) => void;
|
onRegenerate: (modelOverride?: string) => void;
|
||||||
onShowDeleteDialogChange: (show: boolean) => void;
|
|
||||||
showDeleteDialog: boolean;
|
|
||||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
|
||||||
textareaElement?: HTMLTextAreaElement;
|
textareaElement?: HTMLTextAreaElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
class: className = '',
|
class: className = '',
|
||||||
deletionInfo,
|
|
||||||
isLastAssistantMessage = false,
|
isLastAssistantMessage = false,
|
||||||
message,
|
message,
|
||||||
onConfirmDelete,
|
|
||||||
onContinue,
|
onContinue,
|
||||||
onCopy,
|
|
||||||
onDelete,
|
|
||||||
onEdit,
|
|
||||||
onForkConversation,
|
|
||||||
onNavigateToSibling,
|
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
onShowDeleteDialogChange,
|
|
||||||
showDeleteDialog,
|
|
||||||
siblingInfo = null,
|
|
||||||
textareaElement = $bindable(),
|
textareaElement = $bindable(),
|
||||||
toolMessages = []
|
toolMessages = []
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// Get edit context
|
// Get edit context
|
||||||
const editCtx = getMessageEditContext();
|
const editCtx = getChatMessageEditContext();
|
||||||
|
|
||||||
const isAgentic = $derived(hasAgenticContent(message, toolMessages));
|
const isAgentic = $derived(hasAgenticContent(message, toolMessages));
|
||||||
const processingState = useProcessingState();
|
const processingState = useProcessingState();
|
||||||
@@ -207,18 +182,8 @@
|
|||||||
role={MessageRole.ASSISTANT}
|
role={MessageRole.ASSISTANT}
|
||||||
justify="start"
|
justify="start"
|
||||||
actionsPosition="left"
|
actionsPosition="left"
|
||||||
{siblingInfo}
|
|
||||||
{showDeleteDialog}
|
|
||||||
{deletionInfo}
|
|
||||||
{onCopy}
|
|
||||||
{onEdit}
|
|
||||||
{onRegenerate}
|
{onRegenerate}
|
||||||
onContinue={currentConfig.enableContinueGeneration ? onContinue : undefined}
|
onContinue={currentConfig.enableContinueGeneration ? onContinue : undefined}
|
||||||
{onForkConversation}
|
|
||||||
{onDelete}
|
|
||||||
{onConfirmDelete}
|
|
||||||
{onNavigateToSibling}
|
|
||||||
{onShowDeleteDialogChange}
|
|
||||||
showRawOutputSwitch={currentConfig.showRawOutputSwitch}
|
showRawOutputSwitch={currentConfig.showRawOutputSwitch}
|
||||||
rawOutputEnabled={showRawOutput}
|
rawOutputEnabled={showRawOutput}
|
||||||
onRawOutputToggle={(enabled) => (showRawOutput = enabled)}
|
onRawOutputToggle={(enabled) => (showRawOutput = enabled)}
|
||||||
|
|||||||
+4
-44
@@ -4,7 +4,7 @@
|
|||||||
ChatMessageEditForm,
|
ChatMessageEditForm,
|
||||||
ChatMessageMcpPromptContent
|
ChatMessageMcpPromptContent
|
||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import { getMessageEditContext } from '$lib/contexts';
|
import { getChatMessageEditContext } from '$lib/contexts';
|
||||||
import { McpPromptVariant, MessageRole } from '$lib/enums';
|
import { McpPromptVariant, MessageRole } from '$lib/enums';
|
||||||
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
||||||
|
|
||||||
@@ -12,39 +12,12 @@
|
|||||||
class?: string;
|
class?: string;
|
||||||
message: DatabaseMessage;
|
message: DatabaseMessage;
|
||||||
mcpPrompt: DatabaseMessageExtraMcpPrompt;
|
mcpPrompt: DatabaseMessageExtraMcpPrompt;
|
||||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
|
||||||
showDeleteDialog: boolean;
|
|
||||||
deletionInfo: {
|
|
||||||
totalCount: number;
|
|
||||||
userMessages: number;
|
|
||||||
assistantMessages: number;
|
|
||||||
messageTypes: string[];
|
|
||||||
} | null;
|
|
||||||
onCopy: () => void;
|
|
||||||
onEdit: () => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
onConfirmDelete: () => void;
|
|
||||||
onNavigateToSibling?: (siblingId: string) => void;
|
|
||||||
onShowDeleteDialogChange: (show: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { class: className = '', mcpPrompt, message }: Props = $props();
|
||||||
class: className = '',
|
|
||||||
deletionInfo,
|
|
||||||
mcpPrompt,
|
|
||||||
message,
|
|
||||||
onConfirmDelete,
|
|
||||||
onCopy,
|
|
||||||
onDelete,
|
|
||||||
onEdit,
|
|
||||||
onNavigateToSibling,
|
|
||||||
onShowDeleteDialogChange,
|
|
||||||
showDeleteDialog,
|
|
||||||
siblingInfo = null
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
// Get edit context
|
// Get edit context
|
||||||
const editCtx = getMessageEditContext();
|
const editCtx = getChatMessageEditContext();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -63,20 +36,7 @@
|
|||||||
|
|
||||||
{#if message.timestamp}
|
{#if message.timestamp}
|
||||||
<div class="max-w-[80%]">
|
<div class="max-w-[80%]">
|
||||||
<ChatMessageActionIcons
|
<ChatMessageActionIcons actionsPosition="right" justify="end" role={MessageRole.USER} />
|
||||||
actionsPosition="right"
|
|
||||||
{deletionInfo}
|
|
||||||
justify="end"
|
|
||||||
{onConfirmDelete}
|
|
||||||
{onCopy}
|
|
||||||
{onDelete}
|
|
||||||
{onEdit}
|
|
||||||
{onNavigateToSibling}
|
|
||||||
{onShowDeleteDialogChange}
|
|
||||||
{siblingInfo}
|
|
||||||
{showDeleteDialog}
|
|
||||||
role={MessageRole.USER}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
+4
-44
@@ -4,7 +4,7 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Card } from '$lib/components/ui/card';
|
import { Card } from '$lib/components/ui/card';
|
||||||
import { INPUT_CLASSES } from '$lib/constants';
|
import { INPUT_CLASSES } from '$lib/constants';
|
||||||
import { getMessageEditContext } from '$lib/contexts';
|
import { getChatMessageEditContext } from '$lib/contexts';
|
||||||
import { KeyboardKey, MessageRole } from '$lib/enums';
|
import { KeyboardKey, MessageRole } from '$lib/enums';
|
||||||
import { settingsStore } from '$lib/stores';
|
import { settingsStore } from '$lib/stores';
|
||||||
import { autoResizeTextarea, isIMEComposing } from '$lib/utils';
|
import { autoResizeTextarea, isIMEComposing } from '$lib/utils';
|
||||||
@@ -12,39 +12,12 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
message: DatabaseMessage;
|
message: DatabaseMessage;
|
||||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
|
||||||
showDeleteDialog: boolean;
|
|
||||||
deletionInfo: {
|
|
||||||
totalCount: number;
|
|
||||||
userMessages: number;
|
|
||||||
assistantMessages: number;
|
|
||||||
messageTypes: string[];
|
|
||||||
} | null;
|
|
||||||
onCopy: () => void;
|
|
||||||
onEdit: () => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
onConfirmDelete: () => void;
|
|
||||||
onNavigateToSibling?: (siblingId: string) => void;
|
|
||||||
onShowDeleteDialogChange: (show: boolean) => void;
|
|
||||||
textareaElement?: HTMLTextAreaElement;
|
textareaElement?: HTMLTextAreaElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { class: className = '', message, textareaElement = $bindable() }: Props = $props();
|
||||||
class: className = '',
|
|
||||||
deletionInfo,
|
|
||||||
message,
|
|
||||||
onConfirmDelete,
|
|
||||||
onCopy,
|
|
||||||
onDelete,
|
|
||||||
onEdit,
|
|
||||||
onNavigateToSibling,
|
|
||||||
onShowDeleteDialogChange,
|
|
||||||
showDeleteDialog,
|
|
||||||
siblingInfo = null,
|
|
||||||
textareaElement = $bindable()
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
const editCtx = getMessageEditContext();
|
const editCtx = getChatMessageEditContext();
|
||||||
|
|
||||||
function handleEditKeydown(event: KeyboardEvent) {
|
function handleEditKeydown(event: KeyboardEvent) {
|
||||||
if (event.key === KeyboardKey.ENTER && !event.shiftKey && !isIMEComposing(event)) {
|
if (event.key === KeyboardKey.ENTER && !event.shiftKey && !isIMEComposing(event)) {
|
||||||
@@ -218,20 +191,7 @@
|
|||||||
|
|
||||||
{#if message.timestamp}
|
{#if message.timestamp}
|
||||||
<div class="max-w-[80%]">
|
<div class="max-w-[80%]">
|
||||||
<ChatMessageActionIcons
|
<ChatMessageActionIcons actionsPosition="right" justify="end" role={MessageRole.USER} />
|
||||||
actionsPosition="right"
|
|
||||||
{deletionInfo}
|
|
||||||
justify="end"
|
|
||||||
{onConfirmDelete}
|
|
||||||
{onCopy}
|
|
||||||
{onDelete}
|
|
||||||
{onEdit}
|
|
||||||
{onNavigateToSibling}
|
|
||||||
{onShowDeleteDialogChange}
|
|
||||||
{siblingInfo}
|
|
||||||
{showDeleteDialog}
|
|
||||||
role={MessageRole.USER}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
+1
-2
@@ -12,8 +12,7 @@
|
|||||||
import ChatMessageToolCallBlockSearchResults from './ChatMessageToolCallBlockSearchResults.svelte';
|
import ChatMessageToolCallBlockSearchResults from './ChatMessageToolCallBlockSearchResults.svelte';
|
||||||
import ChatMessageToolCallBlockWriteFile from './ChatMessageToolCallBlockWriteFile.svelte';
|
import ChatMessageToolCallBlockWriteFile from './ChatMessageToolCallBlockWriteFile.svelte';
|
||||||
import { BuiltInTool } from '$lib/enums';
|
import { BuiltInTool } from '$lib/enums';
|
||||||
import type { AgenticSection } from '$lib/types';
|
import type { AgenticSection, DatabaseMessageExtra } from '$lib/types';
|
||||||
import type { DatabaseMessageExtra } from '$lib/types';
|
|
||||||
import { extractSearchQuery, extractSearchResults, isWebSearchToolName } from '$lib/utils';
|
import { extractSearchQuery, extractSearchResults, isWebSearchToolName } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
+1
-2
@@ -13,8 +13,7 @@
|
|||||||
import { SETTINGS_KEYS, TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
|
import { SETTINGS_KEYS, TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
|
||||||
import { AttachmentType } from '$lib/enums';
|
import { AttachmentType } from '$lib/enums';
|
||||||
import { settingsStore, toolsStore } from '$lib/stores';
|
import { settingsStore, toolsStore } from '$lib/stores';
|
||||||
import type { AgenticSection, ToolResultLine } from '$lib/types';
|
import type { AgenticSection, DatabaseMessageExtra, ToolResultLine } from '$lib/types';
|
||||||
import type { DatabaseMessageExtra } from '$lib/types';
|
|
||||||
import {
|
import {
|
||||||
abbreviateHome,
|
abbreviateHome,
|
||||||
type ExecShellExitStatus,
|
type ExecShellExitStatus,
|
||||||
|
|||||||
+4
-43
@@ -5,7 +5,7 @@
|
|||||||
ChatMessageStatistics,
|
ChatMessageStatistics,
|
||||||
ChatMessageUserBubble
|
ChatMessageUserBubble
|
||||||
} from '$lib/components/app/chat';
|
} from '$lib/components/app/chat';
|
||||||
import { getMessageEditContext } from '$lib/contexts';
|
import { getChatMessageEditContext } from '$lib/contexts';
|
||||||
import { ChatMessageStatisticsMode, MessageRole } from '$lib/enums';
|
import { ChatMessageStatisticsMode, MessageRole } from '$lib/enums';
|
||||||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||||
import { chatStore, settingsStore } from '$lib/stores';
|
import { chatStore, settingsStore } from '$lib/stores';
|
||||||
@@ -13,44 +13,19 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
message: DatabaseMessage;
|
message: DatabaseMessage;
|
||||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
|
||||||
deletionInfo: {
|
|
||||||
totalCount: number;
|
|
||||||
userMessages: number;
|
|
||||||
assistantMessages: number;
|
|
||||||
messageTypes: string[];
|
|
||||||
} | null;
|
|
||||||
isLastUserMessage?: boolean;
|
isLastUserMessage?: boolean;
|
||||||
nextAssistantMessage?: DatabaseMessage | null;
|
nextAssistantMessage?: DatabaseMessage | null;
|
||||||
showDeleteDialog: boolean;
|
|
||||||
onEdit: () => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
onConfirmDelete: () => void;
|
|
||||||
onForkConversation?: (options: { name: string; includeAttachments: boolean }) => void;
|
|
||||||
onShowDeleteDialogChange: (show: boolean) => void;
|
|
||||||
onNavigateToSibling?: (siblingId: string) => void;
|
|
||||||
onCopy: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
class: className = '',
|
class: className = '',
|
||||||
deletionInfo,
|
|
||||||
isLastUserMessage = false,
|
isLastUserMessage = false,
|
||||||
message,
|
message,
|
||||||
nextAssistantMessage = null,
|
nextAssistantMessage = null
|
||||||
onConfirmDelete,
|
|
||||||
onCopy,
|
|
||||||
onDelete,
|
|
||||||
onEdit,
|
|
||||||
onForkConversation,
|
|
||||||
onNavigateToSibling,
|
|
||||||
onShowDeleteDialogChange,
|
|
||||||
showDeleteDialog,
|
|
||||||
siblingInfo = null
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// Get contexts
|
// Get contexts
|
||||||
const editCtx = getMessageEditContext();
|
const editCtx = getChatMessageEditContext();
|
||||||
const processingState = useProcessingState();
|
const processingState = useProcessingState();
|
||||||
|
|
||||||
const currentConfig = $derived(settingsStore.config);
|
const currentConfig = $derived(settingsStore.config);
|
||||||
@@ -132,21 +107,7 @@
|
|||||||
|
|
||||||
{#if message.timestamp}
|
{#if message.timestamp}
|
||||||
<div class="max-w-[80%]">
|
<div class="max-w-[80%]">
|
||||||
<ChatMessageActionIcons
|
<ChatMessageActionIcons actionsPosition="right" justify="end" role={MessageRole.USER} />
|
||||||
actionsPosition="right"
|
|
||||||
{deletionInfo}
|
|
||||||
justify="end"
|
|
||||||
{onConfirmDelete}
|
|
||||||
{onCopy}
|
|
||||||
{onDelete}
|
|
||||||
{onEdit}
|
|
||||||
{onForkConversation}
|
|
||||||
{onNavigateToSibling}
|
|
||||||
{onShowDeleteDialogChange}
|
|
||||||
{siblingInfo}
|
|
||||||
{showDeleteDialog}
|
|
||||||
role={MessageRole.USER}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ArrowUp, Edit, Trash2 } from '@lucide/svelte';
|
import { ArrowUp, Edit, Trash2 } from '@lucide/svelte';
|
||||||
import { ActionIcon, ChatMessageEditForm, ChatMessageUserBubble } from '$lib/components/app';
|
import { ActionIcon, ChatMessageEditForm, ChatMessageUserBubble } from '$lib/components/app';
|
||||||
import { useMessageEditContext } from '$lib/hooks/use-message-edit-context.svelte';
|
import { useChatMessageEditContext } from '$lib/hooks/use-chat-message-edit-context.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
onSendImmediately
|
onSendImmediately
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const editCtx = useMessageEditContext({
|
const editCtx = useChatMessageEditContext({
|
||||||
getContent: () => content,
|
getContent: () => content,
|
||||||
getExtras: () => extras,
|
getExtras: () => extras,
|
||||||
onSave: (content, extras) => onEdit(content, extras)
|
onSave: (content, extras) => onEdit(content, extras)
|
||||||
|
|||||||
+23
-43
@@ -9,6 +9,7 @@
|
|||||||
import Input from '$lib/components/ui/input/input.svelte';
|
import Input from '$lib/components/ui/input/input.svelte';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
import { Switch } from '$lib/components/ui/switch';
|
import { Switch } from '$lib/components/ui/switch';
|
||||||
|
import { getChatMessageActionsContext, getChatMessageEditContext } from '$lib/contexts';
|
||||||
import { MessageRole } from '$lib/enums';
|
import { MessageRole } from '$lib/enums';
|
||||||
import { conversationsStore } from '$lib/stores';
|
import { conversationsStore } from '$lib/stores';
|
||||||
|
|
||||||
@@ -16,23 +17,8 @@
|
|||||||
role: MessageRole.USER | MessageRole.ASSISTANT;
|
role: MessageRole.USER | MessageRole.ASSISTANT;
|
||||||
justify: 'start' | 'end';
|
justify: 'start' | 'end';
|
||||||
actionsPosition: 'left' | 'right';
|
actionsPosition: 'left' | 'right';
|
||||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
|
||||||
showDeleteDialog: boolean;
|
|
||||||
deletionInfo: {
|
|
||||||
totalCount: number;
|
|
||||||
userMessages: number;
|
|
||||||
assistantMessages: number;
|
|
||||||
messageTypes: string[];
|
|
||||||
} | null;
|
|
||||||
onCopy: () => void;
|
|
||||||
onEdit?: () => void;
|
|
||||||
onRegenerate?: () => void;
|
onRegenerate?: () => void;
|
||||||
onContinue?: () => void;
|
onContinue?: () => void;
|
||||||
onForkConversation?: (options: { name: string; includeAttachments: boolean }) => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
onConfirmDelete: () => void;
|
|
||||||
onNavigateToSibling?: (siblingId: string) => void;
|
|
||||||
onShowDeleteDialogChange: (show: boolean) => void;
|
|
||||||
showRawOutputSwitch?: boolean;
|
showRawOutputSwitch?: boolean;
|
||||||
rawOutputEnabled?: boolean;
|
rawOutputEnabled?: boolean;
|
||||||
onRawOutputToggle?: (enabled: boolean) => void;
|
onRawOutputToggle?: (enabled: boolean) => void;
|
||||||
@@ -40,32 +26,25 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
actionsPosition,
|
actionsPosition,
|
||||||
deletionInfo,
|
|
||||||
justify,
|
justify,
|
||||||
onConfirmDelete,
|
|
||||||
onContinue,
|
onContinue,
|
||||||
onCopy,
|
|
||||||
onDelete,
|
|
||||||
onEdit,
|
|
||||||
onForkConversation,
|
|
||||||
onNavigateToSibling,
|
|
||||||
onRawOutputToggle,
|
onRawOutputToggle,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
onShowDeleteDialogChange,
|
|
||||||
rawOutputEnabled = false,
|
rawOutputEnabled = false,
|
||||||
role,
|
role,
|
||||||
showDeleteDialog,
|
showRawOutputSwitch = false
|
||||||
showRawOutputSwitch = false,
|
|
||||||
siblingInfo = null
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
const messageActions = getChatMessageActionsContext();
|
||||||
|
const editCtx = getChatMessageEditContext();
|
||||||
|
|
||||||
let showForkDialog = $state(false);
|
let showForkDialog = $state(false);
|
||||||
let forkName = $state('');
|
let forkName = $state('');
|
||||||
let forkIncludeAttachments = $state(true);
|
let forkIncludeAttachments = $state(true);
|
||||||
|
|
||||||
function handleConfirmDelete() {
|
function handleConfirmDelete() {
|
||||||
onConfirmDelete();
|
messageActions.confirmDelete();
|
||||||
onShowDeleteDialogChange(false);
|
messageActions.setShowDeleteDialog(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenForkDialog() {
|
function handleOpenForkDialog() {
|
||||||
@@ -77,7 +56,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleConfirmFork() {
|
function handleConfirmFork() {
|
||||||
onForkConversation?.({ includeAttachments: forkIncludeAttachments, name: forkName.trim() });
|
messageActions.forkConversation?.({
|
||||||
|
includeAttachments: forkIncludeAttachments,
|
||||||
|
name: forkName.trim()
|
||||||
|
});
|
||||||
showForkDialog = false;
|
showForkDialog = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -88,18 +70,16 @@
|
|||||||
? 'left-0'
|
? 'left-0'
|
||||||
: 'right-0'} flex items-center gap-2 opacity-100 transition-opacity"
|
: 'right-0'} flex items-center gap-2 opacity-100 transition-opacity"
|
||||||
>
|
>
|
||||||
{#if siblingInfo && siblingInfo.totalSiblings > 1}
|
{#if messageActions.siblingInfo && messageActions.siblingInfo.totalSiblings > 1}
|
||||||
<ChatMessageActionIconsBranchingControls {siblingInfo} {onNavigateToSibling} />
|
<ChatMessageActionIconsBranchingControls />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="pointer-events-auto inset-0 flex items-center gap-1 opacity-100 transition-all duration-150"
|
class="pointer-events-auto inset-0 flex items-center gap-1 opacity-100 transition-all duration-150"
|
||||||
>
|
>
|
||||||
<ActionIcon icon={Copy} tooltip="Copy" onclick={onCopy} />
|
<ActionIcon icon={Copy} tooltip="Copy" onclick={messageActions.copy} />
|
||||||
|
|
||||||
{#if onEdit}
|
<ActionIcon icon={Edit} tooltip="Edit" onclick={editCtx.startEdit} />
|
||||||
<ActionIcon icon={Edit} tooltip="Edit" onclick={onEdit} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if role === MessageRole.ASSISTANT && onRegenerate}
|
{#if role === MessageRole.ASSISTANT && onRegenerate}
|
||||||
<ActionIcon icon={RefreshCw} tooltip="Regenerate" onclick={() => onRegenerate()} />
|
<ActionIcon icon={RefreshCw} tooltip="Regenerate" onclick={() => onRegenerate()} />
|
||||||
@@ -109,11 +89,11 @@
|
|||||||
<ActionIcon icon={ArrowRight} tooltip="Continue" onclick={onContinue} />
|
<ActionIcon icon={ArrowRight} tooltip="Continue" onclick={onContinue} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if onForkConversation}
|
{#if messageActions.forkConversation}
|
||||||
<ActionIcon icon={GitBranch} tooltip="Fork conversation" onclick={handleOpenForkDialog} />
|
<ActionIcon icon={GitBranch} tooltip="Fork conversation" onclick={handleOpenForkDialog} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ActionIcon icon={Trash2} tooltip="Delete" onclick={onDelete} />
|
<ActionIcon icon={Trash2} tooltip="Delete" onclick={messageActions.requestDelete} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -129,19 +109,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogConfirmation
|
<DialogConfirmation
|
||||||
bind:open={showDeleteDialog}
|
open={messageActions.showDeleteDialog}
|
||||||
title="Delete Message"
|
title="Delete Message"
|
||||||
description={deletionInfo && deletionInfo.totalCount > 1
|
description={messageActions.deletionInfo && messageActions.deletionInfo.totalCount > 1
|
||||||
? `This will delete ${deletionInfo.totalCount} messages including: ${deletionInfo.userMessages} user message${deletionInfo.userMessages > 1 ? 's' : ''} and ${deletionInfo.assistantMessages} assistant response${deletionInfo.assistantMessages > 1 ? 's' : ''}. All messages in this branch and their responses will be permanently removed. This action cannot be undone.`
|
? `This will delete ${messageActions.deletionInfo.totalCount} messages including: ${messageActions.deletionInfo.userMessages} user message${messageActions.deletionInfo.userMessages > 1 ? 's' : ''} and ${messageActions.deletionInfo.assistantMessages} assistant response${messageActions.deletionInfo.assistantMessages > 1 ? 's' : ''}. All messages in this branch and their responses will be permanently removed. This action cannot be undone.`
|
||||||
: 'Are you sure you want to delete this message? This action cannot be undone.'}
|
: 'Are you sure you want to delete this message? This action cannot be undone.'}
|
||||||
confirmText={deletionInfo && deletionInfo.totalCount > 1
|
confirmText={messageActions.deletionInfo && messageActions.deletionInfo.totalCount > 1
|
||||||
? `Delete ${deletionInfo.totalCount} Messages`
|
? `Delete ${messageActions.deletionInfo.totalCount} Messages`
|
||||||
: 'Delete'}
|
: 'Delete'}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
onConfirm={handleConfirmDelete}
|
onConfirm={handleConfirmDelete}
|
||||||
onCancel={() => onShowDeleteDialogChange(false)}
|
onCancel={() => messageActions.setShowDeleteDialog(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogConfirmation
|
<DialogConfirmation
|
||||||
|
|||||||
+8
-5
@@ -1,14 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
|
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
|
||||||
import { ActionIcon } from '$lib/components/app';
|
import { ActionIcon } from '$lib/components/app';
|
||||||
|
import { getChatMessageActionsContext } from '$lib/contexts';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
siblingInfo: ChatMessageSiblingInfo | null;
|
|
||||||
onNavigateToSibling?: (siblingId: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let { class: className = '', onNavigateToSibling, siblingInfo }: Props = $props();
|
let { class: className = '' }: Props = $props();
|
||||||
|
|
||||||
|
const messageActions = getChatMessageActionsContext();
|
||||||
|
|
||||||
|
let siblingInfo = $derived(messageActions.siblingInfo);
|
||||||
|
|
||||||
let hasPrevious = $derived(siblingInfo && siblingInfo.currentIndex > 0);
|
let hasPrevious = $derived(siblingInfo && siblingInfo.currentIndex > 0);
|
||||||
let hasNext = $derived(siblingInfo && siblingInfo.currentIndex < siblingInfo.totalSiblings - 1);
|
let hasNext = $derived(siblingInfo && siblingInfo.currentIndex < siblingInfo.totalSiblings - 1);
|
||||||
@@ -31,7 +34,7 @@
|
|||||||
tooltip="Previous version"
|
tooltip="Previous version"
|
||||||
disabled={!hasPrevious}
|
disabled={!hasPrevious}
|
||||||
class="h-5 w-5 p-0 {!hasPrevious ? '!cursor-not-allowed opacity-30' : ''}"
|
class="h-5 w-5 p-0 {!hasPrevious ? '!cursor-not-allowed opacity-30' : ''}"
|
||||||
onclick={() => onNavigateToSibling?.(previousSiblingId!)}
|
onclick={() => messageActions.navigateToSibling(previousSiblingId!)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span class="px-1 font-mono text-xs">
|
<span class="px-1 font-mono text-xs">
|
||||||
@@ -43,7 +46,7 @@
|
|||||||
tooltip="Next version"
|
tooltip="Next version"
|
||||||
disabled={!hasNext}
|
disabled={!hasNext}
|
||||||
class="h-5 w-5 p-0 {!hasNext ? 'opacity-30' : ''}"
|
class="h-5 w-5 p-0 {!hasNext ? 'opacity-30' : ''}"
|
||||||
onclick={() => onNavigateToSibling?.(nextSiblingId!)}
|
onclick={() => messageActions.navigateToSibling(nextSiblingId!)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
} from '$lib/components/app';
|
} from '$lib/components/app';
|
||||||
import { AgenticSectionType, ChatMessageStatsView, ToolPermissionDecision } from '$lib/enums';
|
import { AgenticSectionType, ChatMessageStatsView, ToolPermissionDecision } from '$lib/enums';
|
||||||
import { agenticStore, settingsStore } from '$lib/stores';
|
import { agenticStore, settingsStore } from '$lib/stores';
|
||||||
import type { AgenticSection } from '$lib/types';
|
|
||||||
import type {
|
import type {
|
||||||
|
AgenticSection,
|
||||||
ChatMessageAgenticTimings,
|
ChatMessageAgenticTimings,
|
||||||
ChatMessageAgenticTurnStats,
|
ChatMessageAgenticTurnStats,
|
||||||
DatabaseMessage
|
DatabaseMessage
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
import { ChatForm, DialogConfirmation } from '$lib/components/app';
|
import { ChatForm, DialogConfirmation } from '$lib/components/app';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Switch } from '$lib/components/ui/switch';
|
import { Switch } from '$lib/components/ui/switch';
|
||||||
import { getMessageEditContext } from '$lib/contexts';
|
import { getChatMessageEditContext } from '$lib/contexts';
|
||||||
import { KeyboardKey, MessageRole } from '$lib/enums';
|
import { KeyboardKey, MessageRole } from '$lib/enums';
|
||||||
import { chatStore } from '$lib/stores';
|
import { chatStore } from '$lib/stores';
|
||||||
import { processFilesToChatUploaded } from '$lib/utils/browser-only';
|
import { processFilesToChatUploaded } from '$lib/utils/browser-only';
|
||||||
|
|
||||||
const editCtx = getMessageEditContext();
|
const editCtx = getChatMessageEditContext();
|
||||||
|
|
||||||
let saveWithoutRegenerate = $state(false);
|
let saveWithoutRegenerate = $state(false);
|
||||||
let showDiscardDialog = $state(false);
|
let showDiscardDialog = $state(false);
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
import { REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
|
import { REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
|
||||||
import { AgenticSectionType } from '$lib/enums';
|
import { AgenticSectionType } from '$lib/enums';
|
||||||
import { settingsStore } from '$lib/stores';
|
import { settingsStore } from '$lib/stores';
|
||||||
import type { DatabaseMessageExtra } from '$lib/types';
|
import type { AgenticSection, DatabaseMessageExtra } from '$lib/types';
|
||||||
import type { AgenticSection } from '$lib/types';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
section: AgenticSection;
|
section: AgenticSection;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ChatMessage, ChatMessageUserPending } from '$lib/components/app';
|
import { ChatMessage, ChatMessageUserPending } from '$lib/components/app';
|
||||||
import { setChatActionsContext } from '$lib/contexts';
|
|
||||||
import { MessageRole } from '$lib/enums';
|
import { MessageRole } from '$lib/enums';
|
||||||
import { agenticStore, chatStore, conversationsStore, settingsStore } from '$lib/stores';
|
import { agenticStore, chatStore, conversationsStore, settingsStore } from '$lib/stores';
|
||||||
|
import type { ChatMessageActions } from '$lib/types';
|
||||||
import {
|
import {
|
||||||
buildSiblingInfoMap,
|
buildSiblingInfoMap,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
const currentConfig = settingsStore.config;
|
const currentConfig = settingsStore.config;
|
||||||
|
|
||||||
setChatActionsContext({
|
const chatActions: ChatMessageActions = {
|
||||||
continueAssistantMessage: async (message: DatabaseMessage) => {
|
continueAssistantMessage: async (message: DatabaseMessage) => {
|
||||||
onUserAction?.();
|
onUserAction?.();
|
||||||
await chatStore.continueAssistantMessage(message.id);
|
await chatStore.continueAssistantMessage(message.id);
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
await chatStore.regenerateMessageWithBranching(message.id, modelOverride);
|
await chatStore.regenerateMessageWithBranching(message.id, modelOverride);
|
||||||
refreshAllMessages();
|
refreshAllMessages();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
function refreshAllMessages() {
|
function refreshAllMessages() {
|
||||||
const conversation = conversationsStore.activeConversation;
|
const conversation = conversationsStore.activeConversation;
|
||||||
@@ -228,6 +228,7 @@
|
|||||||
{#each displayMessages as { isLastAssistantMessage, isLastUserMessage, message, nextAssistantMessage, siblingInfo, toolMessages } (message.id)}
|
{#each displayMessages as { isLastAssistantMessage, isLastUserMessage, message, nextAssistantMessage, siblingInfo, toolMessages } (message.id)}
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
class="mx-auto mt-12 w-full max-w-3xl"
|
class="mx-auto mt-12 w-full max-w-3xl"
|
||||||
|
{chatActions}
|
||||||
{message}
|
{message}
|
||||||
{toolMessages}
|
{toolMessages}
|
||||||
{isLastAssistantMessage}
|
{isLastAssistantMessage}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
SETTINGS_CHAT_SECTIONS,
|
SETTINGS_CHAT_SECTIONS,
|
||||||
SETTINGS_SECTION_TITLES
|
SETTINGS_SECTION_TITLES
|
||||||
} from '$lib/constants';
|
} from '$lib/constants';
|
||||||
import { setChatSettingsConfigContext } from '$lib/contexts';
|
|
||||||
import { ColorMode } from '$lib/enums/ui.enums';
|
import { ColorMode } from '$lib/enums/ui.enums';
|
||||||
import { RouterService } from '$lib/services/router.service';
|
import { RouterService } from '$lib/services/router.service';
|
||||||
import { modelsStore, serverStore, settingsReferrer, settingsStore } from '$lib/stores';
|
import { modelsStore, serverStore, settingsReferrer, settingsStore } from '$lib/stores';
|
||||||
@@ -122,14 +121,6 @@
|
|||||||
export function reset() {
|
export function reset() {
|
||||||
localConfig = { ...settingsStore.config };
|
localConfig = { ...settingsStore.config };
|
||||||
}
|
}
|
||||||
|
|
||||||
setChatSettingsConfigContext({
|
|
||||||
handleConfigChange,
|
|
||||||
handleThemeChange,
|
|
||||||
get localConfig() {
|
|
||||||
return localConfig;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto flex h-full w-full flex-col md:pl-8" in:fade={{ duration: 150 }}>
|
<div class="mx-auto flex h-full w-full flex-col md:pl-8" in:fade={{ duration: 150 }}>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export const CONTEXT_KEY_MESSAGE_EDIT = 'chat-message-edit';
|
export const CONTEXT_KEY_CHAT_MESSAGE_EDIT = 'chat-message-edit';
|
||||||
export const CONTEXT_KEY_CHAT_ACTIONS = 'chat-actions';
|
export const CONTEXT_KEY_CHAT_MESSAGE_ACTIONS = 'chat-message-actions';
|
||||||
export const CONTEXT_KEY_CHAT_SETTINGS_CONFIG = 'chat-settings-config';
|
export const CONTEXT_KEY_CHAT_FORM_ACTIONS = 'chat-form-actions';
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { CONTEXT_KEY_CHAT_ACTIONS } from '$lib/constants';
|
|
||||||
import { getContext, setContext } from 'svelte';
|
|
||||||
|
|
||||||
export interface ChatActionsContext {
|
|
||||||
copy: (message: DatabaseMessage) => void;
|
|
||||||
delete: (message: DatabaseMessage) => void;
|
|
||||||
navigateToSibling: (siblingId: string) => void;
|
|
||||||
editWithBranching: (
|
|
||||||
message: DatabaseMessage,
|
|
||||||
newContent: string,
|
|
||||||
newExtras?: DatabaseMessageExtra[]
|
|
||||||
) => void;
|
|
||||||
editWithReplacement: (
|
|
||||||
message: DatabaseMessage,
|
|
||||||
newContent: string,
|
|
||||||
shouldBranch: boolean
|
|
||||||
) => void;
|
|
||||||
editUserMessagePreserveResponses: (
|
|
||||||
message: DatabaseMessage,
|
|
||||||
newContent: string,
|
|
||||||
newExtras?: DatabaseMessageExtra[]
|
|
||||||
) => void;
|
|
||||||
regenerateWithBranching: (message: DatabaseMessage, modelOverride?: string) => void;
|
|
||||||
continueAssistantMessage: (message: DatabaseMessage) => void;
|
|
||||||
forkConversation: (
|
|
||||||
message: DatabaseMessage,
|
|
||||||
options: { name: string; includeAttachments: boolean }
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CHAT_ACTIONS_KEY = Symbol.for(CONTEXT_KEY_CHAT_ACTIONS);
|
|
||||||
|
|
||||||
export function setChatActionsContext(ctx: ChatActionsContext): ChatActionsContext {
|
|
||||||
return setContext(CHAT_ACTIONS_KEY, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getChatActionsContext(): ChatActionsContext {
|
|
||||||
return getContext(CHAT_ACTIONS_KEY);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { CONTEXT_KEY_CHAT_FORM_ACTIONS } from '$lib/constants';
|
||||||
|
import type { ChatFormActionsContext } from '$lib/types';
|
||||||
|
import { getContext, setContext } from 'svelte';
|
||||||
|
|
||||||
|
const CHAT_FORM_ACTIONS_KEY = Symbol.for(CONTEXT_KEY_CHAT_FORM_ACTIONS);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the chat form actions context. Call in the parent component (ChatFormActions.svelte).
|
||||||
|
*/
|
||||||
|
export function setChatFormActionsContext(ctx: ChatFormActionsContext): ChatFormActionsContext {
|
||||||
|
return setContext(CHAT_FORM_ACTIONS_KEY, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the chat form actions context. Call in child components.
|
||||||
|
*/
|
||||||
|
export function getChatFormActionsContext(): ChatFormActionsContext {
|
||||||
|
return getContext(CHAT_FORM_ACTIONS_KEY);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { CONTEXT_KEY_CHAT_MESSAGE_ACTIONS } from '$lib/constants';
|
||||||
|
import type { ChatMessageActionsContext } from '$lib/types';
|
||||||
|
import { getContext, setContext } from 'svelte';
|
||||||
|
|
||||||
|
const CHAT_MESSAGE_ACTIONS_KEY = Symbol.for(CONTEXT_KEY_CHAT_MESSAGE_ACTIONS);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the per-message actions context. Call this in the parent component (ChatMessage.svelte).
|
||||||
|
*/
|
||||||
|
export function setChatMessageActionsContext(
|
||||||
|
ctx: ChatMessageActionsContext
|
||||||
|
): ChatMessageActionsContext {
|
||||||
|
return setContext(CHAT_MESSAGE_ACTIONS_KEY, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the per-message actions context. Call this in child components.
|
||||||
|
*/
|
||||||
|
export function getChatMessageActionsContext(): ChatMessageActionsContext {
|
||||||
|
return getContext(CHAT_MESSAGE_ACTIONS_KEY);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { CONTEXT_KEY_CHAT_MESSAGE_EDIT } from '$lib/constants';
|
||||||
|
import type { ChatMessageEditContext } from '$lib/types';
|
||||||
|
import { getContext, setContext } from 'svelte';
|
||||||
|
|
||||||
|
const CHAT_MESSAGE_EDIT_KEY = Symbol.for(CONTEXT_KEY_CHAT_MESSAGE_EDIT);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the message edit context. Call this in the parent component (ChatMessage.svelte).
|
||||||
|
*/
|
||||||
|
export function setChatMessageEditContext(ctx: ChatMessageEditContext): ChatMessageEditContext {
|
||||||
|
return setContext(CHAT_MESSAGE_EDIT_KEY, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the message edit context. Call this in child components.
|
||||||
|
*/
|
||||||
|
export function getChatMessageEditContext(): ChatMessageEditContext {
|
||||||
|
return getContext(CHAT_MESSAGE_EDIT_KEY);
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { CONTEXT_KEY_CHAT_SETTINGS_CONFIG } from '$lib/constants';
|
|
||||||
import { getContext, setContext } from 'svelte';
|
|
||||||
|
|
||||||
export interface ChatSettingsConfigContext {
|
|
||||||
readonly localConfig: SettingsConfigType;
|
|
||||||
handleConfigChange: (key: string, value: string | boolean) => void;
|
|
||||||
handleThemeChange: (theme: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CHAT_SETTINGS_CONFIG_KEY = Symbol.for(CONTEXT_KEY_CHAT_SETTINGS_CONFIG);
|
|
||||||
|
|
||||||
export function setChatSettingsConfigContext(
|
|
||||||
ctx: ChatSettingsConfigContext
|
|
||||||
): ChatSettingsConfigContext {
|
|
||||||
return setContext(CHAT_SETTINGS_CONFIG_KEY, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getChatSettingsConfigContext(): ChatSettingsConfigContext {
|
|
||||||
return getContext(CHAT_SETTINGS_CONFIG_KEY);
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,8 @@
|
|||||||
export {
|
export { getChatMessageEditContext, setChatMessageEditContext } from './chat-message-edit.context';
|
||||||
getMessageEditContext,
|
|
||||||
setMessageEditContext,
|
|
||||||
type MessageEditContext,
|
|
||||||
type MessageEditState,
|
|
||||||
type MessageEditActions
|
|
||||||
} from './message-edit.context';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getChatActionsContext,
|
getChatMessageActionsContext,
|
||||||
setChatActionsContext,
|
setChatMessageActionsContext
|
||||||
type ChatActionsContext
|
} from './chat-message-actions.context';
|
||||||
} from './chat-actions.context';
|
|
||||||
|
|
||||||
export {
|
export { getChatFormActionsContext, setChatFormActionsContext } from './chat-form-actions.context';
|
||||||
getChatSettingsConfigContext,
|
|
||||||
setChatSettingsConfigContext,
|
|
||||||
type ChatSettingsConfigContext
|
|
||||||
} from './chat-settings-config.context';
|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { CONTEXT_KEY_MESSAGE_EDIT } from '$lib/constants';
|
|
||||||
import { MessageRole } from '$lib/enums';
|
|
||||||
import { getContext, setContext } from 'svelte';
|
|
||||||
|
|
||||||
export interface MessageEditState {
|
|
||||||
readonly isEditing: boolean;
|
|
||||||
readonly editedContent: string;
|
|
||||||
readonly editedExtras: DatabaseMessageExtra[];
|
|
||||||
readonly editedUploadedFiles: ChatUploadedFile[];
|
|
||||||
readonly originalContent: string;
|
|
||||||
readonly originalExtras: DatabaseMessageExtra[];
|
|
||||||
readonly showSaveOnlyOption: boolean;
|
|
||||||
readonly showBranchAfterEditOption: boolean;
|
|
||||||
readonly shouldBranchAfterEdit: boolean;
|
|
||||||
readonly messageRole: MessageRole;
|
|
||||||
readonly rawEditContent?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageEditActions {
|
|
||||||
setContent: (content: string) => void;
|
|
||||||
setExtras: (extras: DatabaseMessageExtra[]) => void;
|
|
||||||
setUploadedFiles: (files: ChatUploadedFile[]) => void;
|
|
||||||
save: () => void;
|
|
||||||
saveOnly: () => void;
|
|
||||||
cancel: () => void;
|
|
||||||
startEdit: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssistantEditActions {
|
|
||||||
setShouldBranchAfterEdit: (value: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MessageEditContext = MessageEditState &
|
|
||||||
MessageEditActions &
|
|
||||||
Partial<AssistantEditActions>;
|
|
||||||
|
|
||||||
const MESSAGE_EDIT_KEY = Symbol.for(CONTEXT_KEY_MESSAGE_EDIT);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the message edit context. Call this in the parent component (ChatMessage.svelte).
|
|
||||||
*/
|
|
||||||
export function setMessageEditContext(ctx: MessageEditContext): MessageEditContext {
|
|
||||||
return setContext(MESSAGE_EDIT_KEY, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the message edit context. Call this in child components.
|
|
||||||
*/
|
|
||||||
export function getMessageEditContext(): MessageEditContext {
|
|
||||||
return getContext(MESSAGE_EDIT_KEY);
|
|
||||||
}
|
|
||||||
+4
-4
@@ -1,15 +1,15 @@
|
|||||||
import { setMessageEditContext } from '$lib/contexts';
|
import { setChatMessageEditContext } from '$lib/contexts';
|
||||||
import { MessageRole } from '$lib/enums';
|
import { MessageRole } from '$lib/enums';
|
||||||
import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra';
|
import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra';
|
||||||
|
|
||||||
interface UseMessageEditContextOptions {
|
interface UseChatMessageEditContextOptions {
|
||||||
getContent: () => string;
|
getContent: () => string;
|
||||||
getExtras: () => DatabaseMessageExtra[];
|
getExtras: () => DatabaseMessageExtra[];
|
||||||
showSaveOnlyOption?: boolean;
|
showSaveOnlyOption?: boolean;
|
||||||
onSave: (content: string, extras?: DatabaseMessageExtra[]) => void;
|
onSave: (content: string, extras?: DatabaseMessageExtra[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMessageEditContext(options: UseMessageEditContextOptions) {
|
export function useChatMessageEditContext(options: UseChatMessageEditContextOptions) {
|
||||||
let isEditing = $state(false);
|
let isEditing = $state(false);
|
||||||
let editedContent = $state('');
|
let editedContent = $state('');
|
||||||
let editedExtras = $state<DatabaseMessageExtra[]>([]);
|
let editedExtras = $state<DatabaseMessageExtra[]>([]);
|
||||||
@@ -45,7 +45,7 @@ export function useMessageEditContext(options: UseMessageEditContextOptions) {
|
|||||||
isEditing = false;
|
isEditing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessageEditContext({
|
setChatMessageEditContext({
|
||||||
cancel: handleCancelEdit,
|
cancel: handleCancelEdit,
|
||||||
get editedContent() {
|
get editedContent() {
|
||||||
return editedContent;
|
return editedContent;
|
||||||
Vendored
+110
-1
@@ -7,7 +7,8 @@ import type {
|
|||||||
AttachmentMenuItemId,
|
AttachmentMenuItemId,
|
||||||
ChatFormCommandAction,
|
ChatFormCommandAction,
|
||||||
ErrorDialogType,
|
ErrorDialogType,
|
||||||
FileMentionEntryType
|
FileMentionEntryType,
|
||||||
|
MessageRole
|
||||||
} from '$lib/enums';
|
} from '$lib/enums';
|
||||||
import type { Component } from 'svelte';
|
import type { Component } from 'svelte';
|
||||||
|
|
||||||
@@ -235,3 +236,111 @@ export interface ChatFormCommand {
|
|||||||
action: ChatFormCommandAction;
|
action: ChatFormCommandAction;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data shown in the message delete confirmation dialog.
|
||||||
|
*/
|
||||||
|
export interface ChatMessageDeletionInfo {
|
||||||
|
totalCount: number;
|
||||||
|
userMessages: number;
|
||||||
|
assistantMessages: number;
|
||||||
|
messageTypes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conversation-level message operations owned by ChatMessages (store calls + list
|
||||||
|
* refresh + user-action notification), passed to each ChatMessage as a prop.
|
||||||
|
*/
|
||||||
|
export interface ChatMessageActions {
|
||||||
|
copy: (message: DatabaseMessage) => void;
|
||||||
|
delete: (message: DatabaseMessage) => void;
|
||||||
|
navigateToSibling: (siblingId: string) => void;
|
||||||
|
editWithBranching: (
|
||||||
|
message: DatabaseMessage,
|
||||||
|
newContent: string,
|
||||||
|
newExtras?: DatabaseMessageExtra[]
|
||||||
|
) => void;
|
||||||
|
editWithReplacement: (
|
||||||
|
message: DatabaseMessage,
|
||||||
|
newContent: string,
|
||||||
|
shouldBranch: boolean
|
||||||
|
) => void;
|
||||||
|
editUserMessagePreserveResponses: (
|
||||||
|
message: DatabaseMessage,
|
||||||
|
newContent: string,
|
||||||
|
newExtras?: DatabaseMessageExtra[]
|
||||||
|
) => void;
|
||||||
|
regenerateWithBranching: (message: DatabaseMessage, modelOverride?: string) => void;
|
||||||
|
continueAssistantMessage: (message: DatabaseMessage) => void;
|
||||||
|
forkConversation: (
|
||||||
|
message: DatabaseMessage,
|
||||||
|
options: { name: string; includeAttachments: boolean }
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-message actions and state. Set once per message in ChatMessage.svelte and
|
||||||
|
* consumed by its descendants (action icons, branching controls).
|
||||||
|
*/
|
||||||
|
export interface ChatMessageActionsContext {
|
||||||
|
readonly siblingInfo: ChatMessageSiblingInfo | null;
|
||||||
|
readonly deletionInfo: ChatMessageDeletionInfo | null;
|
||||||
|
readonly showDeleteDialog: boolean;
|
||||||
|
copy: () => void;
|
||||||
|
requestDelete: () => void;
|
||||||
|
confirmDelete: () => void;
|
||||||
|
setShowDeleteDialog: (show: boolean) => void;
|
||||||
|
navigateToSibling: (siblingId: string) => void;
|
||||||
|
forkConversation?: (options: { name: string; includeAttachments: boolean }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMessageEditState {
|
||||||
|
readonly isEditing: boolean;
|
||||||
|
readonly editedContent: string;
|
||||||
|
readonly editedExtras: DatabaseMessageExtra[];
|
||||||
|
readonly editedUploadedFiles: ChatUploadedFile[];
|
||||||
|
readonly originalContent: string;
|
||||||
|
readonly originalExtras: DatabaseMessageExtra[];
|
||||||
|
readonly showSaveOnlyOption: boolean;
|
||||||
|
readonly showBranchAfterEditOption: boolean;
|
||||||
|
readonly shouldBranchAfterEdit: boolean;
|
||||||
|
readonly messageRole: MessageRole;
|
||||||
|
readonly rawEditContent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMessageEditActions {
|
||||||
|
setContent: (content: string) => void;
|
||||||
|
setExtras: (extras: DatabaseMessageExtra[]) => void;
|
||||||
|
setUploadedFiles: (files: ChatUploadedFile[]) => void;
|
||||||
|
save: () => void;
|
||||||
|
saveOnly: () => void;
|
||||||
|
cancel: () => void;
|
||||||
|
startEdit: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMessageAssistantEditActions {
|
||||||
|
setShouldBranchAfterEdit: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChatMessageEditContext = ChatMessageEditState &
|
||||||
|
ChatMessageEditActions &
|
||||||
|
Partial<ChatMessageAssistantEditActions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions and capability flags for the ChatForm add-menu. Set once in
|
||||||
|
* ChatFormActions.svelte and consumed by its deep descendants (the add sheet,
|
||||||
|
* dropdown and MCP servers submenu) to avoid relaying them through props.
|
||||||
|
*/
|
||||||
|
export interface ChatFormActionsContext {
|
||||||
|
readonly disabled: boolean;
|
||||||
|
readonly hasAudioModality: boolean;
|
||||||
|
readonly hasVideoModality: boolean;
|
||||||
|
readonly hasVisionModality: boolean;
|
||||||
|
readonly hasMcpPromptsSupport: boolean;
|
||||||
|
readonly hasMcpResourcesSupport: boolean;
|
||||||
|
onFileUpload?: () => void;
|
||||||
|
onSystemPromptClick?: () => void;
|
||||||
|
onMcpPromptClick?: () => void;
|
||||||
|
onMcpResourcesClick?: () => void;
|
||||||
|
onMcpSettingsClick?: () => void;
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ export type {
|
|||||||
ChatUploadedFile,
|
ChatUploadedFile,
|
||||||
ChatAttachmentDisplayItem,
|
ChatAttachmentDisplayItem,
|
||||||
ChatMessageSiblingInfo,
|
ChatMessageSiblingInfo,
|
||||||
|
ChatMessageActions,
|
||||||
|
ChatMessageActionsContext,
|
||||||
|
ChatMessageDeletionInfo,
|
||||||
|
ChatMessageEditContext,
|
||||||
|
ChatMessageEditState,
|
||||||
|
ChatMessageEditActions,
|
||||||
|
ChatMessageAssistantEditActions,
|
||||||
|
ChatFormActionsContext,
|
||||||
ChatMessagePromptProgress,
|
ChatMessagePromptProgress,
|
||||||
ChatMessageTimings,
|
ChatMessageTimings,
|
||||||
ChatMessageAgenticTimings,
|
ChatMessageAgenticTimings,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script module lang="ts">
|
<script module lang="ts">
|
||||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
import ChatMessage from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte';
|
import ChatMessage from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte';
|
||||||
|
import type { ChatMessageActions } from '$lib/types';
|
||||||
|
|
||||||
const { Story } = defineMeta({
|
const { Story } = defineMeta({
|
||||||
component: ChatMessage,
|
component: ChatMessage,
|
||||||
@@ -10,6 +11,18 @@
|
|||||||
title: 'Components/ChatScreen/ChatMessage'
|
title: 'Components/ChatScreen/ChatMessage'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const chatActions: ChatMessageActions = {
|
||||||
|
continueAssistantMessage: () => {},
|
||||||
|
copy: () => {},
|
||||||
|
delete: () => {},
|
||||||
|
editUserMessagePreserveResponses: () => {},
|
||||||
|
editWithBranching: () => {},
|
||||||
|
editWithReplacement: () => {},
|
||||||
|
forkConversation: () => {},
|
||||||
|
navigateToSibling: () => {},
|
||||||
|
regenerateWithBranching: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
// Mock messages for different scenarios
|
// Mock messages for different scenarios
|
||||||
const userMessage: DatabaseMessage = {
|
const userMessage: DatabaseMessage = {
|
||||||
children: [],
|
children: [],
|
||||||
@@ -190,7 +203,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="w-[56rem]">
|
<div class="w-[56rem]">
|
||||||
<ChatMessage message={streamingMessage} />
|
<ChatMessage message={streamingMessage} {chatActions} />
|
||||||
</div>
|
</div>
|
||||||
</Story>
|
</Story>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user