Restore chat-panel UI improvements
This commit is contained in:
@@ -9,7 +9,7 @@ import React, {
|
|||||||
type CSSProperties,
|
type CSSProperties,
|
||||||
} from "react";
|
} from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowDown, useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useParams, usePathname } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
@@ -284,14 +284,11 @@ function summarizeToolResult(result?: string): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plain-text heuristics
|
// Plain-text heuristics
|
||||||
// We explicitly ignore 'error' and 'exception' here because tools like dev_server_logs
|
|
||||||
// or browser_console legitimately return stack traces when working correctly.
|
|
||||||
// A raw string with 'error' inside it shouldn't auto-fail the tool execution pill.
|
|
||||||
const lower = raw.toLowerCase();
|
const lower = raw.toLowerCase();
|
||||||
if (
|
if (
|
||||||
/(econnrefused|enoent|permission denied|command not found)/.test(
|
/(econnrefused|enoent|error|failed|traceback|exception|not found|permission denied|cannot)/.test(
|
||||||
lower,
|
lower,
|
||||||
) && !raw.includes("dev_server_logs") && !raw.includes("browser_console")
|
)
|
||||||
) {
|
) {
|
||||||
return { ok: false, label: `Failed — ${firstLine(raw)}` };
|
return { ok: false, label: `Failed — ${firstLine(raw)}` };
|
||||||
}
|
}
|
||||||
@@ -520,21 +517,28 @@ const MessageBubble = React.memo(function MessageBubble({
|
|||||||
style={{
|
style={{
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "#f4f4f5", // Zinc-100 instead of black
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
|
border: "1px solid #e4e4e7",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ color: "#a1a1aa" }}>
|
<span
|
||||||
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>
|
style={{
|
||||||
<path d="M5 3v4"/>
|
color: "#18181b", // Dark gray instead of white
|
||||||
<path d="M19 17v4"/>
|
fontSize: "0.6rem",
|
||||||
<path d="M3 5h4"/>
|
fontWeight: 700,
|
||||||
<path d="M17 19h4"/>
|
fontFamily: "var(--font-lora),serif",
|
||||||
</svg>
|
fontStyle: "italic",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
V.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
@@ -664,76 +668,79 @@ function Timeline({ entries, isActiveStream }: { entries: TimelineEntry[], isAct
|
|||||||
|
|
||||||
function TimelineThought({ text, isStreaming }: { text: string; isStreaming?: boolean }) {
|
function TimelineThought({ text, isStreaming }: { text: string; isStreaming?: boolean }) {
|
||||||
const [expanded, setExpanded] = React.useState(true);
|
const [expanded, setExpanded] = React.useState(true);
|
||||||
|
|
||||||
// Auto-collapse when streaming stops
|
|
||||||
const textLenRef = React.useRef(text.length);
|
const textLenRef = React.useRef(text.length);
|
||||||
React.useEffect(() => {
|
|
||||||
let t = setTimeout(() => {
|
|
||||||
if (text.length === textLenRef.current && !isStreaming) {
|
|
||||||
setExpanded(false);
|
|
||||||
}
|
|
||||||
}, 1500);
|
|
||||||
return () => clearTimeout(t);
|
|
||||||
}, [text, isStreaming]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
textLenRef.current = text.length;
|
// If not streaming, auto-collapse after a short delay so the user isn't stuck with huge thinking blocks
|
||||||
}, [text]);
|
if (!isStreaming) {
|
||||||
|
const t = setTimeout(() => setExpanded(false), 500);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}
|
||||||
|
}, [isStreaming]);
|
||||||
|
|
||||||
|
const proseWrap: React.CSSProperties = {
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
minWidth: 0,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
margin: "8px 0",
|
margin: "12px 0",
|
||||||
fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
|
fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => setExpanded((v) => !v)}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 8,
|
gap: 6,
|
||||||
background: "transparent",
|
background: "#f4f4f5",
|
||||||
border: "none",
|
border: "1px solid #e4e4e7",
|
||||||
padding: 0,
|
borderRadius: "999px",
|
||||||
|
padding: "4px 10px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.75rem",
|
||||||
color: "#71717a",
|
color: "#52525b",
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
boxShadow: "0 1px 2px rgba(0,0,0,0.02)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<Sparkles
|
||||||
|
size={14}
|
||||||
|
className={isStreaming ? "animate-pulse" : ""}
|
||||||
|
style={{ color: "#8b5cf6" }}
|
||||||
|
/>
|
||||||
|
<span className={isStreaming ? "animate-pulse" : ""} style={{ transition: "opacity 0.2s ease" }}>
|
||||||
|
{isStreaming ? "Thinking..." : "Thought Process"}
|
||||||
|
</span>
|
||||||
|
<ChevronRight
|
||||||
|
size={14}
|
||||||
style={{
|
style={{
|
||||||
transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
|
transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
|
||||||
transition: "transform 0.15s ease",
|
transition: "transform 0.2s ease",
|
||||||
display: "inline-block",
|
|
||||||
fontSize: "0.65rem",
|
|
||||||
color: "#a1a1aa"
|
color: "#a1a1aa"
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
▶
|
|
||||||
</span>
|
|
||||||
<span className={isStreaming ? "animate-pulse" : ""} style={{ transition: "opacity 0.2s ease" }}>
|
|
||||||
Analyzed task
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
marginLeft: 6,
|
padding: "10px 14px",
|
||||||
paddingLeft: 12,
|
borderLeft: "2px solid #c4b5fd",
|
||||||
borderLeft: "2px solid #e5e7eb",
|
fontSize: "0.85rem",
|
||||||
fontSize: "0.84rem",
|
|
||||||
color: "#52525b",
|
color: "#52525b",
|
||||||
lineHeight: 1.6,
|
background: "linear-gradient(to right, #f5f3ff 0%, transparent 100%)",
|
||||||
whiteSpace: "pre-wrap",
|
borderRadius: "0 8px 8px 0",
|
||||||
overflowWrap: "anywhere",
|
...proseWrap,
|
||||||
wordBreak: "break-word",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
style={proseWrap}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: renderMarkdown(stripRawToolLogs(text)) + (isStreaming ? `<span class="animate-pulse" style="display:inline-block; width:6px; height:13px; background-color:#9ca3af; vertical-align:-1px; margin-left:2px; border-radius:1px;"></span>` : ""),
|
__html: renderMarkdown(stripRawToolLogs(text)) + (isStreaming ? `<span class="animate-pulse" style="display:inline-block; width:6px; height:13px; background-color:#9ca3af; vertical-align:-1px; margin-left:2px; border-radius:1px;"></span>` : ""),
|
||||||
}}
|
}}
|
||||||
@@ -1139,13 +1146,12 @@ export function ChatPanel({
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [projectId, workspace, status]);
|
}, [projectId, workspace, status]);
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||||
const [currentPhaseLabel, setCurrentPhaseLabel] = useState<string | null>(
|
const [currentPhaseLabel, setCurrentPhaseLabel] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [showThreads, setShowThreads] = useState(false);
|
const [showThreads, setShowThreads] = useState(false);
|
||||||
const [mcpToken, setMcpToken] = useState<string | null>(null);
|
const [mcpToken, setMcpToken] = useState<string | null>(null);
|
||||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [isChatMinimized, setIsChatMinimized] = useState<boolean>(false);
|
const [isChatMinimized, setIsChatMinimized] = useState<boolean>(false);
|
||||||
|
|
||||||
// Auto-minimize when navigating to dashboard, auto-open when navigating to preview
|
// Auto-minimize when navigating to dashboard, auto-open when navigating to preview
|
||||||
@@ -1629,15 +1635,12 @@ export function ChatPanel({
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 2. If it's a raw string (like a bash crash), scan for fatal keywords
|
// 2. If it's a raw string (like a bash crash), scan for fatal keywords
|
||||||
// We skip this check for log-reading tools since they legitimately contain errors.
|
|
||||||
const lower = ev.result.toLowerCase();
|
const lower = ev.result.toLowerCase();
|
||||||
if (
|
if (
|
||||||
!ev.name?.includes("logs") &&
|
lower.includes("error") ||
|
||||||
!ev.name?.includes("console") &&
|
lower.includes("failed") ||
|
||||||
(lower.includes("econnrefused") ||
|
lower.includes("unexpected") ||
|
||||||
lower.includes("enoent") ||
|
lower.includes("not found")
|
||||||
lower.includes("permission denied") ||
|
|
||||||
lower.includes("command not found"))
|
|
||||||
) {
|
) {
|
||||||
isToolErr = true;
|
isToolErr = true;
|
||||||
}
|
}
|
||||||
@@ -1974,6 +1977,11 @@ export function ChatPanel({
|
|||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div
|
<div
|
||||||
|
onScroll={(e) => {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
||||||
|
const distanceToBottom = scrollHeight - scrollTop - clientHeight;
|
||||||
|
setShowScrollButton(distanceToBottom > 150);
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
@@ -2203,6 +2211,35 @@ export function ChatPanel({
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Scroll to bottom button */}
|
||||||
|
{showScrollButton && (
|
||||||
|
<button
|
||||||
|
onClick={scrollToBottom}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "calc(100% + 12px)",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
background: "#ffffff",
|
||||||
|
border: "1px solid #e4e4e7",
|
||||||
|
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
|
||||||
|
borderRadius: "50%",
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
zIndex: 50,
|
||||||
|
color: "#52525b",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
aria-label="Scroll to bottom"
|
||||||
|
>
|
||||||
|
<ArrowDown size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<ProjectPreviewChatInputWrap unifiedShell={unifiedProjectShell}>
|
<ProjectPreviewChatInputWrap unifiedShell={unifiedProjectShell}>
|
||||||
{(selectToggle) => (
|
{(selectToggle) => (
|
||||||
<div
|
<div
|
||||||
@@ -2998,36 +3035,3 @@ export function ChatPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function ScrollToBottomButton({ onClick, visible }: { onClick: () => void, visible: boolean }) {
|
|
||||||
if (!visible) return null;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
bottom: "80px",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
background: "#ffffff",
|
|
||||||
border: "1px solid #e5e7eb",
|
|
||||||
borderRadius: "999px",
|
|
||||||
padding: "6px",
|
|
||||||
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
|
|
||||||
cursor: "pointer",
|
|
||||||
color: "#4b5563",
|
|
||||||
zIndex: 50,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
transition: "all 0.2s ease"
|
|
||||||
}}
|
|
||||||
aria-label="Scroll to bottom"
|
|
||||||
>
|
|
||||||
<ArrowDown size={16} />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user