OwnGPT.v2 / client /src /components /UI /WorkspacePanel.jsx
parthib07's picture
Upload 199 files
212c959 verified
import * as ScrollArea from '@radix-ui/react-scroll-area'
import { motion } from 'framer-motion'
import {
Activity,
BarChart3,
Bot,
Clock3,
FileText,
Layers3,
MessageSquareText,
Sparkles,
X,
} from 'lucide-react'
import Button from './Button'
export default function WorkspacePanel({
sidePanel,
selectedFile,
stats,
insights,
isAuthenticated,
runtimeStatus,
activeProvider,
activeModel,
onClose,
onPreviewAnalytics,
onAskAboutFile,
}) {
const isVisible = sidePanel && (sidePanel !== 'file' || selectedFile)
if (!isVisible) return null
return (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-40 bg-slate-950/35 backdrop-blur-sm xl:hidden"
onClick={onClose}
/>
<motion.aside
initial={{ opacity: 0, x: 24 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 24 }}
className="fixed inset-y-0 right-0 z-50 flex w-[min(100vw,24rem)] flex-col border-l border-border/70 bg-background/94 shadow-panel backdrop-blur-2xl xl:static xl:z-auto xl:w-[360px] xl:bg-background/72"
>
<div className="flex items-center justify-between border-b border-border/70 px-4 py-4">
<div>
<h2 className="font-display text-lg font-semibold text-foreground">
{sidePanel === 'file' ? 'File Preview' : 'Workspace Insights'}
</h2>
<p className="text-sm leading-6 text-muted-foreground">
{sidePanel === 'file'
? 'Review extracted file context beside the conversation.'
: 'Quick status for the current model, history, and workspace health.'}
</p>
</div>
<button
type="button"
onClick={onClose}
className="rounded-full p-2 text-muted-foreground transition hover:bg-secondary hover:text-foreground"
aria-label="Close side panel"
>
<X className="h-4 w-4" />
</button>
</div>
<ScrollArea.Root className="flex-1 overflow-hidden">
<ScrollArea.Viewport className="h-full w-full">
{sidePanel === 'file' && selectedFile ? (
<div className="space-y-4 p-4">
<div className="surface-soft p-4">
<div className="flex items-start gap-3">
<div className="rounded-2xl bg-accent/10 p-3 text-accent">
<FileText className="h-5 w-5" />
</div>
<div>
<h3 className="text-sm font-semibold text-foreground">{selectedFile.filename}</h3>
<p className="text-xs uppercase tracking-[0.16em] text-muted-foreground">
{selectedFile.kind}
</p>
</div>
</div>
<Button
variant="primary"
className="mt-4 w-full"
onClick={() => onAskAboutFile(selectedFile)}
>
<MessageSquareText className="h-4 w-4" />
Ask about this file
</Button>
</div>
<div className="surface-soft p-4">
<p className="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
Extracted context
</p>
<pre className="whitespace-pre-wrap break-words font-mono text-xs leading-6 text-foreground/85">
{selectedFile.extracted_text || 'No extracted text available.'}
</pre>
</div>
</div>
) : (
<div className="space-y-4 p-4">
<div className="grid gap-3">
<StatCard
icon={Activity}
label="Backend"
value={formatRuntimeLabel(runtimeStatus?.status)}
note={formatRuntimeNote(runtimeStatus)}
tone={runtimeStatus?.status === 'ready' ? 'accent' : runtimeStatus?.status === 'degraded' ? 'danger' : 'default'}
/>
<StatCard
icon={Layers3}
label="Workspace Reach"
value={String(stats.totalSessions)}
note={`${stats.totalMessages} total messages across ${stats.activeDays} active days`}
/>
<StatCard
icon={Bot}
label="Active Model"
value={activeModel || 'Not selected'}
note={activeProvider ? `${activeProvider} currently selected` : 'Choose a provider to start chatting'}
/>
<StatCard
icon={Sparkles}
label="Current Mode"
value={stats.modeLabel}
note={stats.modeDescription}
/>
<StatCard
icon={BarChart3}
label="Visible Messages"
value={String(stats.messageCount)}
note="Messages in the current transcript"
/>
<StatCard
icon={Clock3}
label="Average Depth"
value={String(stats.averageMessagesPerSession)}
note="Average messages per conversation in your workspace"
/>
</div>
<InsightSection
title="Top modes"
emptyLabel={isAuthenticated ? 'No mode data yet.' : 'Sign in to unlock usage insights.'}
items={insights?.mode_breakdown}
/>
<InsightSection
title="Top models"
emptyLabel={isAuthenticated ? 'No model activity captured yet.' : 'Usage data will appear here after sign-in.'}
items={insights?.model_breakdown}
/>
<InsightSection
title="Recent conversations"
emptyLabel={isAuthenticated ? 'Recent conversations will appear here.' : 'Your recent chats will appear here after sign-in.'}
items={insights?.recent_titles}
valueFormatter={null}
/>
<Button variant="secondary" className="w-full" onClick={onPreviewAnalytics}>
Keep insights open
</Button>
</div>
)}
</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="vertical" className="w-2.5 bg-transparent p-0.5">
<ScrollArea.Thumb className="rounded-full bg-border/80" />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</motion.aside>
</>
)
}
function formatRuntimeLabel(status) {
if (status === 'ready') return 'Ready'
if (status === 'degraded') return 'Degraded'
return 'Checking'
}
function formatRuntimeNote(runtimeStatus) {
if (!runtimeStatus) return 'Backend status is still loading.'
const mongo = runtimeStatus.checks?.mongo || 'unknown'
const vectorMemory = runtimeStatus.checks?.vector_memory || 'unknown'
const providerCount = runtimeStatus.enabled_providers?.length || 0
return `Mongo: ${mongo} / Memory: ${vectorMemory} / Providers: ${providerCount} / v${runtimeStatus.version || 'unknown'}`
}
function StatCard({ icon: Icon, label, value, note, tone = 'default' }) {
const iconToneClass =
tone === 'accent'
? 'bg-accent/10 text-accent'
: tone === 'danger'
? 'bg-danger/10 text-danger'
: 'bg-accent/10 text-accent'
return (
<div className="surface-soft p-4">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
{label}
</p>
<p className="mt-2 font-display text-2xl font-semibold text-foreground">{value}</p>
<p className="mt-1 text-sm leading-6 text-muted-foreground">{note}</p>
</div>
<div className={`rounded-2xl p-3 ${iconToneClass}`}>
<Icon className="h-5 w-5" />
</div>
</div>
</div>
)
}
function InsightSection({ title, items, emptyLabel, valueFormatter = (value) => value }) {
const entries = Array.isArray(items)
? items.map((item) => ({ label: item, value: null }))
: Object.entries(items || {}).slice(0, 5).map(([label, value]) => ({ label, value }))
return (
<div className="surface-soft p-4">
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
{title}
</p>
{entries.length ? (
<div className="mt-3 space-y-2">
{entries.map((entry) => (
<div
key={entry.label}
className="flex items-center justify-between gap-3 rounded-2xl bg-background/80 px-3 py-2"
>
<span className="truncate text-sm text-foreground">{entry.label}</span>
{valueFormatter && entry.value !== null ? (
<span className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
{valueFormatter(entry.value)}
</span>
) : null}
</div>
))}
</div>
) : (
<p className="mt-3 text-sm text-muted-foreground">{emptyLabel}</p>
)}
</div>
)
}