From 110cccb09cd7fcf15a4c65a6a4c32e1ad1cf16a3 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Wed, 3 Dec 2025 21:49:34 +0900 Subject: [PATCH] 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 --- app/globals.css | 253 ++++++++++++++++++------- app/layout.tsx | 14 +- app/page.tsx | 73 ++++---- components/chat-example-panel.tsx | 131 ++++++++----- components/chat-input.tsx | 245 ++++++++++++------------ components/chat-message-display.tsx | 281 ++++++++++++++++++---------- components/chat-panel.tsx | 139 +++++++------- components/code-block.tsx | 39 ++++ package-lock.json | 20 ++ package.json | 1 + 10 files changed, 746 insertions(+), 450 deletions(-) create mode 100644 components/code-block.tsx diff --git a/app/globals.css b/app/globals.css index b70a101..d0cdabb 100644 --- a/app/globals.css +++ b/app/globals.css @@ -7,8 +7,8 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -45,72 +45,102 @@ } :root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); + --radius: 0.75rem; + + /* Clean Light Modern Palette */ + --background: oklch(0.985 0.002 240); + --foreground: oklch(0.23 0.02 260); + --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); + --card-foreground: oklch(0.23 0.02 260); + --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --popover-foreground: oklch(0.23 0.02 260); + + /* Dark primary - slightly lighter */ + --primary: oklch(0.35 0.01 260); + --primary-foreground: oklch(0.99 0 0); + + /* Warm gray secondary */ + --secondary: oklch(0.96 0.005 260); + --secondary-foreground: oklch(0.35 0.02 260); + + /* Light muted tones */ + --muted: oklch(0.965 0.005 260); + --muted-foreground: oklch(0.50 0.02 260); + + /* Soft lavender accent */ + --accent: oklch(0.94 0.03 280); + --accent-foreground: oklch(0.35 0.08 270); + + /* Coral destructive */ + --destructive: oklch(0.60 0.20 25); + + /* Subtle borders */ + --border: oklch(0.92 0.01 260); + --input: oklch(0.94 0.01 260); + --ring: oklch(0.25 0.01 260); + + /* Chart colors - harmonious palette */ + --chart-1: oklch(0.55 0.18 265); + --chart-2: oklch(0.65 0.15 170); + --chart-3: oklch(0.70 0.18 45); + --chart-4: oklch(0.60 0.20 330); + --chart-5: oklch(0.50 0.15 200); + + /* Sidebar */ + --sidebar: oklch(0.99 0.002 260); + --sidebar-foreground: oklch(0.23 0.02 260); + --sidebar-primary: oklch(0.55 0.18 265); + --sidebar-primary-foreground: oklch(0.99 0 0); + --sidebar-accent: oklch(0.96 0.02 270); + --sidebar-accent-foreground: oklch(0.35 0.05 265); + --sidebar-border: oklch(0.93 0.01 260); + --sidebar-ring: oklch(0.55 0.18 265); } .dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); + --background: oklch(0.15 0.015 260); + --foreground: oklch(0.95 0.01 260); + + --card: oklch(0.20 0.015 260); + --card-foreground: oklch(0.95 0.01 260); + + --popover: oklch(0.20 0.015 260); + --popover-foreground: oklch(0.95 0.01 260); + + --primary: oklch(0.70 0.16 265); + --primary-foreground: oklch(0.15 0.02 260); + + --secondary: oklch(0.25 0.015 260); + --secondary-foreground: oklch(0.90 0.01 260); + + --muted: oklch(0.25 0.015 260); + --muted-foreground: oklch(0.65 0.02 260); + + --accent: oklch(0.30 0.04 280); + --accent-foreground: oklch(0.90 0.03 270); + + --destructive: oklch(0.65 0.22 25); + + --border: oklch(0.28 0.015 260); + --input: oklch(0.25 0.015 260); + --ring: oklch(0.70 0.16 265); + + --chart-1: oklch(0.70 0.16 265); + --chart-2: oklch(0.70 0.13 170); + --chart-3: oklch(0.75 0.16 45); + --chart-4: oklch(0.70 0.18 330); + --chart-5: oklch(0.60 0.13 200); + + --sidebar: oklch(0.18 0.015 260); + --sidebar-foreground: oklch(0.95 0.01 260); + --sidebar-primary: oklch(0.70 0.16 265); + --sidebar-primary-foreground: oklch(0.15 0.02 260); + --sidebar-accent: oklch(0.25 0.03 270); + --sidebar-accent-foreground: oklch(0.90 0.02 265); + --sidebar-border: oklch(0.28 0.015 260); + --sidebar-ring: oklch(0.70 0.16 265); } @layer base { @@ -118,6 +148,101 @@ @apply border-border outline-ring/50; } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground font-sans; } } + +/* Custom scrollbar */ +@layer utilities { + .scrollbar-thin { + scrollbar-width: thin; + scrollbar-color: oklch(0.85 0.01 260) transparent; + } + + .scrollbar-thin::-webkit-scrollbar { + width: 6px; + } + + .scrollbar-thin::-webkit-scrollbar-track { + background: transparent; + } + + .scrollbar-thin::-webkit-scrollbar-thumb { + background-color: oklch(0.85 0.01 260); + border-radius: 3px; + } + + .scrollbar-thin::-webkit-scrollbar-thumb:hover { + background-color: oklch(0.75 0.01 260); + } +} + +/* Smooth page transitions */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(16px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.animate-fade-in { + animation: fadeIn 0.3s ease-out forwards; +} + +.animate-slide-in-right { + animation: slideInRight 0.3s ease-out forwards; +} + +/* Message bubble animations */ +@keyframes messageIn { + from { + opacity: 0; + transform: translateY(12px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.animate-message-in { + animation: messageIn 0.25s ease-out forwards; +} + +/* Subtle floating shadow for cards */ +.shadow-soft { + box-shadow: + 0 1px 2px oklch(0.23 0.02 260 / 0.04), + 0 4px 12px oklch(0.23 0.02 260 / 0.06), + 0 8px 24px oklch(0.23 0.02 260 / 0.04); +} + +.shadow-soft-lg { + box-shadow: + 0 2px 4px oklch(0.23 0.02 260 / 0.04), + 0 8px 20px oklch(0.23 0.02 260 / 0.08), + 0 16px 40px oklch(0.23 0.02 260 / 0.06); +} + +/* Gradient text utility */ +.text-gradient-primary { + background: linear-gradient(135deg, oklch(0.55 0.18 265), oklch(0.60 0.20 290)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} diff --git a/app/layout.tsx b/app/layout.tsx index bf028fc..8a5b84e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,19 +1,21 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Plus_Jakarta_Sans, JetBrains_Mono } from "next/font/google"; import { Analytics } from "@vercel/analytics/react"; import { GoogleAnalytics } from "@next/third-parties/google"; import { DiagramProvider } from "@/contexts/diagram-context"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", +const plusJakarta = Plus_Jakarta_Sans({ + variable: "--font-sans", subsets: ["latin"], + weight: ["400", "500", "600", "700"], }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const jetbrainsMono = JetBrains_Mono({ + variable: "--font-mono", subsets: ["latin"], + weight: ["400", "500"], }); export const metadata: Metadata = { @@ -91,7 +93,7 @@ export default function RootLayout({ /> {children} diff --git a/app/page.tsx b/app/page.tsx index e1ef22e..d575d94 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,6 +3,7 @@ import React, { useState, useEffect } from "react"; import { DrawIoEmbed } from "react-drawio"; import ChatPanel from "@/components/chat-panel"; import { useDiagram } from "@/contexts/diagram-context"; +import { Monitor } from "lucide-react"; export default function Home() { const { drawioRef, handleDiagramExport } = useDiagram(); @@ -14,17 +15,11 @@ export default function Home() { setIsMobile(window.innerWidth < 768); }; - // Check on mount checkMobile(); - - // Add event listener for resize window.addEventListener("resize", checkMobile); - - // Cleanup return () => window.removeEventListener("resize", checkMobile); }, []); - // Add keyboard shortcut for toggling chat panel (Ctrl+B) useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if ((event.ctrlKey || event.metaKey) && event.key === 'b') { @@ -34,42 +29,56 @@ export default function Home() { }; window.addEventListener('keydown', handleKeyDown); - - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; + return () => window.removeEventListener('keydown', handleKeyDown); }, []); return ( -
- {/* Mobile warning overlay - keeps components mounted */} +
+ {/* Mobile warning overlay */} {isMobile && ( -
-
-

- Please open this application on a desktop or laptop +
+
+
+ +
+

+ Desktop Required

+

+ This application works best on desktop or laptop devices. Please open it on a larger screen for the full experience. +

)} -
- + {/* Draw.io Canvas */} +
+
+ +
-
- setIsChatVisible(!isChatVisible)} - /> + + {/* Chat Panel */} +
+
+ setIsChatVisible(!isChatVisible)} + /> +
); diff --git a/components/chat-example-panel.tsx b/components/chat-example-panel.tsx index 114c268..591e623 100644 --- a/components/chat-example-panel.tsx +++ b/components/chat-example-panel.tsx @@ -1,3 +1,37 @@ +"use client"; + +import { Zap, Cloud, GitBranch, Palette } from "lucide-react"; + +interface ExampleCardProps { + icon: React.ReactNode; + title: string; + description: string; + onClick: () => void; +} + +function ExampleCard({ icon, title, description, onClick }: ExampleCardProps) { + return ( + + ); +} + export default function ExamplePanel({ setInput, setFiles, @@ -5,80 +39,85 @@ export default function ExamplePanel({ setInput: (input: string) => void; setFiles: (files: File[]) => void; }) { - // New handler for the "Replicate this flowchart" button const handleReplicateFlowchart = async () => { setInput("Replicate this flowchart."); try { - // Fetch the example image const response = await fetch("/example.png"); const blob = await response.blob(); const file = new File([blob], "example.png", { type: "image/png" }); - - // Set the file to the files state setFiles([file]); } catch (error) { console.error("Error loading example image:", error); } }; - // Handler for the "Replicate this in aws style" button const handleReplicateArchitecture = async () => { setInput("Replicate this in aws style"); try { - // Fetch the architecture image const response = await fetch("/architecture.png"); const blob = await response.blob(); const file = new File([blob], "architecture.png", { type: "image/png", }); - - // Set the file to the files state setFiles([file]); } catch (error) { console.error("Error loading architecture image:", error); } }; + return ( -
-

- {" "} - Start a conversation to generate or modify diagrams. -

-

- {" "} - You can also upload images to use as references. -

-

- Try these examples{" "} - (cached for instant response): -

-
- - - - +
+ {/* Welcome section */} +
+

+ Create diagrams with AI +

+

+ Describe what you want to create or upload an image to replicate +

+
+ + {/* Examples grid */} +
+

+ Quick Examples +

+ +
+ } + title="Animated Diagram" + description="Draw a transformer architecture with animated connectors" + onClick={() => setInput("Give me a **animated connector** diagram of transformer's architecture")} + /> + + } + title="AWS Architecture" + description="Create a cloud architecture diagram with AWS icons" + onClick={handleReplicateArchitecture} + /> + + } + title="Replicate Flowchart" + description="Upload and replicate an existing flowchart" + onClick={handleReplicateFlowchart} + /> + + } + title="Creative Drawing" + description="Draw something fun and creative" + onClick={() => setInput("Draw a cat for me")} + /> +
+ +

+ Examples are cached for instant response +

); diff --git a/components/chat-input.tsx b/components/chat-input.tsx index 0e91c2d..e8cfd52 100644 --- a/components/chat-input.tsx +++ b/components/chat-input.tsx @@ -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) => { 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) => { 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 (
- - -