diff --git a/apps/x/apps/renderer/src/App.css b/apps/x/apps/renderer/src/App.css index 05538f95..015306b3 100644 --- a/apps/x/apps/renderer/src/App.css +++ b/apps/x/apps/renderer/src/App.css @@ -89,71 +89,71 @@ :root { --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --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); + --background: var(--bg-color, oklch(1 0 0)); + --foreground: var(--text-color, oklch(0.145 0 0)); + --card: var(--bg-color, oklch(1 0 0)); + --card-foreground: var(--text-color, oklch(0.145 0 0)); + --popover: var(--bg-color, oklch(1 0 0)); + --popover-foreground: var(--text-color, oklch(0.145 0 0)); + --primary: var(--main-color, oklch(0.205 0 0)); + --primary-foreground: var(--bg-color, oklch(0.985 0 0)); + --secondary: var(--sub-alt-color, oklch(0.97 0 0)); + --secondary-foreground: var(--text-color, oklch(0.205 0 0)); + --muted: var(--sub-alt-color, oklch(0.97 0 0)); + --muted-foreground: var(--sub-color, oklch(0.556 0 0)); + --accent: var(--sub-color, oklch(0.97 0 0)); + --accent-foreground: var(--text-color, oklch(0.205 0 0)); + --destructive: var(--error-color, oklch(0.577 0.245 27.325)); + --border: var(--sub-alt-color, oklch(0.922 0 0)); + --input: var(--sub-alt-color, oklch(0.922 0 0)); + --ring: var(--main-color, 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); + --sidebar: var(--bg-color, oklch(0.985 0 0)); + --sidebar-foreground: var(--text-color, oklch(0.145 0 0)); + --sidebar-primary: var(--main-color, oklch(0.205 0 0)); + --sidebar-primary-foreground: var(--bg-color, oklch(0.985 0 0)); + --sidebar-accent: var(--sub-color, oklch(0.97 0 0)); + --sidebar-accent-foreground: var(--text-color, oklch(0.205 0 0)); + --sidebar-border: var(--sub-alt-color, oklch(0.922 0 0)); + --sidebar-ring: var(--main-color, oklch(0.708 0 0)); } .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); + --background: var(--bg-color, oklch(0.145 0 0)); + --foreground: var(--text-color, oklch(0.985 0 0)); + --card: var(--bg-color, oklch(0.205 0 0)); + --card-foreground: var(--text-color, oklch(0.985 0 0)); + --popover: var(--bg-color, oklch(0.205 0 0)); + --popover-foreground: var(--text-color, oklch(0.985 0 0)); + --primary: var(--main-color, oklch(0.922 0 0)); + --primary-foreground: var(--bg-color, oklch(0.205 0 0)); + --secondary: var(--sub-alt-color, oklch(0.269 0 0)); + --secondary-foreground: var(--text-color, oklch(0.985 0 0)); + --muted: var(--sub-alt-color, oklch(0.269 0 0)); + --muted-foreground: var(--sub-color, oklch(0.708 0 0)); + --accent: var(--sub-color, oklch(0.269 0 0)); + --accent-foreground: var(--text-color, oklch(0.985 0 0)); + --destructive: var(--error-color, oklch(0.704 0.191 22.216)); + --border: var(--sub-alt-color, oklch(1 0 0 / 10%)); + --input: var(--sub-alt-color, oklch(1 0 0 / 15%)); + --ring: var(--main-color, 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); + --sidebar: var(--bg-color, oklch(0.205 0 0)); + --sidebar-foreground: var(--text-color, oklch(0.985 0 0)); + --sidebar-primary: var(--main-color, oklch(0.488 0.243 264.376)); + --sidebar-primary-foreground: var(--bg-color, oklch(0.985 0 0)); + --sidebar-accent: var(--sub-color, oklch(0.269 0 0)); + --sidebar-accent-foreground: var(--text-color, oklch(0.985 0 0)); + --sidebar-border: var(--sub-alt-color, oklch(1 0 0 / 10%)); + --sidebar-ring: var(--main-color, oklch(0.556 0 0)); } @layer base { diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 4398f7e5..5955160e 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -1,3 +1,4 @@ +import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { workspace } from '@x/shared'; import { RunEvent } from '@x/shared/src/runs.js'; @@ -6,7 +7,9 @@ import './App.css' import z from 'zod'; import { Button } from './components/ui/button'; import { MessageSquare } from 'lucide-react'; -import { AppSidebar } from '@/components/app-sidebar'; +import { SidebarIcon } from '@/components/sidebar-icon'; +import { SidebarContentPanel } from '@/components/sidebar-content'; +import { SidebarSectionProvider } from '@/contexts/sidebar-context'; import { Conversation, ConversationContent, @@ -41,20 +44,13 @@ import { ContextReasoningUsage, ContextTrigger, } from '@/components/ai-elements/context'; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb" -import { Separator } from "@/components/ui/separator" import { SidebarInset, SidebarProvider, SidebarTrigger, } from "@/components/ui/sidebar" +import { TooltipProvider } from "@/components/ui/tooltip" +import { Separator } from "@/components/ui/separator" type DirEntry = z.infer type RunEventType = z.infer @@ -115,6 +111,8 @@ const toToolState = (status: ToolCall['status']): ToolState => { } } +const DEFAULT_SIDEBAR_WIDTH = 256 + const normalizeUsage = (usage?: Partial | null): LanguageModelUsage | null => { if (!usage) return null const hasNumbers = Object.values(usage).some((value) => typeof value === 'number') @@ -200,15 +198,35 @@ function buildTree(entries: DirEntry[]): TreeNode[] { return sortNodes(roots) } +// Sample chat history (will be replaced with real data later) +const chatHistory = [ + { + id: 'project-kickoff', + title: 'Project kickoff', + preview: 'Scope, roles, and milestones.', + time: 'Today', + }, + { + id: 'design-review', + title: 'Design review', + preview: 'UI polish and sidebar UX.', + time: 'Yesterday', + }, + { + id: 'tools-audit', + title: 'Tools audit', + preview: 'MCP inventory and tool gaps.', + time: 'Mon', + }, +] + function App() { - // Sidebar view state - const [activeSidebarView, setActiveSidebarView] = useState<'files' | 'accounts'>('files') - - // File browser state - const [tree, setTree] = useState([]) - const [expandedPaths, setExpandedPaths] = useState>(new Set()) + // File browser state (for Knowledge section) + const [_knowledgeContent, setKnowledgeContent] = useState('') const [selectedPath, setSelectedPath] = useState(null) const [fileContent, setFileContent] = useState('') + const [tree, setTree] = useState([]) + const [expandedPaths, setExpandedPaths] = useState>(new Set()) // Chat state const [message, setMessage] = useState('') @@ -235,23 +253,42 @@ function App() { } }, []) - // Load initial tree + // Load knowledge file content + const loadKnowledge = useCallback(async () => { + try { + const result = await window.ipc.invoke('workspace:readFile', { + path: 'knowledge', + encoding: 'utf8' + }) + return result.data + } catch (err) { + console.error('Failed to load knowledge file:', err) + return '' + } + }, []) + + // Load initial tree and knowledge content useEffect(() => { async function process() { - const tree = await loadDirectory(); - setTree(tree) + const [treeData, content] = await Promise.all([ + loadDirectory(), + loadKnowledge() + ]); + setTree(treeData) + setKnowledgeContent(content) } process(); - }, [loadDirectory]) + }, [loadDirectory, loadKnowledge]) // Listen to workspace change events useEffect(() => { const cleanup = window.ipc.on('workspace:didChange', () => { - // Reload tree on any change + // Reload tree and knowledge on any change loadDirectory().then(result => setTree(result)) + loadKnowledge().then(result => setKnowledgeContent(result)) }) return cleanup - }, [loadDirectory]) + }, [loadDirectory, loadKnowledge]) // Load file content when selected useEffect(() => { @@ -346,7 +383,7 @@ function App() { setCurrentAssistantMessage(currentMsg => { if (currentMsg) { setConversation(prev => { - const exists = prev.some(m => + const exists = prev.some(m => m.id === event.messageId && 'role' in m && m.role === 'assistant' ) if (exists) return prev @@ -564,152 +601,151 @@ function App() { } as LanguageModelUsage const hasConversation = conversation.length > 0 || currentAssistantMessage || currentReasoning + const conversationContentClassName = hasConversation + ? "mx-auto w-full max-w-4xl pb-28" + : "mx-auto w-full max-w-4xl min-h-full items-center justify-center pb-0" const submitStatus: ChatStatus = isProcessing ? 'streaming' : 'ready' const canSubmit = Boolean(message.trim()) && !isProcessing return ( - - - -
- - - - - - Workspace - - - - + + +
+ {/* Icon sidebar - always visible, fixed position */} + + + {/* Spacer for the fixed icon sidebar */} +
+ + {/* Content sidebar with SidebarProvider for collapse functionality */} + + + + {/* Header with sidebar trigger */} +
+ + + {selectedPath ? selectedPath : 'Chat'} - - - - -
- - {selectedPath ? ( - <> -
-
{selectedPath}
- -
-
-
-                {fileContent || 'Loading...'}
-              
-
- - ) : ( -
- - - {!hasConversation ? ( - } - title="Start a conversation" - /> - ) : ( - <> - {conversation.map(item => renderConversationItem(item))} - - {currentReasoning && ( - - - {currentReasoning} - - )} - - {currentAssistantMessage && ( - - - {currentAssistantMessage} - - - )} - - {isProcessing && !currentAssistantMessage && !currentReasoning && ( - - - Thinking... - - - )} - + + {selectedPath && ( + )} - - - +
-
-
-
- - - setMessage(e.target.value)} - placeholder="Type your message..." - disabled={isProcessing} - /> - - - - - - - - - - - - - - - - - - - + {selectedPath ? ( +
+
+                    {fileContent || 'Loading...'}
+                  
+
+ ) : ( +
+ + + {!hasConversation ? ( + +
+ RowboatX +
+
+ ) : ( + <> + {conversation.map(item => renderConversationItem(item))} + + {currentReasoning && ( + + + {currentReasoning} + + )} + + {currentAssistantMessage && ( + + + {currentAssistantMessage} + + + )} + + {isProcessing && !currentAssistantMessage && !currentReasoning && ( + + + Thinking... + + + )} + + )} +
+ +
+ +
+
+
+ + + setMessage(e.target.value)} + placeholder="Type your message..." + disabled={isProcessing} + /> + + + + + + + + + + + + + + + + + + + +
+
-
-
- )} - - + )} + + +
+ + ) } diff --git a/apps/x/apps/renderer/src/components/app-sidebar.tsx b/apps/x/apps/renderer/src/components/app-sidebar.tsx deleted file mode 100644 index 05bffa77..00000000 --- a/apps/x/apps/renderer/src/components/app-sidebar.tsx +++ /dev/null @@ -1,189 +0,0 @@ -"use client" - -import * as React from "react" -import { ChevronRight, File, Folder, Plug } from "lucide-react" -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible" -import { - Sidebar, - SidebarContent, - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - useSidebar, -} from "@/components/ui/sidebar" -import { ConnectedAccountsSidebar } from "@/components/connected-accounts-sidebar" - -type TreeNode = { - name: string - path: string - kind: 'file' | 'dir' - children?: TreeNode[] -} - -type SidebarView = 'files' | 'accounts' - -type AppSidebarProps = React.ComponentProps & { - tree: TreeNode[] - selectedPath: string | null - expandedPaths: Set - onSelectFile: (path: string, kind: 'file' | 'dir') => void - activeView: SidebarView - onViewChange: (view: SidebarView) => void -} - -export function AppSidebar({ - tree, - selectedPath, - expandedPaths, - onSelectFile, - activeView, - onViewChange, - ...props -}: AppSidebarProps) { - const { setOpen } = useSidebar() - - return ( - - {/* This is the first sidebar */} - {/* We disable collapsible and adjust width to icon. */} - {/* This will make the sidebar appear as icons. */} - - - - - - - { - onViewChange('files') - setOpen(true) - }} - isActive={activeView === 'files'} - className="px-2.5 md:px-2" - > - - Files - - - - { - onViewChange('accounts') - setOpen(true) - }} - isActive={activeView === 'accounts'} - className="px-2.5 md:px-2" - > - - Connected Accounts - - - - - - - - - {/* This is the second sidebar */} - {/* We disable collapsible and let it fill remaining space */} - {activeView === 'files' ? ( - - - - Files - - - {tree.map((item, index) => ( - - ))} - - - - - - ) : ( - - )} - - ) -} - -type TreeProps = { - item: TreeNode - selectedPath: string | null - expandedPaths: Set - onSelect: (path: string, kind: 'file' | 'dir') => void -} - -function Tree({ item, selectedPath, expandedPaths, onSelect }: TreeProps) { - const hasChildren = item.children && item.children.length > 0 - const isExpanded = expandedPaths.has(item.path) - const isSelected = selectedPath === item.path - - if (!hasChildren) { - return ( - onSelect(item.path, item.kind)} - > - - {item.name} - - ) - } - - return ( - - onSelect(item.path, item.kind)} - className="group/collapsible [&[data-state=open]>button>svg:first-child]:rotate-90" - > - - - - - {item.name} - - - - - {item.children!.map((subItem, index) => ( - - ))} - - - - - ) -} diff --git a/apps/x/apps/renderer/src/components/connected-accounts-sidebar.tsx b/apps/x/apps/renderer/src/components/connected-accounts-sidebar.tsx deleted file mode 100644 index 6522e5b1..00000000 --- a/apps/x/apps/renderer/src/components/connected-accounts-sidebar.tsx +++ /dev/null @@ -1,187 +0,0 @@ -"use client" - -import * as React from "react" -import { useState, useEffect, useCallback } from "react" -import { Loader2, Plug, Database } from "lucide-react" -import { - Sidebar, - SidebarContent, - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar" -import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" -import { Switch } from "@/components/ui/switch" -import { useOAuth, useAvailableProviders } from "@/hooks/useOAuth" -import { toast } from "@/lib/toast" - -type ConnectedAccountsSidebarProps = React.ComponentProps - -/** - * Hook for managing Granola sync config - */ -function useGranolaConfig() { - const [enabled, setEnabled] = useState(false); - const [isLoading, setIsLoading] = useState(true); - - const loadConfig = useCallback(async () => { - try { - setIsLoading(true); - const result = await window.ipc.invoke('granola:getConfig', null); - setEnabled(result.enabled); - } catch (error) { - console.error('Failed to load Granola config:', error); - setEnabled(false); - } finally { - setIsLoading(false); - } - }, []); - - useEffect(() => { - loadConfig(); - }, [loadConfig]); - - const updateConfig = useCallback(async (newEnabled: boolean) => { - try { - setIsLoading(true); - await window.ipc.invoke('granola:setConfig', { enabled: newEnabled }); - setEnabled(newEnabled); - toast( - newEnabled ? 'Granola sync enabled' : 'Granola sync disabled', - 'success' - ); - } catch (error) { - console.error('Failed to update Granola config:', error); - toast('Failed to update Granola sync settings', 'error'); - } finally { - setIsLoading(false); - } - }, []); - - return { enabled, isLoading, updateConfig }; -} - -export function ConnectedAccountsSidebar({ ...props }: ConnectedAccountsSidebarProps) { - const { providers, isLoading: providersLoading } = useAvailableProviders() - const { enabled: granolaEnabled, isLoading: granolaLoading, updateConfig: updateGranolaConfig } = useGranolaConfig() - - return ( - - - - Connected Accounts - - - {providersLoading ? ( - - - - Loading... - - - ) : providers.length === 0 ? ( - - - No providers available - - - ) : ( - providers.map((provider) => ( - - )) - )} - - - - - Data Sources - - - -
- - -
- Granola Sync - - Sync notes from Granola - -
- {granolaLoading && ( - - )} -
-
-
-
-
-
-
- ) -} - -function ProviderItem({ provider }: { provider: string }) { - const { isConnected, isLoading, isConnecting, connect, disconnect } = useOAuth(provider) - const providerDisplayName = provider.charAt(0).toUpperCase() + provider.slice(1) - - return ( - -
-
- - {providerDisplayName} - {isLoading ? ( - - ) : ( - - {isConnected ? "Connected" : "Not Connected"} - - )} -
-
- {isConnected ? ( - - ) : ( - - )} -
-
-
- ) -} - diff --git a/apps/x/apps/renderer/src/components/nav-user.tsx b/apps/x/apps/renderer/src/components/nav-user.tsx deleted file mode 100644 index 4b9c1e60..00000000 --- a/apps/x/apps/renderer/src/components/nav-user.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { - BadgeCheck, - Bell, - ChevronsUpDown, - CreditCard, - LogOut, - Sparkles, -} from "lucide-react" - -import { - Avatar, - AvatarFallback, - AvatarImage, -} from "@/components/ui/avatar" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar" - -export function NavUser({ - user, -}: { - user: { - name: string - email: string - avatar: string - } -}) { - const { isMobile } = useSidebar() - - return ( - - - - - - - - CN - -
- {user.name} - {user.email} -
- -
-
- - -
- - - CN - -
- {user.name} - {user.email} -
-
-
- - - - - Upgrade to Pro - - - - - - - Account - - - - Billing - - - - Notifications - - - - - - Log out - -
-
-
-
- ) -} diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx new file mode 100644 index 00000000..d05796a8 --- /dev/null +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -0,0 +1,522 @@ +"use client" + +import * as React from "react" +import { useState, useEffect, useCallback } from "react" +import { + ArrowDownAZ, + CalendarDays, + ChevronRight, + ChevronsDownUp, + ChevronsUpDown, + Database, + File, + FilePlus, + Folder, + FolderPlus, + Loader2, + Mail, + MessageSquare, + MessageSquarePlus, + Microscope, + Network, + Plug, + Plus, +} from "lucide-react" + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarRail, +} from "@/components/ui/sidebar" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Switch } from "@/components/ui/switch" +import { useSidebarSection } from "@/contexts/sidebar-context" +import { useOAuth, useAvailableProviders } from "@/hooks/useOAuth" +import { toast } from "@/lib/toast" + +interface TreeNode { + path: string + name: string + kind: "file" | "dir" + children?: TreeNode[] + loaded?: boolean +} + +type SidebarContentPanelProps = { + tree: TreeNode[] + selectedPath: string | null + expandedPaths: Set + onSelectFile: (path: string, kind: "file" | "dir") => void + chats: { id: string; title: string; preview: string; time: string }[] +} & React.ComponentProps + +const sectionTitles = { + "ask-ai": "Ask AI", + knowledge: "Knowledge", + agents: "Agents", +} + +const quickActions = [ + { icon: FilePlus, label: "New Note", action: () => console.log("New note") }, + { icon: FolderPlus, label: "New Folder", action: () => console.log("New folder") }, + { icon: Network, label: "Graph View", action: () => console.log("Graph view") }, + { icon: ArrowDownAZ, label: "Sort", action: () => console.log("Sort") }, +] + +const agentPresets = [ + { + name: "Email Assistant", + description: "Draft replies, summarize threads.", + icon: Mail, + }, + { + name: "Meeting Prep", + description: "Build briefs and talking points.", + icon: CalendarDays, + }, + { + name: "Research", + description: "Gather sources, outline findings.", + icon: Microscope, + }, +] + +/** + * Hook for managing Granola sync config + */ +function useGranolaConfig() { + const [enabled, setEnabled] = useState(false) + const [isLoading, setIsLoading] = useState(true) + + const loadConfig = useCallback(async () => { + try { + setIsLoading(true) + const result = await window.ipc.invoke('granola:getConfig', null) + setEnabled(result.enabled) + } catch (error) { + console.error('Failed to load Granola config:', error) + setEnabled(false) + } finally { + setIsLoading(false) + } + }, []) + + useEffect(() => { + loadConfig() + }, [loadConfig]) + + const updateConfig = useCallback(async (newEnabled: boolean) => { + try { + setIsLoading(true) + await window.ipc.invoke('granola:setConfig', { enabled: newEnabled }) + setEnabled(newEnabled) + toast( + newEnabled ? 'Granola sync enabled' : 'Granola sync disabled', + 'success' + ) + } catch (error) { + console.error('Failed to update Granola config:', error) + toast('Failed to update Granola sync settings', 'error') + } finally { + setIsLoading(false) + } + }, []) + + return { enabled, isLoading, updateConfig } +} + +export function SidebarContentPanel({ + tree, + selectedPath, + expandedPaths, + onSelectFile, + chats, + ...props +}: SidebarContentPanelProps) { + const { activeSection } = useSidebarSection() + const [allExpanded, setAllExpanded] = React.useState(false) + + const toggleExpandAll = () => { + setAllExpanded(!allExpanded) + } + + return ( + + +
+ {sectionTitles[activeSection]} +
+
+ + {activeSection === "ask-ai" && ( + + )} + {activeSection === "knowledge" && ( + + )} + {activeSection === "agents" && ( + + )} + + +
+ ) +} + +// Chat Section +function ChatSection({ chats }: { chats: { id: string; title: string; preview: string; time: string }[] }) { + return ( + + + Recent Chats + + + + + New Chat + + + + + {chats.map((chat) => ( + + + +
+
+ {chat.title} + {chat.time} +
+ {chat.preview} +
+
+
+ ))} +
+
+
+ ) +} + +// Knowledge Section +function KnowledgeSection({ + tree, + selectedPath, + expandedPaths, + onSelectFile, + allExpanded, + onToggleExpandAll, +}: { + tree: TreeNode[] + selectedPath: string | null + expandedPaths: Set + onSelectFile: (path: string, kind: "file" | "dir") => void + allExpanded: boolean + onToggleExpandAll: () => void +}) { + return ( + +
+ {quickActions.map((action) => ( + + + + + {action.label} + + ))} + + + + + + {allExpanded ? "Collapse All" : "Expand All"} + + +
+ + + {tree.map((item, index) => ( + + ))} + + +
+ ) +} + +// Tree component for file browser +function Tree({ + item, + selectedPath, + expandedPaths, + onSelect, +}: { + item: TreeNode + selectedPath: string | null + expandedPaths: Set + onSelect: (path: string, kind: "file" | "dir") => void +}) { + const hasChildren = item.children && item.children.length > 0 + const isExpanded = expandedPaths.has(item.path) + const isSelected = selectedPath === item.path + + if (!hasChildren) { + return ( + + onSelect(item.path, item.kind)} + > + + {item.name} + + + ) + } + + return ( + + onSelect(item.path, item.kind)} + className="group/collapsible [&[data-state=open]>button>svg:first-child]:rotate-90" + > + + + + + {item.name} + + + + + {item.children!.map((subItem, index) => ( + + ))} + + + + + ) +} + +// Agents Section with Connected Accounts +function AgentsSection() { + return ( + <> + {/* Agent Presets */} + + + Agent Presets + + + + + New Agent + + + + + {agentPresets.map((agent) => ( + + + +
+ {agent.name} + {agent.description} +
+
+
+ ))} +
+
+
+ + {/* Connectors (Connected Accounts) */} + + + {/* Data Sources */} + + + ) +} + +// Data Sources Section (Granola sync, etc.) +function DataSourcesSection() { + const { enabled: granolaEnabled, isLoading: granolaLoading, updateConfig: updateGranolaConfig } = useGranolaConfig() + + return ( + + Data Sources + + + +
+ + +
+ Granola Sync + + Sync notes from Granola + +
+ {granolaLoading && ( + + )} +
+
+
+
+
+ ) +} + +// Connectors Section (formerly Connected Accounts) +function ConnectorsSection() { + const { providers, isLoading: providersLoading } = useAvailableProviders() + + return ( + + Connectors + + + {providersLoading ? ( + + + + Loading... + + + ) : providers.length === 0 ? ( + + + No connectors available + + + ) : ( + providers.map((provider) => ( + + )) + )} + + + + ) +} + +function ProviderItem({ provider }: { provider: string }) { + const { isConnected, isLoading, isConnecting, connect, disconnect } = useOAuth(provider) + const providerDisplayName = provider.charAt(0).toUpperCase() + provider.slice(1) + + return ( + +
+
+ + {providerDisplayName} + {isLoading ? ( + + ) : ( + + {isConnected ? "Connected" : "Not Connected"} + + )} +
+
+ {isConnected ? ( + + ) : ( + + )} +
+
+
+ ) +} diff --git a/apps/x/apps/renderer/src/components/sidebar-icon.tsx b/apps/x/apps/renderer/src/components/sidebar-icon.tsx new file mode 100644 index 00000000..99ba7eb2 --- /dev/null +++ b/apps/x/apps/renderer/src/components/sidebar-icon.tsx @@ -0,0 +1,101 @@ +"use client" + +import * as React from "react" +import { + Bot, + Brain, + HelpCircle, + Settings, + Ship, + Sparkles, + Trash2, +} from "lucide-react" + +import { cn } from "@/lib/utils" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { type ActiveSection, useSidebarSection } from "@/contexts/sidebar-context" + +type NavItem = { + id: ActiveSection + title: string + icon: React.ElementType +} + +type SecondaryItem = { + id: string + title: string + icon: React.ElementType + action?: () => void +} + +const navItems: NavItem[] = [ + { id: "ask-ai", title: "Ask AI", icon: Sparkles }, + { id: "knowledge", title: "Knowledge", icon: Brain }, + { id: "agents", title: "Agents", icon: Bot }, +] + +const secondaryItems: SecondaryItem[] = [ + { id: "settings", title: "Settings", icon: Settings }, + { id: "trash", title: "Trash", icon: Trash2 }, + { id: "help", title: "Help", icon: HelpCircle }, +] + +export function SidebarIcon() { + const { activeSection, setActiveSection } = useSidebarSection() + + return ( +
+ {/* Logo */} +
+ +
+ + {/* Main navigation */} + + + {/* Secondary navigation (bottom) */} + +
+ ) +} diff --git a/apps/x/apps/renderer/src/components/ui/avatar.tsx b/apps/x/apps/renderer/src/components/ui/avatar.tsx deleted file mode 100644 index 71e428b4..00000000 --- a/apps/x/apps/renderer/src/components/ui/avatar.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client" - -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" - -import { cn } from "@/lib/utils" - -function Avatar({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AvatarImage({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AvatarFallback({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { Avatar, AvatarImage, AvatarFallback } diff --git a/apps/x/apps/renderer/src/components/ui/breadcrumb.tsx b/apps/x/apps/renderer/src/components/ui/breadcrumb.tsx deleted file mode 100644 index eb88f321..00000000 --- a/apps/x/apps/renderer/src/components/ui/breadcrumb.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" - -import { cn } from "@/lib/utils" - -function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return