mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-08-12 22:31:11 +04:00
refactor: Contexts & types
This commit is contained in:
Vendored
+1
-1
@@ -32,8 +32,8 @@ import type {
|
||||
ApiRouterModelsStatusResponse,
|
||||
ApiRouterModelsUnloadRequest,
|
||||
ApiRouterModelsUnloadResponse,
|
||||
// Chat types
|
||||
ChatAttachmentDisplayItem,
|
||||
// Chat types
|
||||
ChatMessagePromptProgress,
|
||||
ChatMessageSiblingInfo,
|
||||
ChatMessageTimings,
|
||||
|
||||
+22
-38
@@ -15,37 +15,16 @@
|
||||
ICON_CLASS_DEFAULT,
|
||||
TOOLTIP_DELAY_DURATION
|
||||
} from '$lib/constants';
|
||||
import { getChatFormActionsContext } from '$lib/contexts';
|
||||
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
||||
|
||||
interface Props {
|
||||
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 {
|
||||
class: className = '',
|
||||
disabled = false,
|
||||
hasAudioModality = false,
|
||||
hasMcpPromptsSupport = false,
|
||||
hasMcpResourcesSupport = false,
|
||||
hasVideoModality = false,
|
||||
hasVisionModality = false,
|
||||
onFileUpload,
|
||||
onMcpPromptClick,
|
||||
onMcpResourcesClick,
|
||||
onMcpSettingsClick,
|
||||
onSystemPromptClick
|
||||
}: Props = $props();
|
||||
let { class: className = '' }: Props = $props();
|
||||
|
||||
const chatFormActions = getChatFormActionsContext();
|
||||
|
||||
let dropdownOpen = $state(false);
|
||||
// The system message action moves focus to the message editor, so the menu
|
||||
@@ -54,18 +33,23 @@
|
||||
|
||||
function handleMcpSettingsClick() {
|
||||
dropdownOpen = false;
|
||||
onMcpSettingsClick?.();
|
||||
chatFormActions.onMcpSettingsClick?.();
|
||||
}
|
||||
|
||||
const attachmentMenu = useAttachmentMenu(
|
||||
() => ({
|
||||
hasAudioModality,
|
||||
hasMcpPromptsSupport,
|
||||
hasMcpResourcesSupport,
|
||||
hasVideoModality,
|
||||
hasVisionModality
|
||||
hasAudioModality: chatFormActions.hasAudioModality,
|
||||
hasMcpPromptsSupport: chatFormActions.hasMcpPromptsSupport,
|
||||
hasMcpResourcesSupport: chatFormActions.hasMcpResourcesSupport,
|
||||
hasVideoModality: chatFormActions.hasVideoModality,
|
||||
hasVisionModality: chatFormActions.hasVisionModality
|
||||
}),
|
||||
() => ({
|
||||
onFileUpload: chatFormActions.onFileUpload,
|
||||
onMcpPromptClick: chatFormActions.onMcpPromptClick,
|
||||
onMcpResourcesClick: chatFormActions.onMcpResourcesClick,
|
||||
onSystemPromptClick: chatFormActions.onSystemPromptClick
|
||||
}),
|
||||
() => ({ onFileUpload, onMcpPromptClick, onMcpResourcesClick, onSystemPromptClick }),
|
||||
() => {
|
||||
dropdownOpen = false;
|
||||
}
|
||||
@@ -85,7 +69,7 @@
|
||||
buttonVariants({ variant: 'secondary' }),
|
||||
'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>
|
||||
|
||||
@@ -162,7 +146,7 @@
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={() => {
|
||||
suppressCloseAutoFocus = true;
|
||||
onSystemPromptClick?.();
|
||||
chatFormActions.onSystemPromptClick?.();
|
||||
}}
|
||||
>
|
||||
<MessageSquare class={ICON_CLASS_DEFAULT} />
|
||||
@@ -174,12 +158,12 @@
|
||||
|
||||
<ChatFormActionAddMcpServersSubmenu onMcpSettingsClick={handleMcpSettingsClick} />
|
||||
|
||||
{#if hasMcpPromptsSupport}
|
||||
{#if chatFormActions.hasMcpPromptsSupport}
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={onMcpPromptClick}
|
||||
onclick={chatFormActions.onMcpPromptClick}
|
||||
>
|
||||
<Zap class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
@@ -187,10 +171,10 @@
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
|
||||
{#if hasMcpResourcesSupport}
|
||||
{#if chatFormActions.hasMcpResourcesSupport}
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={onMcpResourcesClick}
|
||||
onclick={chatFormActions.onMcpResourcesClick}
|
||||
>
|
||||
<FolderOpen class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
|
||||
+18
-33
@@ -19,6 +19,7 @@
|
||||
ICON_CLASS_DEFAULT,
|
||||
TOOLTIP_DELAY_DURATION
|
||||
} from '$lib/constants';
|
||||
import { getChatFormActionsContext } from '$lib/contexts';
|
||||
import { HealthCheckStatus } from '$lib/enums';
|
||||
import { AttachmentAction } from '$lib/enums/attachment.enums';
|
||||
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
||||
@@ -29,33 +30,12 @@
|
||||
|
||||
interface Props {
|
||||
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 }]>;
|
||||
}
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
disabled = false,
|
||||
hasAudioModality = false,
|
||||
hasMcpPromptsSupport = false,
|
||||
hasMcpResourcesSupport = false,
|
||||
hasVideoModality = false,
|
||||
hasVisionModality = false,
|
||||
onFileUpload,
|
||||
onMcpPromptClick,
|
||||
onMcpResourcesClick,
|
||||
onSystemPromptClick,
|
||||
trigger
|
||||
}: Props = $props();
|
||||
let { class: className = '', trigger }: Props = $props();
|
||||
|
||||
const chatFormActions = getChatFormActionsContext();
|
||||
|
||||
let sheetOpen = $state(false);
|
||||
let reasoningExpanded = $state(false);
|
||||
@@ -65,13 +45,18 @@
|
||||
|
||||
const attachmentMenu = useAttachmentMenu(
|
||||
() => ({
|
||||
hasAudioModality,
|
||||
hasMcpPromptsSupport,
|
||||
hasMcpResourcesSupport,
|
||||
hasVideoModality,
|
||||
hasVisionModality
|
||||
hasAudioModality: chatFormActions.hasAudioModality,
|
||||
hasMcpPromptsSupport: chatFormActions.hasMcpPromptsSupport,
|
||||
hasMcpResourcesSupport: chatFormActions.hasMcpResourcesSupport,
|
||||
hasVideoModality: chatFormActions.hasVideoModality,
|
||||
hasVisionModality: chatFormActions.hasVisionModality
|
||||
}),
|
||||
() => ({
|
||||
onFileUpload: chatFormActions.onFileUpload,
|
||||
onMcpPromptClick: chatFormActions.onMcpPromptClick,
|
||||
onMcpResourcesClick: chatFormActions.onMcpResourcesClick,
|
||||
onSystemPromptClick: chatFormActions.onSystemPromptClick
|
||||
}),
|
||||
() => ({ onFileUpload, onMcpPromptClick, onMcpResourcesClick, onSystemPromptClick }),
|
||||
() => {
|
||||
sheetOpen = false;
|
||||
}
|
||||
@@ -91,7 +76,7 @@
|
||||
|
||||
<div class="flex items-center gap-1 {className}">
|
||||
<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.Header>
|
||||
@@ -350,7 +335,7 @@
|
||||
<span>System Message</span>
|
||||
</button>
|
||||
|
||||
{#if hasMcpPromptsSupport}
|
||||
{#if chatFormActions.hasMcpPromptsSupport}
|
||||
<button
|
||||
type="button"
|
||||
class={sheetItemClass}
|
||||
@@ -362,7 +347,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if hasMcpResourcesSupport}
|
||||
{#if chatFormActions.hasMcpResourcesSupport}
|
||||
<button
|
||||
type="button"
|
||||
class={sheetItemClass}
|
||||
|
||||
+2
-53
@@ -3,65 +3,14 @@
|
||||
import ChatFormActionAddDropdown from './ChatFormActionAddDropdown.svelte';
|
||||
import ChatFormActionAddSheet from './ChatFormActionAddSheet.svelte';
|
||||
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>
|
||||
|
||||
{#if isMobile.current}
|
||||
<ChatFormActionAddSheet
|
||||
{disabled}
|
||||
{hasAudioModality}
|
||||
{hasVideoModality}
|
||||
{hasVisionModality}
|
||||
{hasMcpPromptsSupport}
|
||||
{hasMcpResourcesSupport}
|
||||
{onFileUpload}
|
||||
{onSystemPromptClick}
|
||||
{onMcpPromptClick}
|
||||
{onMcpResourcesClick}
|
||||
>
|
||||
<ChatFormActionAddSheet>
|
||||
{#snippet trigger({ disabled, onclick })}
|
||||
<ChatFormActionAddButton {disabled} {onclick} />
|
||||
{/snippet}
|
||||
</ChatFormActionAddSheet>
|
||||
{:else}
|
||||
<ChatFormActionAddDropdown
|
||||
{disabled}
|
||||
{hasAudioModality}
|
||||
{hasVideoModality}
|
||||
{hasVisionModality}
|
||||
{hasMcpPromptsSupport}
|
||||
{hasMcpResourcesSupport}
|
||||
{onFileUpload}
|
||||
{onMcpPromptClick}
|
||||
{onMcpResourcesClick}
|
||||
{onMcpSettingsClick}
|
||||
{onSystemPromptClick}
|
||||
/>
|
||||
<ChatFormActionAddDropdown />
|
||||
{/if}
|
||||
|
||||
+38
-13
@@ -11,6 +11,7 @@
|
||||
} from '$lib/components/app';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { ICON_CLASS_DEFAULT, ROUTES } from '$lib/constants';
|
||||
import { setChatFormActionsContext } from '$lib/contexts';
|
||||
import { FileTypeCategory, MessageRole } from '$lib/enums';
|
||||
import { ChatService } from '$lib/services';
|
||||
import { chatStore, conversationsStore, mcpStore, settingsStore } from '$lib/stores';
|
||||
@@ -132,6 +133,42 @@
|
||||
|
||||
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>
|
||||
|
||||
<div
|
||||
@@ -140,19 +177,7 @@
|
||||
>
|
||||
{#if showAddButton}
|
||||
<div class="mr-auto flex items-center gap-2">
|
||||
<ChatFormActionsAdd
|
||||
{disabled}
|
||||
{hasAudioModality}
|
||||
{hasVideoModality}
|
||||
{hasVisionModality}
|
||||
{hasMcpPromptsSupport}
|
||||
{hasMcpResourcesSupport}
|
||||
{onFileUpload}
|
||||
{onSystemPromptClick}
|
||||
{onMcpPromptClick}
|
||||
{onMcpResourcesClick}
|
||||
onMcpSettingsClick={() => goto(ROUTES.MCP_SERVERS)}
|
||||
/>
|
||||
<ChatFormActionsAdd />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -8,21 +8,21 @@
|
||||
ChatMessageUser
|
||||
} from '$lib/components/app/chat';
|
||||
import { REASONING_TAGS, ROUTES, SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
|
||||
import {
|
||||
type ChatMessageDeletionInfo,
|
||||
getChatActionsContext,
|
||||
setChatMessageActionsContext,
|
||||
setMessageEditContext
|
||||
} from '$lib/contexts';
|
||||
import { setChatMessageActionsContext, setChatMessageEditContext } from '$lib/contexts';
|
||||
import { AgenticSectionType, AttachmentType, MessageRole } from '$lib/enums';
|
||||
import { DatabaseService } from '$lib/services/database.service';
|
||||
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 { parseFilesToMessageExtras } from '$lib/utils/browser-only';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
chatActions: ChatMessageActions;
|
||||
message: DatabaseMessage;
|
||||
toolMessages?: DatabaseMessage[];
|
||||
isLastAssistantMessage?: boolean;
|
||||
@@ -32,6 +32,7 @@
|
||||
}
|
||||
|
||||
let {
|
||||
chatActions,
|
||||
class: className = '',
|
||||
isLastAssistantMessage = false,
|
||||
isLastUserMessage = false,
|
||||
@@ -41,8 +42,6 @@
|
||||
toolMessages = []
|
||||
}: Props = $props();
|
||||
|
||||
const chatActions = getChatActionsContext();
|
||||
|
||||
let deletionInfo = $state<ChatMessageDeletionInfo | null>(null);
|
||||
// The system message placeholder must never surface as editable content; keeping
|
||||
// it in the derived (not just in handleEdit) guards against prop invalidation
|
||||
@@ -112,7 +111,7 @@
|
||||
let showSaveOnlyOption = $derived(message.role === MessageRole.USER);
|
||||
let showBranchAfterEditOption = $derived(message.role === MessageRole.ASSISTANT);
|
||||
|
||||
setMessageEditContext({
|
||||
setChatMessageEditContext({
|
||||
cancel: handleCancelEdit,
|
||||
get editedContent() {
|
||||
return editedContent;
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@
|
||||
ChatMessageAssistantStatistics,
|
||||
ChatMessageEditForm
|
||||
} from '$lib/components/app';
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { getChatMessageEditContext } from '$lib/contexts';
|
||||
import { MessageRole } from '$lib/enums';
|
||||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||
import { chatStore, modelsStore, serverStore, settingsStore } from '$lib/stores';
|
||||
@@ -36,7 +36,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
// Get edit context
|
||||
const editCtx = getMessageEditContext();
|
||||
const editCtx = getChatMessageEditContext();
|
||||
|
||||
const isAgentic = $derived(hasAgenticContent(message, toolMessages));
|
||||
const processingState = useProcessingState();
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
ChatMessageEditForm,
|
||||
ChatMessageMcpPromptContent
|
||||
} from '$lib/components/app';
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { getChatMessageEditContext } from '$lib/contexts';
|
||||
import { McpPromptVariant, MessageRole } from '$lib/enums';
|
||||
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
let { class: className = '', mcpPrompt, message }: Props = $props();
|
||||
|
||||
// Get edit context
|
||||
const editCtx = getMessageEditContext();
|
||||
const editCtx = getChatMessageEditContext();
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { INPUT_CLASSES } from '$lib/constants';
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { getChatMessageEditContext } from '$lib/contexts';
|
||||
import { KeyboardKey, MessageRole } from '$lib/enums';
|
||||
import { settingsStore } from '$lib/stores';
|
||||
import { autoResizeTextarea, isIMEComposing } from '$lib/utils';
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
let { class: className = '', message, textareaElement = $bindable() }: Props = $props();
|
||||
|
||||
const editCtx = getMessageEditContext();
|
||||
const editCtx = getChatMessageEditContext();
|
||||
|
||||
function handleEditKeydown(event: KeyboardEvent) {
|
||||
if (event.key === KeyboardKey.ENTER && !event.shiftKey && !isIMEComposing(event)) {
|
||||
|
||||
+1
-2
@@ -11,8 +11,7 @@
|
||||
import ChatMessageToolCallBlockSearchResults from './ChatMessageToolCallBlockSearchResults.svelte';
|
||||
import ChatMessageToolCallBlockWriteFile from './ChatMessageToolCallBlockWriteFile.svelte';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import type { DatabaseMessageExtra } from '$lib/types';
|
||||
import type { AgenticSection, DatabaseMessageExtra } from '$lib/types';
|
||||
import { extractSearchQuery, extractSearchResults, isWebSearchToolName } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
|
||||
+1
-2
@@ -8,8 +8,7 @@
|
||||
import { MarkdownContent, SyntaxHighlightedCode } from '$lib/components/app';
|
||||
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
|
||||
import { FileTypeText, ToolResultKind } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import type { DatabaseMessageExtra } from '$lib/types';
|
||||
import type { AgenticSection, DatabaseMessageExtra } from '$lib/types';
|
||||
import {
|
||||
classifyToolResult,
|
||||
formatJsonPretty,
|
||||
|
||||
+1
-2
@@ -12,8 +12,7 @@
|
||||
import { CollapsibleTerminalBlock } from '$lib/components/app';
|
||||
import { SETTINGS_KEYS, TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
|
||||
import { settingsStore, toolsStore } from '$lib/stores';
|
||||
import type { AgenticSection, ToolResultLine } from '$lib/types';
|
||||
import type { DatabaseMessageExtra } from '$lib/types';
|
||||
import type { AgenticSection, DatabaseMessageExtra, ToolResultLine } from '$lib/types';
|
||||
import {
|
||||
abbreviateHome,
|
||||
type ExecShellExitStatus,
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
ChatMessageStatistics,
|
||||
ChatMessageUserBubble
|
||||
} from '$lib/components/app/chat';
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { getChatMessageEditContext } from '$lib/contexts';
|
||||
import { ChatMessageStatisticsMode, MessageRole } from '$lib/enums';
|
||||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||
import { chatStore, settingsStore } from '$lib/stores';
|
||||
@@ -25,7 +25,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
// Get contexts
|
||||
const editCtx = getMessageEditContext();
|
||||
const editCtx = getChatMessageEditContext();
|
||||
const processingState = useProcessingState();
|
||||
|
||||
const currentConfig = $derived(settingsStore.config);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ArrowUp, Edit, Trash2 } from '@lucide/svelte';
|
||||
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 {
|
||||
class?: string;
|
||||
@@ -21,7 +21,7 @@
|
||||
onSendImmediately
|
||||
}: Props = $props();
|
||||
|
||||
const editCtx = useMessageEditContext({
|
||||
const editCtx = useChatMessageEditContext({
|
||||
getContent: () => content,
|
||||
getExtras: () => extras,
|
||||
onSave: (content, extras) => onEdit(content, extras)
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import { getChatMessageActionsContext, getMessageEditContext } from '$lib/contexts';
|
||||
import { getChatMessageActionsContext, getChatMessageEditContext } from '$lib/contexts';
|
||||
import { MessageRole } from '$lib/enums';
|
||||
import { conversationsStore } from '$lib/stores';
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
const messageActions = getChatMessageActionsContext();
|
||||
const editCtx = getMessageEditContext();
|
||||
const editCtx = getChatMessageEditContext();
|
||||
|
||||
let showForkDialog = $state(false);
|
||||
let forkName = $state('');
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
} from '$lib/components/app';
|
||||
import { AgenticSectionType, ChatMessageStatsView, ToolPermissionDecision } from '$lib/enums';
|
||||
import { agenticStore, settingsStore } from '$lib/stores';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import type {
|
||||
AgenticSection,
|
||||
ChatMessageAgenticTimings,
|
||||
ChatMessageAgenticTurnStats,
|
||||
DatabaseMessage
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import { ChatForm, DialogConfirmation } from '$lib/components/app';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { getChatMessageEditContext } from '$lib/contexts';
|
||||
import { KeyboardKey, MessageRole } from '$lib/enums';
|
||||
import { chatStore } from '$lib/stores';
|
||||
import { processFilesToChatUploaded } from '$lib/utils/browser-only';
|
||||
|
||||
const editCtx = getMessageEditContext();
|
||||
const editCtx = getChatMessageEditContext();
|
||||
|
||||
let saveWithoutRegenerate = $state(false);
|
||||
let showDiscardDialog = $state(false);
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
import { REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
|
||||
import { AgenticSectionType } from '$lib/enums';
|
||||
import { settingsStore } from '$lib/stores';
|
||||
import type { DatabaseMessageExtra } from '$lib/types';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import type { AgenticSection, DatabaseMessageExtra } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { ChatMessage, ChatMessageUserPending } from '$lib/components/app';
|
||||
import { setChatActionsContext } from '$lib/contexts';
|
||||
import { MessageRole } from '$lib/enums';
|
||||
import { agenticStore, chatStore, conversationsStore, settingsStore } from '$lib/stores';
|
||||
import type { ChatMessageActions } from '$lib/types';
|
||||
import {
|
||||
buildSiblingInfoMap,
|
||||
copyToClipboard,
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
const currentConfig = settingsStore.config;
|
||||
|
||||
setChatActionsContext({
|
||||
const chatActions: ChatMessageActions = {
|
||||
continueAssistantMessage: async (message: DatabaseMessage) => {
|
||||
onUserAction?.();
|
||||
await chatStore.continueAssistantMessage(message.id);
|
||||
@@ -91,7 +91,7 @@
|
||||
await chatStore.regenerateMessageWithBranching(message.id, modelOverride);
|
||||
refreshAllMessages();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function refreshAllMessages() {
|
||||
const conversation = conversationsStore.activeConversation;
|
||||
@@ -228,6 +228,7 @@
|
||||
{#each displayMessages as { isLastAssistantMessage, isLastUserMessage, message, nextAssistantMessage, siblingInfo, toolMessages } (message.id)}
|
||||
<ChatMessage
|
||||
class="mx-auto mt-12 w-full max-w-3xl"
|
||||
{chatActions}
|
||||
{message}
|
||||
{toolMessages}
|
||||
{isLastAssistantMessage}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const CONTEXT_KEY_MESSAGE_EDIT = 'chat-message-edit';
|
||||
export const CONTEXT_KEY_CHAT_ACTIONS = 'chat-actions';
|
||||
export const CONTEXT_KEY_CHAT_MESSAGE_EDIT = 'chat-message-edit';
|
||||
export const CONTEXT_KEY_CHAT_MESSAGE_ACTIONS = 'chat-message-actions';
|
||||
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);
|
||||
}
|
||||
@@ -1,25 +1,7 @@
|
||||
import { CONTEXT_KEY_CHAT_MESSAGE_ACTIONS } from '$lib/constants';
|
||||
import type { ChatMessageActionsContext } from '$lib/types';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
|
||||
export interface ChatMessageDeletionInfo {
|
||||
totalCount: number;
|
||||
userMessages: number;
|
||||
assistantMessages: number;
|
||||
messageTypes: string[];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const CHAT_MESSAGE_ACTIONS_KEY = Symbol.for(CONTEXT_KEY_CHAT_MESSAGE_ACTIONS);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 +1,8 @@
|
||||
export {
|
||||
getMessageEditContext,
|
||||
setMessageEditContext,
|
||||
type MessageEditContext,
|
||||
type MessageEditState,
|
||||
type MessageEditActions
|
||||
} from './message-edit.context';
|
||||
|
||||
export {
|
||||
getChatActionsContext,
|
||||
setChatActionsContext,
|
||||
type ChatActionsContext
|
||||
} from './chat-actions.context';
|
||||
export { getChatMessageEditContext, setChatMessageEditContext } from './chat-message-edit.context';
|
||||
|
||||
export {
|
||||
getChatMessageActionsContext,
|
||||
setChatMessageActionsContext,
|
||||
type ChatMessageActionsContext,
|
||||
type ChatMessageDeletionInfo
|
||||
setChatMessageActionsContext
|
||||
} from './chat-message-actions.context';
|
||||
|
||||
export { getChatFormActionsContext, setChatFormActionsContext } from './chat-form-actions.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 { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra';
|
||||
|
||||
interface UseMessageEditContextOptions {
|
||||
interface UseChatMessageEditContextOptions {
|
||||
getContent: () => string;
|
||||
getExtras: () => DatabaseMessageExtra[];
|
||||
showSaveOnlyOption?: boolean;
|
||||
onSave: (content: string, extras?: DatabaseMessageExtra[]) => void;
|
||||
}
|
||||
|
||||
export function useMessageEditContext(options: UseMessageEditContextOptions) {
|
||||
export function useChatMessageEditContext(options: UseChatMessageEditContextOptions) {
|
||||
let isEditing = $state(false);
|
||||
let editedContent = $state('');
|
||||
let editedExtras = $state<DatabaseMessageExtra[]>([]);
|
||||
@@ -45,7 +45,7 @@ export function useMessageEditContext(options: UseMessageEditContextOptions) {
|
||||
isEditing = false;
|
||||
}
|
||||
|
||||
setMessageEditContext({
|
||||
setChatMessageEditContext({
|
||||
cancel: handleCancelEdit,
|
||||
get editedContent() {
|
||||
return editedContent;
|
||||
Vendored
+110
-1
@@ -7,7 +7,8 @@ import type {
|
||||
AttachmentMenuItemId,
|
||||
ChatFormCommandAction,
|
||||
ErrorDialogType,
|
||||
FileMentionEntryType
|
||||
FileMentionEntryType,
|
||||
MessageRole
|
||||
} from '$lib/enums';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
@@ -235,3 +236,111 @@ export interface ChatFormCommand {
|
||||
action: ChatFormCommandAction;
|
||||
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,
|
||||
ChatAttachmentDisplayItem,
|
||||
ChatMessageSiblingInfo,
|
||||
ChatMessageActions,
|
||||
ChatMessageActionsContext,
|
||||
ChatMessageDeletionInfo,
|
||||
ChatMessageEditContext,
|
||||
ChatMessageEditState,
|
||||
ChatMessageEditActions,
|
||||
ChatMessageAssistantEditActions,
|
||||
ChatFormActionsContext,
|
||||
ChatMessagePromptProgress,
|
||||
ChatMessageTimings,
|
||||
ChatMessageAgenticTimings,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import ChatMessage from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte';
|
||||
import type { ChatMessageActions } from '$lib/types';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
component: ChatMessage,
|
||||
@@ -10,6 +11,18 @@
|
||||
title: 'Components/ChatScreen/ChatMessage'
|
||||
});
|
||||
|
||||
const chatActions: ChatMessageActions = {
|
||||
continueAssistantMessage: () => {},
|
||||
copy: () => {},
|
||||
delete: () => {},
|
||||
editUserMessagePreserveResponses: () => {},
|
||||
editWithBranching: () => {},
|
||||
editWithReplacement: () => {},
|
||||
forkConversation: () => {},
|
||||
navigateToSibling: () => {},
|
||||
regenerateWithBranching: () => {}
|
||||
};
|
||||
|
||||
// Mock messages for different scenarios
|
||||
const userMessage: DatabaseMessage = {
|
||||
children: [],
|
||||
@@ -190,7 +203,7 @@
|
||||
}}
|
||||
>
|
||||
<div class="w-[56rem]">
|
||||
<ChatMessage message={streamingMessage} />
|
||||
<ChatMessage message={streamingMessage} {chatActions} />
|
||||
</div>
|
||||
</Story>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user