Skip to content

Commit 325a666

Browse files
authored
improvement(landing): insert prompt into copilot panel from landing, open panel on entry (#1363)
* update infra and remove railway * improvement(landing): insert prompt into copilot panel from landing, open panel on entry * Revert "update infra and remove railway" This reverts commit abfa2f8. * fixes * remove debug logs * go back to old env
1 parent 009e1da commit 325a666

File tree

7 files changed

+241
-9
lines changed

7 files changed

+241
-9
lines changed

apps/sim/app/(landing)/components/hero/hero.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
StripeIcon,
3333
SupabaseIcon,
3434
} from '@/components/icons'
35+
import { LandingPromptStorage } from '@/lib/browser-storage'
3536
import { soehne } from '@/app/fonts/soehne/soehne'
3637
import {
3738
CARD_WIDTH,
@@ -271,6 +272,7 @@ export default function Hero() {
271272
*/
272273
const handleSubmit = () => {
273274
if (!isEmpty) {
275+
LandingPromptStorage.store(textValue)
274276
router.push('/signup')
275277
}
276278
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/welcome/welcome.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

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

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

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface CopilotProps {
3030

3131
interface CopilotRef {
3232
createNewChat: () => void
33+
setInputValueAndFocus: (value: string) => void
3334
}
3435

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

330+
const handleSetInputValueAndFocus = useCallback(
331+
(value: string) => {
332+
setInputValue(value)
333+
setTimeout(() => {
334+
userInputRef.current?.focus()
335+
}, 150)
336+
},
337+
[setInputValue]
338+
)
339+
329340
// Expose functions to parent
330341
useImperativeHandle(
331342
ref,
332343
() => ({
333344
createNewChat: handleStartNewChat,
345+
setInputValueAndFocus: handleSetInputValueAndFocus,
334346
}),
335-
[handleStartNewChat]
347+
[handleStartNewChat, handleSetInputValueAndFocus]
336348
)
337349

338350
// Handle abort action

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '@/components/ui/dropdown-menu'
1010
import { ScrollArea } from '@/components/ui/scroll-area'
1111
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
12+
import { LandingPromptStorage } from '@/lib/browser-storage'
1213
import { createLogger } from '@/lib/logs/console/logger'
1314
import { useCopilotStore } from '@/stores/copilot/store'
1415
import { useChatStore } from '@/stores/panel/chat/store'
@@ -31,6 +32,7 @@ export function Panel() {
3132
const [resizeStartWidth, setResizeStartWidth] = useState(0)
3233
const copilotRef = useRef<{
3334
createNewChat: () => void
35+
setInputValueAndFocus: (value: string) => void
3436
}>(null)
3537
const lastLoadedWorkflowRef = useRef<string | null>(null)
3638

@@ -289,17 +291,40 @@ export function Panel() {
289291
}
290292
}, [activeWorkflowId, copilotWorkflowId, ensureCopilotDataLoaded])
291293

294+
useEffect(() => {
295+
const storedPrompt = LandingPromptStorage.consume()
296+
297+
if (storedPrompt && storedPrompt.trim().length > 0) {
298+
setActiveTab('copilot')
299+
if (!isOpen) {
300+
togglePanel()
301+
}
302+
303+
setTimeout(() => {
304+
if (copilotRef.current) {
305+
copilotRef.current.setInputValueAndFocus(storedPrompt)
306+
} else {
307+
setTimeout(() => {
308+
if (copilotRef.current) {
309+
copilotRef.current.setInputValueAndFocus(storedPrompt)
310+
}
311+
}, 500)
312+
}
313+
}, 200)
314+
}
315+
}, []) // eslint-disable-line react-hooks/exhaustive-deps -- Run only on mount
316+
292317
return (
293318
<>
294319
{/* Tab Selector - Always visible */}
295320
<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'>
296321
<button
297-
onClick={() => handleTabClick('chat')}
322+
onClick={() => handleTabClick('copilot')}
298323
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 ${
299-
isOpen && activeTab === 'chat' ? 'panel-tab-active' : 'panel-tab-inactive'
324+
isOpen && activeTab === 'copilot' ? 'panel-tab-active' : 'panel-tab-inactive'
300325
}`}
301326
>
302-
Chat
327+
Copilot
303328
</button>
304329
<button
305330
onClick={() => handleTabClick('console')}
@@ -310,12 +335,12 @@ export function Panel() {
310335
Console
311336
</button>
312337
<button
313-
onClick={() => handleTabClick('copilot')}
338+
onClick={() => handleTabClick('chat')}
314339
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 ${
315-
isOpen && activeTab === 'copilot' ? 'panel-tab-active' : 'panel-tab-inactive'
340+
isOpen && activeTab === 'chat' ? 'panel-tab-active' : 'panel-tab-inactive'
316341
}`}
317342
>
318-
Copilot
343+
Chat
319344
</button>
320345
<button
321346
onClick={() => handleTabClick('variables')}

apps/sim/lib/browser-storage.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Safe localStorage utilities with SSR support
3+
* Provides clean error handling and type safety for browser storage operations
4+
*/
5+
6+
import { createLogger } from '@/lib/logs/console/logger'
7+
8+
const logger = createLogger('BrowserStorage')
9+
10+
/**
11+
* Safe localStorage operations with fallbacks
12+
*/
13+
export class BrowserStorage {
14+
/**
15+
* Safely gets an item from localStorage
16+
* @param key - The storage key
17+
* @param defaultValue - The default value to return if key doesn't exist or access fails
18+
* @returns The stored value or default value
19+
*/
20+
static getItem<T = string>(key: string, defaultValue: T): T {
21+
if (typeof window === 'undefined') {
22+
return defaultValue
23+
}
24+
25+
try {
26+
const item = window.localStorage.getItem(key)
27+
if (item === null) {
28+
return defaultValue
29+
}
30+
31+
try {
32+
return JSON.parse(item) as T
33+
} catch {
34+
return item as T
35+
}
36+
} catch (error) {
37+
logger.warn(`Failed to get localStorage item "${key}":`, error)
38+
return defaultValue
39+
}
40+
}
41+
42+
/**
43+
* Safely sets an item in localStorage
44+
* @param key - The storage key
45+
* @param value - The value to store
46+
* @returns True if successful, false otherwise
47+
*/
48+
static setItem<T>(key: string, value: T): boolean {
49+
if (typeof window === 'undefined') {
50+
return false
51+
}
52+
53+
try {
54+
const serializedValue = typeof value === 'string' ? value : JSON.stringify(value)
55+
window.localStorage.setItem(key, serializedValue)
56+
return true
57+
} catch (error) {
58+
logger.warn(`Failed to set localStorage item "${key}":`, error)
59+
return false
60+
}
61+
}
62+
63+
/**
64+
* Safely removes an item from localStorage
65+
* @param key - The storage key to remove
66+
* @returns True if successful, false otherwise
67+
*/
68+
static removeItem(key: string): boolean {
69+
if (typeof window === 'undefined') {
70+
return false
71+
}
72+
73+
try {
74+
window.localStorage.removeItem(key)
75+
return true
76+
} catch (error) {
77+
logger.warn(`Failed to remove localStorage item "${key}":`, error)
78+
return false
79+
}
80+
}
81+
82+
/**
83+
* Check if localStorage is available
84+
* @returns True if localStorage is available and accessible
85+
*/
86+
static isAvailable(): boolean {
87+
if (typeof window === 'undefined') {
88+
return false
89+
}
90+
91+
try {
92+
const testKey = '__test_localStorage_availability__'
93+
window.localStorage.setItem(testKey, 'test')
94+
window.localStorage.removeItem(testKey)
95+
return true
96+
} catch {
97+
return false
98+
}
99+
}
100+
}
101+
102+
/**
103+
* Constants for localStorage keys to avoid typos and provide centralized management
104+
*/
105+
export const STORAGE_KEYS = {
106+
LANDING_PAGE_PROMPT: 'sim_landing_page_prompt',
107+
} as const
108+
109+
/**
110+
* Specialized utility for managing the landing page prompt
111+
*/
112+
export class LandingPromptStorage {
113+
private static readonly KEY = STORAGE_KEYS.LANDING_PAGE_PROMPT
114+
115+
/**
116+
* Store a prompt from the landing page
117+
* @param prompt - The prompt text to store
118+
* @returns True if successful, false otherwise
119+
*/
120+
static store(prompt: string): boolean {
121+
if (!prompt || prompt.trim().length === 0) {
122+
return false
123+
}
124+
125+
const data = {
126+
prompt: prompt.trim(),
127+
timestamp: Date.now(),
128+
}
129+
130+
return BrowserStorage.setItem(LandingPromptStorage.KEY, data)
131+
}
132+
133+
/**
134+
* Retrieve and consume the stored prompt
135+
* @param maxAge - Maximum age of the prompt in milliseconds (default: 24 hours)
136+
* @returns The stored prompt or null if not found/expired
137+
*/
138+
static consume(maxAge: number = 24 * 60 * 60 * 1000): string | null {
139+
const data = BrowserStorage.getItem<{ prompt: string; timestamp: number } | null>(
140+
LandingPromptStorage.KEY,
141+
null
142+
)
143+
144+
if (!data || !data.prompt || !data.timestamp) {
145+
return null
146+
}
147+
148+
const age = Date.now() - data.timestamp
149+
if (age > maxAge) {
150+
LandingPromptStorage.clear()
151+
return null
152+
}
153+
154+
LandingPromptStorage.clear()
155+
return data.prompt
156+
}
157+
158+
/**
159+
* Check if there's a stored prompt without consuming it
160+
* @param maxAge - Maximum age of the prompt in milliseconds (default: 24 hours)
161+
* @returns True if there's a valid prompt, false otherwise
162+
*/
163+
static hasPrompt(maxAge: number = 24 * 60 * 60 * 1000): boolean {
164+
const data = BrowserStorage.getItem<{ prompt: string; timestamp: number } | null>(
165+
LandingPromptStorage.KEY,
166+
null
167+
)
168+
169+
if (!data || !data.prompt || !data.timestamp) {
170+
return false
171+
}
172+
173+
const age = Date.now() - data.timestamp
174+
if (age > maxAge) {
175+
LandingPromptStorage.clear()
176+
return false
177+
}
178+
179+
return true
180+
}
181+
182+
/**
183+
* Clear the stored prompt
184+
* @returns True if successful, false otherwise
185+
*/
186+
static clear(): boolean {
187+
return BrowserStorage.removeItem(LandingPromptStorage.KEY)
188+
}
189+
}

apps/sim/stores/panel/store.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export const usePanelStore = create<PanelStore>()(
2323
const clampedWidth = Math.max(308, Math.min(800, width))
2424
set({ panelWidth: clampedWidth })
2525
},
26+
27+
openCopilotPanel: () => {
28+
set({ isOpen: true, activeTab: 'copilot' })
29+
},
2630
}),
2731
{
2832
name: 'panel-store',

apps/sim/stores/panel/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export interface PanelStore {
77
togglePanel: () => void
88
setActiveTab: (tab: PanelTab) => void
99
setPanelWidth: (width: number) => void
10+
openCopilotPanel: () => void
1011
}

0 commit comments

Comments
 (0)