Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions apps/webclaw/src/screens/chat/components/chat-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HugeiconsIcon } from '@hugeicons/react'
import {
PencilEdit02Icon,
QuestionMarkIcon,
Search01Icon,
Settings01Icon,
SidebarLeft01Icon,
Expand All @@ -13,6 +14,7 @@ import { useDeleteSession } from '../hooks/use-delete-session'
import { useRenameSession } from '../hooks/use-rename-session'
import { useSessionShortcuts } from '../hooks/use-session-shortcuts'
import { SettingsDialog } from './settings-dialog'
import { KeyboardShortcutsDialog } from './keyboard-shortcuts-dialog'
import { SessionRenameDialog } from './sidebar/session-rename-dialog'
import { SessionDeleteDialog } from './sidebar/session-delete-dialog'
import { SidebarSessions } from './sidebar/sidebar-sessions'
Expand Down Expand Up @@ -76,6 +78,7 @@ function ChatSidebarComponent({
const [deleteFriendlyId, setDeleteFriendlyId] = useState<string | null>(null)
const [deleteSessionTitle, setDeleteSessionTitle] = useState('')
const [searchDialogOpen, setSearchDialogOpen] = useState(false)
const [shortcutsDialogOpen, setShortcutsDialogOpen] = useState(false)
const navigate = useNavigate()

useSessionShortcuts({
Expand Down Expand Up @@ -344,6 +347,39 @@ function ChatSidebarComponent({
</AnimatePresence>
</Button>
</motion.div>
<motion.div
layout
transition={{ layout: transition }}
className="w-full mt-1"
>
<Button
variant="ghost"
size="sm"
onClick={() => setShortcutsDialogOpen(true)}
title={isCollapsed ? 'Keyboard shortcuts' : undefined}
className="w-full justify-start pl-1.5"
>
<HugeiconsIcon
icon={QuestionMarkIcon}
size={20}
strokeWidth={1.5}
className="min-w-5"
/>
<AnimatePresence initial={false} mode="wait">
{!isCollapsed && (
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={transition}
className="overflow-hidden whitespace-nowrap"
>
Shortcuts
</motion.span>
)}
</AnimatePresence>
</Button>
</motion.div>
</div>

<SettingsDialog
Expand All @@ -357,6 +393,11 @@ function ChatSidebarComponent({
onCopyStorePath={copyStorePath}
/>

<KeyboardShortcutsDialog
open={shortcutsDialogOpen}
onOpenChange={setShortcutsDialogOpen}
/>

<SessionRenameDialog
open={renameDialogOpen}
onOpenChange={setRenameDialogOpen}
Expand Down
124 changes: 124 additions & 0 deletions apps/webclaw/src/screens/chat/components/keyboard-shortcuts-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { HugeiconsIcon } from '@hugeicons/react'
import {
Add01Icon,
Cancel01Icon,
CommandIcon,
QuestionMarkIcon,
Search01Icon,
} from '@hugeicons/core-free-icons'
import {
DialogClose,
DialogContent,
DialogDescription,
DialogRoot,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'

type ShortcutItem = {
keys: Array<string>
description: string
icon?: typeof CommandIcon
}

const SHORTCUTS: Array<ShortcutItem> = [
{
keys: ['Mod', 'K'],
description: 'Search sessions',
icon: Search01Icon,
},
{
keys: ['Mod', 'Shift', 'O'],
description: 'Create new session',
icon: Add01Icon,
},
]

type KeyboardShortcutsDialogProps = {
open: boolean
onOpenChange: (open: boolean) => void
}

function formatKey(key: string): string {
if (key === 'Mod') {
return typeof navigator !== 'undefined' && navigator.platform.includes('Mac')
? '⌘'
: 'Ctrl'
}
return key
}

export function KeyboardShortcutsDialog({
open,
onOpenChange,
}: KeyboardShortcutsDialogProps) {
return (
<DialogRoot open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[420px] p-0 gap-0 overflow-hidden">
<div className="flex items-center justify-between px-6 pt-5 pb-4 border-b border-primary-200">
<DialogTitle className="text-base font-medium text-primary-900 flex items-center gap-2">
<HugeiconsIcon
icon={CommandIcon}
size={20}
strokeWidth={1.5}
className="text-primary-600"
/>
Keyboard Shortcuts
</DialogTitle>
<DialogClose asChild>
<Button size="icon-sm" variant="ghost" className="shrink-0">
<HugeiconsIcon icon={Cancel01Icon} size={20} strokeWidth={1.5} />
</Button>
</DialogClose>
</div>

<DialogDescription className="sr-only">
List of available keyboard shortcuts for WebClaw
</DialogDescription>

<div className="px-6 py-4">
<div className="space-y-3">
{SHORTCUTS.map((shortcut, index) => (
<div
key={index}
className="flex items-center justify-between py-2"
>
<div className="flex items-center gap-3">
{shortcut.icon && (
<HugeiconsIcon
icon={shortcut.icon}
size={18}
strokeWidth={1.5}
className="text-primary-500"
/>
)}
<span className="text-sm text-primary-800">
{shortcut.description}
</span>
</div>
<div className="flex items-center gap-1">
{shortcut.keys.map((key, keyIndex) => (
<span key={keyIndex} className="flex items-center">
<kbd className="inline-flex items-center justify-center min-w-[28px] h-7 px-1.5 text-xs font-medium text-primary-700 bg-primary-100 border border-primary-200 rounded">
{formatKey(key)}
</kbd>
{keyIndex < shortcut.keys.length - 1 && (
<span className="mx-1 text-xs text-primary-400">+</span>
)}
</span>
))}
</div>
</div>
))}
</div>
</div>

<div className="px-6 py-4 bg-primary-50 border-t border-primary-100">
<p className="text-xs text-primary-500 text-center">
Press <kbd className="px-1.5 py-0.5 bg-primary-100 border border-primary-200 rounded text-[10px]">?</kbd> anytime to show this dialog
</p>
</div>
</DialogContent>
</DialogRoot>
)
}