From 4db6bf272aab666d5cad09eadb041fa11a8d97cb Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Mon, 19 Jan 2026 14:32:27 -0800 Subject: [PATCH] fix(copilot): chat loading; refactor(copilot): components, utils, hooks --- .../chat-history-skeleton.tsx | 22 +++ .../components/chat-history-skeleton/index.ts | 1 + .../checkpoint-discard-modal.tsx | 56 +++++++ .../checkpoint-discard-modal/index.ts | 1 + .../{ => file-display}/file-display.tsx | 0 .../components/file-display/index.ts | 1 + .../copilot-message/components/index.ts | 4 +- .../components/markdown-renderer/index.ts | 1 + .../markdown-renderer.tsx | 0 .../restore-checkpoint-modal/index.ts | 1 + .../restore-checkpoint-modal.tsx | 46 ++++++ .../components/smooth-streaming/index.ts | 1 + .../smooth-streaming.tsx | 2 +- .../components/thinking-block/index.ts | 1 + .../{ => thinking-block}/thinking-block.tsx | 2 +- .../components/usage-limit-actions/index.ts | 1 + .../usage-limit-actions.tsx | 0 .../copilot-message/copilot-message.tsx | 142 ++++-------------- .../components/copilot-message/hooks/index.ts | 1 + .../hooks/use-message-content-analysis.ts | 31 ++++ .../hooks/use-message-editing.ts | 33 ++-- .../components/copilot-message/index.ts | 1 + .../components/copilot/components/index.ts | 15 +- .../components/plan-mode-section/index.ts | 1 + .../plan-mode-section/plan-mode-section.tsx | 2 +- .../components/queued-messages/index.ts | 1 + .../copilot/components/todo-list/index.ts | 1 + .../copilot/components/tool-call/index.ts | 1 + .../components/tool-call/tool-call.tsx | 2 +- .../attached-files-display/index.ts | 1 + .../bottom-controls/bottom-controls.tsx | 127 ++++++++++++++++ .../components/bottom-controls/index.ts | 1 + .../components/context-pills/index.ts | 1 + .../components/user-input/components/index.ts | 13 +- .../components/mention-menu/index.ts | 1 + .../components/mode-selector/index.ts | 1 + .../components/model-selector/index.ts | 1 + .../user-input/components/slash-menu/index.ts | 1 + .../components/user-input/hooks/index.ts | 1 + .../hooks/use-context-management.ts | 4 +- .../user-input/hooks/use-mention-system.ts | 107 +++++++++++++ .../copilot/components/user-input/index.ts | 1 + .../components/user-input/user-input.tsx | 122 ++++----------- .../copilot/components/user-input/utils.ts | 97 ++++++++++++ .../copilot/components/welcome/index.ts | 1 + .../panel/components/copilot/copilot.tsx | 49 +++--- apps/sim/stores/panel/copilot/store.ts | 5 + 47 files changed, 640 insertions(+), 265 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/chat-history-skeleton/chat-history-skeleton.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/chat-history-skeleton/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-discard-modal/checkpoint-discard-modal.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-discard-modal/index.ts rename apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/{ => file-display}/file-display.tsx (100%) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/index.ts rename apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/{ => markdown-renderer}/markdown-renderer.tsx (100%) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/restore-checkpoint-modal/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/restore-checkpoint-modal/restore-checkpoint-modal.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/index.ts rename apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/{ => smooth-streaming}/smooth-streaming.tsx (95%) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/index.ts rename apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/{ => thinking-block}/thinking-block.tsx (99%) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/index.ts rename apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/{ => usage-limit-actions}/usage-limit-actions.tsx (100%) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-content-analysis.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/attached-files-display/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/bottom-controls.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/context-pills/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-system.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/index.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/chat-history-skeleton/chat-history-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/chat-history-skeleton/chat-history-skeleton.tsx new file mode 100644 index 000000000..ce623aa80 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/chat-history-skeleton/chat-history-skeleton.tsx @@ -0,0 +1,22 @@ +import { PopoverSection } from '@/components/emcn' + +/** + * Skeleton loading component for chat history dropdown + * Displays placeholder content while chats are being loaded + */ +export function ChatHistorySkeleton() { + return ( + <> + +
+ +
+ {[1, 2, 3].map((i) => ( +
+
+
+ ))} +
+ + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/chat-history-skeleton/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/chat-history-skeleton/index.ts new file mode 100644 index 000000000..d2d5b9412 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/chat-history-skeleton/index.ts @@ -0,0 +1 @@ +export * from './chat-history-skeleton' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-discard-modal/checkpoint-discard-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-discard-modal/checkpoint-discard-modal.tsx new file mode 100644 index 000000000..18dc0cac8 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-discard-modal/checkpoint-discard-modal.tsx @@ -0,0 +1,56 @@ +import { Button } from '@/components/emcn' + +interface CheckpointDiscardModalProps { + isProcessingDiscard: boolean + onCancel: () => void + onRevert: () => void + onContinue: () => void +} + +/** + * Inline confirmation modal for discarding checkpoints during message editing + * Shows options to cancel, revert to checkpoint, or continue without reverting + */ +export function CheckpointDiscardModal({ + isProcessingDiscard, + onCancel, + onRevert, + onContinue, +}: CheckpointDiscardModalProps) { + return ( +
+

+ Continue from a previous message? +

+
+ + + +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-discard-modal/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-discard-modal/index.ts new file mode 100644 index 000000000..b90d263c0 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/checkpoint-discard-modal/index.ts @@ -0,0 +1 @@ +export * from './checkpoint-discard-modal' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/file-display.tsx similarity index 100% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/file-display.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/index.ts new file mode 100644 index 000000000..feaf05e59 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/file-display/index.ts @@ -0,0 +1 @@ +export * from './file-display' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/index.ts index 75eb97187..cdd473002 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/index.ts @@ -1,5 +1,7 @@ +export * from './checkpoint-discard-modal' export * from './file-display' -export { default as CopilotMarkdownRenderer } from './markdown-renderer' +export { CopilotMarkdownRenderer } from './markdown-renderer' +export * from './restore-checkpoint-modal' export * from './smooth-streaming' export * from './thinking-block' export * from './usage-limit-actions' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/index.ts new file mode 100644 index 000000000..62e0a916c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/index.ts @@ -0,0 +1 @@ +export { default as CopilotMarkdownRenderer } from './markdown-renderer' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/markdown-renderer.tsx similarity index 100% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer/markdown-renderer.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/restore-checkpoint-modal/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/restore-checkpoint-modal/index.ts new file mode 100644 index 000000000..7ad1c23c0 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/restore-checkpoint-modal/index.ts @@ -0,0 +1 @@ +export * from './restore-checkpoint-modal' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/restore-checkpoint-modal/restore-checkpoint-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/restore-checkpoint-modal/restore-checkpoint-modal.tsx new file mode 100644 index 000000000..766de5518 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/restore-checkpoint-modal/restore-checkpoint-modal.tsx @@ -0,0 +1,46 @@ +import { Button } from '@/components/emcn' + +interface RestoreCheckpointModalProps { + isReverting: boolean + onCancel: () => void + onConfirm: () => void +} + +/** + * Inline confirmation modal for restoring a checkpoint + * Warns user that the action cannot be undone + */ +export function RestoreCheckpointModal({ + isReverting, + onCancel, + onConfirm, +}: RestoreCheckpointModalProps) { + return ( +
+

+ Revert to checkpoint? This will restore your workflow to the state saved at this checkpoint.{' '} + This action cannot be undone. +

+
+ + +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/index.ts new file mode 100644 index 000000000..96c0d8364 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/index.ts @@ -0,0 +1 @@ +export * from './smooth-streaming' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/smooth-streaming.tsx similarity index 95% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/smooth-streaming.tsx index 62a9ae6ba..41883e606 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming/smooth-streaming.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useRef, useState } from 'react' import { cn } from '@/lib/core/utils/cn' -import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' +import { CopilotMarkdownRenderer } from '../markdown-renderer' /** * Character animation delay in milliseconds diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/index.ts new file mode 100644 index 000000000..515f72bb0 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/index.ts @@ -0,0 +1 @@ +export * from './thinking-block' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx similarity index 99% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx index 2b5b02336..616dbe076 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block/thinking-block.tsx @@ -3,7 +3,7 @@ import { memo, useEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' import { ChevronUp } from 'lucide-react' -import CopilotMarkdownRenderer from './markdown-renderer' +import { CopilotMarkdownRenderer } from '../markdown-renderer' /** * Removes thinking tags (raw or escaped) from streamed content. diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/index.ts new file mode 100644 index 000000000..0d34dbbe5 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/index.ts @@ -0,0 +1 @@ +export * from './usage-limit-actions' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/usage-limit-actions.tsx similarity index 100% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions.tsx rename to apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/usage-limit-actions/usage-limit-actions.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx index 821ee7b70..9ee41bc15 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx @@ -9,18 +9,22 @@ import { ToolCall, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components' import { + CheckpointDiscardModal, FileAttachmentDisplay, + RestoreCheckpointModal, SmoothStreamingText, StreamingIndicator, ThinkingBlock, UsageLimitActions, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components' -import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' +import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' import { useCheckpointManagement, + useMessageContentAnalysis, useMessageEditing, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks' import { UserInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input' +import { buildMentionHighlightNodes } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils' import type { CopilotMessage as CopilotMessageType } from '@/stores/panel' import { useCopilotStore } from '@/stores/panel' @@ -191,6 +195,9 @@ const CopilotMessage: FC = memo( [sendMessage] ) + // Analyze message content for visibility (used for assistant messages) + const { hasVisibleContent } = useMessageContentAnalysis({ message }) + // Memoize content blocks to avoid re-rendering unchanged blocks // No entrance animations to prevent layout shift const memoizedContentBlocks = useMemo(() => { @@ -290,40 +297,12 @@ const CopilotMessage: FC = memo( {/* Inline Checkpoint Discard Confirmation - shown below input in edit mode */} {showCheckpointDiscardModal && ( -
-

- Continue from a previous message? -

-
- - - -
-
+ )}
) : ( @@ -348,46 +327,15 @@ const CopilotMessage: FC = memo( ref={messageContentRef} className={`relative whitespace-pre-wrap break-words px-[2px] py-1 font-medium font-sans text-[var(--text-primary)] text-sm leading-[1.25rem] ${isSendingMessage && isLastUserMessage && isHoveringMessage ? 'pr-7' : ''} ${!isExpanded && needsExpansion ? 'max-h-[60px] overflow-hidden' : 'overflow-visible'}`} > - {(() => { - const text = message.content || '' - const contexts: any[] = Array.isArray((message as any).contexts) - ? ((message as any).contexts as any[]) - : [] - - // Build tokens with their prefixes (@ for mentions, / for commands) - const tokens = contexts - .filter((c) => c?.kind !== 'current_workflow' && c?.label) - .map((c) => { - const prefix = c?.kind === 'slash_command' ? '/' : '@' - return `${prefix}${c.label}` - }) - if (!tokens.length) return text - - const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const pattern = new RegExp(`(${tokens.map(escapeRegex).join('|')})`, 'g') - - const nodes: React.ReactNode[] = [] - let lastIndex = 0 - let match: RegExpExecArray | null - while ((match = pattern.exec(text)) !== null) { - const i = match.index - const before = text.slice(lastIndex, i) - if (before) nodes.push(before) - const mention = match[0] - nodes.push( - - {mention} - - ) - lastIndex = i + mention.length - } - const tail = text.slice(lastIndex) - if (tail) nodes.push(tail) - return nodes - })()} + {buildMentionHighlightNodes( + message.content || '', + message.contexts || [], + (token, key) => ( + + {token} + + ) + )}
{/* Gradient fade when truncated - applies to entire message box */} @@ -439,50 +387,16 @@ const CopilotMessage: FC = memo( {/* Inline Restore Checkpoint Confirmation */} {showRestoreConfirmation && ( -
-

- Revert to checkpoint? This will restore your workflow to the state saved at this - checkpoint.{' '} - This action cannot be undone. -

-
- - -
-
+ )} ) } - // Check if there's any visible content in the blocks - const hasVisibleContent = useMemo(() => { - if (!message.contentBlocks || message.contentBlocks.length === 0) return false - return message.contentBlocks.some((block) => { - if (block.type === 'text') { - const parsed = parseSpecialTags(block.content) - return parsed.cleanContent.trim().length > 0 - } - return block.type === 'thinking' || block.type === 'tool_call' - }) - }, [message.contentBlocks]) - if (isAssistant) { return (
{ + if (!message.contentBlocks || message.contentBlocks.length === 0) return false + return message.contentBlocks.some((block) => { + if (block.type === 'text') { + const parsed = parseSpecialTags(block.content) + return parsed.cleanContent.trim().length > 0 + } + return block.type === 'thinking' || block.type === 'tool_call' + }) + }, [message.contentBlocks]) + + return { + hasVisibleContent, + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts index bdef4401b..940501ac6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/hooks/use-message-editing.ts @@ -2,11 +2,18 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import type { CopilotMessage } from '@/stores/panel' +import type { ChatContext, CopilotMessage, MessageFileAttachment } from '@/stores/panel' import { useCopilotStore } from '@/stores/panel' const logger = createLogger('useMessageEditing') +/** + * Ref interface for UserInput component + */ +interface UserInputRef { + focus: () => void +} + /** * Message truncation height in pixels */ @@ -32,8 +39,8 @@ interface UseMessageEditingProps { setShowCheckpointDiscardModal: (show: boolean) => void pendingEditRef: React.MutableRefObject<{ message: string - fileAttachments?: any[] - contexts?: any[] + fileAttachments?: MessageFileAttachment[] + contexts?: ChatContext[] } | null> /** * When true, disables the internal document click-outside handler. @@ -69,7 +76,7 @@ export function useMessageEditing(props: UseMessageEditingProps) { const editContainerRef = useRef(null) const messageContentRef = useRef(null) - const userInputRef = useRef(null) + const userInputRef = useRef(null) const { sendMessage, isSendingMessage, abortMessage, currentChat } = useCopilotStore() @@ -121,7 +128,11 @@ export function useMessageEditing(props: UseMessageEditingProps) { * Truncates messages after edited message and resends with same ID */ const performEdit = useCallback( - async (editedMessage: string, fileAttachments?: any[], contexts?: any[]) => { + async ( + editedMessage: string, + fileAttachments?: MessageFileAttachment[], + contexts?: ChatContext[] + ) => { const currentMessages = messages const editIndex = currentMessages.findIndex((m) => m.id === message.id) @@ -134,7 +145,7 @@ export function useMessageEditing(props: UseMessageEditingProps) { ...message, content: editedMessage, fileAttachments: fileAttachments || message.fileAttachments, - contexts: contexts || (message as any).contexts, + contexts: contexts || message.contexts, } useCopilotStore.setState({ messages: [...truncatedMessages, updatedMessage] }) @@ -153,7 +164,7 @@ export function useMessageEditing(props: UseMessageEditingProps) { timestamp: m.timestamp, ...(m.contentBlocks && { contentBlocks: m.contentBlocks }), ...(m.fileAttachments && { fileAttachments: m.fileAttachments }), - ...((m as any).contexts && { contexts: (m as any).contexts }), + ...(m.contexts && { contexts: m.contexts }), })), }), }) @@ -164,7 +175,7 @@ export function useMessageEditing(props: UseMessageEditingProps) { await sendMessage(editedMessage, { fileAttachments: fileAttachments || message.fileAttachments, - contexts: contexts || (message as any).contexts, + contexts: contexts || message.contexts, messageId: message.id, queueIfBusy: false, }) @@ -178,7 +189,11 @@ export function useMessageEditing(props: UseMessageEditingProps) { * Checks for checkpoints and shows confirmation if needed */ const handleSubmitEdit = useCallback( - async (editedMessage: string, fileAttachments?: any[], contexts?: any[]) => { + async ( + editedMessage: string, + fileAttachments?: MessageFileAttachment[], + contexts?: ChatContext[] + ) => { if (!editedMessage.trim()) return if (isSendingMessage) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/index.ts new file mode 100644 index 000000000..d2cf90344 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/index.ts @@ -0,0 +1 @@ +export * from './copilot-message' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts index 3ac19aac2..632dae142 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/index.ts @@ -1,7 +1,8 @@ -export * from './copilot-message/copilot-message' -export * from './plan-mode-section/plan-mode-section' -export * from './queued-messages/queued-messages' -export * from './todo-list/todo-list' -export * from './tool-call/tool-call' -export * from './user-input/user-input' -export * from './welcome/welcome' +export * from './chat-history-skeleton' +export * from './copilot-message' +export * from './plan-mode-section' +export * from './queued-messages' +export * from './todo-list' +export * from './tool-call' +export * from './user-input' +export * from './welcome' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/index.ts new file mode 100644 index 000000000..fb80d1dda --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/index.ts @@ -0,0 +1 @@ +export * from './plan-mode-section' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx index cadc5f1e8..c4f17704f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/plan-mode-section/plan-mode-section.tsx @@ -29,7 +29,7 @@ import { Check, GripHorizontal, Pencil, X } from 'lucide-react' import { Button, Textarea } from '@/components/emcn' import { Trash } from '@/components/emcn/icons/trash' import { cn } from '@/lib/core/utils/cn' -import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' +import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' /** * Shared border and background styles diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/index.ts new file mode 100644 index 000000000..498f56dfb --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/index.ts @@ -0,0 +1 @@ +export * from './queued-messages' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/index.ts new file mode 100644 index 000000000..4f6ca9852 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/todo-list/index.ts @@ -0,0 +1 @@ +export * from './todo-list' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/index.ts new file mode 100644 index 000000000..0269fca39 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/index.ts @@ -0,0 +1 @@ +export * from './tool-call' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index bba129df7..f526b6ac7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -15,7 +15,7 @@ import { hasInterrupt as hasInterruptFromConfig, isSpecialTool as isSpecialToolFromConfig, } from '@/lib/copilot/tools/client/ui-config' -import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' +import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' import { SmoothStreamingText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming' import { ThinkingBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block' import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/attached-files-display/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/attached-files-display/index.ts new file mode 100644 index 000000000..ef4e37411 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/attached-files-display/index.ts @@ -0,0 +1 @@ +export * from './attached-files-display' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/bottom-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/bottom-controls.tsx new file mode 100644 index 000000000..f01b583c8 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/bottom-controls.tsx @@ -0,0 +1,127 @@ +'use client' + +import { ArrowUp, Image, Loader2 } from 'lucide-react' +import { Badge, Button } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import { ModeSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/mode-selector' +import { ModelSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/model-selector' + +interface BottomControlsProps { + mode: 'ask' | 'build' | 'plan' + onModeChange?: (mode: 'ask' | 'build' | 'plan') => void + selectedModel: string + onModelSelect: (model: string) => void + isNearTop: boolean + disabled: boolean + hideModeSelector: boolean + canSubmit: boolean + isLoading: boolean + isAborting: boolean + showAbortButton: boolean + onSubmit: () => void + onAbort: () => void + onFileSelect: () => void +} + +/** + * Bottom controls section of the user input + * Contains mode selector, model selector, file attachment button, and submit/abort buttons + */ +export function BottomControls({ + mode, + onModeChange, + selectedModel, + onModelSelect, + isNearTop, + disabled, + hideModeSelector, + canSubmit, + isLoading, + isAborting, + showAbortButton, + onSubmit, + onAbort, + onFileSelect, +}: BottomControlsProps) { + return ( +
+ {/* Left side: Mode Selector + Model Selector */} +
+ {!hideModeSelector && ( + + )} + + +
+ + {/* Right side: Attach Button + Send Button */} +
+ + + + + {showAbortButton ? ( + + ) : ( + + )} +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/index.ts new file mode 100644 index 000000000..7a0d61937 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/bottom-controls/index.ts @@ -0,0 +1 @@ +export * from './bottom-controls' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/context-pills/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/context-pills/index.ts new file mode 100644 index 000000000..09b096342 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/context-pills/index.ts @@ -0,0 +1 @@ +export * from './context-pills' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts index 1d0da42d4..acea9d9b2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/index.ts @@ -1,6 +1,7 @@ -export { AttachedFilesDisplay } from './attached-files-display/attached-files-display' -export { ContextPills } from './context-pills/context-pills' -export { type MentionFolderNav, MentionMenu } from './mention-menu/mention-menu' -export { ModeSelector } from './mode-selector/mode-selector' -export { ModelSelector } from './model-selector/model-selector' -export { type SlashFolderNav, SlashMenu } from './slash-menu/slash-menu' +export { AttachedFilesDisplay } from './attached-files-display' +export { BottomControls } from './bottom-controls' +export { ContextPills } from './context-pills' +export { type MentionFolderNav, MentionMenu } from './mention-menu' +export { ModeSelector } from './mode-selector' +export { ModelSelector } from './model-selector' +export { type SlashFolderNav, SlashMenu } from './slash-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/index.ts new file mode 100644 index 000000000..9a6118aee --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mention-menu/index.ts @@ -0,0 +1 @@ +export * from './mention-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/index.ts new file mode 100644 index 000000000..119be9858 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/mode-selector/index.ts @@ -0,0 +1 @@ +export * from './mode-selector' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/index.ts new file mode 100644 index 000000000..9bd04ddd9 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/model-selector/index.ts @@ -0,0 +1 @@ +export * from './model-selector' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/index.ts new file mode 100644 index 000000000..7b7f088ca --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/slash-menu/index.ts @@ -0,0 +1 @@ +export * from './slash-menu' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts index 858a39c13..af44a7a4e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/index.ts @@ -5,5 +5,6 @@ export { useMentionData } from './use-mention-data' export { useMentionInsertHandlers } from './use-mention-insert-handlers' export { useMentionKeyboard } from './use-mention-keyboard' export { useMentionMenu } from './use-mention-menu' +export { useMentionSystem } from './use-mention-system' export { useMentionTokens } from './use-mention-tokens' export { useTextareaAutoResize } from './use-textarea-auto-resize' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts index 90b5d6bc9..819d0ef5d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { + escapeRegex, filterOutContext, isContextAlreadySelected, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils' @@ -22,9 +23,6 @@ interface UseContextManagementProps { export function useContextManagement({ message, initialContexts }: UseContextManagementProps) { const [selectedContexts, setSelectedContexts] = useState(initialContexts ?? []) const initializedRef = useRef(false) - const escapeRegex = useCallback((value: string) => { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - }, []) // Initialize with initial contexts when they're first provided (for edit mode) useEffect(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-system.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-system.ts new file mode 100644 index 000000000..d67a1d22c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-system.ts @@ -0,0 +1,107 @@ +import type { MentionFolderNav } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components' +import { useContextManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-context-management' +import { useFileAttachments } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments' +import { useMentionData } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data' +import { useMentionInsertHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-insert-handlers' +import { useMentionKeyboard } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-keyboard' +import { useMentionMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu' +import { useMentionTokens } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-tokens' +import { useTextareaAutoResize } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-textarea-auto-resize' +import type { ChatContext } from '@/stores/panel' + +interface UseMentionSystemProps { + message: string + setMessage: (message: string) => void + workflowId: string | null + workspaceId: string + userId?: string + panelWidth: number + disabled: boolean + isLoading: boolean + inputContainerRef: HTMLDivElement | null + initialContexts?: ChatContext[] + mentionFolderNav: MentionFolderNav | null +} + +/** + * Composite hook that combines all mention-related hooks into a single interface. + * Reduces import complexity in components that need full mention functionality. + * + * @param props - Configuration for all mention system hooks + * @returns Combined interface for mention system functionality + */ +export function useMentionSystem({ + message, + setMessage, + workflowId, + workspaceId, + userId, + panelWidth, + disabled, + isLoading, + inputContainerRef, + initialContexts, + mentionFolderNav, +}: UseMentionSystemProps) { + const contextManagement = useContextManagement({ message, initialContexts }) + + const mentionMenu = useMentionMenu({ + message, + selectedContexts: contextManagement.selectedContexts, + onContextSelect: contextManagement.addContext, + onMessageChange: setMessage, + }) + + const mentionTokens = useMentionTokens({ + message, + selectedContexts: contextManagement.selectedContexts, + mentionMenu, + setMessage, + setSelectedContexts: contextManagement.setSelectedContexts, + }) + + const { overlayRef } = useTextareaAutoResize({ + message, + panelWidth, + selectedContexts: contextManagement.selectedContexts, + textareaRef: mentionMenu.textareaRef, + containerRef: inputContainerRef, + }) + + const mentionData = useMentionData({ + workflowId, + workspaceId, + }) + + const fileAttachments = useFileAttachments({ + userId, + disabled, + isLoading, + }) + + const insertHandlers = useMentionInsertHandlers({ + mentionMenu, + workflowId, + selectedContexts: contextManagement.selectedContexts, + onContextAdd: contextManagement.addContext, + mentionFolderNav, + }) + + const mentionKeyboard = useMentionKeyboard({ + mentionMenu, + mentionData, + insertHandlers, + mentionFolderNav, + }) + + return { + contextManagement, + mentionMenu, + mentionTokens, + overlayRef, + mentionData, + fileAttachments, + insertHandlers, + mentionKeyboard, + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/index.ts new file mode 100644 index 000000000..20b536415 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/index.ts @@ -0,0 +1 @@ +export * from './user-input' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx index 3636c6b29..ea340ce67 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx @@ -9,19 +9,19 @@ import { useState, } from 'react' import { createLogger } from '@sim/logger' -import { ArrowUp, AtSign, Image, Loader2 } from 'lucide-react' +import { AtSign } from 'lucide-react' import { useParams } from 'next/navigation' import { createPortal } from 'react-dom' import { Badge, Button, Textarea } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' +import type { CopilotModelId } from '@/lib/copilot/models' import { cn } from '@/lib/core/utils/cn' import { AttachedFilesDisplay, + BottomControls, ContextPills, type MentionFolderNav, MentionMenu, - ModelSelector, - ModeSelector, type SlashFolderNav, SlashMenu, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components' @@ -44,6 +44,10 @@ import { useTextareaAutoResize, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks' import type { MessageFileAttachment } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments' +import { + computeMentionHighlightRanges, + extractContextTokens, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils' import type { ChatContext } from '@/stores/panel' import { useCopilotStore } from '@/stores/panel' @@ -306,7 +310,7 @@ const UserInput = forwardRef( size: f.size, })) - onSubmit(trimmedMessage, fileAttachmentsForApi, contextManagement.selectedContexts as any) + onSubmit(trimmedMessage, fileAttachmentsForApi, contextManagement.selectedContexts) const shouldClearInput = clearOnSubmit && !options.preserveInput && !overrideMessage if (shouldClearInput) { @@ -657,7 +661,7 @@ const UserInput = forwardRef( const handleModelSelect = useCallback( (model: string) => { - setSelectedModel(model as any) + setSelectedModel(model as CopilotModelId) }, [setSelectedModel] ) @@ -677,15 +681,17 @@ const UserInput = forwardRef( return {displayText} } - const elements: React.ReactNode[] = [] - const ranges = mentionTokensWithContext.computeMentionRanges() + const tokens = extractContextTokens(contexts) + const ranges = computeMentionHighlightRanges(message, tokens) if (ranges.length === 0) { const displayText = message.endsWith('\n') ? `${message}\u200B` : message return {displayText} } + const elements: React.ReactNode[] = [] let lastIndex = 0 + for (let i = 0; i < ranges.length; i++) { const range = ranges[i] @@ -694,13 +700,12 @@ const UserInput = forwardRef( elements.push({before}) } - const mentionText = message.slice(range.start, range.end) elements.push( - {mentionText} + {range.token} ) lastIndex = range.end @@ -713,7 +718,7 @@ const UserInput = forwardRef( } return elements.length > 0 ? elements : {'\u00A0'} - }, [message, contextManagement.selectedContexts, mentionTokensWithContext]) + }, [message, contextManagement.selectedContexts]) return (
(
{/* Bottom Row: Mode Selector + Model Selector + Attach Button + Send Button */} -
- {/* Left side: Mode Selector + Model Selector */} -
- {!hideModeSelector && ( - - )} - - -
- - {/* Right side: Attach Button + Send Button */} -
- - - - - {showAbortButton ? ( - - ) : ( - - )} -
-
+ void handleSubmit()} + onAbort={handleAbort} + onFileSelect={fileAttachments.handleFileSelect} + /> {/* Hidden File Input - enabled during streaming so users can prepare images for the next message */} c.kind !== 'current_workflow' && c.label) + .map((c) => { + const prefix = c.kind === 'slash_command' ? '/' : '@' + return `${prefix}${c.label}` + }) +} + +/** + * Mention range for text highlighting + */ +export interface MentionHighlightRange { + start: number + end: number + token: string +} + +/** + * Computes mention ranges in text for highlighting + * @param text - Text to search + * @param tokens - Prefixed tokens to find (e.g., "@workflow", "/web") + * @returns Array of ranges with start, end, and matched token + */ +export function computeMentionHighlightRanges( + text: string, + tokens: string[] +): MentionHighlightRange[] { + if (!tokens.length || !text) return [] + + const pattern = new RegExp(`(${tokens.map(escapeRegex).join('|')})`, 'g') + const ranges: MentionHighlightRange[] = [] + let match: RegExpExecArray | null + + while ((match = pattern.exec(text)) !== null) { + ranges.push({ + start: match.index, + end: match.index + match[0].length, + token: match[0], + }) + } + + return ranges +} + +/** + * Builds React nodes with highlighted mention tokens + * @param text - Text to render + * @param contexts - Chat contexts to highlight + * @param createHighlightSpan - Function to create highlighted span element + * @returns Array of React nodes with highlighted mentions + */ +export function buildMentionHighlightNodes( + text: string, + contexts: ChatContext[], + createHighlightSpan: (token: string, key: string) => ReactNode +): ReactNode[] { + const tokens = extractContextTokens(contexts) + if (!tokens.length) return [text] + + const ranges = computeMentionHighlightRanges(text, tokens) + if (!ranges.length) return [text] + + const nodes: ReactNode[] = [] + let lastIndex = 0 + + for (const range of ranges) { + if (range.start > lastIndex) { + nodes.push(text.slice(lastIndex, range.start)) + } + nodes.push(createHighlightSpan(range.token, `mention-${range.start}-${range.end}`)) + lastIndex = range.end + } + + if (lastIndex < text.length) { + nodes.push(text.slice(lastIndex)) + } + + return nodes +} + /** * Gets the data array for a folder ID from mentionData. * Uses FOLDER_CONFIGS as the source of truth for key mapping. diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/index.ts new file mode 100644 index 000000000..425106f69 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/index.ts @@ -0,0 +1 @@ +export * from './welcome' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx index 03053ccf3..19aeadd24 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx @@ -24,6 +24,7 @@ import { import { Trash } from '@/components/emcn/icons/trash' import { cn } from '@/lib/core/utils/cn' import { + ChatHistorySkeleton, CopilotMessage, PlanModeSection, QueuedMessages, @@ -40,6 +41,7 @@ import { useTodoManagement, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/hooks' import { useScrollManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' +import type { ChatContext } from '@/stores/panel' import { useCopilotStore } from '@/stores/panel' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -74,10 +76,12 @@ export const Copilot = forwardRef(({ panelWidth }, ref const copilotContainerRef = useRef(null) const cancelEditCallbackRef = useRef<(() => void) | null>(null) const [editingMessageId, setEditingMessageId] = useState(null) - const [isEditingMessage, setIsEditingMessage] = useState(false) const [revertingMessageId, setRevertingMessageId] = useState(null) const [isHistoryDropdownOpen, setIsHistoryDropdownOpen] = useState(false) + // Derived state - editing when there's an editingMessageId + const isEditingMessage = editingMessageId !== null + const { activeWorkflowId } = useWorkflowRegistry() const { @@ -106,9 +110,9 @@ export const Copilot = forwardRef(({ panelWidth }, ref areChatsFresh, workflowId: copilotWorkflowId, setPlanTodos, + closePlanTodos, clearPlanArtifact, savePlanArtifact, - setSelectedModel, loadAutoAllowedTools, } = useCopilotStore() @@ -292,6 +296,15 @@ export const Copilot = forwardRef(({ panelWidth }, ref } }, [abortMessage, showPlanTodos]) + /** + * Handles closing the plan todos section + * Calls store action and clears the todos + */ + const handleClosePlanTodos = useCallback(() => { + closePlanTodos() + setPlanTodos([]) + }, [closePlanTodos, setPlanTodos]) + /** * Handles message submission to the copilot * @param query - The message text to send @@ -299,13 +312,12 @@ export const Copilot = forwardRef(({ panelWidth }, ref * @param contexts - Optional context references */ const handleSubmit = useCallback( - async (query: string, fileAttachments?: MessageFileAttachment[], contexts?: any[]) => { + async (query: string, fileAttachments?: MessageFileAttachment[], contexts?: ChatContext[]) => { // Allow submission even when isSendingMessage - store will queue the message if (!query || !activeWorkflowId) return if (showPlanTodos) { - const store = useCopilotStore.getState() - store.setPlanTodos([]) + setPlanTodos([]) } try { @@ -319,7 +331,7 @@ export const Copilot = forwardRef(({ panelWidth }, ref logger.error('Failed to send message:', error) } }, - [activeWorkflowId, sendMessage, showPlanTodos] + [activeWorkflowId, sendMessage, showPlanTodos, setPlanTodos] ) /** @@ -330,7 +342,6 @@ export const Copilot = forwardRef(({ panelWidth }, ref const handleEditModeChange = useCallback( (messageId: string, isEditing: boolean, cancelCallback?: () => void) => { setEditingMessageId(isEditing ? messageId : null) - setIsEditingMessage(isEditing) cancelEditCallbackRef.current = isEditing ? cancelCallback || null : null logger.info('Edit mode changed', { messageId, isEditing, willDimMessages: isEditing }) }, @@ -375,24 +386,6 @@ export const Copilot = forwardRef(({ panelWidth }, ref [handleHistoryDropdownOpenHook] ) - /** - * Skeleton loading component for chat history - */ - const ChatHistorySkeleton = () => ( - <> - -
- -
- {[1, 2, 3].map((i) => ( -
-
-
- ))} -
- - ) - return ( <>
(({ panelWidth }, ref { - const store = useCopilotStore.getState() - store.closePlanTodos?.() - useCopilotStore.setState({ planTodos: [] }) - }} + onClose={handleClosePlanTodos} />
)} diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index d9cd0aa3e..3a3e8ed11 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -1736,8 +1736,13 @@ const sseHandlers: Record = { } }, done: (_data, context) => { + logger.info('[SSE] DONE EVENT RECEIVED', { + doneEventCount: context.doneEventCount, + data: _data, + }) context.doneEventCount++ if (context.doneEventCount >= 1) { + logger.info('[SSE] Setting streamComplete = true, stream will terminate') context.streamComplete = true } },