feat(FloatingChat): refactor chat width handling to be dynamic; enhance message panel positioning and styling with glass surface effects
This commit is contained in:
@@ -6,7 +6,7 @@ import { X, ArrowUp } from 'lucide-react';
|
||||
import {
|
||||
useFloatingChat,
|
||||
computeDualAnchor,
|
||||
CHAT_WIDTH,
|
||||
getChatWidth,
|
||||
CHAT_HEIGHT,
|
||||
PADDING,
|
||||
} from '@/context/FloatingChatContext';
|
||||
@@ -66,7 +66,7 @@ function FloatingChatInner() {
|
||||
if (route === 'project' && state.projectId) {
|
||||
// Navigate to the project page (stay on same project)
|
||||
// Project sections re-register on mount and pendingSection will auto-open
|
||||
void navigate({ to: '/projects/$projectId', params: { projectId: state.projectId } });
|
||||
void navigate({ to: '/projects', search: { projectId: state.projectId } });
|
||||
} else if (route.startsWith('/')) {
|
||||
void navigate({ to: route });
|
||||
}
|
||||
@@ -154,7 +154,7 @@ function FloatingChatInner() {
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.right > window.innerWidth || rect.bottom > window.innerHeight) {
|
||||
el.style.left = `${Math.max(PADDING, Math.min(state.position.x, window.innerWidth - CHAT_WIDTH - PADDING))}px`;
|
||||
el.style.left = `${Math.max(PADDING, Math.min(state.position.x, window.innerWidth - getChatWidth() - PADDING))}px`;
|
||||
el.style.top = `${Math.max(PADDING, Math.min(state.position.y, window.innerHeight - CHAT_HEIGHT - PADDING))}px`;
|
||||
}
|
||||
}
|
||||
@@ -241,6 +241,10 @@ function FloatingChatInner() {
|
||||
|
||||
const hasMessages = messages.length > 0 || isStreaming;
|
||||
|
||||
// Expand the messages panel upward if there's enough space above the input bar,
|
||||
// otherwise expand downward. 320px = 300px max-h + 8px gap + 12px buffer.
|
||||
const expandUp = state.position.y >= 320;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{state.isOpen && (
|
||||
@@ -260,30 +264,37 @@ function FloatingChatInner() {
|
||||
width: state.position.width,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
className="flex flex-col gap-2"
|
||||
className="relative"
|
||||
>
|
||||
{/* ---- Messages panel (appears when chat has content) ---- */}
|
||||
{/* ---- Messages panel — floats above or below the input bar ---- */}
|
||||
<AnimatePresence>
|
||||
{hasMessages && (
|
||||
<motion.div
|
||||
key="messages-panel"
|
||||
initial={{ opacity: 0, height: 0, scale: 0.97 }}
|
||||
animate={{ opacity: 1, height: 'auto', scale: 1 }}
|
||||
exit={{ opacity: 0, height: 0, scale: 0.97 }}
|
||||
initial={{ opacity: 0, scale: 0.97, y: expandUp ? 8 : -8 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.97, y: expandUp ? 8 : -8 }}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
||||
className="rounded-2xl"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
...(expandUp
|
||||
? { bottom: 'calc(100% + 8px)' }
|
||||
: { top: 'calc(100% + 8px)' }),
|
||||
}}
|
||||
className="rounded-2xl overflow-hidden"
|
||||
>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="max-h-[300px] overflow-y-auto rounded-2xl [&::-webkit-scrollbar]:w-2.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border"
|
||||
className="max-h-[300px] overflow-y-auto rounded-2xl [&::-webkit-scrollbar]:w-2.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/40"
|
||||
>
|
||||
<div className="flex flex-col gap-2.5 p-3">
|
||||
{messages.map((msg) => {
|
||||
if (msg.role === 'user') {
|
||||
return (
|
||||
<div key={msg.id} className="flex justify-end">
|
||||
<div className="max-w-[80%] rounded-2xl rounded-br-md bg-accent text-primary-foreground px-3.5 py-2 shadow-sm">
|
||||
<p className="text-xs whitespace-pre-wrap leading-relaxed">
|
||||
<div className="glass-surface-subtle max-w-[80%] rounded-2xl rounded-br-md px-3.5 py-2">
|
||||
<p className="text-xs whitespace-pre-wrap leading-relaxed text-foreground">
|
||||
{msg.content}
|
||||
</p>
|
||||
</div>
|
||||
@@ -294,7 +305,7 @@ function FloatingChatInner() {
|
||||
if (msg.error) {
|
||||
return (
|
||||
<div key={msg.id} className="flex justify-start">
|
||||
<div className="max-w-[80%] rounded-2xl rounded-bl-md bg-destructive/10 px-3.5 py-2">
|
||||
<div className="glass-surface-subtle max-w-[80%] rounded-2xl rounded-bl-md px-3.5 py-2 !border-destructive/30">
|
||||
<p className="text-xs text-destructive whitespace-pre-wrap leading-relaxed">
|
||||
{msg.content}
|
||||
</p>
|
||||
@@ -305,8 +316,8 @@ function FloatingChatInner() {
|
||||
|
||||
return (
|
||||
<div key={msg.id} className="flex justify-start">
|
||||
<div className="max-w-[80%] rounded-2xl rounded-bl-md bg-primary text-primary-foreground px-3.5 py-2">
|
||||
<div className="text-xs">
|
||||
<div className="glass-surface-subtle max-w-[80%] rounded-2xl rounded-bl-md px-3.5 py-2">
|
||||
<div className="text-xs text-foreground">
|
||||
<ChatMarkdown content={msg.content} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,9 +328,9 @@ function FloatingChatInner() {
|
||||
{/* Streaming */}
|
||||
{isStreaming && (
|
||||
<div className="flex justify-start">
|
||||
<div className="max-w-[80%] rounded-2xl rounded-bl-md bg-primary text-primary-foreground px-3.5 py-2">
|
||||
<div className="glass-surface-subtle max-w-[80%] rounded-2xl rounded-bl-md px-3.5 py-2">
|
||||
{streamingContent ? (
|
||||
<div className="text-xs">
|
||||
<div className="text-xs text-foreground">
|
||||
<ChatMarkdown content={streamingContent} />
|
||||
</div>
|
||||
) : (
|
||||
@@ -338,7 +349,7 @@ function FloatingChatInner() {
|
||||
</AnimatePresence>
|
||||
|
||||
{/* ---- Floating input bar ---- */}
|
||||
<div className="relative rounded-2xl bg-background/80 backdrop-blur-2xl shadow-[0_8px_60px_-12px_rgba(0,0,0,0.5)] border border-border/30 ring-1 ring-white/5 transition-shadow focus-within:shadow-[0_8px_60px_-8px_rgba(0,0,0,0.7)] focus-within:ring-ring/20">
|
||||
<div className="glass-surface relative rounded-2xl transition-shadow focus-within:shadow-[0_8px_60px_-8px_rgba(0,0,0,0.35)]">
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={close}
|
||||
|
||||
Reference in New Issue
Block a user