Revert "fix: resolve chat conversation hanging and stream interruption issues (#1971)"

This reverts commit e68593f22d.
This commit is contained in:
Stijnus
2025-09-07 00:14:13 +02:00
committed by Stijnus
parent e68593f22d
commit 37217a5c7b
61 changed files with 1432 additions and 8811 deletions

View File

@@ -176,18 +176,9 @@ export const AssistantMessage = memo(
</div>
</div>
</>
<div className="prose prose-invert max-w-none text-bolt-elements-textPrimary">
<Markdown
append={append}
chatMode={chatMode}
setChatMode={setChatMode}
model={model}
provider={provider}
html
>
{content}
</Markdown>
</div>
<Markdown append={append} chatMode={chatMode} setChatMode={setChatMode} model={model} provider={provider} html>
{content}
</Markdown>
{toolInvocations && toolInvocations.length > 0 && (
<ToolInvocations
toolInvocations={toolInvocations}

View File

@@ -1,86 +0,0 @@
import { useEffect, useState } from 'react';
import { useNavigate } from '@remix-run/react';
import { ClientOnly } from 'remix-utils/client-only';
import { useStore } from '@nanostores/react';
import { authStore } from '~/lib/stores/auth';
import { BaseChat } from '~/components/chat/BaseChat';
import { Chat } from '~/components/chat/Chat.client';
import { Header } from '~/components/header/Header';
import BackgroundRays from '~/components/ui/BackgroundRays';
import { motion } from 'framer-motion';
import { UserMenu } from '~/components/header/UserMenu';
/**
* Authenticated chat component that ensures user is logged in
*/
export function AuthenticatedChat() {
const navigate = useNavigate();
const authState = useStore(authStore);
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
// Check authentication status after component mounts
const checkAuth = async () => {
// Give auth store time to initialize
await new Promise((resolve) => setTimeout(resolve, 100));
const state = authStore.get();
if (!state.loading) {
if (!state.isAuthenticated) {
navigate('/auth');
} else {
setIsInitialized(true);
}
}
};
checkAuth();
}, [navigate]);
useEffect(() => {
// Subscribe to auth changes
const unsubscribe = authStore.subscribe((state) => {
if (!state.loading && !state.isAuthenticated) {
navigate('/auth');
}
});
return () => {
unsubscribe();
};
}, [navigate]);
// Show loading state
if (authState.loading || !isInitialized) {
return (
<div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
<BackgroundRays />
<div className="flex-1 flex items-center justify-center">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex flex-col items-center gap-4">
<div className="w-16 h-16 rounded-xl bg-bolt-elements-background-depth-2 flex items-center justify-center">
<span className="i-svg-spinners:3-dots-scale text-2xl text-bolt-elements-textPrimary" />
</div>
<p className="text-bolt-elements-textSecondary">Initializing workspace...</p>
</motion.div>
</div>
</div>
);
}
// If not authenticated, don't render (will redirect)
if (!authState.isAuthenticated) {
return null;
}
// Render authenticated content with enhanced header
return (
<div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
<BackgroundRays />
<Header>
<UserMenu />
</Header>
<ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>
</div>
);
}

View File

@@ -73,28 +73,20 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
{isUserMessage ? (
<UserMessage content={content} parts={parts} />
) : (
<>
{props.model?.includes('smartai') && index === messages.length - 1 && isStreaming && (
<div className="flex items-center gap-2 mb-2 text-sm text-blue-400">
<span className="i-ph:sparkle animate-pulse" />
<span className="font-medium">SmartAI is explaining the process...</span>
</div>
)}
<AssistantMessage
content={content}
annotations={message.annotations}
messageId={messageId}
onRewind={handleRewind}
onFork={handleFork}
append={props.append}
chatMode={props.chatMode}
setChatMode={props.setChatMode}
model={props.model}
provider={props.provider}
parts={parts}
addToolResult={props.addToolResult}
/>
</>
<AssistantMessage
content={content}
annotations={message.annotations}
messageId={messageId}
onRewind={handleRewind}
onFork={handleFork}
append={props.append}
chatMode={props.chatMode}
setChatMode={props.setChatMode}
model={props.model}
provider={props.provider}
parts={parts}
addToolResult={props.addToolResult}
/>
)}
</div>
</div>

View File

@@ -1,9 +1,84 @@
import type { ProviderInfo } from '~/types/model';
import { useEffect, useState, useRef } from 'react';
import { useEffect, useState, useRef, useMemo, useCallback } from 'react';
import type { KeyboardEvent } from 'react';
import type { ModelInfo } from '~/lib/modules/llm/types';
import { classNames } from '~/utils/classNames';
// Fuzzy search utilities
const levenshteinDistance = (str1: string, str2: string): number => {
const matrix = [];
for (let i = 0; i <= str2.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= str1.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= str2.length; i++) {
for (let j = 1; j <= str1.length; j++) {
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
}
}
}
return matrix[str2.length][str1.length];
};
const fuzzyMatch = (query: string, text: string): { score: number; matches: boolean } => {
if (!query) {
return { score: 0, matches: true };
}
if (!text) {
return { score: 0, matches: false };
}
const queryLower = query.toLowerCase();
const textLower = text.toLowerCase();
// Exact substring match gets highest score
if (textLower.includes(queryLower)) {
return { score: 100 - (textLower.indexOf(queryLower) / textLower.length) * 20, matches: true };
}
// Fuzzy match with reasonable threshold
const distance = levenshteinDistance(queryLower, textLower);
const maxLen = Math.max(queryLower.length, textLower.length);
const similarity = 1 - distance / maxLen;
return {
score: similarity > 0.6 ? similarity * 80 : 0,
matches: similarity > 0.6,
};
};
const highlightText = (text: string, query: string): string => {
if (!query) {
return text;
}
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return text.replace(regex, '<mark class="bg-yellow-200 dark:bg-yellow-800 text-current">$1</mark>');
};
const formatContextSize = (tokens: number): string => {
if (tokens >= 1000000) {
return `${(tokens / 1000000).toFixed(1)}M`;
}
if (tokens >= 1000) {
return `${(tokens / 1000).toFixed(0)}K`;
}
return tokens.toString();
};
interface ModelSelectorProps {
model?: string;
setModel?: (model: string) => void;
@@ -40,12 +115,14 @@ export const ModelSelector = ({
modelLoading,
}: ModelSelectorProps) => {
const [modelSearchQuery, setModelSearchQuery] = useState('');
const [debouncedModelSearchQuery, setDebouncedModelSearchQuery] = useState('');
const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false);
const [focusedModelIndex, setFocusedModelIndex] = useState(-1);
const modelSearchInputRef = useRef<HTMLInputElement>(null);
const modelOptionsRef = useRef<(HTMLDivElement | null)[]>([]);
const modelDropdownRef = useRef<HTMLDivElement>(null);
const [providerSearchQuery, setProviderSearchQuery] = useState('');
const [debouncedProviderSearchQuery, setDebouncedProviderSearchQuery] = useState('');
const [isProviderDropdownOpen, setIsProviderDropdownOpen] = useState(false);
const [focusedProviderIndex, setFocusedProviderIndex] = useState(-1);
const providerSearchInputRef = useRef<HTMLInputElement>(null);
@@ -53,6 +130,23 @@ export const ModelSelector = ({
const providerDropdownRef = useRef<HTMLDivElement>(null);
const [showFreeModelsOnly, setShowFreeModelsOnly] = useState(false);
// Debounce search queries
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedModelSearchQuery(modelSearchQuery);
}, 150);
return () => clearTimeout(timer);
}, [modelSearchQuery]);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedProviderSearchQuery(providerSearchQuery);
}, 150);
return () => clearTimeout(timer);
}, [providerSearchQuery]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (modelDropdownRef.current && !modelDropdownRef.current.contains(event.target as Node)) {
@@ -71,24 +165,64 @@ export const ModelSelector = ({
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const filteredModels = [...modelList]
.filter((e) => e.provider === provider?.name && e.name)
.filter((model) => {
// Apply free models filter
if (showFreeModelsOnly && !isModelLikelyFree(model, provider?.name)) {
return false;
}
const filteredModels = useMemo(() => {
const baseModels = [...modelList].filter((e) => e.provider === provider?.name && e.name);
// Apply search filter
return (
model.label.toLowerCase().includes(modelSearchQuery.toLowerCase()) ||
model.name.toLowerCase().includes(modelSearchQuery.toLowerCase())
);
});
return baseModels
.filter((model) => {
// Apply free models filter
if (showFreeModelsOnly && !isModelLikelyFree(model, provider?.name)) {
return false;
}
const filteredProviders = providerList.filter((p) =>
p.name.toLowerCase().includes(providerSearchQuery.toLowerCase()),
);
return true;
})
.map((model) => {
// Calculate search scores for fuzzy matching
const labelMatch = fuzzyMatch(debouncedModelSearchQuery, model.label);
const nameMatch = fuzzyMatch(debouncedModelSearchQuery, model.name);
const contextMatch = fuzzyMatch(debouncedModelSearchQuery, formatContextSize(model.maxTokenAllowed));
const bestScore = Math.max(labelMatch.score, nameMatch.score, contextMatch.score);
const matches = labelMatch.matches || nameMatch.matches || contextMatch.matches || !debouncedModelSearchQuery; // Show all if no query
return {
...model,
searchScore: bestScore,
searchMatches: matches,
highlightedLabel: highlightText(model.label, debouncedModelSearchQuery),
highlightedName: highlightText(model.name, debouncedModelSearchQuery),
};
})
.filter((model) => model.searchMatches)
.sort((a, b) => {
// Sort by search score (highest first), then by label
if (debouncedModelSearchQuery) {
return b.searchScore - a.searchScore;
}
return a.label.localeCompare(b.label);
});
}, [modelList, provider?.name, showFreeModelsOnly, debouncedModelSearchQuery]);
const filteredProviders = useMemo(() => {
if (!debouncedProviderSearchQuery) {
return providerList;
}
return providerList
.map((provider) => {
const match = fuzzyMatch(debouncedProviderSearchQuery, provider.name);
return {
...provider,
searchScore: match.score,
searchMatches: match.matches,
highlightedName: highlightText(provider.name, debouncedProviderSearchQuery),
};
})
.filter((provider) => provider.searchMatches)
.sort((a, b) => b.searchScore - a.searchScore);
}, [providerList, debouncedProviderSearchQuery]);
// Reset free models filter when provider changes
useEffect(() => {
@@ -97,11 +231,30 @@ export const ModelSelector = ({
useEffect(() => {
setFocusedModelIndex(-1);
}, [modelSearchQuery, isModelDropdownOpen, showFreeModelsOnly]);
}, [debouncedModelSearchQuery, isModelDropdownOpen, showFreeModelsOnly]);
useEffect(() => {
setFocusedProviderIndex(-1);
}, [providerSearchQuery, isProviderDropdownOpen]);
}, [debouncedProviderSearchQuery, isProviderDropdownOpen]);
// Clear search functions
const clearModelSearch = useCallback(() => {
setModelSearchQuery('');
setDebouncedModelSearchQuery('');
if (modelSearchInputRef.current) {
modelSearchInputRef.current.focus();
}
}, []);
const clearProviderSearch = useCallback(() => {
setProviderSearchQuery('');
setDebouncedProviderSearchQuery('');
if (providerSearchInputRef.current) {
providerSearchInputRef.current.focus();
}
}, []);
useEffect(() => {
if (isModelDropdownOpen && modelSearchInputRef.current) {
@@ -137,6 +290,7 @@ export const ModelSelector = ({
setModel?.(selectedModel.name);
setIsModelDropdownOpen(false);
setModelSearchQuery('');
setDebouncedModelSearchQuery('');
}
break;
@@ -144,12 +298,20 @@ export const ModelSelector = ({
e.preventDefault();
setIsModelDropdownOpen(false);
setModelSearchQuery('');
setDebouncedModelSearchQuery('');
break;
case 'Tab':
if (!e.shiftKey && focusedModelIndex === filteredModels.length - 1) {
setIsModelDropdownOpen(false);
}
break;
case 'k':
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
clearModelSearch();
}
break;
}
};
@@ -186,6 +348,7 @@ export const ModelSelector = ({
setIsProviderDropdownOpen(false);
setProviderSearchQuery('');
setDebouncedProviderSearchQuery('');
}
break;
@@ -193,12 +356,20 @@ export const ModelSelector = ({
e.preventDefault();
setIsProviderDropdownOpen(false);
setProviderSearchQuery('');
setDebouncedProviderSearchQuery('');
break;
case 'Tab':
if (!e.shiftKey && focusedProviderIndex === filteredProviders.length - 1) {
setIsProviderDropdownOpen(false);
}
break;
case 'k':
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
clearProviderSearch();
}
break;
}
};
@@ -292,9 +463,9 @@ export const ModelSelector = ({
type="text"
value={providerSearchQuery}
onChange={(e) => setProviderSearchQuery(e.target.value)}
placeholder="Search providers..."
placeholder="Search providers... (⌘K to clear)"
className={classNames(
'w-full pl-2 py-1.5 rounded-md text-sm',
'w-full pl-8 pr-8 py-1.5 rounded-md text-sm',
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary placeholder:text-bolt-elements-textTertiary',
'focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus',
@@ -307,6 +478,19 @@ export const ModelSelector = ({
<div className="absolute left-2.5 top-1/2 -translate-y-1/2">
<span className="i-ph:magnifying-glass text-bolt-elements-textTertiary" />
</div>
{providerSearchQuery && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
clearProviderSearch();
}}
className="absolute right-2.5 top-1/2 -translate-y-1/2 p-0.5 rounded hover:bg-bolt-elements-background-depth-3 transition-colors"
aria-label="Clear search"
>
<span className="i-ph:x text-bolt-elements-textTertiary text-xs" />
</button>
)}
</div>
</div>
@@ -327,7 +511,18 @@ export const ModelSelector = ({
)}
>
{filteredProviders.length === 0 ? (
<div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">No providers found</div>
<div className="px-3 py-3 text-sm">
<div className="text-bolt-elements-textTertiary mb-1">
{debouncedProviderSearchQuery
? `No providers match "${debouncedProviderSearchQuery}"`
: 'No providers found'}
</div>
{debouncedProviderSearchQuery && (
<div className="text-xs text-bolt-elements-textTertiary">
Try searching for provider names like "OpenAI", "Anthropic", or "Google"
</div>
)}
</div>
) : (
filteredProviders.map((providerOption, index) => (
<div
@@ -360,10 +555,15 @@ export const ModelSelector = ({
setIsProviderDropdownOpen(false);
setProviderSearchQuery('');
setDebouncedProviderSearchQuery('');
}}
tabIndex={focusedProviderIndex === index ? 0 : -1}
>
{providerOption.name}
<div
dangerouslySetInnerHTML={{
__html: (providerOption as any).highlightedName || providerOption.name,
}}
/>
</div>
))
)}
@@ -396,15 +596,7 @@ export const ModelSelector = ({
tabIndex={0}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 truncate">
<span className="truncate">{modelList.find((m) => m.name === model)?.label || 'Select model'}</span>
{modelList.find((m) => m.name === model)?.isSmartAIEnabled && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-md bg-gradient-to-r from-blue-500/20 to-purple-500/20 border border-blue-500/30">
<span className="i-ph:sparkle text-xs text-blue-400" />
<span className="text-xs text-blue-400 font-medium">Active</span>
</span>
)}
</div>
<div className="truncate">{modelList.find((m) => m.name === model)?.label || 'Select model'}</div>
<div
className={classNames(
'i-ph:caret-down w-4 h-4 text-bolt-elements-textSecondary opacity-75',
@@ -449,6 +641,14 @@ export const ModelSelector = ({
</div>
)}
{/* Search Result Count */}
{debouncedModelSearchQuery && filteredModels.length > 0 && (
<div className="text-xs text-bolt-elements-textTertiary px-1">
{filteredModels.length} model{filteredModels.length !== 1 ? 's' : ''} found
{filteredModels.length > 5 && ' (showing best matches)'}
</div>
)}
{/* Search Input */}
<div className="relative">
<input
@@ -456,9 +656,9 @@ export const ModelSelector = ({
type="text"
value={modelSearchQuery}
onChange={(e) => setModelSearchQuery(e.target.value)}
placeholder="Search models..."
placeholder="Search models... (⌘K to clear)"
className={classNames(
'w-full pl-2 py-1.5 rounded-md text-sm',
'w-full pl-8 pr-8 py-1.5 rounded-md text-sm',
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary placeholder:text-bolt-elements-textTertiary',
'focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus',
@@ -471,6 +671,19 @@ export const ModelSelector = ({
<div className="absolute left-2.5 top-1/2 -translate-y-1/2">
<span className="i-ph:magnifying-glass text-bolt-elements-textTertiary" />
</div>
{modelSearchQuery && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
clearModelSearch();
}}
className="absolute right-2.5 top-1/2 -translate-y-1/2 p-0.5 rounded hover:bg-bolt-elements-background-depth-3 transition-colors"
aria-label="Clear search"
>
<span className="i-ph:x text-bolt-elements-textTertiary text-xs" />
</button>
)}
</div>
</div>
@@ -491,16 +704,37 @@ export const ModelSelector = ({
)}
>
{modelLoading === 'all' || modelLoading === provider?.name ? (
<div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">Loading...</div>
<div className="px-3 py-3 text-sm">
<div className="flex items-center gap-2 text-bolt-elements-textTertiary">
<span className="i-ph:spinner animate-spin" />
Loading models...
</div>
</div>
) : filteredModels.length === 0 ? (
<div className="px-3 py-2 text-sm text-bolt-elements-textTertiary">
{showFreeModelsOnly ? 'No free models found' : 'No models found'}
<div className="px-3 py-3 text-sm">
<div className="text-bolt-elements-textTertiary mb-1">
{debouncedModelSearchQuery
? `No models match "${debouncedModelSearchQuery}"${showFreeModelsOnly ? ' (free only)' : ''}`
: showFreeModelsOnly
? 'No free models available'
: 'No models available'}
</div>
{debouncedModelSearchQuery && (
<div className="text-xs text-bolt-elements-textTertiary">
Try searching for model names, context sizes (e.g., "128k", "1M"), or capabilities
</div>
)}
{showFreeModelsOnly && !debouncedModelSearchQuery && (
<div className="text-xs text-bolt-elements-textTertiary">
Try disabling the "Free models only" filter to see all available models
</div>
)}
</div>
) : (
filteredModels.map((modelOption, index) => (
<div
ref={(el) => (modelOptionsRef.current[index] = el)}
key={index} // Consider using modelOption.name if unique
key={modelOption.name}
role="option"
aria-selected={model === modelOption.name}
className={classNames(
@@ -518,22 +752,38 @@ export const ModelSelector = ({
setModel?.(modelOption.name);
setIsModelDropdownOpen(false);
setModelSearchQuery('');
setDebouncedModelSearchQuery('');
}}
tabIndex={focusedModelIndex === index ? 0 : -1}
>
<div className="flex items-center justify-between">
<span className="flex items-center gap-2">
{modelOption.label}
{modelOption.isSmartAIEnabled && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-md bg-gradient-to-r from-blue-500/20 to-purple-500/20 border border-blue-500/30">
<span className="i-ph:sparkle text-xs text-blue-400" />
<span className="text-xs text-blue-400 font-medium">SmartAI</span>
<div className="flex-1 min-w-0">
<div className="truncate">
<span
dangerouslySetInnerHTML={{
__html: (modelOption as any).highlightedLabel || modelOption.label,
}}
/>
</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-bolt-elements-textTertiary">
{formatContextSize(modelOption.maxTokenAllowed)} tokens
</span>
{debouncedModelSearchQuery && (modelOption as any).searchScore > 70 && (
<span className="text-xs text-green-500 font-medium">
{(modelOption as any).searchScore.toFixed(0)}% match
</span>
)}
</div>
</div>
<div className="flex items-center gap-1 ml-2">
{isModelLikelyFree(modelOption, provider?.name) && (
<span className="i-ph:gift text-xs text-purple-400" title="Free model" />
)}
</span>
{isModelLikelyFree(modelOption, provider?.name) && (
<span className="i-ph:gift text-xs text-purple-400 ml-2" title="Free model" />
)}
{model === modelOption.name && (
<span className="i-ph:check text-xs text-green-500" title="Selected" />
)}
</div>
</div>
</div>
))

View File

@@ -1,45 +0,0 @@
import React from 'react';
import { classNames } from '~/utils/classNames';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { ProviderInfo } from '~/types/model';
interface SmartAIToggleProps {
enabled: boolean;
onToggle: (enabled: boolean) => void;
provider?: ProviderInfo;
model?: string;
modelList: ModelInfo[];
}
export const SmartAiToggle: React.FC<SmartAIToggleProps> = ({ enabled, onToggle, provider, model, modelList }) => {
// Check if current model supports SmartAI
const currentModel = modelList.find((m) => m.name === model);
const isSupported = currentModel?.supportsSmartAI && (provider?.name === 'Anthropic' || provider?.name === 'OpenAI');
if (!isSupported) {
return null;
}
return (
<button
onClick={() => onToggle(!enabled)}
className={classNames(
'flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all',
'border border-bolt-elements-borderColor',
enabled
? 'bg-gradient-to-r from-blue-500/20 to-purple-500/20 border-blue-500/30'
: 'bg-bolt-elements-background-depth-2 hover:bg-bolt-elements-background-depth-3',
)}
title="Toggle SmartAI for detailed conversational feedback"
>
<span
className={classNames('i-ph:sparkle text-sm', enabled ? 'text-blue-400' : 'text-bolt-elements-textSecondary')}
/>
<span
className={classNames('text-xs font-medium', enabled ? 'text-blue-400' : 'text-bolt-elements-textSecondary')}
>
SmartAI {enabled ? 'ON' : 'OFF'}
</span>
</button>
);
};

View File

@@ -1,90 +0,0 @@
import React from 'react';
import { useStore } from '@nanostores/react';
import { authStore } from '~/lib/stores/auth';
import { motion } from 'framer-motion';
const EXAMPLE_PROMPTS = [
{ text: 'Create a mobile app about bolt.diy' },
{ text: 'Build a todo app in React using Tailwind' },
{ text: 'Build a simple blog using Astro' },
{ text: 'Create a cookie consent form using Material UI' },
{ text: 'Make a space invaders game' },
{ text: 'Make a Tic Tac Toe game in html, css and js only' },
];
interface WelcomeMessageProps {
sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
}
export function WelcomeMessage({ sendMessage }: WelcomeMessageProps) {
const authState = useStore(authStore);
const timeOfDay = new Date().getHours();
const getGreeting = () => {
if (timeOfDay < 12) {
return 'Good morning';
}
if (timeOfDay < 17) {
return 'Good afternoon';
}
return 'Good evening';
};
return (
<div className="relative flex flex-col gap-6 w-full max-w-3xl mx-auto mt-8">
{/* Personalized Greeting */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-center"
>
<h1 className="text-3xl font-bold text-bolt-elements-textPrimary mb-2">
{getGreeting()}, {authState.user?.firstName || 'Developer'}!
</h1>
<p className="text-lg text-bolt-elements-textSecondary">What would you like to build today?</p>
</motion.div>
{/* Example Prompts */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="flex flex-col gap-3"
>
<p className="text-sm text-bolt-elements-textTertiary text-center">Try one of these examples to get started:</p>
<div className="flex flex-wrap justify-center gap-2">
{EXAMPLE_PROMPTS.map((examplePrompt, index) => (
<motion.button
key={index}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.3 + index * 0.05 }}
onClick={(event) => sendMessage?.(event, examplePrompt.text)}
className="border border-bolt-elements-borderColor rounded-full bg-gray-50 hover:bg-gray-100 dark:bg-gray-950 dark:hover:bg-gray-900 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary px-3 py-1 text-xs transition-all hover:scale-105"
>
{examplePrompt.text}
</motion.button>
))}
</div>
</motion.div>
{/* User Stats */}
{authState.user && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.5 }}
className="text-center text-xs text-bolt-elements-textTertiary"
>
<p>
Logged in as{' '}
<span className="text-bolt-elements-textSecondary font-medium">@{authState.user.username}</span>
</p>
</motion.div>
)}
</div>
);
}