Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/sim/app/(landing)/components/hero/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
StripeIcon,
SupabaseIcon,
} from '@/components/icons'
import { LandingPromptStorage } from '@/lib/browser-storage'
import { soehne } from '@/app/fonts/soehne/soehne'
import {
CARD_WIDTH,
Expand Down Expand Up @@ -271,6 +272,7 @@ export default function Hero() {
*/
const handleSubmit = () => {
if (!isEmpty) {
LandingPromptStorage.store(textValue)
router.push('/signup')
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { Blocks, Bot, LibraryBig, Workflow } from 'lucide-react'
import { Blocks, LibraryBig, Workflow } from 'lucide-react'

interface CopilotWelcomeProps {
onQuestionClick?: (question: string) => void
Expand Down Expand Up @@ -59,7 +59,6 @@ export function CopilotWelcome({ onQuestionClick, mode = 'ask' }: CopilotWelcome
<div className='relative mx-auto w-full max-w-xl'>
{/* Header */}
<div className='flex flex-col items-center text-center'>
<Bot className='h-12 w-12 text-[var(--brand-primary-hover-hex)]' strokeWidth={1.5} />
<h3 className='mt-2 font-medium text-foreground text-lg sm:text-xl'>{subtitle}</h3>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface CopilotProps {

interface CopilotRef {
createNewChat: () => void
setInputValueAndFocus: (value: string) => void
}

export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref) => {
Expand Down Expand Up @@ -326,13 +327,24 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
}, 100) // Small delay to ensure DOM updates are complete
}, [createNewChat])

const handleSetInputValueAndFocus = useCallback(
(value: string) => {
setInputValue(value)
setTimeout(() => {
userInputRef.current?.focus()
}, 150)
},
[setInputValue]
)

// Expose functions to parent
useImperativeHandle(
ref,
() => ({
createNewChat: handleStartNewChat,
setInputValueAndFocus: handleSetInputValueAndFocus,
}),
[handleStartNewChat]
[handleStartNewChat, handleSetInputValueAndFocus]
)

// Handle abort action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { LandingPromptStorage } from '@/lib/browser-storage'
import { createLogger } from '@/lib/logs/console/logger'
import { useCopilotStore } from '@/stores/copilot/store'
import { useChatStore } from '@/stores/panel/chat/store'
Expand All @@ -31,6 +32,7 @@ export function Panel() {
const [resizeStartWidth, setResizeStartWidth] = useState(0)
const copilotRef = useRef<{
createNewChat: () => void
setInputValueAndFocus: (value: string) => void
}>(null)
const lastLoadedWorkflowRef = useRef<string | null>(null)

Expand Down Expand Up @@ -289,17 +291,40 @@ export function Panel() {
}
}, [activeWorkflowId, copilotWorkflowId, ensureCopilotDataLoaded])

useEffect(() => {
const storedPrompt = LandingPromptStorage.consume()

if (storedPrompt && storedPrompt.trim().length > 0) {
setActiveTab('copilot')
if (!isOpen) {
togglePanel()
}

setTimeout(() => {
if (copilotRef.current) {
copilotRef.current.setInputValueAndFocus(storedPrompt)
} else {
setTimeout(() => {
if (copilotRef.current) {
copilotRef.current.setInputValueAndFocus(storedPrompt)
}
}, 500)
}
}, 200)
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps -- Run only on mount

return (
<>
{/* Tab Selector - Always visible */}
<div className='fixed top-[76px] right-4 z-20 flex h-9 w-[308px] items-center gap-1 rounded-[14px] border bg-card px-[2.5px] py-1 shadow-xs'>
<button
onClick={() => handleTabClick('chat')}
onClick={() => handleTabClick('copilot')}
className={`panel-tab-base inline-flex flex-1 cursor-pointer items-center justify-center rounded-[10px] border border-transparent py-1 font-[450] text-sm outline-none transition-colors duration-200 ${
isOpen && activeTab === 'chat' ? 'panel-tab-active' : 'panel-tab-inactive'
isOpen && activeTab === 'copilot' ? 'panel-tab-active' : 'panel-tab-inactive'
}`}
>
Chat
Copilot
</button>
<button
onClick={() => handleTabClick('console')}
Expand All @@ -310,12 +335,12 @@ export function Panel() {
Console
</button>
<button
onClick={() => handleTabClick('copilot')}
onClick={() => handleTabClick('chat')}
className={`panel-tab-base inline-flex flex-1 cursor-pointer items-center justify-center rounded-[10px] border border-transparent py-1 font-[450] text-sm outline-none transition-colors duration-200 ${
isOpen && activeTab === 'copilot' ? 'panel-tab-active' : 'panel-tab-inactive'
isOpen && activeTab === 'chat' ? 'panel-tab-active' : 'panel-tab-inactive'
}`}
>
Copilot
Chat
</button>
<button
onClick={() => handleTabClick('variables')}
Expand Down
189 changes: 189 additions & 0 deletions apps/sim/lib/browser-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/**
* Safe localStorage utilities with SSR support
* Provides clean error handling and type safety for browser storage operations
*/

import { createLogger } from '@/lib/logs/console/logger'

const logger = createLogger('BrowserStorage')

/**
* Safe localStorage operations with fallbacks
*/
export class BrowserStorage {
/**
* Safely gets an item from localStorage
* @param key - The storage key
* @param defaultValue - The default value to return if key doesn't exist or access fails
* @returns The stored value or default value
*/
static getItem<T = string>(key: string, defaultValue: T): T {
if (typeof window === 'undefined') {
return defaultValue
}

try {
const item = window.localStorage.getItem(key)
if (item === null) {
return defaultValue
}

try {
return JSON.parse(item) as T
} catch {
return item as T
}
} catch (error) {
logger.warn(`Failed to get localStorage item "${key}":`, error)
return defaultValue
}
}

/**
* Safely sets an item in localStorage
* @param key - The storage key
* @param value - The value to store
* @returns True if successful, false otherwise
*/
static setItem<T>(key: string, value: T): boolean {
if (typeof window === 'undefined') {
return false
}

try {
const serializedValue = typeof value === 'string' ? value : JSON.stringify(value)
window.localStorage.setItem(key, serializedValue)
return true
} catch (error) {
logger.warn(`Failed to set localStorage item "${key}":`, error)
return false
}
}

/**
* Safely removes an item from localStorage
* @param key - The storage key to remove
* @returns True if successful, false otherwise
*/
static removeItem(key: string): boolean {
if (typeof window === 'undefined') {
return false
}

try {
window.localStorage.removeItem(key)
return true
} catch (error) {
logger.warn(`Failed to remove localStorage item "${key}":`, error)
return false
}
}

/**
* Check if localStorage is available
* @returns True if localStorage is available and accessible
*/
static isAvailable(): boolean {
if (typeof window === 'undefined') {
return false
}

try {
const testKey = '__test_localStorage_availability__'
window.localStorage.setItem(testKey, 'test')
window.localStorage.removeItem(testKey)
return true
} catch {
return false
}
}
}

/**
* Constants for localStorage keys to avoid typos and provide centralized management
*/
export const STORAGE_KEYS = {
LANDING_PAGE_PROMPT: 'sim_landing_page_prompt',
} as const

/**
* Specialized utility for managing the landing page prompt
*/
export class LandingPromptStorage {
private static readonly KEY = STORAGE_KEYS.LANDING_PAGE_PROMPT

/**
* Store a prompt from the landing page
* @param prompt - The prompt text to store
* @returns True if successful, false otherwise
*/
static store(prompt: string): boolean {
if (!prompt || prompt.trim().length === 0) {
return false
}

const data = {
prompt: prompt.trim(),
timestamp: Date.now(),
}

return BrowserStorage.setItem(LandingPromptStorage.KEY, data)
}

/**
* Retrieve and consume the stored prompt
* @param maxAge - Maximum age of the prompt in milliseconds (default: 24 hours)
* @returns The stored prompt or null if not found/expired
*/
static consume(maxAge: number = 24 * 60 * 60 * 1000): string | null {
const data = BrowserStorage.getItem<{ prompt: string; timestamp: number } | null>(
LandingPromptStorage.KEY,
null
)

if (!data || !data.prompt || !data.timestamp) {
return null
}

const age = Date.now() - data.timestamp
if (age > maxAge) {
LandingPromptStorage.clear()
return null
}

LandingPromptStorage.clear()
return data.prompt
}

/**
* Check if there's a stored prompt without consuming it
* @param maxAge - Maximum age of the prompt in milliseconds (default: 24 hours)
* @returns True if there's a valid prompt, false otherwise
*/
static hasPrompt(maxAge: number = 24 * 60 * 60 * 1000): boolean {
const data = BrowserStorage.getItem<{ prompt: string; timestamp: number } | null>(
LandingPromptStorage.KEY,
null
)

if (!data || !data.prompt || !data.timestamp) {
return false
}

const age = Date.now() - data.timestamp
if (age > maxAge) {
LandingPromptStorage.clear()
return false
}

return true
}

/**
* Clear the stored prompt
* @returns True if successful, false otherwise
*/
static clear(): boolean {
return BrowserStorage.removeItem(LandingPromptStorage.KEY)
}
}
4 changes: 4 additions & 0 deletions apps/sim/stores/panel/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export const usePanelStore = create<PanelStore>()(
const clampedWidth = Math.max(308, Math.min(800, width))
set({ panelWidth: clampedWidth })
},

openCopilotPanel: () => {
set({ isOpen: true, activeTab: 'copilot' })
},
}),
{
name: 'panel-store',
Expand Down
1 change: 1 addition & 0 deletions apps/sim/stores/panel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface PanelStore {
togglePanel: () => void
setActiveTab: (tab: PanelTab) => void
setPanelWidth: (width: number) => void
openCopilotPanel: () => void
}