mirror of
https://github.com/james-m-jordan/morphik-core.git
synced 2025-05-09 19:32:38 +00:00
324 lines
12 KiB
TypeScript
324 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { cn } from "@/lib/utils"
|
|
import { Button } from "@/components/ui/button"
|
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
import { FileText, Search, MessageSquare, ChevronLeft, ChevronRight, Network, Copy, Check, ArrowLeft } from "lucide-react"
|
|
import { ModeToggle } from "@/components/mode-toggle"
|
|
import { Input } from "@/components/ui/input"
|
|
|
|
interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
activeSection: string
|
|
onSectionChange: (section: string) => void
|
|
connectionUri?: string
|
|
isReadOnlyUri?: boolean
|
|
onUriChange?: (uri: string) => void
|
|
isCollapsed?: boolean
|
|
setIsCollapsed?: (collapsed: boolean) => void
|
|
onBackClick?: () => void
|
|
}
|
|
|
|
export function Sidebar({
|
|
className,
|
|
activeSection,
|
|
onSectionChange,
|
|
connectionUri,
|
|
isReadOnlyUri = false,
|
|
onUriChange,
|
|
isCollapsed: externalIsCollapsed,
|
|
setIsCollapsed: externalSetIsCollapsed,
|
|
onBackClick,
|
|
...props
|
|
}: SidebarProps) {
|
|
// Use internal state that syncs with external state if provided
|
|
const [internalIsCollapsed, setInternalIsCollapsed] = React.useState(false)
|
|
const [editableUri, setEditableUri] = React.useState('')
|
|
const [isEditingUri, setIsEditingUri] = React.useState(false)
|
|
|
|
// Determine if sidebar is collapsed based on props or internal state
|
|
const isCollapsed = externalIsCollapsed !== undefined ? externalIsCollapsed : internalIsCollapsed
|
|
|
|
// Toggle function that updates both internal and external state if provided
|
|
const toggleCollapsed = () => {
|
|
if (externalSetIsCollapsed) {
|
|
externalSetIsCollapsed(!isCollapsed)
|
|
}
|
|
setInternalIsCollapsed(!isCollapsed)
|
|
}
|
|
|
|
// Initialize from localStorage or props
|
|
React.useEffect(() => {
|
|
// For development/testing - check if we have a stored URI
|
|
const storedUri = typeof window !== 'undefined' ? localStorage.getItem('morphik_uri') : null;
|
|
|
|
if (storedUri) {
|
|
setEditableUri(storedUri);
|
|
// Note: we're removing the auto-notification to avoid refresh loops
|
|
} else if (connectionUri) {
|
|
setEditableUri(connectionUri);
|
|
}
|
|
}, [connectionUri])
|
|
|
|
// Update editable URI when connectionUri changes
|
|
React.useEffect(() => {
|
|
if (connectionUri && connectionUri !== editableUri) {
|
|
setEditableUri(connectionUri);
|
|
}
|
|
}, [connectionUri, editableUri])
|
|
|
|
// Extract connection details if URI is provided
|
|
const isConnected = !!connectionUri;
|
|
let connectionHost = null;
|
|
try {
|
|
if (connectionUri) {
|
|
// Try to extract host from morphik:// format
|
|
const match = connectionUri.match(/^morphik:\/\/[^@]+@(.+)/);
|
|
if (match && match[1]) {
|
|
connectionHost = match[1];
|
|
// If it includes a protocol, remove it to get just the host
|
|
if (connectionHost.includes('://')) {
|
|
connectionHost = new URL(connectionHost).host;
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error parsing connection URI:', error);
|
|
connectionHost = 'localhost';
|
|
}
|
|
|
|
// Handle saving the connection URI
|
|
const handleSaveUri = () => {
|
|
// Store the URI in localStorage for persistence
|
|
if (typeof window !== 'undefined') {
|
|
if (editableUri.trim() === '') {
|
|
// If URI is empty, remove from localStorage to default to local
|
|
localStorage.removeItem('morphik_uri');
|
|
} else {
|
|
localStorage.setItem('morphik_uri', editableUri);
|
|
}
|
|
}
|
|
|
|
// Call the onUriChange callback if provided
|
|
if (onUriChange) {
|
|
// Pass empty string to trigger default localhost connection
|
|
onUriChange(editableUri.trim());
|
|
} else {
|
|
// Fallback for demo purposes if no callback is provided
|
|
console.log('New URI:', editableUri || '(empty - using localhost)');
|
|
}
|
|
|
|
// Close editing mode
|
|
setIsEditingUri(false);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"relative flex flex-col border-r bg-background transition-all duration-300",
|
|
isCollapsed ? "w-16" : "w-64",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<div className="flex flex-col border-b">
|
|
<div className="flex items-center justify-between p-4">
|
|
{!isCollapsed && <h2 className="text-lg font-semibold">Morphik</h2>}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="ml-auto"
|
|
onClick={toggleCollapsed}
|
|
>
|
|
{isCollapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Display connection information when not collapsed */}
|
|
{!isCollapsed && (
|
|
<div className="px-4 pb-3">
|
|
<div className="p-2 bg-muted rounded-md text-xs">
|
|
<div className="font-medium mb-1">
|
|
{isConnected ? "Connected to:" : "Connection Status:"}
|
|
</div>
|
|
<div className="text-muted-foreground">
|
|
{isConnected && connectionHost && !connectionHost.includes('localhost') && !connectionHost.includes('local')
|
|
? <span className="truncate">{connectionHost}</span>
|
|
: <span className="flex items-center">
|
|
<span className="h-2 w-2 rounded-full bg-green-500 mr-1.5"></span>
|
|
Local Connection (localhost:8000)
|
|
</span>
|
|
}
|
|
</div>
|
|
|
|
{/* Connection URI Section */}
|
|
<div className="flex flex-col mt-2 pt-2 border-t border-background">
|
|
<div className="flex items-center justify-between">
|
|
<div className="font-medium">Connection URI:</div>
|
|
{isConnected && !isEditingUri && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6"
|
|
onClick={() => {
|
|
if (connectionUri) {
|
|
navigator.clipboard.writeText(connectionUri);
|
|
// Use the showAlert helper instead of native alert
|
|
const event = new CustomEvent('morphik:alert', {
|
|
detail: {
|
|
type: 'success',
|
|
title: 'Copied!',
|
|
message: 'Connection URI copied to clipboard',
|
|
duration: 3000,
|
|
},
|
|
});
|
|
window.dispatchEvent(event);
|
|
}
|
|
}}
|
|
title="Copy connection URI"
|
|
>
|
|
<Copy className="h-3.5 w-3.5" />
|
|
</Button>
|
|
)}
|
|
|
|
{/* Add Edit button if not editing and not in read-only mode */}
|
|
{!isReadOnlyUri && !isEditingUri && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="text-xs px-2 py-0.5 h-6"
|
|
onClick={() => setIsEditingUri(true)}
|
|
>
|
|
{connectionUri ? "Edit" : "Add URI"}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* URI Display or Editing Area */}
|
|
{isReadOnlyUri ? (
|
|
// Read-only display for production/cloud environments
|
|
<div className="mt-1 bg-background p-1 rounded text-xs font-mono break-all">
|
|
{connectionUri ?
|
|
// Show first and last characters with asterisks in between
|
|
`${connectionUri.substring(0, 12)}...${connectionUri.substring(connectionUri.length - 12)}`
|
|
: 'No URI configured'
|
|
}
|
|
</div>
|
|
) : isEditingUri ? (
|
|
// Editing mode (only available when not read-only)
|
|
<div className="mt-1">
|
|
<div className="flex gap-1 items-center">
|
|
<Input
|
|
value={editableUri}
|
|
onChange={(e) => setEditableUri(e.target.value)}
|
|
placeholder="morphik://token@host (empty for localhost)"
|
|
className="h-7 text-xs font-mono"
|
|
/>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7"
|
|
onClick={() => {
|
|
setEditableUri('');
|
|
handleSaveUri();
|
|
}}
|
|
title="Clear URI (use localhost)"
|
|
>
|
|
<span className="text-xs">X</span>
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7"
|
|
onClick={handleSaveUri}
|
|
title="Save URI"
|
|
>
|
|
<Check className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
<div className="mt-0.5 text-[10px] text-muted-foreground">
|
|
Format: morphik://your_token@your_api_host
|
|
</div>
|
|
</div>
|
|
) : (
|
|
// Display current URI (or placeholder)
|
|
<div className="mt-1 bg-background p-1 rounded text-xs font-mono break-all">
|
|
{connectionUri || "Using localhost by default - click Edit to change"}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<ScrollArea className="flex-1">
|
|
<div className="space-y-4 px-4 py-2">
|
|
<Button
|
|
variant={activeSection === "documents" ? "secondary" : "ghost"}
|
|
className={cn(
|
|
"w-full justify-start",
|
|
isCollapsed && "justify-center"
|
|
)}
|
|
onClick={() => onSectionChange("documents")}
|
|
>
|
|
<FileText className="h-4 w-4" />
|
|
{!isCollapsed && <span className="ml-2">Folders</span>}
|
|
</Button>
|
|
|
|
<Button
|
|
variant={activeSection === "search" ? "secondary" : "ghost"}
|
|
className={cn(
|
|
"w-full justify-start",
|
|
isCollapsed && "justify-center"
|
|
)}
|
|
onClick={() => onSectionChange("search")}
|
|
>
|
|
<Search className="h-4 w-4" />
|
|
{!isCollapsed && <span className="ml-2">Search</span>}
|
|
</Button>
|
|
|
|
<Button
|
|
variant={activeSection === "chat" ? "secondary" : "ghost"}
|
|
className={cn(
|
|
"w-full justify-start",
|
|
isCollapsed && "justify-center"
|
|
)}
|
|
onClick={() => onSectionChange("chat")}
|
|
>
|
|
<MessageSquare className="h-4 w-4" />
|
|
{!isCollapsed && <span className="ml-2">Chat</span>}
|
|
</Button>
|
|
|
|
<Button
|
|
variant={activeSection === "graphs" ? "secondary" : "ghost"}
|
|
className={cn(
|
|
"w-full justify-start",
|
|
isCollapsed && "justify-center"
|
|
)}
|
|
onClick={() => onSectionChange("graphs")}
|
|
>
|
|
<Network className="h-4 w-4" />
|
|
{!isCollapsed && <span className="ml-2">Graphs</span>}
|
|
</Button>
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
<div className={cn("p-4 border-t", isCollapsed ? "flex justify-center" : "flex items-center justify-between gap-2")}>
|
|
{!isCollapsed && onBackClick && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={onBackClick}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
<span>Back to dashboard</span>
|
|
</Button>
|
|
)}
|
|
<ModeToggle />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|