Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,59 @@
.chat-edge-throb { animation: none; }
}

/* Debug mode pulsing effect - red edges */
@keyframes debug-pulse {
0%, 100% {
box-shadow:
inset 4px 0 12px rgba(239, 68, 68, 0.1),
inset -4px 0 12px rgba(239, 68, 68, 0.1);
}
50% {
box-shadow:
inset 4px 0 20px rgba(239, 68, 68, 0.2),
inset -4px 0 20px rgba(239, 68, 68, 0.2);
}
}

.animate-debug-pulse {
animation: debug-pulse 2s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
.animate-debug-pulse { animation: none; }
}

/* Custom scrollbar for debug bubble */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}

.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 3px;
}

.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(239, 68, 68, 0.3);
border-radius: 3px;
}

.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(239, 68, 68, 0.5);
}

.dark .custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}

.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(239, 68, 68, 0.4);
}

.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(239, 68, 68, 0.6);
}

@custom-variant dark (&:is(.dark *));
@plugin '@tailwindcss/typography';
@theme inline {
Expand Down
189 changes: 123 additions & 66 deletions src/routes/chat/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useParams, useSearchParams, useNavigate } from 'react-router';
import { MonacoEditor } from '../../components/monaco-editor/monaco-editor';
import { AnimatePresence, motion } from 'framer-motion';
import { Expand, Github, GitBranch, LoaderCircle, RefreshCw, MoreHorizontal, RotateCcw, X } from 'lucide-react';
import clsx from 'clsx';
import { Blueprint } from './components/blueprint';
import { FileExplorer } from './components/file-explorer';
import { UserMessage, AIMessage } from './components/messages';
Expand Down Expand Up @@ -134,6 +135,10 @@ export default function Chat() {
shouldRefreshPreview,
// Preview deployment state
isPreviewDeploying,
// Issue tracking and debugging state
runtimeErrorCount,
staticIssueCount,
isDebugging,
} = useChat({
chatId: urlChatId,
query: userQuery,
Expand Down Expand Up @@ -410,7 +415,8 @@ export default function Chat() {
}, [isGeneratingBlueprint, view]);

useEffect(() => {
if (doneStreaming && !isGeneratingBlueprint && !blueprint) {
// Only show bootstrap completion message for NEW chats, not when reloading existing ones
if (doneStreaming && !isGeneratingBlueprint && !blueprint && urlChatId === 'new') {
onCompleteBootstrap();
sendAiMessage(
createAIMessage(
Expand All @@ -426,6 +432,7 @@ export default function Chat() {
sendAiMessage,
blueprint,
onCompleteBootstrap,
urlChatId,
]);

const isRunning = useMemo(() => {
Expand All @@ -434,17 +441,15 @@ export default function Chat() {
);
}, [isBootstrapping, isGeneratingBlueprint]);

// Check if chat input should be disabled (before blueprint completion and agentId assignment)
// Check if chat input should be disabled (before blueprint completion, or during debugging)
const isChatDisabled = useMemo(() => {
const blueprintStage = projectStages.find(
(stage) => stage.id === 'blueprint',
);
const isBlueprintComplete = blueprintStage?.status === 'completed';
const hasAgentId = !!chatId;
const blueprintNotCompleted = !blueprintStage || blueprintStage.status !== 'completed';

// Disable until both blueprint is complete AND we have an agentId
return !isBlueprintComplete || !hasAgentId;
}, [projectStages, chatId]);
return blueprintNotCompleted || isDebugging;
}, [projectStages, isDebugging]);

const chatFormRef = useRef<HTMLFormElement>(null);
const { isDragging: isChatDragging, dragHandlers: chatDragHandlers } = useDragDrop({
Expand Down Expand Up @@ -526,7 +531,13 @@ export default function Chat() {
layout="position"
className="flex-1 shrink-0 flex flex-col basis-0 max-w-lg relative z-10 h-full min-h-0"
>
<div className="flex-1 overflow-y-auto min-h-0 chat-messages-scroll" ref={messagesContainerRef}>
<div
className={clsx(
'flex-1 overflow-y-auto min-h-0 chat-messages-scroll',
isDebugging && 'animate-debug-pulse'
)}
ref={messagesContainerRef}
>
<div className="pt-5 px-4 pb-4 text-sm flex flex-col gap-5">
{appLoading ? (
<div className="flex items-center gap-2 text-text-tertiary">
Expand Down Expand Up @@ -559,39 +570,60 @@ export default function Chat() {
)}

{mainMessage && (
<div className="relative">
<div className="relative">
<AIMessage
message={mainMessage.content}
isThinking={mainMessage.ui?.isThinking}
toolEvents={mainMessage.ui?.toolEvents}
/>
{chatId && (
<div className="absolute right-1 top-1">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="hover:bg-bg-3/80 cursor-pointer"
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Chat actions</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setIsResetDialogOpen(true);
}}
>
<RotateCcw className="h-4 w-4 mr-2" />
Reset conversation
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</div>
)}

{otherMessages
.filter(message => message.role === 'assistant' && message.ui?.isThinking)
.map((message) => (
<div key={message.conversationId} className="mb-4">
<AIMessage
message={message.content}
isThinking={true}
toolEvents={message.ui?.toolEvents}
/>
</div>
))}

{isThinking && !otherMessages.some(m => m.ui?.isThinking) && (
<div className="mb-4">
<AIMessage
message={mainMessage.content}
isThinking={mainMessage.ui?.isThinking}
toolEvents={mainMessage.ui?.toolEvents}
message="Planning next phase..."
isThinking={true}
/>
{chatId && (
<div className="absolute right-1 top-1">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="hover:bg-bg-3/80 cursor-pointer"
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Chat actions</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setIsResetDialogOpen(true);
}}
>
<RotateCcw className="h-4 w-4 mr-2" />
Reset conversation
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</div>
)}

Expand All @@ -614,6 +646,11 @@ export default function Chat() {
chatId={chatId}
isDeploying={isDeploying}
handleDeployToCloudflare={handleDeployToCloudflare}
runtimeErrorCount={runtimeErrorCount}
staticIssueCount={staticIssueCount}
isDebugging={isDebugging}
isGenerating={isGenerating}
isThinking={isThinking}
/>

{/* Deployment and Generation Controls */}
Expand Down Expand Up @@ -654,24 +691,27 @@ export default function Chat() {
</motion.div>
)}

{otherMessages.map((message) => {
if (message.role === 'assistant') {
{otherMessages
.filter(message => !message.ui?.isThinking)
.map((message) => {
if (message.role === 'assistant') {
return (
<AIMessage
key={message.conversationId}
message={message.content}
isThinking={message.ui?.isThinking}
toolEvents={message.ui?.toolEvents}
/>
);
}
return (
<AIMessage
<UserMessage
key={message.conversationId}
message={message.content}
isThinking={message.ui?.isThinking}
toolEvents={message.ui?.toolEvents}
/>
);
}
return (
<UserMessage
key={message.conversationId}
message={message.content}
/>
);
})}
})}

</div>
</div>

Expand Down Expand Up @@ -737,11 +777,13 @@ export default function Chat() {
}}
disabled={isChatDisabled}
placeholder={
isChatDisabled
? 'Please wait for blueprint completion...'
: isRunning
? 'Chat with AI while generating...'
: 'Ask a follow up...'
isDebugging
? 'Deep debugging in progress... Please abort to continue'
: isChatDisabled
? 'Please wait for blueprint completion...'
: isRunning
? 'Chat with AI while generating...'
: 'Chat with AI...'
}
rows={1}
className="w-full bg-bg-2 border border-text-primary/10 rounded-xl px-3 pr-20 py-2 text-sm outline-none focus:border-white/20 drop-shadow-2xl text-text-primary placeholder:!text-text-primary/50 disabled:opacity-50 disabled:cursor-not-allowed resize-none overflow-y-auto no-scrollbar min-h-[36px] max-h-[120px]"
Expand All @@ -759,7 +801,7 @@ export default function Chat() {
}}
/>
<div className="absolute right-1.5 bottom-2.5 flex items-center gap-1">
{(isGenerating || isGeneratingBlueprint) && (
{(isGenerating || isGeneratingBlueprint || isDebugging) && (
<button
type="button"
onClick={() => {
Expand Down Expand Up @@ -926,14 +968,29 @@ export default function Chat() {
{view === 'blueprint' && (
<div className="flex-1 flex flex-col bg-bg-3 rounded-xl shadow-md shadow-bg-2 overflow-hidden border border-border-primary">
{/* Toolbar */}
<div className="flex items-center justify-center px-2 h-10 bg-bg-2 border-b">
<div className="flex items-center gap-2">
<span className="text-sm text-text-50/70 font-mono">
Blueprint.md
</span>
{previewUrl && (
<Copy text={previewUrl} />
)}
<div className="grid grid-cols-3 px-2 h-10 bg-bg-2 border-b">
<div className="flex items-center">
<ViewModeSwitch
view={view}
onChange={handleViewModeChange}
previewAvailable={!!previewUrl}
showTooltip={showTooltip}
/>
</div>

<div className="flex items-center justify-center">
<div className="flex items-center gap-2">
<span className="text-sm text-text-50/70 font-mono">
Blueprint.md
</span>
{previewUrl && (
<Copy text={previewUrl} />
)}
</div>
</div>

<div className="flex items-center justify-end">
{/* Right side - can add actions here if needed */}
</div>
</div>
<div className="flex-1 overflow-y-auto bg-bg-3">
Expand Down
Loading