Browse Source

feat(32-02): add Build Workflow button to ChatPanel

Add "Build Workflow" button that appears after conversation starts. Button
extracts user messages to form a description and passes to parent via callback.
Includes loading state support via isBuildingWorkflow prop.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
ea13c7d913
  1. 114
      src/components/ChatPanel.tsx

114
src/components/ChatPanel.tsx

@ -7,9 +7,11 @@ import { DefaultChatTransport } from "ai";
interface ChatPanelProps {
isOpen: boolean;
onClose: () => void;
onBuildWorkflow?: (description: string) => Promise<void>;
isBuildingWorkflow?: boolean;
}
export function ChatPanel({ isOpen, onClose }: ChatPanelProps) {
export function ChatPanel({ isOpen, onClose, onBuildWorkflow, isBuildingWorkflow = false }: ChatPanelProps) {
const messagesEndRef = useRef<HTMLDivElement>(null);
const [input, setInput] = useState("");
@ -19,6 +21,34 @@ export function ChatPanel({ isOpen, onClose }: ChatPanelProps) {
const isLoading = status === "streaming" || status === "submitted";
// Extract conversation description from user messages
const getConversationDescription = () => {
const userMessages = messages
.filter((m) => m.role === "user")
.map((m) => {
const textContent = m.parts
?.filter((part): part is { type: "text"; text: string } => part.type === "text")
.map((part) => part.text)
.join("") || "";
return textContent;
})
.filter((text) => text.trim().length > 0);
return userMessages.join("\n");
};
const handleBuildWorkflow = async () => {
if (!onBuildWorkflow) return;
const description = getConversationDescription();
if (!description.trim()) return;
await onBuildWorkflow(description);
};
// Check if conversation has started (has assistant messages)
const hasConversation = messages.some((m) => m.role === "assistant");
// Auto-scroll to bottom on new messages
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
@ -97,36 +127,58 @@ export function ChatPanel({ isOpen, onClose }: ChatPanelProps) {
</div>
{/* Input area */}
<form
onSubmit={(e) => {
e.preventDefault();
if (input.trim() && !isLoading) {
sendMessage({ text: input });
setInput("");
}
}}
className="p-3 border-t border-neutral-700"
>
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
className="flex-1 bg-neutral-700 border border-neutral-600 rounded-lg px-3 py-2 text-sm text-neutral-200 placeholder-neutral-500 focus:outline-none focus:border-blue-500"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-3 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
</button>
</div>
</form>
<div className="border-t border-neutral-700">
<form
onSubmit={(e) => {
e.preventDefault();
if (input.trim() && !isLoading) {
sendMessage({ text: input });
setInput("");
}
}}
className="p-3"
>
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
className="flex-1 bg-neutral-700 border border-neutral-600 rounded-lg px-3 py-2 text-sm text-neutral-200 placeholder-neutral-500 focus:outline-none focus:border-blue-500"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="px-3 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
</button>
</div>
</form>
{/* Build Workflow button */}
{hasConversation && onBuildWorkflow && (
<div className="px-3 pb-3">
<button
onClick={handleBuildWorkflow}
disabled={isBuildingWorkflow || isLoading}
className="w-full bg-green-600 hover:bg-green-500 text-white rounded-lg px-4 py-2 text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2"
>
{isBuildingWorkflow ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>Building...</span>
</>
) : (
<span>Build Workflow</span>
)}
</button>
</div>
)}
</div>
</div>
);
}

Loading…
Cancel
Save