mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')}
|
||||
|
||||
189
apps/sim/lib/browser-storage.ts
Normal file
189
apps/sim/lib/browser-storage.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -7,4 +7,5 @@ export interface PanelStore {
|
||||
togglePanel: () => void
|
||||
setActiveTab: (tab: PanelTab) => void
|
||||
setPanelWidth: (width: number) => void
|
||||
openCopilotPanel: () => void
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user