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 abfa2f8d51.

* fixes

* remove debug logs

* go back to old env
This commit is contained in:
Waleed
2025-09-17 12:28:22 -07:00
committed by GitHub
parent 009e1da5f1
commit 325a666a8b
7 changed files with 241 additions and 9 deletions

View File

@@ -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,
@@ -271,6 +272,7 @@ export default function Hero() {
*/
const handleSubmit = () => {
if (!isEmpty) {
LandingPromptStorage.store(textValue)
router.push('/signup')
}
}

View File

@@ -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
@@ -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>

View File

@@ -30,6 +30,7 @@ interface CopilotProps {
interface CopilotRef {
createNewChat: () => void
setInputValueAndFocus: (value: string) => void
}
export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref) => {
@@ -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

View File

@@ -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'
@@ -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)
@@ -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')}
@@ -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')}

View File

@@ -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)
}
}

View File

@@ -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',

View File

@@ -7,4 +7,5 @@ export interface PanelStore {
togglePanel: () => void
setActiveTab: (tab: PanelTab) => void
setPanelWidth: (width: number) => void
openCopilotPanel: () => void
}