mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 06:42:27 +08:00
feat: refresh UI with new typography and edit diff display (#63)
- Switch from Geist to Plus Jakarta Sans (body) and JetBrains Mono (code) - Add visual diff display for edit_diagram tool showing search/replace pairs - Update color palette to clean modern OKLCH-based scheme - Improve chat message display with better styling and animations - Add syntax-highlighted code blocks for XML/JSON output - Improve scrollbar and shadow utilities
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
Image as ImageIcon,
|
||||
History,
|
||||
Download,
|
||||
Paperclip,
|
||||
} from "lucide-react";
|
||||
import { ButtonWithTooltip } from "@/components/button-with-tooltip";
|
||||
import { FilePreviewList } from "./file-preview-list";
|
||||
@@ -48,13 +49,12 @@ export function ChatInput({
|
||||
const [showClearDialog, setShowClearDialog] = useState(false);
|
||||
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
||||
|
||||
// Debug: Log status changes
|
||||
const isDisabled = status === "streaming" || status === "submitted";
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[ChatInput] Status changed to:', status, '| Input disabled:', isDisabled);
|
||||
}, [status, isDisabled]);
|
||||
|
||||
// Auto-resize textarea based on content
|
||||
const adjustTextareaHeight = useCallback(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
@@ -67,7 +67,6 @@ export function ChatInput({
|
||||
adjustTextareaHeight();
|
||||
}, [input, adjustTextareaHeight]);
|
||||
|
||||
// Handle keyboard shortcuts and paste events
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -78,7 +77,6 @@ export function ChatInput({
|
||||
}
|
||||
};
|
||||
|
||||
// Handle clipboard paste
|
||||
const handlePaste = async (e: React.ClipboardEvent) => {
|
||||
if (isDisabled) return;
|
||||
|
||||
@@ -92,7 +90,6 @@ export function ChatInput({
|
||||
imageItems.map(async (item) => {
|
||||
const file = item.getAsFile();
|
||||
if (!file) return null;
|
||||
// Create a new file with a unique name
|
||||
return new File(
|
||||
[file],
|
||||
`pasted-image-${Date.now()}.${file.type.split("/")[1]}`,
|
||||
@@ -112,13 +109,11 @@ export function ChatInput({
|
||||
}
|
||||
};
|
||||
|
||||
// Handle file changes
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newFiles = Array.from(e.target.files || []);
|
||||
onFileChange([...files, ...newFiles]);
|
||||
};
|
||||
|
||||
// Remove individual file
|
||||
const handleRemoveFile = (fileToRemove: File) => {
|
||||
onFileChange(files.filter((file) => file !== fileToRemove));
|
||||
if (fileInputRef.current) {
|
||||
@@ -126,12 +121,10 @@ export function ChatInput({
|
||||
}
|
||||
};
|
||||
|
||||
// Trigger file input click
|
||||
const triggerFileInput = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
// Handle drag events
|
||||
const handleDragOver = (e: React.DragEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -153,7 +146,6 @@ export function ChatInput({
|
||||
|
||||
const droppedFiles = e.dataTransfer.files;
|
||||
|
||||
// Only process image files
|
||||
const imageFiles = Array.from(droppedFiles).filter((file) =>
|
||||
file.type.startsWith("image/")
|
||||
);
|
||||
@@ -163,141 +155,148 @@ export function ChatInput({
|
||||
}
|
||||
};
|
||||
|
||||
// Handle clearing conversation and diagram
|
||||
const handleClear = () => {
|
||||
onClearChat();
|
||||
setShowClearDialog(false);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className={`w-full space-y-2 ${
|
||||
className={`w-full transition-all duration-200 ${
|
||||
isDragging
|
||||
? "border-2 border-dashed border-primary p-4 rounded-lg bg-muted/20"
|
||||
? "ring-2 ring-primary ring-offset-2 rounded-2xl"
|
||||
: ""
|
||||
}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<FilePreviewList files={files} onRemoveFile={handleRemoveFile} />
|
||||
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
placeholder="Describe what changes you want to make to the diagram
|
||||
or upload(paste) an image to replicate a diagram.
|
||||
(Press Cmd/Ctrl + Enter to send)"
|
||||
disabled={isDisabled}
|
||||
aria-label="Chat input"
|
||||
className="min-h-[80px] resize-none transition-all duration-200 px-1 py-0"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mr-auto">
|
||||
<ButtonWithTooltip
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowClearDialog(true)}
|
||||
tooltipContent="Clear current conversation and diagram"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</ButtonWithTooltip>
|
||||
|
||||
{/* Warning Modal */}
|
||||
<ResetWarningModal
|
||||
open={showClearDialog}
|
||||
onOpenChange={setShowClearDialog}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
|
||||
<HistoryDialog
|
||||
showHistory={showHistory}
|
||||
onToggleHistory={onToggleHistory}
|
||||
/>
|
||||
{/* File previews */}
|
||||
{files.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<FilePreviewList files={files} onRemoveFile={handleRemoveFile} />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{/* History Button */}
|
||||
<ButtonWithTooltip
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => onToggleHistory(true)}
|
||||
disabled={
|
||||
isDisabled ||
|
||||
diagramHistory.length === 0
|
||||
}
|
||||
title="Diagram History"
|
||||
tooltipContent="View diagram history"
|
||||
>
|
||||
<History className="h-4 w-4" />
|
||||
</ButtonWithTooltip>
|
||||
)}
|
||||
|
||||
{/* Save Diagram Button */}
|
||||
<ButtonWithTooltip
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setShowSaveDialog(true)}
|
||||
disabled={isDisabled}
|
||||
tooltipContent="Save diagram to local file"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</ButtonWithTooltip>
|
||||
{/* Input container */}
|
||||
<div className="relative rounded-2xl border border-border bg-background shadow-sm focus-within:ring-2 focus-within:ring-primary/20 focus-within:border-primary/50 transition-all duration-200">
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
placeholder="Describe your diagram or paste an image..."
|
||||
disabled={isDisabled}
|
||||
aria-label="Chat input"
|
||||
className="min-h-[60px] max-h-[200px] resize-none border-0 bg-transparent px-4 py-3 text-sm focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-muted-foreground/60"
|
||||
/>
|
||||
|
||||
<SaveDialog
|
||||
open={showSaveDialog}
|
||||
onOpenChange={setShowSaveDialog}
|
||||
onSave={saveDiagramToFile}
|
||||
defaultFilename={`diagram-${new Date().toISOString().slice(0, 10)}`}
|
||||
/>
|
||||
{/* Action bar */}
|
||||
<div className="flex items-center justify-between px-3 py-2 border-t border-border/50">
|
||||
{/* Left actions */}
|
||||
<div className="flex items-center gap-1">
|
||||
<ButtonWithTooltip
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowClearDialog(true)}
|
||||
tooltipContent="Clear conversation"
|
||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</ButtonWithTooltip>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={triggerFileInput}
|
||||
disabled={isDisabled}
|
||||
title="Upload image"
|
||||
>
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<ResetWarningModal
|
||||
open={showClearDialog}
|
||||
onOpenChange={setShowClearDialog}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
multiple
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<HistoryDialog
|
||||
showHistory={showHistory}
|
||||
onToggleHistory={onToggleHistory}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right actions */}
|
||||
<div className="flex items-center gap-1">
|
||||
<ButtonWithTooltip
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onToggleHistory(true)}
|
||||
disabled={isDisabled || diagramHistory.length === 0}
|
||||
tooltipContent="Diagram history"
|
||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<History className="h-4 w-4" />
|
||||
</ButtonWithTooltip>
|
||||
|
||||
<ButtonWithTooltip
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowSaveDialog(true)}
|
||||
disabled={isDisabled}
|
||||
tooltipContent="Save diagram"
|
||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</ButtonWithTooltip>
|
||||
|
||||
<SaveDialog
|
||||
open={showSaveDialog}
|
||||
onOpenChange={setShowSaveDialog}
|
||||
onSave={saveDiagramToFile}
|
||||
defaultFilename={`diagram-${new Date().toISOString().slice(0, 10)}`}
|
||||
/>
|
||||
|
||||
<ButtonWithTooltip
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={triggerFileInput}
|
||||
disabled={isDisabled}
|
||||
tooltipContent="Upload image"
|
||||
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
</ButtonWithTooltip>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
multiple
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
|
||||
<div className="w-px h-5 bg-border mx-1" />
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isDisabled || !input.trim()}
|
||||
size="sm"
|
||||
className="h-8 px-4 rounded-xl font-medium shadow-sm"
|
||||
aria-label={isDisabled ? "Sending..." : "Send message"}
|
||||
>
|
||||
{isDisabled ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Send className="h-4 w-4 mr-1.5" />
|
||||
Send
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isDisabled || !input.trim()}
|
||||
className="transition-opacity"
|
||||
aria-label={
|
||||
isDisabled
|
||||
? "Sending message..."
|
||||
: "Send message"
|
||||
}
|
||||
>
|
||||
{isDisabled ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user