design(chat): add paperclip button to chat input and auto-resize textarea
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
Sparkles,
|
||||
Compass,
|
||||
Cpu,
|
||||
Paperclip,
|
||||
} from "lucide-react";
|
||||
import { ProjectIconRail } from "@/components/project/project-icon-rail";
|
||||
import {
|
||||
@@ -1965,118 +1966,6 @@ export function ChatPanel({
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Chat Mode Toggle */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 4,
|
||||
marginBottom: 8,
|
||||
padding: "2px",
|
||||
background: "#f0ede8",
|
||||
borderRadius: 8,
|
||||
border: "1px solid #e8e4dc",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setChatMode("vibe")}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
padding: "6px 0",
|
||||
fontSize: "0.72rem",
|
||||
fontWeight: chatMode === "vibe" ? 600 : 500,
|
||||
borderRadius: 6,
|
||||
border: "none",
|
||||
background: chatMode === "vibe" ? "#fff" : "transparent",
|
||||
color: chatMode === "vibe" ? "#3d5afe" : "#6b6560",
|
||||
cursor: "pointer",
|
||||
boxShadow:
|
||||
chatMode === "vibe" ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
|
||||
transition: "all 0.1s ease",
|
||||
}}
|
||||
title="Vibe Code: Fast, iterative coding with immediate live previews."
|
||||
>
|
||||
<Sparkles
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
color: chatMode === "vibe" ? "#3d5afe" : "#8c8580",
|
||||
}}
|
||||
/>
|
||||
<span>Vibe Code</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setChatMode("collaborate")}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
padding: "6px 0",
|
||||
fontSize: "0.72rem",
|
||||
fontWeight: chatMode === "collaborate" ? 600 : 500,
|
||||
borderRadius: 6,
|
||||
border: "none",
|
||||
background: chatMode === "collaborate" ? "#fff" : "transparent",
|
||||
color: chatMode === "collaborate" ? "#d4a04a" : "#6b6560",
|
||||
cursor: "pointer",
|
||||
boxShadow:
|
||||
chatMode === "collaborate"
|
||||
? "0 1px 2px rgba(0,0,0,0.05)"
|
||||
: "none",
|
||||
transition: "all 0.1s ease",
|
||||
}}
|
||||
title="Architect: Brainstorm, spec out features, and plan architecture without writing code."
|
||||
>
|
||||
<Compass
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
color: chatMode === "collaborate" ? "#d4a04a" : "#8c8580",
|
||||
}}
|
||||
/>
|
||||
<span>Architect</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setChatMode("delegate")}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
padding: "6px 0",
|
||||
fontSize: "0.72rem",
|
||||
fontWeight: chatMode === "delegate" ? 600 : 500,
|
||||
borderRadius: 6,
|
||||
border: "none",
|
||||
background: chatMode === "delegate" ? "#fff" : "transparent",
|
||||
color: chatMode === "delegate" ? "#2e7d32" : "#6b6560",
|
||||
cursor: "pointer",
|
||||
boxShadow:
|
||||
chatMode === "delegate" ? "0 1px 2px rgba(0,0,0,0.05)" : "none",
|
||||
transition: "all 0.1s ease",
|
||||
}}
|
||||
title="Delegate Offline: Send this task to the background runner to build automatically while you step away."
|
||||
>
|
||||
<Cpu
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
color: chatMode === "delegate" ? "#2e7d32" : "#8c8580",
|
||||
}}
|
||||
/>
|
||||
<span>Delegate</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!mcpToken && (
|
||||
<div
|
||||
style={{
|
||||
@@ -2241,13 +2130,13 @@ export function ChatPanel({
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
alignItems: "center",
|
||||
background: "#fff",
|
||||
borderRadius: 10,
|
||||
borderRadius: 12,
|
||||
border: "1px solid #e8e4dc",
|
||||
padding: "8px 10px",
|
||||
boxShadow: "0 1px 3px #1a1a1a05",
|
||||
padding: "10px 12px",
|
||||
boxShadow: "0 2px 6px #1a1a1a08",
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
@@ -2258,86 +2147,116 @@ export function ChatPanel({
|
||||
placeholder={
|
||||
sending ? "Esc to stop generating…" : "Ask Vibn AI anything…"
|
||||
}
|
||||
rows={1}
|
||||
rows={3}
|
||||
disabled={!activeThread}
|
||||
style={{
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
background: "transparent",
|
||||
fontSize: "0.84rem",
|
||||
lineHeight: 1.5,
|
||||
fontSize: "0.86rem",
|
||||
lineHeight: 1.55,
|
||||
resize: "none",
|
||||
fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
|
||||
color: "#1a1a1a",
|
||||
maxHeight: 120,
|
||||
maxHeight: 240,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
onInput={(e) => {
|
||||
const el = e.currentTarget;
|
||||
const newlines = (el.value.match(/\n/g) || []).length;
|
||||
// Cache lastNewlines on the DOM element to avoid state re-render lag
|
||||
if ((el as any).lastNewlines !== newlines) {
|
||||
(el as any).lastNewlines = newlines;
|
||||
el.style.height = "auto";
|
||||
el.style.height = Math.min(el.scrollHeight, 120) + "px";
|
||||
el.style.height = Math.min(el.scrollHeight, 240) + "px";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{selectToggle}
|
||||
{(() => {
|
||||
// While the AI is streaming or running tools, the button
|
||||
// turns into a Stop control. Click → AbortController fires,
|
||||
// server bails between rounds, partial text gets persisted.
|
||||
const isActive = sending;
|
||||
const canSend = !sending && input.trim() && activeThread;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingTop: 4,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={isActive ? cancelMessage : () => sendMessage()}
|
||||
disabled={!isActive && !canSend}
|
||||
aria-label={isActive ? "Stop generating" : "Send message"}
|
||||
title={isActive ? "Stop generating (Esc)" : "Send"}
|
||||
onClick={() => {
|
||||
const fname = window.prompt(
|
||||
"Enter exact file path to attach (e.g. src/app/page.tsx):",
|
||||
);
|
||||
if (fname && fname.trim()) {
|
||||
setAttachedFiles((prev) => [...prev, fname.trim()]);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
...COMPOSER_ACTION_BTN_BASE,
|
||||
background: isActive
|
||||
? "#1a1a1a"
|
||||
: canSend
|
||||
? "#1a1a1a"
|
||||
: "#e8e4dc",
|
||||
color: isActive || canSend ? "#fff" : "#a09a90",
|
||||
border: "none",
|
||||
cursor: isActive || canSend ? "pointer" : "default",
|
||||
transition: "all 0.15s",
|
||||
position: "relative",
|
||||
background: "transparent",
|
||||
color: "#8c8580",
|
||||
border: "1px solid transparent",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.1s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = "#f0ede8";
|
||||
e.currentTarget.style.color = "#1a1a1a";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = "transparent";
|
||||
e.currentTarget.style.color = "#8c8580";
|
||||
}}
|
||||
title="Attach file to context"
|
||||
>
|
||||
{isActive ? (
|
||||
<>
|
||||
<Loader2
|
||||
style={{
|
||||
width: 15,
|
||||
height: 15,
|
||||
position: "absolute",
|
||||
opacity: 0.35,
|
||||
}}
|
||||
className="animate-spin"
|
||||
/>
|
||||
<Square
|
||||
style={{
|
||||
width: 9,
|
||||
height: 9,
|
||||
fill: "#fff",
|
||||
strokeWidth: 0,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Send style={{ width: 15, height: 15 }} />
|
||||
)}
|
||||
<Paperclip style={{ width: 14, height: 14 }} />
|
||||
</button>
|
||||
);
|
||||
})()}
|
||||
{selectToggle}
|
||||
</div>
|
||||
{(() => {
|
||||
// While the AI is streaming or running tools, the button
|
||||
// turns into a Stop control. Click → AbortController fires,
|
||||
// server bails between rounds, partial text gets persisted.
|
||||
const isActive = sending;
|
||||
const canSend = !sending && input.trim() && activeThread;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={isActive ? cancelMessage : () => sendMessage()}
|
||||
disabled={!isActive && !canSend}
|
||||
aria-label={isActive ? "Stop generating" : "Send message"}
|
||||
title={isActive ? "Stop generating (Esc)" : "Send"}
|
||||
style={{
|
||||
...COMPOSER_ACTION_BTN_BASE,
|
||||
background: isActive
|
||||
? "#1a1a1a"
|
||||
: canSend
|
||||
? "#1a1a1a"
|
||||
: "#e8e4dc",
|
||||
color: isActive || canSend ? "#fff" : "#a09a90",
|
||||
border: "none",
|
||||
cursor: isActive || canSend ? "pointer" : "not-allowed",
|
||||
transition: "all 0.15s ease",
|
||||
}}
|
||||
>
|
||||
{isActive ? (
|
||||
<>
|
||||
<Square
|
||||
style={{
|
||||
width: 11,
|
||||
height: 11,
|
||||
fill: "currentColor",
|
||||
strokeWidth: 3,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Send style={{ width: 14, height: 14 }} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPreviewChatInputWrap>
|
||||
|
||||
Reference in New Issue
Block a user