Initial commit: Chat-to-diagram v1.0

- Chat interface with OpenAI GPT integration
- Automatic diagram generation from text descriptions
- Tldraw canvas with Dagre layout engine
- REST API instead of WebSocket approach

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-09 23:23:56 +01:00
commit 18ba831a2a
31 changed files with 11875 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
'use client';
import { useEffect, useRef } from 'react';
import { Editor } from 'tldraw';
import { useChatAPI } from '@/hooks/useChatAPI';
import { ChatMessage } from '@/components/ui/ChatMessage';
import { ChatInput } from '@/components/ui/ChatInput';
import { generateTldrawShapes, clearCanvas } from '@/lib/tldraw-helpers';
import { getAutoLayout } from '@/lib/layout-engine';
import { MessageSquare, Trash2 } from 'lucide-react';
interface ChatPanelProps {
editor: Editor | null;
}
/**
* Main chat panel component with message history and input
*/
export function ChatPanel({ editor }: ChatPanelProps) {
const { messages, isLoading, error, sendMessage, clearMessages } = useChatAPI();
const messagesEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// Generate diagram when AI returns diagram data
useEffect(() => {
const lastMessage = messages[messages.length - 1];
if (lastMessage?.diagramData && editor) {
console.log('Generating diagram from AI response:', lastMessage.diagramData);
// Clear canvas and generate new diagram
clearCanvas(editor);
const layout = getAutoLayout(lastMessage.diagramData);
generateTldrawShapes(layout, editor);
editor.zoomToFit();
}
}, [messages, editor]);
return (
<div className="flex flex-col h-full bg-white border-r border-gray-200">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 bg-gray-50">
<div className="flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-blue-500" />
<h2 className="font-semibold text-gray-900">Diagram Chat</h2>
</div>
<button
onClick={clearMessages}
disabled={messages.length === 0}
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Clear conversation"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
{/* Messages Area */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center p-6">
<MessageSquare className="w-12 h-12 text-gray-300 mb-3" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
Welcome!
</h3>
<p className="text-sm text-gray-600 max-w-sm">
Describe the diagram you want to create, and I'll generate it for you.
Try something like "Create a login flow diagram" or "Show me an e-commerce checkout process."
</p>
</div>
) : (
<>
{messages.map((message) => (
<ChatMessage key={message.id} message={message} />
))}
<div ref={messagesEndRef} />
</>
)}
</div>
{/* Error Display */}
{error && (
<div className="px-4 py-2 bg-red-50 border-t border-red-200">
<p className="text-sm text-red-600">Error: {error}</p>
</div>
)}
{/* Input Area */}
<div className="p-4 border-t border-gray-200 bg-gray-50">
<ChatInput
onSend={sendMessage}
isLoading={isLoading}
disabled={!editor}
/>
{!editor && (
<p className="text-xs text-gray-500 mt-2">
Waiting for canvas to load...
</p>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,22 @@
'use client';
import { Tldraw, Editor } from 'tldraw';
import 'tldraw/tldraw.css';
interface TldrawCanvasProps {
onEditorMount?: (editor: Editor) => void;
}
export function TldrawCanvas({ onEditorMount }: TldrawCanvasProps) {
return (
<div className="w-full h-screen">
<Tldraw
onMount={(editor) => {
if (onEditorMount) {
onEditorMount(editor);
}
}}
/>
</div>
);
}

View File

@@ -0,0 +1,56 @@
'use client';
import { useState, KeyboardEvent } from 'react';
import { Send, Loader2 } from 'lucide-react';
interface ChatInputProps {
onSend: (message: string) => void;
isLoading: boolean;
disabled?: boolean;
}
/**
* Chat input component with send button
*/
export function ChatInput({ onSend, isLoading, disabled }: ChatInputProps) {
const [input, setInput] = useState('');
const handleSend = () => {
if (input.trim() && !isLoading && !disabled) {
onSend(input.trim());
setInput('');
}
};
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div className="flex gap-2 items-end">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Describe the diagram you want to create..."
disabled={isLoading || disabled}
className="flex-1 min-h-[60px] max-h-[120px] p-3 border border-gray-300 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed text-gray-900 bg-white placeholder:text-gray-400"
rows={2}
/>
<button
onClick={handleSend}
disabled={!input.trim() || isLoading || disabled}
className="px-4 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
>
{isLoading ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Send className="w-5 h-5" />
)}
</button>
</div>
);
}

View File

@@ -0,0 +1,62 @@
'use client';
import { ChatMessage as ChatMessageType } from '@/hooks/useChatAPI';
import { User, Bot, CheckCircle } from 'lucide-react';
interface ChatMessageProps {
message: ChatMessageType;
}
/**
* Individual chat message display component
*/
export function ChatMessage({ message }: ChatMessageProps) {
const isUser = message.role === 'user';
const isAssistant = message.role === 'assistant';
return (
<div
className={`flex gap-3 p-4 ${
isUser ? 'bg-blue-50' : 'bg-gray-50'
} rounded-lg`}
>
{/* Avatar */}
<div
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
isUser ? 'bg-blue-500' : 'bg-gray-700'
}`}
>
{isUser ? (
<User className="w-5 h-5 text-white" />
) : (
<Bot className="w-5 h-5 text-white" />
)}
</div>
{/* Message Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-sm text-gray-900">
{isUser ? 'You' : 'Assistant'}
</span>
<span className="text-xs text-gray-500">
{message.timestamp.toLocaleTimeString()}
</span>
{message.diagramData && (
<span className="flex items-center gap-1 text-xs text-green-600 font-medium">
<CheckCircle className="w-3 h-3" />
Diagram generated
</span>
)}
</div>
<div className="text-sm text-gray-800 whitespace-pre-wrap break-words">
{/* Remove diagram markers from display */}
{message.content
.replace(/\[DIAGRAM_START\][\s\S]*?\[DIAGRAM_END\]/g, '[Diagram generated]')
.trim()}
</div>
</div>
</div>
);
}