fix: remove hero animations and ensure new chat tab always exists
- Remove framer-motion animations from hero greeting, input bar, and suggestion buttons so they render instantly - Fix SSR hydration mismatch by deferring hero render until client mount - Always create a "New Chat" tab on fresh load and when closing last tab Made-with: Cursor
This commit is contained in:
parent
f39130c8f7
commit
5f8b9ed98e
@ -777,12 +777,14 @@ export const ChatMessage = memo(function ChatMessage({ message, isStreaming, onS
|
|||||||
const attachmentInfo = parseAttachments(textContent);
|
const attachmentInfo = parseAttachments(textContent);
|
||||||
const richHtml = userHtmlMap?.get(message.id) ?? userHtmlMap?.get(textContent) ?? userHtmlMap?.get(attachmentInfo?.message ?? "");
|
const richHtml = userHtmlMap?.get(message.id) ?? userHtmlMap?.get(textContent) ?? userHtmlMap?.get(attachmentInfo?.message ?? "");
|
||||||
|
|
||||||
const bubbleContent = <p className="whitespace-pre-wrap wrap-break-word m-0">{attachmentInfo?.message ?? textContent}</p>;
|
const bubbleContent = richHtml
|
||||||
|
? <div className="chat-user-html-content" dangerouslySetInnerHTML={{ __html: richHtml }} />
|
||||||
|
: <p className="whitespace-pre-wrap break-words">{attachmentInfo?.message ?? textContent}</p>;
|
||||||
|
|
||||||
if (attachmentInfo) {
|
if (attachmentInfo) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-end gap-1.5 py-2">
|
<div className="flex flex-col items-end gap-1.5 py-2">
|
||||||
<AttachedFilesCard paths={attachmentInfo.paths} />
|
{!richHtml && <AttachedFilesCard paths={attachmentInfo.paths} />}
|
||||||
{(attachmentInfo.message || richHtml) && (
|
{(attachmentInfo.message || richHtml) && (
|
||||||
<div
|
<div
|
||||||
className="max-w-[80%] w-fit rounded-2xl rounded-br-sm px-3 py-2 text-sm leading-6 break-words chat-message-font"
|
className="max-w-[80%] w-fit rounded-2xl rounded-br-sm px-3 py-2 text-sm leading-6 break-words chat-message-font"
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { motion, LayoutGroup } from "framer-motion";
|
|
||||||
import {
|
import {
|
||||||
Mail, Users, DollarSign, Calendar, Zap, FileText, Database,
|
Mail, Users, DollarSign, Calendar, Zap, FileText, Database,
|
||||||
Code, Bug, Clock, BarChart3, PenTool, Globe, Search, Sparkles,
|
Code, Bug, Clock, BarChart3, PenTool, Globe, Search, Sparkles,
|
||||||
@ -856,6 +855,9 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
|||||||
const [showFilePicker, setShowFilePicker] =
|
const [showFilePicker, setShowFilePicker] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
useEffect(() => { setMounted(true); }, []);
|
||||||
|
|
||||||
// ── Reconnection state ──
|
// ── Reconnection state ──
|
||||||
const [isReconnecting, setIsReconnecting] = useState(false);
|
const [isReconnecting, setIsReconnecting] = useState(false);
|
||||||
const reconnectAbortRef = useRef<AbortController | null>(null);
|
const reconnectAbortRef = useRef<AbortController | null>(null);
|
||||||
@ -884,10 +886,13 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
|||||||
const [rawView, _setRawView] = useState(false);
|
const [rawView, _setRawView] = useState(false);
|
||||||
|
|
||||||
// ── Hero state (new chat screen) ──
|
// ── Hero state (new chat screen) ──
|
||||||
const [greeting, setGreeting] = useState("");
|
const [greeting, setGreeting] = useState("How can I help?");
|
||||||
const [visiblePrompts, setVisiblePrompts] = useState<typeof PROMPT_SUGGESTIONS>([]);
|
const [visiblePrompts, setVisiblePrompts] = useState(PROMPT_SUGGESTIONS.slice(0, 7));
|
||||||
|
const heroInitRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (heroInitRef.current) return;
|
||||||
|
heroInitRef.current = true;
|
||||||
const greetings = [
|
const greetings = [
|
||||||
"Ready to build?",
|
"Ready to build?",
|
||||||
"Let's automate something?",
|
"Let's automate something?",
|
||||||
@ -904,9 +909,6 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
|||||||
};
|
};
|
||||||
const allGreetings = [getTimeGreeting(), ...greetings];
|
const allGreetings = [getTimeGreeting(), ...greetings];
|
||||||
setGreeting(allGreetings[Math.floor(Math.random() * allGreetings.length)]);
|
setGreeting(allGreetings[Math.floor(Math.random() * allGreetings.length)]);
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const shuffled = [...PROMPT_SUGGESTIONS].sort(() => 0.5 - Math.random());
|
const shuffled = [...PROMPT_SUGGESTIONS].sort(() => 0.5 - Math.random());
|
||||||
setVisiblePrompts(shuffled.slice(0, 7));
|
setVisiblePrompts(shuffled.slice(0, 7));
|
||||||
}, []);
|
}, []);
|
||||||
@ -2156,7 +2158,6 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
|||||||
// ── Render ──
|
// ── Render ──
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutGroup>
|
|
||||||
<div
|
<div
|
||||||
className="h-full flex flex-col"
|
className="h-full flex flex-col"
|
||||||
style={{ background: "var(--color-main-bg)" }}
|
style={{ background: "var(--color-main-bg)" }}
|
||||||
@ -2319,6 +2320,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
|||||||
<div
|
<div
|
||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
className="flex-1 overflow-y-auto min-h-0"
|
className="flex-1 overflow-y-auto min-h-0"
|
||||||
|
style={{ scrollbarGutter: "stable" }}
|
||||||
>
|
>
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div
|
<div
|
||||||
@ -2342,70 +2344,27 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (showHeroState && !mounted) ? (
|
||||||
|
<div className="flex items-center justify-center h-full min-h-[60vh]" />
|
||||||
) : showHeroState ? (
|
) : showHeroState ? (
|
||||||
<div className="flex flex-col items-center justify-center min-h-[75vh] py-12">
|
<div className="flex flex-col items-center justify-center min-h-[75vh] py-12">
|
||||||
{/* Hero greeting */}
|
{/* Hero greeting */}
|
||||||
{greeting && (
|
{greeting && (
|
||||||
<motion.h1
|
<h1
|
||||||
layout="position"
|
|
||||||
className="text-4xl md:text-5xl font-light tracking-normal font-instrument mb-10 text-center"
|
className="text-4xl md:text-5xl font-light tracking-normal font-instrument mb-10 text-center"
|
||||||
style={{ color: "var(--color-text)" }}
|
style={{ color: "var(--color-text)" }}
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
variants={{
|
|
||||||
hidden: { opacity: 0 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: { staggerChildren: 0.12, delayChildren: 0.2 },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
transition={{ layout: { type: "spring", stiffness: 260, damping: 30 } }}
|
|
||||||
>
|
>
|
||||||
{greeting.split(" ").map((word, i) => (
|
{greeting}
|
||||||
<motion.span
|
</h1>
|
||||||
key={i}
|
|
||||||
className="inline-block mr-2"
|
|
||||||
variants={{
|
|
||||||
hidden: { opacity: 0, y: 20, filter: "blur(8px)" },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
filter: "blur(0px)",
|
|
||||||
transition: { duration: 0.8, ease: [0.2, 0.65, 0.3, 0.9] },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{word}
|
|
||||||
</motion.span>
|
|
||||||
))}
|
|
||||||
</motion.h1>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Centered input bar */}
|
{/* Centered input bar */}
|
||||||
<motion.div
|
<div className="w-full max-w-[720px] mx-auto px-4">
|
||||||
layout="position"
|
{inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)}
|
||||||
className="w-full max-w-[720px] mx-auto px-4"
|
</div>
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.8, ease: [0.22, 1, 0.36, 1], layout: { type: "spring", stiffness: 260, damping: 30 } }}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
layout
|
|
||||||
layoutId="chat-input-bar"
|
|
||||||
transition={{ type: "spring", stiffness: 260, damping: 30 }}
|
|
||||||
>
|
|
||||||
{inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)}
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Prompt suggestion pills */}
|
{/* Prompt suggestion pills */}
|
||||||
<motion.div
|
<div className="mt-6 flex flex-col gap-2.5 w-full max-w-[720px] mx-auto px-4">
|
||||||
layout="position"
|
|
||||||
className="mt-6 flex flex-col gap-2.5 w-full max-w-[720px] mx-auto px-4"
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5, delay: 1.0, ease: [0.22, 1, 0.36, 1], layout: { type: "spring", stiffness: 260, damping: 30 } }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center gap-2 flex-wrap">
|
<div className="flex items-center justify-center gap-2 flex-wrap">
|
||||||
{visiblePrompts.slice(0, 3).map((template) => {
|
{visiblePrompts.slice(0, 3).map((template) => {
|
||||||
const Icon = template.icon;
|
const Icon = template.icon;
|
||||||
@ -2448,7 +2407,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : messages.length === 0 ? (
|
) : messages.length === 0 ? (
|
||||||
<div className="flex items-center justify-center h-full min-h-[60vh]">
|
<div className="flex items-center justify-center h-full min-h-[60vh]">
|
||||||
@ -2546,13 +2505,7 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
|||||||
style={{ background: "var(--color-bg-glass)" }}
|
style={{ background: "var(--color-bg-glass)" }}
|
||||||
>
|
>
|
||||||
<div className={compact ? "" : "max-w-[720px] mx-auto"}>
|
<div className={compact ? "" : "max-w-[720px] mx-auto"}>
|
||||||
<motion.div
|
{inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)}
|
||||||
layout
|
|
||||||
layoutId="chat-input-bar"
|
|
||||||
transition={{ type: "spring", stiffness: 260, damping: 30 }}
|
|
||||||
>
|
|
||||||
{inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -2569,7 +2522,6 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</LayoutGroup>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -83,7 +83,7 @@ export const FileMentionNode = Node.create({
|
|||||||
},
|
},
|
||||||
HTMLAttributes,
|
HTMLAttributes,
|
||||||
),
|
),
|
||||||
`@${label}`,
|
label,
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -529,7 +529,17 @@ function WorkspacePageInner() {
|
|||||||
if (tabLoadedForWorkspace.current === key) return;
|
if (tabLoadedForWorkspace.current === key) return;
|
||||||
tabLoadedForWorkspace.current = key;
|
tabLoadedForWorkspace.current = key;
|
||||||
const loaded = loadTabs(key);
|
const loaded = loadTabs(key);
|
||||||
setTabState(loaded);
|
const hasNonHomeTabs = loaded.tabs.some((t) => t.id !== HOME_TAB_ID);
|
||||||
|
if (!hasNonHomeTabs) {
|
||||||
|
const newTab: Tab = {
|
||||||
|
id: generateTabId(),
|
||||||
|
type: "chat",
|
||||||
|
title: "New Chat",
|
||||||
|
};
|
||||||
|
setTabState(openTab(loaded, newTab));
|
||||||
|
} else {
|
||||||
|
setTabState(loaded);
|
||||||
|
}
|
||||||
}, [workspaceName]);
|
}, [workspaceName]);
|
||||||
|
|
||||||
// Persist tabs to localStorage on change (only after initial load for this workspace)
|
// Persist tabs to localStorage on change (only after initial load for this workspace)
|
||||||
@ -1017,7 +1027,25 @@ function WorkspacePageInner() {
|
|||||||
|
|
||||||
const handleTabClose = useCallback((tabId: string) => {
|
const handleTabClose = useCallback((tabId: string) => {
|
||||||
const prev = tabState;
|
const prev = tabState;
|
||||||
const next = closeTab(prev, tabId);
|
let next = closeTab(prev, tabId);
|
||||||
|
const hasNonHomeTabs = next.tabs.some((t) => t.id !== HOME_TAB_ID);
|
||||||
|
if (!hasNonHomeTabs) {
|
||||||
|
const newTab: Tab = {
|
||||||
|
id: generateTabId(),
|
||||||
|
type: "chat",
|
||||||
|
title: "New Chat",
|
||||||
|
};
|
||||||
|
next = openTab(next, newTab);
|
||||||
|
setTabState(next);
|
||||||
|
setActivePath(null);
|
||||||
|
setContent({ kind: "none" });
|
||||||
|
setActiveSessionId(null);
|
||||||
|
setActiveSubagentKey(null);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
void chatRef.current?.newSession();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
setTabState(next);
|
setTabState(next);
|
||||||
if (next.activeTabId !== prev.activeTabId) {
|
if (next.activeTabId !== prev.activeTabId) {
|
||||||
const newActive = next.tabs.find((t) => t.id === next.activeTabId);
|
const newActive = next.tabs.find((t) => t.id === next.activeTabId);
|
||||||
@ -1062,10 +1090,20 @@ function WorkspacePageInner() {
|
|||||||
|
|
||||||
const handleTabCloseAll = useCallback(() => {
|
const handleTabCloseAll = useCallback(() => {
|
||||||
setTabState((prev) => {
|
setTabState((prev) => {
|
||||||
const next = closeAllTabs(prev);
|
const closed = closeAllTabs(prev);
|
||||||
setActivePath(null);
|
setActivePath(null);
|
||||||
setContent({ kind: "none" });
|
setContent({ kind: "none" });
|
||||||
return next;
|
setActiveSessionId(null);
|
||||||
|
setActiveSubagentKey(null);
|
||||||
|
const newTab: Tab = {
|
||||||
|
id: generateTabId(),
|
||||||
|
type: "chat",
|
||||||
|
title: "New Chat",
|
||||||
|
};
|
||||||
|
return openTab(closed, newTab);
|
||||||
|
});
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
void chatRef.current?.newSession();
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user