diff --git a/apps/sim/app/(auth)/components/auth-button-classes.ts b/apps/sim/app/(auth)/components/auth-button-classes.ts
new file mode 100644
index 00000000000..02d1d5e47ed
--- /dev/null
+++ b/apps/sim/app/(auth)/components/auth-button-classes.ts
@@ -0,0 +1,3 @@
+/** Shared className for primary auth form submit buttons across all auth pages. */
+export const AUTH_SUBMIT_BTN =
+ 'inline-flex h-[32px] w-full items-center justify-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[var(--border-1)] hover:bg-[var(--border-1)] disabled:cursor-not-allowed disabled:opacity-50' as const
diff --git a/apps/sim/app/(auth)/components/branded-button.tsx b/apps/sim/app/(auth)/components/branded-button.tsx
deleted file mode 100644
index dab6a6ce3a8..00000000000
--- a/apps/sim/app/(auth)/components/branded-button.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-'use client'
-
-import { forwardRef, useState } from 'react'
-import { ArrowRight, ChevronRight, Loader2 } from 'lucide-react'
-import { cn } from '@/lib/core/utils/cn'
-import { useBrandConfig } from '@/ee/whitelabeling'
-
-export interface BrandedButtonProps extends React.ButtonHTMLAttributes {resetStatus.message}
This will allow the application to:
- Build and deploy AI agent workflows -
-- Immediately connect to 100+ models and apps -
- - {/* Sliding tickers */} -- {authorName} - {usageCount.toLocaleString()} copies -
-- {tweet.text} -
-+
{showOtpVerification ? `A verification code has been sent to ${email}` : 'This chat requires email verification'} @@ -240,13 +241,20 @@ export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps) )}
+
Enter the 6-digit code to verify your account. If you don't see it in your inbox, check your spam folder.
@@ -282,22 +290,30 @@ export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps)+
Didn't receive a code?{' '} {countdown > 0 ? ( Resend in{' '} - {countdown}s + + {countdown}s + ) : (
Attach files
-+
{chatConfig?.description || 'Ask me anything.'}
{JSON.stringify(message.content, null, 2)}
) : (
@@ -184,7 +184,7 @@ export const ClientChatMessage = memo(
+{JSON.stringify(cleanTextContent, null, 2)}) : ( @@ -206,7 +206,7 @@ export const ClientChatMessage = memo({ const contentToCopy = typeof cleanTextContent === 'string' diff --git a/apps/sim/app/chat/components/voice-interface/voice-interface.tsx b/apps/sim/app/chat/components/voice-interface/voice-interface.tsx index fa9fd90984b..f056dd46d5c 100644 --- a/apps/sim/app/chat/components/voice-interface/voice-interface.tsx +++ b/apps/sim/app/chat/components/voice-interface/voice-interface.tsx @@ -493,7 +493,12 @@ export function VoiceInterface({ } return ( - +@@ -528,7 +535,7 @@ export function VoiceInterface({ onClick={handleCallEnd} variant='outline' size='icon' - className='h-14 w-14 rounded-full border-gray-300 hover:bg-gray-50' + className='h-14 w-14 rounded-full border-[var(--border-1)] hover:bg-[var(--landing-bg-elevated)]' >-{currentTranscript && ( -)}+
{currentTranscript}
+
{getStatusText()} - {isMuted && (Muted)} + {isMuted && ( + (Muted) + )}
@@ -539,8 +546,8 @@ export function VoiceInterface({ size='icon' disabled={!isInitialized} className={cn( - 'h-14 w-14 rounded-full border-gray-300 bg-transparent hover:bg-gray-50', - isMuted ? 'text-gray-400' : 'text-gray-600' + 'h-14 w-14 rounded-full border-[var(--border-1)] bg-transparent hover:bg-[var(--landing-bg-elevated)]', + isMuted ? 'text-[var(--landing-text-muted)]' : 'text-[var(--landing-text)]' )} > {getButtonContent()} diff --git a/apps/sim/app/form/[identifier]/components/error-state.tsx b/apps/sim/app/form/[identifier]/components/error-state.tsx index e9c27f608bc..15bddc07003 100644 --- a/apps/sim/app/form/[identifier]/components/error-state.tsx +++ b/apps/sim/app/form/[identifier]/components/error-state.tsx @@ -1,7 +1,6 @@ 'use client' import { useRouter } from 'next/navigation' -import { BrandedButton } from '@/app/(auth)/components/branded-button' import { StatusPageLayout } from '@/app/(auth)/components/status-page-layout' interface FormErrorStateProps { @@ -13,7 +12,12 @@ export function FormErrorState({ error }: FormErrorStateProps) { return ( - ) } diff --git a/apps/sim/app/form/[identifier]/components/loading-state.tsx b/apps/sim/app/form/[identifier]/components/loading-state.tsx index e64b77b0f17..83a18fb9672 100644 --- a/apps/sim/app/form/[identifier]/components/loading-state.tsx +++ b/apps/sim/app/form/[identifier]/components/loading-state.tsx @@ -1,10 +1,14 @@ import { Skeleton } from '@/components/emcn' import AuthBackground from '@/app/(auth)/components/auth-background' +import Navbar from '@/app/(home)/components/navbar/navbar' export function FormLoadingState() { return ( -router.push('/workspace')}>Return to Workspace +router.push('/workspace')} + className='inline-flex h-[32px] w-full items-center justify-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[var(--border-1)] hover:bg-[var(--border-1)] disabled:cursor-not-allowed disabled:opacity-50' + > + Return to Workspace + - + + + + + diff --git a/apps/sim/app/form/[identifier]/components/password-auth.tsx b/apps/sim/app/form/[identifier]/components/password-auth.tsx index f18f3b38923..ecd0ec9d9fb 100644 --- a/apps/sim/app/form/[identifier]/components/password-auth.tsx +++ b/apps/sim/app/form/[identifier]/components/password-auth.tsx @@ -1,11 +1,10 @@ 'use client' import { useState } from 'react' -import { Eye, EyeOff } from 'lucide-react' +import { Eye, EyeOff, Loader2 } from 'lucide-react' import { Input, Label } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import AuthBackground from '@/app/(auth)/components/auth-background' -import { BrandedButton } from '@/app/(auth)/components/branded-button' import { SupportFooter } from '@/app/(auth)/components/support-footer' import Navbar from '@/app/(home)/components/navbar/navbar' @@ -33,7 +32,7 @@ export function PasswordAuth({ onSubmit, error }: PasswordAuthProps) { return (diff --git a/apps/sim/app/form/[identifier]/components/powered-by-sim.tsx b/apps/sim/app/form/[identifier]/components/powered-by-sim.tsx index 339c24e4d2d..3ce11b36889 100644 --- a/apps/sim/app/form/[identifier]/components/powered-by-sim.tsx +++ b/apps/sim/app/form/[identifier]/components/powered-by-sim.tsx @@ -9,7 +9,7 @@ export function PoweredBySim() { return ( ) diff --git a/apps/sim/app/form/[identifier]/error.tsx b/apps/sim/app/form/[identifier]/error.tsx index ff7ab7bbc61..3203bfffa38 100644 --- a/apps/sim/app/form/[identifier]/error.tsx +++ b/apps/sim/app/form/[identifier]/error.tsx @@ -2,7 +2,6 @@ import { useEffect } from 'react' import { createLogger } from '@sim/logger' -import { BrandedButton } from '@/app/(auth)/components/branded-button' import { StatusPageLayout } from '@/app/(auth)/components/status-page-layout' const logger = createLogger('FormError') @@ -22,7 +21,12 @@ export default function FormError({ error, reset }: FormErrorProps) { title='Something went wrong' description='We encountered an error loading this form. Please try again.' > -- + @@ -41,10 +40,10 @@ export function PasswordAuth({ onSubmit, error }: PasswordAuthProps) { --@@ -65,7 +64,7 @@ export function PasswordAuth({ onSubmit, error }: PasswordAuthProps) {+
Password Required
-+
Enter the password to access this form.
setShowPassword(!showPassword)} - className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--text-subtle)] hover:text-[var(--landing-text)]' + className='-translate-y-1/2 absolute top-1/2 right-3 text-[var(--landing-text-muted)] hover:text-[var(--landing-text)]' > {showPassword ? @@ -73,14 +72,20 @@ export function PasswordAuth({ onSubmit, error }: PasswordAuthProps) { {error &&: } {error}
}- Continue - + {isSubmitting ? ( + ++ Verifying... + + ) : ( + 'Continue' + )} + Try again ++ Try again + ) } diff --git a/apps/sim/app/form/[identifier]/form.tsx b/apps/sim/app/form/[identifier]/form.tsx index cd5cfe305c3..966471d5af3 100644 --- a/apps/sim/app/form/[identifier]/form.tsx +++ b/apps/sim/app/form/[identifier]/form.tsx @@ -2,10 +2,11 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' +import { Loader2 } from 'lucide-react' import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono' import AuthBackground from '@/app/(auth)/components/auth-background' -import { BrandedButton } from '@/app/(auth)/components/branded-button' import { SupportFooter } from '@/app/(auth)/components/support-footer' +import Navbar from '@/app/(home)/components/navbar/navbar' import { FormErrorState, FormField, @@ -238,7 +239,10 @@ export default function Form({ identifier }: { identifier: string }) { if (isSubmitted && thankYouData) { return (- + + + + - - ++ + ++ {/* Form title */}-+
{formConfig.title}
{formConfig.description && ( -+
{formConfig.description}
)} @@ -287,7 +294,7 @@ export default function Form({ identifier }: { identifier: string }) {-) @@ -140,11 +139,16 @@ function UnsubscribeContent() {+ Invalid Unsubscribe Link
-{error}
+{error}
-) @@ -157,14 +161,19 @@ function UnsubscribeContent() {window.history.back()}>Go Back +window.history.back()} + className='inline-flex h-[32px] w-full items-center justify-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[var(--border-1)] hover:bg-[var(--border-1)] disabled:cursor-not-allowed disabled:opacity-50' + > + Go Back + Important Account Emails
-+
Transactional emails like password resets, account confirmations, and security alerts cannot be unsubscribed from as they contain essential information for your account.
-) @@ -177,14 +186,19 @@ function UnsubscribeContent() {window.close()}>Close +window.close()} + className='inline-flex h-[32px] w-full items-center justify-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[var(--border-1)] hover:bg-[var(--border-1)] disabled:cursor-not-allowed disabled:opacity-50' + > + Close + Successfully Unsubscribed
-+
You have been unsubscribed from our emails. You will stop receiving emails within 48 hours.
-) @@ -198,72 +212,81 @@ function UnsubscribeContent() {window.close()}>Close +window.close()} + className='inline-flex h-[32px] w-full items-center justify-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[var(--border-1)] hover:bg-[var(--border-1)] disabled:cursor-not-allowed disabled:opacity-50' + > + Close + Email Preferences
-+
Choose which emails you'd like to stop receiving.
-{data?.email}
+{data?.email}
-handleUnsubscribe('all')} disabled={processing || isAlreadyUnsubscribedFromAll} - loading={processing} - loadingText='Unsubscribing' + className='inline-flex h-[32px] w-full items-center justify-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[var(--border-1)] hover:bg-[var(--border-1)] disabled:cursor-not-allowed disabled:opacity-50' > - {isAlreadyUnsubscribedFromAll - ? 'Unsubscribed from All Emails' - : 'Unsubscribe from All Marketing Emails'} - + {processing ? ( + ++ Unsubscribing... + + ) : isAlreadyUnsubscribedFromAll ? ( + 'Unsubscribed from All Emails' + ) : ( + 'Unsubscribe from All Marketing Emails' + )} + - + or choose specific types-handleUnsubscribe('marketing')} disabled={ processing || isAlreadyUnsubscribedFromAll || data?.currentPreferences.unsubscribeMarketing } + className='inline-flex h-[32px] w-full items-center justify-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[var(--border-1)] hover:bg-[var(--border-1)] disabled:cursor-not-allowed disabled:opacity-50' > {data?.currentPreferences.unsubscribeMarketing ? 'Unsubscribed from Marketing' : 'Unsubscribe from Marketing Emails'} - + -handleUnsubscribe('updates')} disabled={ processing || isAlreadyUnsubscribedFromAll || data?.currentPreferences.unsubscribeUpdates } + className='inline-flex h-[32px] w-full items-center justify-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[var(--border-1)] hover:bg-[var(--border-1)] disabled:cursor-not-allowed disabled:opacity-50' > {data?.currentPreferences.unsubscribeUpdates ? 'Unsubscribed from Updates' : 'Unsubscribe from Product Updates'} - + -handleUnsubscribe('notifications')} disabled={ processing || isAlreadyUnsubscribedFromAll || data?.currentPreferences.unsubscribeNotifications } + className='inline-flex h-[32px] w-full items-center justify-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-[430] font-season text-black text-sm transition-colors hover:border-[var(--border-1)] hover:bg-[var(--border-1)] disabled:cursor-not-allowed disabled:opacity-50' > {data?.currentPreferences.unsubscribeNotifications ? 'Unsubscribed from Notifications' : 'Unsubscribe from Notifications'} - +-+
You'll continue receiving important account emails like password resets and security alerts.
@@ -281,12 +304,12 @@ export default function Unsubscribe() {Loading
-+
Validating your unsubscribe link...
-} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx index b22267cd7fd..692cb510ac9 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-panel.tsx @@ -6,8 +6,8 @@ import remarkBreaks from 'remark-breaks' import remarkGfm from 'remark-gfm' import { cn } from '@/lib/core/utils/cn' import { getFileExtension } from '@/lib/uploads/utils/file-utils' -import { useAutoScroll } from '@/app/workspace/[workspaceId]/home/hooks/use-auto-scroll' -import { useStreamingReveal } from '@/app/workspace/[workspaceId]/home/hooks/use-streaming-reveal' +import { useAutoScroll } from '@/hooks/use-auto-scroll' +import { useStreamingReveal } from '@/hooks/use-streaming-reveal' type PreviewType = 'markdown' | 'html' | 'csv' | 'svg' | null @@ -63,12 +63,12 @@ const PREVIEW_MARKDOWN_COMPONENTS = { ), h1: ({ children }: any) => ( -+ +
{children}
), h2: ({ children }: any) => ( -+
{children}
), @@ -143,7 +143,7 @@ const PREVIEW_MARKDOWN_COMPONENTS = {), table: ({ children }: any) => ( -
+) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx index a849d0a6ad0..8249422b73b 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx @@ -15,7 +15,7 @@ import { parseSpecialTags, SpecialTags, } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags' -import { useStreamingReveal } from '@/app/workspace/[workspaceId]/home/hooks/use-streaming-reveal' +import { useStreamingReveal } from '@/hooks/use-streaming-reveal' import { useStreamingText } from '@/hooks/use-streaming-text' const REMARK_PLUGINS = [remarkGfm] @@ -49,12 +49,11 @@ const PROSE_CLASSES = cn( 'prose-headings:font-[600] prose-headings:tracking-[0] prose-headings:text-[var(--text-primary)]', 'prose-headings:mb-3 prose-headings:mt-6 first:prose-headings:mt-0', 'prose-p:text-base prose-p:leading-[25px] prose-p:text-[var(--text-primary)]', - 'first:prose-p:mt-0 last:prose-p:mb-0', 'prose-li:text-base prose-li:leading-[25px] prose-li:text-[var(--text-primary)]', 'prose-li:my-1', 'prose-ul:my-4 prose-ol:my-4', 'prose-strong:font-[600] prose-strong:text-[var(--text-primary)]', - 'prose-a:text-[var(--text-primary)] prose-a:underline prose-a:decoration-dashed prose-a:underline-offset-2', + 'prose-a:text-[var(--text-primary)] prose-a:underline prose-a:decoration-dashed prose-a:underline-offset-4', 'prose-code:rounded prose-code:bg-[var(--surface-5)] prose-code:px-1.5 prose-code:py-0.5 prose-code:text-small prose-code:font-mono prose-code:font-[400] prose-code:text-[var(--text-primary)]', 'prose-code:before:content-none prose-code:after:content-none', 'prose-hr:border-[var(--divider)] prose-hr:my-6', @@ -68,7 +67,9 @@ const MARKDOWN_COMPONENTS: React.ComponentProps), diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx index 98161bfd7ef..41a0011a81d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import { ChevronDown, PillsRing } from '@/components/emcn' +import { ChevronDown, Expandable, ExpandableContent, PillsRing } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import type { ToolCallData } from '../../../../types' import { getAgentIcon } from '../../utils' @@ -20,8 +20,6 @@ interface AgentGroupProps { defaultExpanded?: boolean } -const FADE_MS = 300 - function isToolDone(status: ToolCallData['status']): boolean { return status === 'success' || status === 'error' || status === 'cancelled' } @@ -42,14 +40,12 @@ export function AgentGroup({ const allDone = toolItems.length > 0 && toolItems.every((t) => isToolDone(t.data.status)) const [expanded, setExpanded] = useState(defaultExpanded || !allDone) - const [mounted, setMounted] = useState(defaultExpanded || !allDone) const didAutoCollapseRef = useRef(allDone) const wasAutoExpandedRef = useRef(defaultExpanded) useEffect(() => { if (defaultExpanded) { wasAutoExpandedRef.current = true - setMounted(true) setExpanded(true) return } @@ -66,15 +62,6 @@ export function AgentGroup({ setExpanded(false) }, [autoCollapse]) - useEffect(() => { - if (expanded) { - setMounted(true) - return - } - const timer = setTimeout(() => setMounted(false), FADE_MS) - return () => clearTimeout(timer) - }, [expanded]) - return ({children}
{hasItems ? ( @@ -113,31 +100,30 @@ export function AgentGroup({ {agentLabel})} - {hasItems && mounted && ( -- {items.map((item, idx) => - item.type === 'tool' ? ( -+ {hasItems && ( +- ) : ( - - {item.content.trim()} - - ) - )} - + )}+ ++ {items.map((item, idx) => + item.type === 'tool' ? ( +++ ) : ( + + {item.content.trim()} + + ) + )} + ['component table({ children }) { return ( -) }, @@ -142,7 +143,7 @@ const MARKDOWN_COMPONENTS: React.ComponentProps{children}
++ {children} +
['component return ( @@ -162,13 +163,13 @@ const MARKDOWN_COMPONENTS: React.ComponentProps ['component li({ children, className }) { if (className?.includes('task-list-item')) { return ( - + {children} ) } return ( -+ {children} ) @@ -187,6 +188,33 @@ interface ChatContentProps { onOptionSelect?: (id: string) => void } +function MarkdownChunk({ + content, + animate = false, + trimTop = true, + trimBottom = true, +}: { + content: string + animate?: boolean + trimTop?: boolean + trimBottom?: boolean +}) { + return ( +:first-child]:mt-0', + trimBottom && '[&>:last-child]:mb-0', + animate && 'animate-stream-fade-in' + )} + > ++ ) +} + export function ChatContent({ content, isStreaming = false, onOptionSelect }: ChatContentProps) { const rendered = useStreamingText(content, isStreaming) @@ -200,13 +228,8 @@ export function ChatContent({ content, isStreaming = false, onOptionSelect }: Ch ) const committedMarkdown = useMemo( - () => - committed ? ( -+ {content} + +- {committed} - - ) : null, - [committed] + () => (committed ?: null), + [committed, incoming] ) if (hasSpecialContent) { @@ -214,13 +237,7 @@ export function ChatContent({ content, isStreaming = false, onOptionSelect }: Ch {parsed.segments.map((segment, i) => { if (segment.type === 'text' || segment.type === 'thinking') { - return ( --- ) + return- {segment.content} - -} return ( @@ -232,17 +249,16 @@ export function ChatContent({ content, isStreaming = false, onOptionSelect }: Ch } return ( - +{committedMarkdown} {incoming && ( -) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index b9518ba0425..2cc87d84368 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -1,8 +1,8 @@ 'use client' -import { createElement } from 'react' +import { createElement, useState } from 'react' import { useParams } from 'next/navigation' -import { ArrowRight } from '@/components/emcn' +import { ArrowRight, ChevronDown, Expandable, ExpandableContent } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth' @@ -355,39 +355,60 @@ interface OptionsDisplayProps { } function OptionsDisplay({ data, onSelect }: OptionsDisplayProps) { + const disabled = !onSelect + const [expanded, setExpanded] = useState(!disabled) const entries = Object.entries(data) if (entries.length === 0) return null - const disabled = !onSelect - return (:first-child]:mt-0')} - > -+ content={incoming} + trimTop + trimBottom + animate={isStreaming} + /> )}- {incoming} - -- Suggested follow-ups -) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index 237d08d9ba5..e6c03863f0b 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -12,12 +12,12 @@ import { PendingTagIndicator } from '@/app/workspace/[workspaceId]/home/componen import { QueuedMessages } from '@/app/workspace/[workspaceId]/home/components/queued-messages' import { UserInput } from '@/app/workspace/[workspaceId]/home/components/user-input' import { UserMessageContent } from '@/app/workspace/[workspaceId]/home/components/user-message-content' -import { useAutoScroll } from '@/app/workspace/[workspaceId]/home/hooks' import type { ChatMessage, FileAttachmentForApi, QueuedMessage, } from '@/app/workspace/[workspaceId]/home/types' +import { useAutoScroll } from '@/hooks/use-auto-scroll' import type { ChatContext } from '@/stores/panel' interface MothershipChatProps { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx index e2bcccb2dde..bd0cf8cc792 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx @@ -60,7 +60,6 @@ export function useAvailableResources( id: w.id, name: w.name, color: w.color, - folderId: w.folderId, isOpen: existingKeys.has(`workflow:${w.id}`), })), }, @@ -176,6 +175,7 @@ export function AddResourceDropdown({ align='start' sideOffset={8} className='flex w-[240px] flex-col overflow-hidden' + onCloseAutoFocus={(e) => e.preventDefault()} >- {entries.map(([key, value], i) => { - const title = value.title - - return ( -+ {disabled ? ( +onSelect?.(title)} - className={cn( - 'flex items-center gap-2 border-[var(--divider)] px-2 py-2 text-left transition-colors', - disabled ? 'cursor-not-allowed' : 'hover-hover:bg-[var(--surface-5)]', - i > 0 && 'border-t' - )} - > - - ) - })} -- {i + 1} -- {title} -- setExpanded((prev) => !prev)} + aria-expanded={expanded} + className='flex items-center gap-2' + > + Suggested follow-ups + + ) : ( + Suggested follow-ups + )} ++ + + ++ {entries.map(([key, value], i) => { + const title = value.title + + return ( ++onSelect?.(title)} + className={cn( + 'flex items-center gap-2 border-[var(--divider)] px-2 py-2 text-left transition-colors', + disabled ? 'cursor-not-allowed' : 'hover-hover:bg-[var(--surface-5)]', + i > 0 && 'border-t' + )} + > + + ) + })} ++ {i + 1} ++ {title} ++ (null) + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [data.entries.length]) + + if (data.entries.length === 0) { + return ( + ++ ) + } + + return ( +No results yet
++ {data.entries.map((entry) => ( ++ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index 5d336a3df29..6e5913caa1e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -20,11 +20,15 @@ import { FileViewer, type PreviewMode, } from '@/app/workspace/[workspaceId]/files/components/file-viewer' +import { GenericResourceContent } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/generic-resource-content' import { RESOURCE_TAB_ICON_BUTTON_CLASS, RESOURCE_TAB_ICON_CLASS, } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls' -import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types' +import type { + GenericResourceData, + MothershipResource, +} from '@/app/workspace/[workspaceId]/home/types' import { KnowledgeBase } from '@/app/workspace/[workspaceId]/knowledge/[id]/base' import { useUserPermissionsContext, @@ -53,6 +57,7 @@ interface ResourceContentProps { resource: MothershipResource previewMode?: PreviewMode streamingFile?: { fileName: string; content: string } | null + genericResourceData?: GenericResourceData } /** @@ -67,6 +72,7 @@ export const ResourceContent = memo(function ResourceContent({ resource, previewMode, streamingFile, + genericResourceData, }: ResourceContentProps) { const streamFileName = streamingFile?.fileName || 'file.md' const streamingExtractedContent = useMemo(() => { @@ -140,6 +146,11 @@ export const ResourceContent = memo(function ResourceContent({ /> ) + case 'generic': + return ( +++ ))} + ++ {entry.status === 'executing' && ( ++ {entry.streamingArgs && ( ++ )} + + {entry.displayTitle} + + {entry.status === 'error' && ( + Error + )} + + {entry.streamingArgs} ++ )} + {!entry.streamingArgs && entry.result?.output != null && ( ++ {typeof entry.result.output === 'string' + ? entry.result.output + : JSON.stringify(entry.result.output, null, 2)} ++ )} + {entry.result?.error && ( +{entry.result.error}
+ )} ++ ) + default: return null } @@ -160,6 +171,8 @@ export function ResourceActions({ workspaceId, resource }: ResourceActionsProps) return ( ) + case 'generic': + return null default: return null } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx index acc2a63c2c1..bf5c3029d3d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx @@ -2,7 +2,12 @@ import type { ElementType, ReactNode } from 'react' import type { QueryClient } from '@tanstack/react-query' -import { Database, File as FileIcon, Table as TableIcon } from '@/components/emcn/icons' +import { + Database, + File as FileIcon, + Table as TableIcon, + TerminalWindow, +} from '@/components/emcn/icons' import { WorkflowIcon } from '@/components/icons' import { getDocumentIcon } from '@/components/icons/document-icons' import { cn } from '@/lib/core/utils/cn' @@ -74,6 +79,15 @@ function FileDropdownItem({ item }: DropdownItemRenderProps) { } export const RESOURCE_REGISTRY: Record = { + generic: { + type: 'generic', + label: 'Results', + icon: TerminalWindow, + renderTabIcon: (_resource, className) => ( + + ), + renderDropdownItem: (props) => , + }, workflow: { type: 'workflow', label: 'Workflows', @@ -119,8 +133,10 @@ export function getResourceConfig(type: MothershipResourceType): ResourceTypeCon return RESOURCE_REGISTRY[type] } +type CacheableResourceType = Exclude + const RESOURCE_INVALIDATORS: Record< - MothershipResourceType, + CacheableResourceType, (qc: QueryClient, workspaceId: string, resourceId: string) => void > = { table: (qc, _wId, id) => { @@ -153,5 +169,6 @@ export function invalidateResourceQueries( resourceType: MothershipResourceType, resourceId: string ): void { + if (resourceType === 'generic') return RESOURCE_INVALIDATORS[resourceType](queryClient, workspaceId, resourceId) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx index 985788e7cdf..20e132089c9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx @@ -9,6 +9,7 @@ import { } from 'react' import { Button, Tooltip } from '@/components/emcn' import { Columns3, Eye, PanelLeft, Pencil } from '@/components/emcn/icons' +import { isEphemeralResource } from '@/lib/copilot/resource-extraction' import { cn } from '@/lib/core/utils/cn' import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer' import { AddResourceDropdown } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown' @@ -142,7 +143,9 @@ export function ResourceTabs({ (e: React.MouseEvent, resource: MothershipResource) => { e.stopPropagation() if (!chatId) return - removeResource.mutate({ chatId, resourceType: resource.type, resourceId: resource.id }) + if (!isEphemeralResource(resource)) { + removeResource.mutate({ chatId, resourceType: resource.type, resourceId: resource.id }) + } onRemoveResource(resource.type, resource.id) }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -240,7 +243,10 @@ export function ResourceTabs({ reordered.splice(insertAt, 0, moved) onReorderResources(reordered) if (chatId) { - reorderResources.mutate({ chatId, resources: reordered }) + const persistable = reordered.filter((r) => !isEphemeralResource(r)) + if (persistable.length > 0) { + reorderResources.mutate({ chatId, resources: persistable }) + } } setDraggedIdx(null) setDropGapIdx(null) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx index 0bf54b0e638..6338c65cfa9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx @@ -6,6 +6,7 @@ import { getFileExtension } from '@/lib/uploads/utils/file-utils' import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer' import { RICH_PREVIEWABLE_EXTENSIONS } from '@/app/workspace/[workspaceId]/files/components/file-viewer' import type { + GenericResourceData, MothershipResource, MothershipResourceType, } from '@/app/workspace/[workspaceId]/home/types' @@ -65,6 +66,7 @@ interface MothershipViewProps { isCollapsed: boolean className?: string streamingFile?: { fileName: string; content: string } | null + genericResourceData?: GenericResourceData } export const MothershipView = memo( @@ -82,6 +84,7 @@ export const MothershipView = memo( isCollapsed, className, streamingFile, + genericResourceData, }: MothershipViewProps, ref ) { @@ -112,8 +115,8 @@ export const MothershipView = memo( @@ -141,6 +144,7 @@ export const MothershipView = memo( resource={active} previewMode={isActivePreviewable ? previewMode : undefined} streamingFile={streamingForActive} + genericResourceData={active.type === 'generic' ? genericResourceData : undefined} /> ) : ()} -diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/animated-placeholder-effect.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/animated-placeholder-effect.tsx similarity index 88% rename from apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/animated-placeholder-effect.tsx rename to apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/animated-placeholder-effect.tsx index 857ed1d4e0d..c4818729da2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/animated-placeholder-effect.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/animated-placeholder-effect.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect } from 'react' -import { useAnimatedPlaceholder } from '@/app/workspace/[workspaceId]/home/hooks' +import { useAnimatedPlaceholder } from '@/hooks/use-animated-placeholder' interface AnimatedPlaceholderEffectProps { textareaRef: React.RefObject-diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/attached-files-list.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list.tsx similarity index 100% rename from apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/attached-files-list.tsx rename to apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/constants.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts similarity index 93% rename from apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/constants.ts rename to apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts index 678aa9ba87a..dc6fa0aab58 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/constants.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts @@ -39,7 +39,7 @@ export interface PlusMenuHandle { export const TEXTAREA_BASE_CLASSES = cn( 'm-0 box-border h-auto min-h-[24px] w-full resize-none', - 'overflow-y-auto overflow-x-hidden break-all border-0 bg-transparent', + 'overflow-y-auto overflow-x-hidden break-words [overflow-wrap:anywhere] border-0 bg-transparent', 'px-1 py-1 font-body text-[15px] leading-[24px] tracking-[-0.015em]', 'text-transparent caret-[var(--text-primary)] outline-none', 'placeholder:font-[380] placeholder:text-[var(--text-subtle)]', @@ -49,7 +49,7 @@ export const TEXTAREA_BASE_CLASSES = cn( export const OVERLAY_CLASSES = cn( 'pointer-events-none absolute top-0 left-0 m-0 box-border h-auto w-full resize-none', - 'overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-all border-0 bg-transparent', + 'overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-words [overflow-wrap:anywhere] border-0 bg-transparent', 'px-1 py-1 font-body text-[15px] leading-[24px] tracking-[-0.015em]', 'text-[var(--text-primary)] outline-none', '[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/drop-overlay.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/drop-overlay.tsx similarity index 100% rename from apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/drop-overlay.tsx rename to apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/drop-overlay.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/index.ts similarity index 100% rename from apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/index.ts rename to apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/index.ts diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/mic-button.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mic-button.tsx similarity index 100% rename from apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/mic-button.tsx rename to apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mic-button.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/plus-menu-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown.tsx similarity index 99% rename from apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/plus-menu-dropdown.tsx rename to apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown.tsx index e7ff53e4937..1b7606b7822 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/plus-menu-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown.tsx @@ -16,7 +16,7 @@ import { Plus, Sim } from '@/components/emcn/icons' import { cn } from '@/lib/core/utils/cn' import type { useAvailableResources } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown' import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' -import type { PlusMenuHandle } from '@/app/workspace/[workspaceId]/home/components/user-input/_components/constants' +import type { PlusMenuHandle } from '@/app/workspace/[workspaceId]/home/components/user-input/components/constants' import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types' export type AvailableResourceGroup = ReturnType [number] diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/send-button.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/send-button.tsx similarity index 95% rename from apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/send-button.tsx rename to apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/send-button.tsx index 74edc7d60d3..b795d6969d8 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/_components/send-button.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/send-button.tsx @@ -8,7 +8,7 @@ import { SEND_BUTTON_ACTIVE, SEND_BUTTON_BASE, SEND_BUTTON_DISABLED, -} from '@/app/workspace/[workspaceId]/home/components/user-input/_components/constants' +} from '@/app/workspace/[workspaceId]/home/components/user-input/components/constants' interface SendButtonProps { isSending: boolean @@ -27,6 +27,7 @@ export const SendButton = React.memo(function SendButton({ return ( @@ -43,6 +44,7 @@ export const SendButton = React.memo(function SendButton({ return ( diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx index 0a4b06e123a..809c157945b 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx @@ -15,7 +15,7 @@ import type { SpeechRecognitionEvent, SpeechRecognitionInstance, WindowWithSpeech, -} from '@/app/workspace/[workspaceId]/home/components/user-input/_components' +} from '@/app/workspace/[workspaceId]/home/components/user-input/components' import { AnimatedPlaceholderEffect, AttachedFilesList, @@ -29,7 +29,7 @@ import { SendButton, SPEECH_RECOGNITION_LANG, TEXTAREA_BASE_CLASSES, -} from '@/app/workspace/[workspaceId]/home/components/user-input/_components' +} from '@/app/workspace/[workspaceId]/home/components/user-input/components' import type { FileAttachmentForApi, MothershipResource, diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx index 9bb8ebcde36..fd1f42d5585 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx @@ -6,7 +6,7 @@ import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/type import { useWorkflowRegistry } from '@/stores/workflows/registry/store' const USER_MESSAGE_CLASSES = - 'whitespace-pre-wrap break-all font-[430] font-[family-name:var(--font-inter)] text-base text-[var(--text-primary)] leading-[23px] tracking-[0] antialiased' + 'whitespace-pre-wrap break-words [overflow-wrap:anywhere] font-[430] font-[family-name:var(--font-inter)] text-base text-[var(--text-primary)] leading-[23px] tracking-[0] antialiased' interface UserMessageContentProps { content: string @@ -20,7 +20,6 @@ function escapeRegex(str: string): string { interface MentionRange { start: number end: number - token: string context: ChatMessageContext } @@ -36,7 +35,7 @@ function computeMentionRanges(text: string, contexts: ChatMessageContext[]): Men const leadingSpace = match[1] const tokenStart = match.index + leadingSpace.length const tokenEnd = tokenStart + token.length - ranges.push({ start: tokenStart, end: tokenEnd, token, context: ctx }) + ranges.push({ start: tokenStart, end: tokenEnd, context: ctx }) } } diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 0f71c62221b..001489783ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -114,7 +114,6 @@ export function Home({ chatId }: HomeProps = {}) { const { mothershipRef, handleResizePointerDown, clearWidth } = useMothershipResize() const [isResourceCollapsed, setIsResourceCollapsed] = useState(true) - const [isResourceAnimatingIn, setIsResourceAnimatingIn] = useState(false) const [skipResourceTransition, setSkipResourceTransition] = useState(false) const isResourceCollapsedRef = useRef(isResourceCollapsed) isResourceCollapsedRef.current = isResourceCollapsed @@ -123,27 +122,16 @@ export function Home({ chatId }: HomeProps = {}) { clearWidth() setIsResourceCollapsed(true) }, [clearWidth]) - const animatingInTimerRef = useRef | null>(null) - const startAnimatingIn = useCallback(() => { - if (animatingInTimerRef.current) clearTimeout(animatingInTimerRef.current) - setIsResourceAnimatingIn(true) - animatingInTimerRef.current = setTimeout(() => { - setIsResourceAnimatingIn(false) - animatingInTimerRef.current = null - }, 400) - }, []) const expandResource = useCallback(() => { setIsResourceCollapsed(false) - startAnimatingIn() - }, [startAnimatingIn]) + }, []) const handleResourceEvent = useCallback(() => { if (isResourceCollapsedRef.current) { setIsResourceCollapsed(false) - startAnimatingIn() } - }, [startAnimatingIn]) + }, []) const { messages, @@ -163,6 +151,7 @@ export function Home({ chatId }: HomeProps = {}) { sendNow, editQueuedMessage, streamingFile, + genericResourceData, } = useChat( workspaceId, chatId, @@ -379,13 +368,8 @@ export function Home({ chatId }: HomeProps = {}) { onCollapse={collapseResource} isCollapsed={isResourceCollapsed} streamingFile={streamingFile} - className={ - isResourceAnimatingIn - ? 'animate-slide-in-right' - : skipResourceTransition - ? '!transition-none' - : undefined - } + genericResourceData={genericResourceData} + className={skipResourceTransition ? '!transition-none' : undefined} /> {isResourceCollapsed && ( diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/index.ts index 9cce1f6eec1..611521fbcc1 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/index.ts @@ -1,5 +1,3 @@ -export { useAnimatedPlaceholder } from './use-animated-placeholder' -export { useAutoScroll } from './use-auto-scroll' export type { UseChatReturn } from './use-chat' export { getMothershipUseChatOptions, @@ -7,4 +5,3 @@ export { useChat, } from './use-chat' export { useMothershipResize } from './use-mothership-resize' -export { useStreamingReveal } from './use-streaming-reveal' diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 1f4e4b2a08b..291060c9129 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -15,6 +15,7 @@ import { } from '@/lib/copilot/constants' import { extractResourcesFromToolResult, + isEphemeralResource, isResourceToolName, } from '@/lib/copilot/resource-extraction' import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resource-types' @@ -47,6 +48,8 @@ import type { ContentBlock, ContentBlockType, FileAttachmentForApi, + GenericResourceData, + GenericResourceEntry, MothershipResource, MothershipResourceType, QueuedMessage, @@ -78,6 +81,7 @@ export interface UseChatReturn { sendNow: (id: string) => Promise editQueuedMessage: (id: string) => QueuedMessage | undefined streamingFile: { fileName: string; content: string } | null + genericResourceData: GenericResourceData } const STATE_TO_STATUS: Record = { @@ -422,6 +426,11 @@ export function useChat( const streamingFileRef = useRef(streamingFile) streamingFileRef.current = streamingFile + const [genericResourceData, setGenericResourceData] = useState ({ + entries: [], + }) + const genericResourceDataRef = useRef ({ entries: [] }) + const [messageQueue, setMessageQueue] = useState ([]) const messageQueueRef = useRef ([]) messageQueueRef.current = messageQueue @@ -483,7 +492,7 @@ export function useChat( }) setActiveResourceId(resource.id) - if (resource.id === 'streaming-file') { + if (isEphemeralResource(resource)) { return true } @@ -547,6 +556,8 @@ export function useChat( setActiveResourceId(null) setStreamingFile(null) streamingFileRef.current = null + genericResourceDataRef.current = { entries: [] } + setGenericResourceData({ entries: [] }) setMessageQueue([]) lastEventIdRef.current = 0 clientExecutionStartedRef.current.clear() @@ -571,6 +582,8 @@ export function useChat( setActiveResourceId(null) setStreamingFile(null) streamingFileRef.current = null + genericResourceDataRef.current = { entries: [] } + setGenericResourceData({ entries: [] }) setMessageQueue([]) lastEventIdRef.current = 0 clientExecutionStartedRef.current.clear() @@ -900,6 +913,13 @@ export function useChat( const blocks: ContentBlock[] = preserveExistingState ? [...streamingBlocksRef.current] : [] const toolMap = new Map () const toolArgsMap = new Map >() + // Maps toolCallId → index in genericResourceDataRef.current.entries for fast lookup + const genericEntryMap = new Map () + if (preserveExistingState) { + for (const [idx, entry] of genericResourceDataRef.current.entries.entries()) { + genericEntryMap.set(entry.toolCallId, idx) + } + } const clientExecutionStarted = clientExecutionStartedRef.current let activeSubagent: string | undefined let activeCompactionId: string | undefined @@ -981,6 +1001,23 @@ export function useChat( }) } + const appendGenericEntry = (entry: GenericResourceEntry): number => { + const entries = [...genericResourceDataRef.current.entries, entry] + genericResourceDataRef.current.entries = entries + setGenericResourceData({ entries }) + return entries.length - 1 + } + + const updateGenericEntry = ( + entryIdx: number, + changes: Partial + ): void => { + const entries = genericResourceDataRef.current.entries.slice() + entries[entryIdx] = { ...entries[entryIdx], ...changes } + genericResourceDataRef.current.entries = entries + setGenericResourceData({ entries }) + } + try { while (true) { const { done, value } = await reader.read() @@ -1160,6 +1197,32 @@ export function useChat( } flush() + // TODO: Uncomment when rich UI for Results tab is ready + // if (shouldOpenGenericResource(name)) { + // if (!genericEntryMap.has(id)) { + // const entryIdx = appendGenericEntry({ + // toolCallId: id, + // toolName: name, + // displayTitle: displayTitle ?? name, + // status: 'executing', + // params: args, + // }) + // genericEntryMap.set(id, entryIdx) + // const opened = addResource({ type: 'generic', id: 'results', title: 'Results' }) + // if (opened) onResourceEventRef.current?.() + // else setActiveResourceId('results') + // } else { + // const entryIdx = genericEntryMap.get(id) + // if (entryIdx !== undefined) { + // updateGenericEntry(entryIdx, { + // toolName: name, + // ...(displayTitle && { displayTitle }), + // ...(args && { params: args }), + // }) + // } + // } + // } + if ( parsed.type === 'tool_call' && ui?.clientExecutable && @@ -1254,6 +1317,20 @@ export function useChat( tc.streamingArgs = (tc.streamingArgs ?? '') + delta flush() } + + // TODO: Uncomment when rich UI for Results tab is ready + // if (toolName && shouldOpenGenericResource(toolName)) { + // const entryIdx = genericEntryMap.get(id) + // if (entryIdx !== undefined) { + // const entry = genericResourceDataRef.current.entries[entryIdx] + // if (entry) { + // updateGenericEntry(entryIdx, { + // streamingArgs: (entry.streamingArgs ?? '') + delta, + // }) + // } + // } + // } + break } case 'tool_result': { @@ -1360,6 +1437,34 @@ export function useChat( setResources((rs) => rs.filter((r) => r.id !== 'streaming-file')) } } + + // TODO: Uncomment when rich UI for Results tab is ready + // if ( + // shouldOpenGenericResource(tc.name) || + // (isDeferredResourceTool(tc.name) && extractedResources.length === 0) + // ) { + // const entryIdx = genericEntryMap.get(id) + // if (entryIdx !== undefined) { + // updateGenericEntry(entryIdx, { + // status: tc.status, + // result: tc.result ?? undefined, + // streamingArgs: undefined, + // }) + // } else { + // const newIdx = appendGenericEntry({ + // toolCallId: id, + // toolName: tc.name, + // displayTitle: tc.displayTitle ?? tc.name, + // status: tc.status, + // params: toolArgsMap.get(id) as Record | undefined, + // result: tc.result ?? undefined, + // }) + // genericEntryMap.set(id, newIdx) + // if (addResource({ type: 'generic', id: 'results', title: 'Results' })) { + // onResourceEventRef.current?.() + // } + // } + // } } break @@ -1447,13 +1552,22 @@ export function useChat( if (!id) break const idx = toolMap.get(id) if (idx !== undefined && blocks[idx].toolCall) { + const toolCallName = blocks[idx].toolCall!.name blocks[idx].toolCall!.status = 'error' - if (blocks[idx].toolCall?.name === 'workspace_file') { + if (toolCallName === 'workspace_file') { setStreamingFile(null) streamingFileRef.current = null setResources((rs) => rs.filter((resource) => resource.id !== 'streaming-file')) } flush() + + // TODO: Uncomment when rich UI for Results tab is ready + // if (toolCallName && shouldOpenGenericResource(toolCallName)) { + // const entryIdx = genericEntryMap.get(id) + // if (entryIdx !== undefined) { + // updateGenericEntry(entryIdx, { status: 'error', streamingArgs: undefined }) + // } + // } } break } @@ -2159,5 +2273,6 @@ export function useChat( sendNow, editQueuedMessage, streamingFile, + genericResourceData, } } diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index c7593eacdd7..9cfd6b99850 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -190,6 +190,22 @@ export interface ToolCallResult { error?: string } +/** A single tool call result entry in the generic Results resource tab. */ +export interface GenericResourceEntry { + toolCallId: string + toolName: string + displayTitle: string + status: ToolCallStatus + params?: Record + streamingArgs?: string + result?: ToolCallResult +} + +/** Accumulated feed of tool call results shown in the generic Results tab. */ +export interface GenericResourceData { + entries: GenericResourceEntry[] +} + export interface ToolCallData { id: string toolName: string diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx index ed05ace47c9..f2b7e4cabe0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx @@ -34,6 +34,12 @@ import { useUpdateChat, } from '@/hooks/queries/chats' import { useIdentifierValidation } from './hooks' +import { + getPasswordHelperText, + getPasswordPlaceholder, + hasExistingPassword, + isPasswordRequired, +} from './utils' const logger = createLogger('ChatDeploy') @@ -69,6 +75,7 @@ export interface ExistingChat { welcomeMessage?: string imageUrl?: string } + hasPassword: boolean isActive: boolean } @@ -128,6 +135,7 @@ export function ChatDeploy({ const deleteChatMutation = useDeleteChat() const [isIdentifierValid, setIsIdentifierValid] = useState(false) const [hasInitializedForm, setHasInitializedForm] = useState(false) + const existingPassword = hasExistingPassword(existingChat) const updateField = (field: K, value: ChatFormData[K]) => { setFormData((prev) => ({ ...prev, [field]: value })) @@ -140,7 +148,7 @@ export function ChatDeploy({ setErrors((prev) => ({ ...prev, [field]: message })) } - const validateForm = (isExistingChat: boolean): boolean => { + const validateForm = (): boolean => { const newErrors: FormErrors = {} if (!formData.identifier.trim()) { @@ -153,7 +161,7 @@ export function ChatDeploy({ newErrors.title = 'Title is required' } - if (formData.authType === 'password' && !isExistingChat && !formData.password.trim()) { + if (isPasswordRequired(formData.authType, formData.password, existingPassword)) { newErrors.password = 'Password is required when using password protection' } @@ -176,9 +184,7 @@ export function ChatDeploy({ isIdentifierValid && Boolean(formData.title.trim()) && formData.selectedOutputBlocks.length > 0 && - (formData.authType !== 'password' || - Boolean(formData.password.trim()) || - Boolean(existingChat)) && + !isPasswordRequired(formData.authType, formData.password, existingPassword) && ((formData.authType !== 'email' && formData.authType !== 'sso') || formData.emails.length > 0) useEffect(() => { @@ -228,7 +234,7 @@ export function ChatDeploy({ const newTab = isNewChat ? window.open('', '_blank') : null try { - if (!validateForm(!!existingChat)) { + if (!validateForm()) { newTab?.close() setChatSubmitting(false) return @@ -381,7 +387,7 @@ export function ChatDeploy({ onPasswordChange={(password) => updateField('password', password)} onEmailsChange={(emails) => updateField('emails', emails)} disabled={chatSubmitting} - isExistingChat={!!existingChat} + hasExistingPassword={existingPassword} error={errors.password || errors.emails} /> @@ -605,7 +611,7 @@ interface AuthSelectorProps { onPasswordChange: (password: string) => void onEmailsChange: (emails: string[]) => void disabled?: boolean - isExistingChat?: boolean + hasExistingPassword?: boolean error?: string } @@ -624,7 +630,7 @@ function AuthSelector({ onPasswordChange, onEmailsChange, disabled = false, - isExistingChat = false, + hasExistingPassword = false, error, }: AuthSelectorProps) { const [showPassword, setShowPassword] = useState(false) @@ -712,12 +718,12 @@ function AuthSelector({)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/utils.test.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/utils.test.ts new file mode 100644 index 00000000000..ce35a849135 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/utils.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest' +import { + getPasswordHelperText, + getPasswordPlaceholder, + hasExistingPassword, + isPasswordRequired, +} from './utils' + +describe.concurrent('chat password state', () => { + it('treats an existing password-protected chat as having a stored password', () => { + expect(hasExistingPassword({ authType: 'password', hasPassword: true })).toBe(true) + }) + + it('does not treat a non-password chat as having a stored password', () => { + expect(hasExistingPassword({ authType: 'public', hasPassword: true })).toBe(false) + }) + + it('requires a password when switching an existing public chat to password auth', () => { + expect(isPasswordRequired('password', '', false)).toBe(true) + }) + + it('allows an empty password only when one is already stored', () => { + expect(isPasswordRequired('password', '', true)).toBe(false) + }) + + it('returns copy that matches the stored-password state', () => { + expect(getPasswordPlaceholder(true)).toBe('Enter new password to change') + expect(getPasswordHelperText(true)).toBe('Leave empty to keep the current password') + expect(getPasswordPlaceholder(false)).toBe('Enter password') + expect(getPasswordHelperText(false)).toBe('This password will be required to access your chat') + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/utils.ts new file mode 100644 index 00000000000..bb1967aba9c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/utils.ts @@ -0,0 +1,30 @@ +import type { AuthType } from '@/hooks/queries/chats' + +type ExistingPasswordState = { + authType: AuthType + hasPassword: boolean +} + +export function hasExistingPassword( + existingChat: ExistingPasswordState | null | undefined +): boolean { + return existingChat?.authType === 'password' && existingChat.hasPassword +} + +export function isPasswordRequired( + authType: AuthType, + password: string, + existingPassword: boolean +): boolean { + return authType === 'password' && !existingPassword && !password.trim() +} + +export function getPasswordPlaceholder(existingPassword: boolean): string { + return existingPassword ? 'Enter new password to change' : 'Enter password' +} + +export function getPasswordHelperText(existingPassword: boolean): string { + return existingPassword + ? 'Leave empty to keep the current password' + : 'This password will be required to access your chat' +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/_components/command-items.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items.tsx similarity index 100% rename from apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/_components/command-items.tsx rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/_components/search-groups.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups.tsx similarity index 100% rename from apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/_components/search-groups.tsx rename to apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx index 838083bec01..592d3562d6a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx @@ -31,7 +31,7 @@ import { TriggersGroup, WorkflowsGroup, WorkspacesGroup, -} from './_components/search-groups' +} from './components/search-groups' import type { PageItem, SearchModalProps, TaskItem, WorkflowItem, WorkspaceItem } from './utils' import { filterAndSort } from './utils' diff --git a/apps/sim/components/emcn/components/button-group/button-group.tsx b/apps/sim/components/emcn/components/button-group/button-group.tsx index 9be9c842bc6..3a719472e79 100644 --- a/apps/sim/components/emcn/components/button-group/button-group.tsx +++ b/apps/sim/components/emcn/components/button-group/button-group.tsx @@ -79,7 +79,7 @@ function ButtonGroup({ return (onPasswordChange(e.target.value)} disabled={disabled} className='pr-[88px]' - required={!isExistingChat} + required={!hasExistingPassword} autoComplete='new-password' />@@ -779,9 +785,7 @@ function AuthSelector({- {isExistingChat - ? 'Leave empty to keep the current password' - : 'This password will be required to access your chat'} + {getPasswordHelperText(hasExistingPassword)}
- +{validChildren.map((child, index) => { const position: 'first' | 'middle' | 'last' | 'only' = childCount === 1 @@ -104,7 +104,7 @@ const buttonGroupItemVariants = cva( { variants: { active: { - true: 'bg-[var(--text-primary)] text-[var(--text-inverse)] border-[var(--text-primary)] hover-hover:bg-[var(--surface-4)] hover-hover:border-[var(--border)] dark:bg-white dark:text-[var(--bg)] dark:border-white dark:hover-hover:bg-[var(--border-1)] dark:hover-hover:text-[var(--text-inverse)] dark:hover-hover:border-[var(--border-1)]', + true: 'bg-[var(--text-primary)] text-[var(--text-inverse)] border-[var(--text-primary)] hover-hover:bg-[var(--text-primary)] hover-hover:text-[var(--text-inverse)] hover-hover:border-[var(--text-primary)] dark:bg-white dark:text-[var(--bg)] dark:border-white dark:hover-hover:bg-white dark:hover-hover:text-[var(--bg)] dark:hover-hover:border-white', false: 'bg-[var(--surface-4)] text-[var(--text-secondary)] border-[var(--border)] hover-hover:text-[var(--text-primary)] hover-hover:bg-[var(--surface-6)] hover-hover:border-[var(--border-1)]', }, diff --git a/apps/sim/components/emcn/components/button/button.tsx b/apps/sim/components/emcn/components/button/button.tsx index 26570f2854c..d04dfcd47a4 100644 --- a/apps/sim/components/emcn/components/button/button.tsx +++ b/apps/sim/components/emcn/components/button/button.tsx @@ -3,14 +3,14 @@ import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/core/utils/cn' const buttonVariants = cva( - 'inline-flex items-center justify-center font-medium transition-colors active:scale-[0.98] disabled:pointer-events-none disabled:opacity-70 outline-none focus:outline-none focus-visible:outline-none rounded-[5px]', + 'inline-flex items-center justify-center font-medium transition-colors disabled:pointer-events-none disabled:opacity-70 outline-none focus:outline-none focus-visible:outline-none rounded-[5px]', { variants: { variant: { default: 'text-[var(--text-secondary)] hover-hover:text-[var(--text-primary)] bg-[var(--surface-4)] hover-hover:bg-[var(--surface-6)] border border-[var(--border)] hover-hover:border-[var(--border-1)] dark:hover-hover:bg-[var(--surface-5)]', active: - 'bg-[var(--surface-5)] hover-hover:bg-[var(--surface-7)] text-[var(--text-primary)] border border-[var(--border-1)] dark:hover-hover:bg-[var(--border-1)]', + 'bg-[var(--surface-5)] hover-hover:bg-[var(--surface-6)] text-[var(--text-primary)] hover-hover:text-[var(--text-primary)] border border-[var(--border-1)] hover-hover:border-[var(--border-1)] dark:hover-hover:bg-[var(--border-1)]', '3d': 'text-[var(--text-tertiary)] border-t border-l border-r border-[var(--border-1)] shadow-[0_2px_0_0_var(--border-1)] hover-hover:shadow-[0_4px_0_0_var(--border-1)] transition-[transform,box-shadow,color] hover-hover:-translate-y-0.5 hover-hover:text-[var(--text-primary)]', outline: 'text-[var(--text-secondary)] hover-hover:text-[var(--text-primary)] border border-[var(--text-muted)] bg-transparent hover-hover:border-[var(--text-secondary)]', @@ -25,15 +25,10 @@ const buttonVariants = cva( subtle: 'text-[var(--text-body)] hover-hover:text-[var(--text-body)] hover-hover:bg-[var(--surface-4)]', 'ghost-secondary': 'text-[var(--text-muted)] hover-hover:text-[var(--text-primary)]', - /** Branded button - requires branded-button-gradient or branded-button-custom class for colors */ - branded: - 'rounded-[10px] border text-white hover-hover:text-white text-base transition-[transform,background-color,color,border-color] duration-200', }, size: { sm: 'px-1.5 py-1 text-[length:11px]', md: 'px-2 py-1.5 text-[length:12px]', - /** Branded size - matches login form button padding */ - branded: 'py-1.5 pr-2.5 pl-3', }, }, defaultVariants: { diff --git a/apps/sim/components/emcn/components/expandable/expandable.tsx b/apps/sim/components/emcn/components/expandable/expandable.tsx index 28d70b5dfc5..2c04cb56532 100644 --- a/apps/sim/components/emcn/components/expandable/expandable.tsx +++ b/apps/sim/components/emcn/components/expandable/expandable.tsx @@ -50,7 +50,7 @@ const ExpandableContent = React.forwardRef<diff --git a/apps/sim/ee/sso/components/sso-form.tsx b/apps/sim/ee/sso/components/sso-form.tsx index 9e4aeb0341c..3352d16e3e5 100644 --- a/apps/sim/ee/sso/components/sso-form.tsx +++ b/apps/sim/ee/sso/components/sso-form.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react' import { createLogger } from '@sim/logger' +import { Loader2 } from 'lucide-react' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { Button, Input, Label } from '@/components/emcn' @@ -10,7 +11,7 @@ import { env, isFalsy } from '@/lib/core/config/env' import { validateCallbackUrl } from '@/lib/core/security/input-validation' import { cn } from '@/lib/core/utils/cn' import { quickValidateEmail } from '@/lib/messaging/email/validation' -import { BrandedButton } from '@/app/(auth)/components/branded-button' +import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes' const logger = createLogger('SSOForm') @@ -50,13 +51,11 @@ export default function SSOForm() { } } - // Pre-fill email if provided in URL (e.g., from deployed chat SSO) const emailParam = searchParams.get('email') if (emailParam) { setEmail(emailParam) } - // Check for SSO error from redirect const error = searchParams.get('error') if (error) { const errorMessages: Record- + @@ -105,10 +106,10 @@ export default function SSOAuth({ identifier }: SSOAuthProps) { --@@ -152,9 +153,16 @@ export default function SSOAuth({ identifier }: SSOAuthProps) { )}+
SSO Authentication
-+
This chat requires SSO authentication
- Continue with SSO - ++ {isLoading ? ( + + + Redirecting to SSO... + + ) : ( + 'Continue with SSO' + )} + = { @@ -134,10 +133,18 @@ export default function SSOForm() { return ( <> -@@ -175,14 +182,16 @@ export default function SSOForm() {+
Sign in with SSO
-+
Enter your work email to continue
- Continue with SSO - ++ {isLoading ? ( + + {/* Only show divider and email signin button if email/password is enabled */} @@ -217,20 +226,20 @@ export default function SSOForm() { Don't have an account? Sign up+ Redirecting to SSO provider... + + ) : ( + 'Continue with SSO' + )} + +By signing in, you agree to our{' '} Terms of Service {' '} @@ -239,7 +248,7 @@ export default function SSOForm() { href='/privacy' target='_blank' rel='noopener noreferrer' - className='text-[var(--text-subtle)] underline-offset-4 transition hover:text-[var(--text-primary)] hover:underline' + className='text-[var(--landing-text-muted)] underline-offset-4 transition hover:text-[var(--landing-text)] hover:underline' > Privacy Policy diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-animated-placeholder.ts b/apps/sim/hooks/use-animated-placeholder.ts similarity index 100% rename from apps/sim/app/workspace/[workspaceId]/home/hooks/use-animated-placeholder.ts rename to apps/sim/hooks/use-animated-placeholder.ts diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-auto-scroll.ts b/apps/sim/hooks/use-auto-scroll.ts similarity index 100% rename from apps/sim/app/workspace/[workspaceId]/home/hooks/use-auto-scroll.ts rename to apps/sim/hooks/use-auto-scroll.ts diff --git a/apps/sim/hooks/use-branded-button-class.ts b/apps/sim/hooks/use-branded-button-class.ts deleted file mode 100644 index 7039857ffe6..00000000000 --- a/apps/sim/hooks/use-branded-button-class.ts +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' - -const DEFAULT_BRAND_ACCENT = '#6f3dfa' - -export type BrandedButtonClass = 'branded-button-gradient' | 'branded-button-custom' - -/** - * Hook to determine the appropriate button class based on brand customization. - * Returns 'branded-button-gradient' for default Sim branding, 'branded-button-custom' for whitelabeled instances. - */ -export function useBrandedButtonClass(): BrandedButtonClass { - const [buttonClass, setButtonClass] = useState('branded-button-gradient') - - useEffect(() => { - const checkCustomBrand = () => { - const computedStyle = getComputedStyle(document.documentElement) - const brandAccent = computedStyle.getPropertyValue('--brand-link').trim() - - if (brandAccent && brandAccent !== DEFAULT_BRAND_ACCENT) { - setButtonClass('branded-button-custom') - } else { - setButtonClass('branded-button-gradient') - } - } - - checkCustomBrand() - - window.addEventListener('resize', checkCustomBrand) - const observer = new MutationObserver(checkCustomBrand) - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['style', 'class'], - }) - - return () => { - window.removeEventListener('resize', checkCustomBrand) - observer.disconnect() - } - }, []) - - return buttonClass -} diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-streaming-reveal.ts b/apps/sim/hooks/use-streaming-reveal.ts similarity index 92% rename from apps/sim/app/workspace/[workspaceId]/home/hooks/use-streaming-reveal.ts rename to apps/sim/hooks/use-streaming-reveal.ts index 0d6d9549644..8ce1beeac75 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-streaming-reveal.ts +++ b/apps/sim/hooks/use-streaming-reveal.ts @@ -75,6 +75,16 @@ export function useStreamingReveal(content: string, isStreaming: boolean): Strea }, [content, isStreaming]) if (!isStreaming) { + const preservedSplit = prevSplitRef.current + + if (preservedSplit > 0 && preservedSplit < content.length) { + return { + committed: content.slice(0, preservedSplit), + incoming: content.slice(preservedSplit), + generation, + } + } + return { committed: content, incoming: '', generation } } diff --git a/apps/sim/lib/copilot/resource-extraction.ts b/apps/sim/lib/copilot/resource-extraction.ts index 5eeda39f027..9887ea5ded3 100644 --- a/apps/sim/lib/copilot/resource-extraction.ts +++ b/apps/sim/lib/copilot/resource-extraction.ts @@ -3,21 +3,94 @@ import type { MothershipResource, MothershipResourceType } from '@/lib/copilot/r type ChatResource = MothershipResource type ResourceType = MothershipResourceType -const RESOURCE_TOOL_NAMES = new Set([ - 'user_table', - 'workspace_file', - 'download_to_workspace_file', - 'create_workflow', - 'edit_workflow', - 'function_execute', - 'knowledge_base', - 'knowledge', - 'generate_visualization', - 'generate_image', -]) +/** + * Defines how each tool's result is surfaced in the resource panel: + * - `dedicated` — opens its own resource tab (table, file, workflow, knowledgebase) + * - `deferred` — may open a dedicated tab; falls back to the Results tab if no resource is produced + * - `excluded` — hidden from the resource panel (internal tools, management, subagent wrappers) + * + * Any tool not listed here appears in the generic Results tab by default. + */ +const TOOL_PANEL_BEHAVIOR: Record = { + // Dedicated resource tab openers + user_table: 'dedicated', + workspace_file: 'dedicated', + download_to_workspace_file: 'dedicated', + create_workflow: 'dedicated', + edit_workflow: 'dedicated', + knowledge_base: 'dedicated', + knowledge: 'dedicated', + generate_visualization: 'dedicated', + generate_image: 'dedicated', + // Deferred: may produce a dedicated resource; falls back to Results tab otherwise + function_execute: 'deferred', + // Excluded: saves files without opening a resource tab + materialize_file: 'excluded', + // Excluded: internal / invisible + user_memory: 'excluded', + context_write: 'excluded', + context_compaction: 'excluded', + // Excluded: workflow and folder management + rename_workflow: 'excluded', + move_workflow: 'excluded', + delete_workflow: 'excluded', + create_folder: 'excluded', + delete_folder: 'excluded', + move_folder: 'excluded', + list_folders: 'excluded', + list_user_workspaces: 'excluded', + open_resource: 'excluded', + // Excluded: settings and credential management + set_environment_variables: 'excluded', + set_global_workflow_variables: 'excluded', + manage_mcp_tool: 'excluded', + manage_skill: 'excluded', + manage_credential: 'excluded', + manage_custom_tool: 'excluded', + oauth_get_auth_link: 'excluded', + oauth_request_access: 'excluded', + update_workspace_mcp_server: 'excluded', + delete_workspace_mcp_server: 'excluded', + create_workspace_mcp_server: 'excluded', + list_workspace_mcp_servers: 'excluded', + // Excluded: subagent wrappers — inner tools fire as individual events + build: 'excluded', + run: 'excluded', + deploy: 'excluded', + auth: 'excluded', + table: 'excluded', + job: 'excluded', + agent: 'excluded', + custom_tool: 'excluded', + research: 'excluded', + plan: 'excluded', + debug: 'excluded', + edit: 'excluded', + fast_edit: 'excluded', +} +/** + * Returns true for resources that are client-only and must never be persisted to the server. + * This covers the generic Results tab and the in-flight streaming-file preview. + */ +export function isEphemeralResource(resource: { id: string; type: string }): boolean { + return resource.type === 'generic' || resource.id === 'streaming-file' +} + +/** Returns true for tools that open a dedicated resource tab or may fall back to the Results tab. */ export function isResourceToolName(toolName: string): boolean { - return RESOURCE_TOOL_NAMES.has(toolName) + const b = TOOL_PANEL_BEHAVIOR[toolName] + return b === 'dedicated' || b === 'deferred' +} + +/** Returns true if the tool's result should appear in the Results tab at call time. */ +export function shouldOpenGenericResource(toolName: string): boolean { + return !(toolName in TOOL_PANEL_BEHAVIOR) +} + +/** Returns true for tools that may fall back to the Results tab at completion time. */ +export function isDeferredResourceTool(toolName: string): boolean { + return TOOL_PANEL_BEHAVIOR[toolName] === 'deferred' } function asRecord(value: unknown): Record { diff --git a/apps/sim/lib/copilot/resource-types.ts b/apps/sim/lib/copilot/resource-types.ts index 6dd11ad9b5a..a3b3f3f4bfb 100644 --- a/apps/sim/lib/copilot/resource-types.ts +++ b/apps/sim/lib/copilot/resource-types.ts @@ -1,4 +1,4 @@ -export type MothershipResourceType = 'table' | 'file' | 'workflow' | 'knowledgebase' +export type MothershipResourceType = 'table' | 'file' | 'workflow' | 'knowledgebase' | 'generic' export interface MothershipResource { type: MothershipResourceType diff --git a/apps/sim/lib/copilot/resources.ts b/apps/sim/lib/copilot/resources.ts index d2aac2f391e..5efec3dd50b 100644 --- a/apps/sim/lib/copilot/resources.ts +++ b/apps/sim/lib/copilot/resources.ts @@ -2,12 +2,14 @@ import { db } from '@sim/db' import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq, sql } from 'drizzle-orm' +import { isEphemeralResource } from '@/lib/copilot/resource-extraction' import type { MothershipResource } from '@/lib/copilot/resource-types' export { extractDeletedResourcesFromToolResult, extractResourcesFromToolResult, hasDeleteCapability, + isEphemeralResource, isResourceToolName, } from '@/lib/copilot/resource-extraction' export type { @@ -25,7 +27,7 @@ export async function persistChatResources( chatId: string, newResources: MothershipResource[] ): Promise { - const toMerge = newResources.filter((r) => r.id !== 'streaming-file') + const toMerge = newResources.filter((r) => !isEphemeralResource(r)) if (toMerge.length === 0) return try { diff --git a/apps/sim/public/new/logo/colorized-bg.svg b/apps/sim/public/new/logo/colorized-bg.svg deleted file mode 100644 index 47baed4b667..00000000000 --- a/apps/sim/public/new/logo/colorized-bg.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/sim/public/new/logo/colorized.svg b/apps/sim/public/new/logo/colorized.svg deleted file mode 100644 index 9942a90458a..00000000000 --- a/apps/sim/public/new/logo/colorized.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/sim/public/new/logo/plain.svg b/apps/sim/public/new/logo/plain.svg deleted file mode 100644 index 06ff9731d5d..00000000000 --- a/apps/sim/public/new/logo/plain.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/sim/public/static/automate-blocks.svg b/apps/sim/public/static/automate-blocks.svg deleted file mode 100644 index eb79872c9c6..00000000000 --- a/apps/sim/public/static/automate-blocks.svg +++ /dev/null @@ -1,349 +0,0 @@ - \ No newline at end of file diff --git a/apps/sim/public/static/copilot.gif b/apps/sim/public/static/copilot.gif deleted file mode 100644 index c84f3f4db03..00000000000 Binary files a/apps/sim/public/static/copilot.gif and /dev/null differ diff --git a/apps/sim/public/static/discord-icon.png b/apps/sim/public/static/discord-icon.png deleted file mode 100644 index 7c645e7197a..00000000000 Binary files a/apps/sim/public/static/discord-icon.png and /dev/null differ diff --git a/apps/sim/public/static/github-icon.png b/apps/sim/public/static/github-icon.png deleted file mode 100644 index 1f6d5ae3e40..00000000000 Binary files a/apps/sim/public/static/github-icon.png and /dev/null differ diff --git a/apps/sim/public/static/knowledge.gif b/apps/sim/public/static/knowledge.gif deleted file mode 100644 index 82abd00515c..00000000000 Binary files a/apps/sim/public/static/knowledge.gif and /dev/null differ diff --git a/apps/sim/public/static/preview.png b/apps/sim/public/static/preview.png deleted file mode 100644 index 93727768292..00000000000 Binary files a/apps/sim/public/static/preview.png and /dev/null differ diff --git a/apps/sim/public/static/sim.png b/apps/sim/public/static/sim.png deleted file mode 100644 index d56abed087e..00000000000 Binary files a/apps/sim/public/static/sim.png and /dev/null differ diff --git a/apps/sim/public/static/sync-blocks.svg b/apps/sim/public/static/sync-blocks.svg deleted file mode 100644 index 6e65508d769..00000000000 --- a/apps/sim/public/static/sync-blocks.svg +++ /dev/null @@ -1,355 +0,0 @@ - \ No newline at end of file diff --git a/apps/sim/public/static/workflow.gif b/apps/sim/public/static/workflow.gif deleted file mode 100644 index 644bc2e25d3..00000000000 Binary files a/apps/sim/public/static/workflow.gif and /dev/null differ diff --git a/apps/sim/public/static/x-icon.png b/apps/sim/public/static/x-icon.png deleted file mode 100644 index 58d635b3fb8..00000000000 Binary files a/apps/sim/public/static/x-icon.png and /dev/null differ diff --git a/apps/sim/public/twitter/daniel.jpg b/apps/sim/public/twitter/daniel.jpg deleted file mode 100644 index 7ae0c1145e0..00000000000 Binary files a/apps/sim/public/twitter/daniel.jpg and /dev/null differ diff --git a/apps/sim/public/twitter/github-projects.jpg b/apps/sim/public/twitter/github-projects.jpg deleted file mode 100644 index 32925be2786..00000000000 Binary files a/apps/sim/public/twitter/github-projects.jpg and /dev/null differ diff --git a/apps/sim/public/twitter/hasan.jpg b/apps/sim/public/twitter/hasan.jpg deleted file mode 100644 index c46d9badf63..00000000000 Binary files a/apps/sim/public/twitter/hasan.jpg and /dev/null differ diff --git a/apps/sim/public/twitter/lazukars.png b/apps/sim/public/twitter/lazukars.png deleted file mode 100644 index 47acfbe22b7..00000000000 Binary files a/apps/sim/public/twitter/lazukars.png and /dev/null differ diff --git a/apps/sim/public/twitter/nizzy.jpg b/apps/sim/public/twitter/nizzy.jpg deleted file mode 100644 index 37b54be197e..00000000000 Binary files a/apps/sim/public/twitter/nizzy.jpg and /dev/null differ diff --git a/apps/sim/public/twitter/samarth.jpg b/apps/sim/public/twitter/samarth.jpg deleted file mode 100644 index 5843eba7726..00000000000 Binary files a/apps/sim/public/twitter/samarth.jpg and /dev/null differ diff --git a/apps/sim/public/twitter/syamrajk.jpg b/apps/sim/public/twitter/syamrajk.jpg deleted file mode 100644 index 317e89454d2..00000000000 Binary files a/apps/sim/public/twitter/syamrajk.jpg and /dev/null differ diff --git a/apps/sim/public/twitter/xyflow.jpg b/apps/sim/public/twitter/xyflow.jpg deleted file mode 100644 index 7d96ca7854b..00000000000 Binary files a/apps/sim/public/twitter/xyflow.jpg and /dev/null differ