mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-28 09:56:23 +02:00
style:
- improve how chat history is shown on UI - add time stamp to past chats - imrove icon look and placement in title bar feat: - add new chat icon to title bar
This commit is contained in:
parent
b905a19747
commit
72e6d17e5d
2 changed files with 130 additions and 32 deletions
|
|
@ -6,7 +6,7 @@ import type { LanguageModelUsage, ToolUIPart } from 'ai';
|
|||
import './App.css'
|
||||
import z from 'zod';
|
||||
import { Button } from './components/ui/button';
|
||||
import { CheckIcon, LoaderIcon, ArrowUp, PanelLeftIcon, PanelRightIcon, Square, X, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
||||
import { CheckIcon, LoaderIcon, ArrowUp, PanelLeftIcon, PanelRightIcon, Square, X, ChevronLeftIcon, ChevronRightIcon, SquarePen } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { MarkdownEditor } from './components/markdown-editor';
|
||||
import { ChatInputBar } from './components/chat-button';
|
||||
|
|
@ -126,6 +126,12 @@ const graphPalette = [
|
|||
]
|
||||
|
||||
const MACOS_TRAFFIC_LIGHTS_RESERVED_PX = 12 + 12 * 3 + 8 * 2
|
||||
const TITLEBAR_BUTTON_PX = 32
|
||||
const TITLEBAR_BUTTON_GAP_PX = 4
|
||||
const TITLEBAR_HEADER_GAP_PX = 8
|
||||
const TITLEBAR_TOGGLE_MARGIN_LEFT_PX = 12
|
||||
const TITLEBAR_BUTTONS_COLLAPSED = 4
|
||||
const TITLEBAR_BUTTON_GAPS_COLLAPSED = 3
|
||||
|
||||
const clampNumber = (value: number, min: number, max: number) =>
|
||||
Math.min(max, Math.max(min, value))
|
||||
|
|
@ -469,15 +475,18 @@ function FixedSidebarToggle({
|
|||
onNavigateForward,
|
||||
canNavigateBack,
|
||||
canNavigateForward,
|
||||
onNewChat,
|
||||
leftInsetPx,
|
||||
}: {
|
||||
onNavigateBack: () => void
|
||||
onNavigateForward: () => void
|
||||
canNavigateBack: boolean
|
||||
canNavigateForward: boolean
|
||||
onNewChat: () => void
|
||||
leftInsetPx: number
|
||||
}) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
const { toggleSidebar, state } = useSidebar()
|
||||
const isCollapsed = state === "collapsed"
|
||||
return (
|
||||
<div className="fixed left-0 top-0 z-50 flex h-10 items-center" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
|
||||
<div aria-hidden="true" className="h-10 shrink-0" style={{ width: leftInsetPx }} />
|
||||
|
|
@ -485,36 +494,62 @@ function FixedSidebarToggle({
|
|||
<button
|
||||
type="button"
|
||||
onClick={toggleSidebar}
|
||||
className="ml-3 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
||||
className="ml-3 flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
||||
aria-label="Toggle Sidebar"
|
||||
>
|
||||
<PanelLeftIcon className="size-4" />
|
||||
<PanelLeftIcon className="size-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNewChat}
|
||||
className="ml-1 flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
||||
aria-label="New chat"
|
||||
>
|
||||
<SquarePen className="size-5" />
|
||||
</button>
|
||||
{/* Back / Forward navigation */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNavigateBack}
|
||||
disabled={!canNavigateBack}
|
||||
className="ml-1 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors disabled:opacity-30 disabled:pointer-events-none"
|
||||
aria-label="Go back"
|
||||
>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNavigateForward}
|
||||
disabled={!canNavigateForward}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors disabled:opacity-30 disabled:pointer-events-none"
|
||||
aria-label="Go forward"
|
||||
>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</button>
|
||||
{isCollapsed && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNavigateBack}
|
||||
disabled={!canNavigateBack}
|
||||
className="ml-1 flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors disabled:opacity-30 disabled:pointer-events-none"
|
||||
aria-label="Go back"
|
||||
>
|
||||
<ChevronLeftIcon className="size-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNavigateForward}
|
||||
disabled={!canNavigateForward}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors disabled:opacity-30 disabled:pointer-events-none"
|
||||
aria-label="Go forward"
|
||||
>
|
||||
<ChevronRightIcon className="size-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Main content header that adjusts padding based on sidebar state */
|
||||
function ContentHeader({ children }: { children: React.ReactNode }) {
|
||||
function ContentHeader({
|
||||
children,
|
||||
onNavigateBack,
|
||||
onNavigateForward,
|
||||
canNavigateBack,
|
||||
canNavigateForward,
|
||||
collapsedLeftPaddingPx,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onNavigateBack?: () => void
|
||||
onNavigateForward?: () => void
|
||||
canNavigateBack?: boolean
|
||||
canNavigateForward?: boolean
|
||||
collapsedLeftPaddingPx?: number
|
||||
}) {
|
||||
const { state } = useSidebar()
|
||||
const isCollapsed = state === "collapsed"
|
||||
return (
|
||||
|
|
@ -523,9 +558,35 @@ function ContentHeader({ children }: { children: React.ReactNode }) {
|
|||
"titlebar-drag-region flex h-10 shrink-0 items-center gap-2 border-b border-border px-3 bg-sidebar transition-[padding] duration-200 ease-linear",
|
||||
// When the sidebar is collapsed the content area shifts left, so we need enough left padding
|
||||
// to avoid overlapping the fixed traffic-lights/toggle/back/forward controls.
|
||||
isCollapsed && "pl-[168px]"
|
||||
isCollapsed && !collapsedLeftPaddingPx && "pl-[168px]"
|
||||
)}
|
||||
style={isCollapsed && collapsedLeftPaddingPx ? { paddingLeft: collapsedLeftPaddingPx } : undefined}
|
||||
>
|
||||
{!isCollapsed && onNavigateBack && onNavigateForward ? (
|
||||
<div className="titlebar-no-drag flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNavigateBack}
|
||||
disabled={!canNavigateBack}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors disabled:opacity-30 disabled:pointer-events-none"
|
||||
aria-label="Go back"
|
||||
>
|
||||
<ChevronLeftIcon className="size-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNavigateForward}
|
||||
disabled={!canNavigateForward}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors disabled:opacity-30 disabled:pointer-events-none"
|
||||
aria-label="Go forward"
|
||||
>
|
||||
<ChevronRightIcon className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{onNavigateBack && onNavigateForward ? (
|
||||
<div className="titlebar-no-drag self-stretch w-px bg-border/70" aria-hidden="true" />
|
||||
) : null}
|
||||
{children}
|
||||
</header>
|
||||
)
|
||||
|
|
@ -549,6 +610,13 @@ function App() {
|
|||
const [graphStatus, setGraphStatus] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle')
|
||||
const [graphError, setGraphError] = useState<string | null>(null)
|
||||
const [isChatSidebarOpen, setIsChatSidebarOpen] = useState(true)
|
||||
const isMac = typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('mac')
|
||||
const collapsedLeftPaddingPx =
|
||||
(isMac ? MACOS_TRAFFIC_LIGHTS_RESERVED_PX : 0) +
|
||||
TITLEBAR_TOGGLE_MARGIN_LEFT_PX +
|
||||
TITLEBAR_BUTTON_PX * TITLEBAR_BUTTONS_COLLAPSED +
|
||||
TITLEBAR_BUTTON_GAP_PX * TITLEBAR_BUTTON_GAPS_COLLAPSED +
|
||||
TITLEBAR_HEADER_GAP_PX
|
||||
|
||||
// Keep the latest selected path in a ref (avoids stale async updates when switching rapidly)
|
||||
const selectedPathRef = useRef<string | null>(null)
|
||||
|
|
@ -602,7 +670,6 @@ function App() {
|
|||
|
||||
// Onboarding state
|
||||
const [showOnboarding, setShowOnboarding] = useState(false)
|
||||
const isMac = typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('mac')
|
||||
|
||||
// Background tasks state
|
||||
type BackgroundTaskItem = {
|
||||
|
|
@ -2216,7 +2283,13 @@ function App() {
|
|||
/>
|
||||
<SidebarInset className="overflow-hidden! min-h-0">
|
||||
{/* Header - also serves as titlebar drag region, adjusts padding when sidebar collapsed */}
|
||||
<ContentHeader>
|
||||
<ContentHeader
|
||||
onNavigateBack={() => { void navigateBack() }}
|
||||
onNavigateForward={() => { void navigateForward() }}
|
||||
canNavigateBack={canNavigateBack}
|
||||
canNavigateForward={canNavigateForward}
|
||||
collapsedLeftPaddingPx={collapsedLeftPaddingPx}
|
||||
>
|
||||
<span className="text-sm font-medium text-muted-foreground flex-1 min-w-0 truncate">
|
||||
{headerTitle}
|
||||
</span>
|
||||
|
|
@ -2249,20 +2322,20 @@ function App() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleCloseFullScreenChat}
|
||||
className="titlebar-no-drag flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
||||
className="titlebar-no-drag flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
||||
aria-label="Return to file"
|
||||
>
|
||||
<X className="size-4" />
|
||||
<X className="size-5" />
|
||||
</button>
|
||||
)}
|
||||
{(selectedPath || isGraphOpen) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsChatSidebarOpen(!isChatSidebarOpen)}
|
||||
className="titlebar-no-drag flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors -mr-1"
|
||||
className="titlebar-no-drag flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors -mr-1"
|
||||
aria-label="Toggle Chat Sidebar"
|
||||
>
|
||||
<PanelRightIcon className="size-4" />
|
||||
<PanelRightIcon className="size-5" />
|
||||
</button>
|
||||
)}
|
||||
</ContentHeader>
|
||||
|
|
@ -2447,6 +2520,7 @@ function App() {
|
|||
onNavigateForward={() => { void navigateForward() }}
|
||||
canNavigateBack={canNavigateBack}
|
||||
canNavigateForward={canNavigateForward}
|
||||
onNewChat={handleNewChat}
|
||||
leftInsetPx={isMac ? MACOS_TRAFFIC_LIGHTS_RESERVED_PX : 0}
|
||||
/>
|
||||
</SidebarProvider>
|
||||
|
|
|
|||
|
|
@ -159,6 +159,25 @@ function formatEventTime(ts: string): string {
|
|||
return date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })
|
||||
}
|
||||
|
||||
function formatRunTime(ts: string): string {
|
||||
const date = new Date(ts)
|
||||
if (Number.isNaN(date.getTime())) return ""
|
||||
const now = Date.now()
|
||||
const diffMs = Math.max(0, now - date.getTime())
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60))
|
||||
const diffHours = Math.floor(diffMinutes / 60)
|
||||
const diffDays = Math.floor(diffHours / 24)
|
||||
const diffWeeks = Math.floor(diffDays / 7)
|
||||
const diffMonths = Math.floor(diffDays / 30)
|
||||
|
||||
if (diffMinutes < 1) return "just now"
|
||||
if (diffMinutes < 60) return `${diffMinutes} m`
|
||||
if (diffHours < 24) return `${diffHours} h`
|
||||
if (diffDays < 7) return `${diffDays} d`
|
||||
if (diffWeeks < 4) return `${diffWeeks} w`
|
||||
return `${Math.max(1, diffMonths)} m`
|
||||
}
|
||||
|
||||
function SyncStatusBar() {
|
||||
const { state, isMobile } = useSidebar()
|
||||
const [activeServices, setActiveServices] = useState<Map<string, string>>(new Map())
|
||||
|
|
@ -1045,11 +1064,16 @@ function TasksSection({
|
|||
isActive={currentRunId === run.id}
|
||||
onClick={() => actions?.onSelectRun(run.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex w-full items-center gap-2 min-w-0">
|
||||
{processingRunIds?.has(run.id) ? (
|
||||
<span className="size-2 shrink-0 rounded-full bg-emerald-500 animate-pulse" />
|
||||
) : null}
|
||||
<span className="truncate text-sm">{run.title || '(Untitled chat)'}</span>
|
||||
<span className="min-w-0 flex-1 truncate text-sm">{run.title || '(Untitled chat)'}</span>
|
||||
{run.createdAt ? (
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground">
|
||||
{formatRunTime(run.createdAt)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue