Skip to content
Merged
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
7 changes: 7 additions & 0 deletions app/ui_layer/browser/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/ui_layer/browser/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"@reduxjs/toolkit": "^2.12.0",
"@tanstack/react-virtual": "^3.13.23",
"driver.js": "^1.8.0",
"lucide-react": "^0.344.0",
"prism-react-renderer": "^2.4.1",
"react": "^18.2.0",
Expand Down
30 changes: 18 additions & 12 deletions app/ui_layer/browser/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SettingsPage } from './pages/Settings'
import { OnboardingPage } from './pages/Onboarding'
import { LivingUIPage } from './pages/LivingUI'
import { useWebSocket } from './contexts/WebSocketContext'
import { TourProvider } from './tour'
import { LoadingMascot } from '@mascot'

// Forces LivingUIPage to remount per-project so useState initializers
Expand Down Expand Up @@ -74,19 +75,24 @@ function App() {
return <OnboardingPage />
}

// TourProvider wraps the ready app (past hard onboarding), so the first-run
// walkthrough can never collide with the onboarding wizard. It sits inside
// the router, so the tour can navigate between pages.
return (
<Layout>
<Routes>
<Route path="/" element={<ChatPage key="main" sessionId="main" />} />
<Route path="/session/:id" element={<SessionChatRoute />} />
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/screen" element={<ScreenPage />} />
<Route path="/workspace" element={<WorkspacePage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/living-ui/:projectId" element={<LivingUIPageRoute />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Layout>
<TourProvider autoStartEnabled>
<Layout>
<Routes>
<Route path="/" element={<ChatPage key="main" sessionId="main" />} />
<Route path="/session/:id" element={<SessionChatRoute />} />
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/screen" element={<ScreenPage />} />
<Route path="/workspace" element={<WorkspacePage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/living-ui/:projectId" element={<LivingUIPageRoute />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Layout>
</TourProvider>
)
}

Expand Down
3 changes: 3 additions & 0 deletions app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import { selectSessionActivity } from '../../store/selectors/activity'
import { selectSessionBusy, selectSessionRunState } from '../../store/selectors/agent'
import type { ActionItem, ChatMessage } from '../../types'
import { tourAnchorProps } from '../../tour'
import styles from './Chat.module.css'

// Pending attachment type
Expand Down Expand Up @@ -1359,6 +1360,7 @@ export function Chat({ sessionId, placeholder }: ChatProps) {
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
{...tourAnchorProps('chat-composer')}
>
{replyTarget && (
<div className={styles.replyBar}>
Expand Down Expand Up @@ -1443,6 +1445,7 @@ export function Chat({ sessionId, placeholder }: ChatProps) {
title="Attach and tools"
aria-label="Attach and tools"
aria-expanded={plusOpen}
{...tourAnchorProps('chat-plus')}
>
{enhancing
? <Loader2 size={18} className={styles.uploadingSpinner} />
Expand Down
15 changes: 15 additions & 0 deletions app/ui_layer/browser/frontend/src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import { useLocation } from 'react-router-dom'
import { Menu, X } from 'lucide-react'
import { NavBar } from './NavBar'
import { useFullscreen } from '../../contexts/FullscreenContext'
import { useTourEnvAction } from '../../tour'
import styles from './Layout.module.css'

// Matches the mobile breakpoint in Layout.module.css, where the sidebar
// becomes an off-canvas drawer.
const MOBILE_QUERY = '(max-width: 768px)'

interface LayoutProps {
children: ReactNode
}
Expand Down Expand Up @@ -54,6 +59,16 @@ export function Layout({ children }: LayoutProps) {
})
}

// Let the guided tour reveal the sidebar before highlighting a nav item.
// Expanding it in memory only (not persisting COLLAPSED_KEY) keeps the user's
// saved preference intact for their next session.
useTourEnvAction('ensureSidebarVisible', () => {
setCollapsed(false)
if (window.matchMedia(MOBILE_QUERY).matches) {
setMobileOpen(true)
}
})

return (
<div className={styles.layout}>
{!isFullscreen && (
Expand Down
26 changes: 22 additions & 4 deletions app/ui_layer/browser/frontend/src/components/layout/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from 'lucide-react'
import { useWebSocket } from '../../contexts/WebSocketContext'
import { useTheme } from '../../contexts/ThemeContext'
import { tourAnchorProps, useTourEnvAction, type TourAnchorId } from '../../tour'
import { useSkillCreator } from '../../hooks'
import { CreateLivingUIModal } from '../ui/CreateLivingUIModal'
import { SkillCreatorModal } from '../ui/SkillCreatorModal'
Expand All @@ -39,6 +40,7 @@ interface NavItem {
label: string
icon: React.ReactNode
path: string
tourAnchor?: TourAnchorId
}

// Sidebar title with a typewriter reveal: when the auto-title replaces the
Expand Down Expand Up @@ -84,8 +86,8 @@ function AnimatedSessionTitle({ title }: { title: string }) {
}

const utilityNavItems: NavItem[] = [
{ id: 'dashboard', label: 'Dashboard', icon: <LayoutDashboard size={16} />, path: '/dashboard' },
{ id: 'workspace', label: 'Workspace', icon: <FolderOpen size={16} />, path: '/workspace' },
{ id: 'dashboard', label: 'Dashboard', icon: <LayoutDashboard size={16} />, path: '/dashboard', tourAnchor: 'nav-dashboard' },
{ id: 'workspace', label: 'Workspace', icon: <FolderOpen size={16} />, path: '/workspace', tourAnchor: 'nav-workspace' },
]

const settingsItem: NavItem = { id: 'settings', label: 'Settings', icon: <Settings size={16} />, path: '/settings' }
Expand Down Expand Up @@ -322,6 +324,19 @@ export function NavBar({ collapsed = false, onToggleCollapsed }: NavBarProps) {
navigate('/session/new')
}

// Let the guided tour open a fresh New Chat via the exact same action as the
// button, so the chat is demonstrated on a clean draft, not the Main session.
useTourEnvAction('openNewChat', startNewChat)

// Let the tour expand the Chats group so the pinned Main row is on screen
// before it highlights it.
useTourEnvAction('ensureChatsExpanded', () => setChatsExpanded(true))

// Let the tour open and close the "Add Living UI" modal while it walks the
// creation methods.
useTourEnvAction('openLivingUIModal', () => setShowCreateModal(true))
useTourEnvAction('closeLivingUIModal', () => setShowCreateModal(false))

// Close any open context menu when clicking anywhere else.
useEffect(() => {
if (!menu) return
Expand Down Expand Up @@ -455,6 +470,7 @@ export function NavBar({ collapsed = false, onToggleCollapsed }: NavBarProps) {
key={session.id}
className={`${styles.sessionRow} ${active ? styles.sessionRowActive : ''} ${opts.isMain ? styles.sessionRowMain : ''}`}
title={opts.isMain ? 'Main' : session.title}
{...(opts.isMain ? tourAnchorProps('nav-main-session') : {})}
>
{renaming ? (
<input
Expand Down Expand Up @@ -582,6 +598,7 @@ export function NavBar({ collapsed = false, onToggleCollapsed }: NavBarProps) {
className={`${styles.navItem} ${location.pathname === '/session/new' ? styles.active : ''}`}
onClick={startNewChat}
title="New Chat"
{...tourAnchorProps('nav-new-chat')}
>
<span className={styles.icon}><SquarePen size={16} /></span>
<span className={styles.label}>New Chat</span>
Expand All @@ -594,6 +611,7 @@ export function NavBar({ collapsed = false, onToggleCollapsed }: NavBarProps) {
className={`${styles.navItem} ${isActive(item.path) ? styles.active : ''}`}
onClick={() => navigate(item.path)}
title={item.label}
{...(item.tourAnchor ? tourAnchorProps(item.tourAnchor) : {})}
>
<span className={styles.icon}>{item.icon}</span>
<span className={styles.label}>{item.label}</span>
Expand Down Expand Up @@ -631,7 +649,7 @@ export function NavBar({ collapsed = false, onToggleCollapsed }: NavBarProps) {
) : (
<>
{/* Living UI group */}
<div className={styles.groupRow}>
<div className={styles.groupRow} {...tourAnchorProps('nav-living-ui')}>
<button
className={styles.groupToggle}
onClick={() => setLivingUIExpanded(v => !v)}
Expand Down Expand Up @@ -697,7 +715,7 @@ export function NavBar({ collapsed = false, onToggleCollapsed }: NavBarProps) {
<div className={styles.innerDivider} aria-hidden="true" />

{/* Chats group — Main always pinned first inside it */}
<div className={styles.groupRow}>
<div className={styles.groupRow} {...tourAnchorProps('nav-chats')}>
<button
className={styles.groupToggle}
onClick={() => setChatsExpanded(v => !v)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ import { Button } from './Button'
import { Modal } from './Modal'
import { CreateCustomWizard } from './CreateCustomWizard'
import { useSettingsWebSocket } from '../../pages/Settings/useSettingsWebSocket'
import { tourAnchorProps, useTourEnvAction, type TourAnchorId } from '../../tour'
import styles from './CreateLivingUIModal.module.css'

// The modal's tabs, in the order the guided tour walks them.
const TAB_TOUR_ANCHORS: Record<'marketplace' | 'custom' | 'import', TourAnchorId> = {
marketplace: 'livingui-tab-marketplace',
custom: 'livingui-tab-custom',
import: 'livingui-tab-import',
}

export interface CreateLivingUIModalProps {
isOpen: boolean
onClose: () => void
Expand Down Expand Up @@ -63,6 +71,13 @@ export function CreateLivingUIModal({ isOpen, onClose, onInstalled }: CreateLivi
useEffect(() => { onInstalledRef.current = onInstalled }, [onInstalled])
useEffect(() => () => { installTimeoutsRef.current.forEach(t => clearTimeout(t)) }, [])

// Let the guided tour switch the modal's tab so each creation method is shown.
useTourEnvAction('openLivingUITab', (arg) => {
if (arg === 'marketplace' || arg === 'custom' || arg === 'import') {
setActiveTab(arg)
}
})

// Chat-path requirements phase: living_ui_scaffold generated setup
// questions (creating nothing yet) and the backend summons the SAME
// Create Custom wizard, pre-seeded and opened at the interview step
Expand Down Expand Up @@ -348,6 +363,7 @@ export function CreateLivingUIModal({ isOpen, onClose, onInstalled }: CreateLivi
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`${styles.tab} ${activeTab === tab.id ? styles.tabActive : ''}`}
{...tourAnchorProps(TAB_TOUR_ANCHORS[tab.id])}
>
{tab.icon}
{tab.label}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useState, useRef, useEffect } from 'react'
import { Cloud, Users, Github, Box, ChevronRight, ArrowLeft, ExternalLink } from 'lucide-react'
import { Cloud, Users, Github, Box, ChevronRight, ArrowLeft, ExternalLink, Compass } from 'lucide-react'
import { CraftBotMascot, useMascotState, getPose } from '@mascot'
import type { MascotState } from '@mascot'
import { Button } from '../../../components/ui'
import { useTour } from '../../../tour'
import styles from './widgets.module.css'

interface IntroCard {
Expand Down Expand Up @@ -99,6 +100,7 @@ const CARDS: IntroCard[] = [
]

export function CraftBotIntroWidget() {
const { startTour } = useTour()
const mascotState = useMascotState()
// This widget's mascot never sleeps: any state whose pose renders the
// sleeping silhouette shows the awake 'resting' pose here instead. Scoped to
Expand Down Expand Up @@ -347,6 +349,20 @@ export function CraftBotIntroWidget() {
>
Learn More
</Button>

{/* Replay the first-run walkthrough. Hidden at the smallest widget size
so it never crowds the mascot + Learn More stack. */}
{isEnlarged && (
<Button
variant="ghost"
size="sm"
icon={<Compass size={14} />}
onClick={() => startTour('core', { restart: true })}
style={{ marginTop: 'var(--space-2)' }}
>
Take a tour
</Button>
)}
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Trash2,
Package,
PackageOpen,
Compass,
} from 'lucide-react'
import {
Button,
Expand All @@ -26,6 +27,7 @@ import {
} from '../../components/ui'
import { useTheme } from '../../contexts/ThemeContext'
import { useWebSocket } from '../../contexts/WebSocketContext'
import { useTour } from '../../tour'
import { useConfirmModal } from '../../hooks'
import styles from './SettingsPage.module.css'
import { useSettingsWebSocket } from './useSettingsWebSocket'
Expand Down Expand Up @@ -73,6 +75,7 @@ function getInitialAgentName(): string {
export function GeneralSettings() {
const { send, onMessage, isConnected } = useSettingsWebSocket()
const { agentProfilePictureUrl, agentProfilePictureHasCustom } = useWebSocket()
const { startTour } = useTour()
const version = useAppSelector(selectVersion)
const dispatch = useAppDispatch()
const { theme: globalTheme, setTheme: setGlobalTheme } = useTheme()
Expand Down Expand Up @@ -713,6 +716,22 @@ export function GeneralSettings() {
<option value="system">System</option>
</select>
</div>

<div className={styles.formGroup}>
<label>Product Tour</label>
<div>
<Button
variant="secondary"
icon={<Compass size={14} />}
onClick={() => startTour('core', { restart: true })}
>
Take the tour
</Button>
</div>
<span className={styles.hint}>
Replay the guided walkthrough of the CraftBot interface.
</span>
</div>
</div>

<div className={styles.sectionFooter}>
Expand Down
Loading