Release v3.9.10

Published from npm package build
Source: https://github.com/thedotmack/claude-mem-source
This commit is contained in:
Alex Newman
2025-10-03 18:27:36 -04:00
parent 85ed7c3d2f
commit 5b30764fa8
23 changed files with 2 additions and 6630 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "3.9.9",
"version": "3.9.10",
"description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [
"claude",
-819
View File
@@ -1,819 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import {
Dialog,
DialogBackdrop,
DialogPanel,
TransitionChild,
} from '@headlessui/react';
import {
Bars3Icon,
MagnifyingGlassIcon,
XMarkIcon,
} from '@heroicons/react/20/solid';
import OverviewCard from './src/components/OverviewCard';
function classNames(...classes) {
return classes.filter(Boolean).join(' ');
}
export default function MemoryStream() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [overviewsOpen, setOverviewsOpen] = useState(false);
const [memories, setMemories] = useState([]);
const [overviews, setOverviews] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [status, setStatus] = useState('connecting');
const [connected, setConnected] = useState(false);
const [selectedProject, setSelectedProject] = useState('all');
const [selectedTag, setSelectedTag] = useState(null);
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [isAwaitingOverview, setIsAwaitingOverview] = useState(false);
const [debugOverviewCard, setDebugOverviewCard] = useState(false);
const eventSourceRef = useRef(null);
let filteredMemories = selectedProject === 'all'
? memories
: memories.filter(m => m.project === selectedProject);
if (selectedTag) {
filteredMemories = filteredMemories.filter(m => m.concepts?.includes(selectedTag));
}
const filteredOverviews = selectedProject === 'all'
? overviews
: overviews.filter(o => o.project === selectedProject);
const existingCount = filteredMemories.filter(m => !m.isNew).length;
const newCount = filteredMemories.filter(m => m.isNew).length;
const stats = {
total: filteredMemories.length,
new: newCount,
existing: existingCount,
sessions: new Set(filteredMemories.map(m => m.session_id)).size,
projects: new Set(memories.map(m => m.project)).size
};
const projects = ['all', ...new Set(memories.map(m => m.project).filter(Boolean))];
useEffect(() => {
setStatus('connecting');
const eventSource = new EventSource('http://localhost:3001/stream');
eventSourceRef.current = eventSource;
eventSource.onopen = () => {
setStatus('connected');
setConnected(true);
};
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'initial_load') {
const existingMemories = data.memories.map(m => ({ ...m, isNew: false }));
setMemories(existingMemories);
const existingOverviews = data.overviews.map(o => ({ ...o, isNew: false }));
setOverviews(existingOverviews);
setInitialLoadComplete(true);
setCurrentIndex(0);
} else if (data.type === 'new_memories') {
const newMemories = data.memories.map(m => ({ ...m, isNew: true }));
setMemories(prev => [...newMemories, ...prev]);
setCurrentIndex(0);
} else if (data.type === 'new_overviews') {
const newOverviews = data.overviews.map(o => ({ ...o, isNew: true }));
// Remove placeholders for the same projects as the incoming real overviews
const incomingProjects = new Set(newOverviews.map(o => o.project));
setOverviews(prev => {
const withoutPlaceholders = prev.filter(o =>
!o.isPlaceholder || !incomingProjects.has(o.project)
);
return [...newOverviews, ...withoutPlaceholders];
});
setIsAwaitingOverview(false);
} else if (data.type === 'session_start') {
// Only process for current project (or 'all')
if (selectedProject === 'all' || data.project === selectedProject) {
setIsProcessing(true);
setIsAwaitingOverview(true);
// Create placeholder overview card
const placeholderOverview = {
id: `placeholder-${Date.now()}`,
project: data.project,
content: '⏳ Session in progress...',
created_at: new Date().toISOString(),
session_id: null,
isNew: true,
isPlaceholder: true
};
setOverviews(prev => [placeholderOverview, ...prev]);
}
} else if (data.type === 'session_end') {
// Only process for current project (or 'all')
if (selectedProject === 'all' || data.project === selectedProject) {
setIsProcessing(false);
setIsAwaitingOverview(false);
}
}
};
eventSource.onerror = () => {
setStatus('reconnecting');
setConnected(false);
eventSource.close();
setTimeout(() => window.location.reload(), 2000);
};
return () => eventSource.close();
}, []);
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'ArrowLeft') {
e.preventDefault();
setCurrentIndex(i => (i - 1 + filteredMemories.length) % filteredMemories.length);
} else if (e.key === 'ArrowRight') {
e.preventDefault();
setCurrentIndex(i => (i + 1) % filteredMemories.length);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [filteredMemories.length]);
const formatTimestamp = (timestamp) => {
const date = new Date(timestamp);
const diff = Date.now() - date;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (seconds < 60) return `${seconds}s ago`;
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
};
const memory = filteredMemories[currentIndex] || {};
// Extract unique tags from all memories
const allTags = [...new Set(memories.flatMap(m => m.concepts || []))];
const tagCounts = allTags.reduce((acc, tag) => {
acc[tag] = memories.filter(m => m.concepts?.includes(tag)).length;
return acc;
}, {});
const sortedTags = allTags.sort((a, b) => tagCounts[b] - tagCounts[a]);
return (
<>
<div className="min-h-screen bg-black text-gray-100 relative overflow-hidden">
{/* Background Effects */}
<div className="fixed inset-0 opacity-20">
<div className="absolute inset-0" style={{
backgroundImage: 'linear-gradient(rgba(59, 130, 246, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(59, 130, 246, 0.1) 1px, transparent 1px)',
backgroundSize: '50px 50px'
}} />
</div>
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-0 left-0 w-full h-full" style={{
background: 'radial-gradient(ellipse at 20% 30%, rgba(59, 130, 246, 0.15) 0%, transparent 50%)'
}} />
<div className="absolute top-0 right-0 w-full h-full" style={{
background: 'radial-gradient(ellipse at 80% 20%, rgba(139, 92, 246, 0.15) 0%, transparent 50%)'
}} />
<div className="absolute bottom-0 left-1/2 w-full h-full" style={{
background: 'radial-gradient(ellipse at 50% 80%, rgba(16, 185, 129, 0.1) 0%, transparent 50%)'
}} />
</div>
{/* Mobile sidebar */}
<Dialog open={sidebarOpen} onClose={setSidebarOpen} className="relative z-50 xl:hidden">
<DialogBackdrop
transition
className="fixed inset-0 bg-gray-900/80 transition-opacity duration-300 ease-linear data-[closed]:opacity-0"
/>
<div className="fixed inset-0 flex">
<DialogPanel
transition
className="relative mr-16 flex w-full max-w-xs flex-1 transform transition duration-300 ease-in-out data-[closed]:-translate-x-full"
>
<TransitionChild>
<div className="absolute left-full top-0 flex w-16 justify-center pt-5 duration-300 ease-in-out data-[closed]:opacity-0">
<button type="button" onClick={() => setSidebarOpen(false)} className="-m-2.5 p-2.5">
<span className="sr-only">Close sidebar</span>
<XMarkIcon aria-hidden="true" className="size-6 text-white" />
</button>
</div>
</TransitionChild>
<div className="relative flex grow flex-col gap-y-5 overflow-y-auto bg-gray-900/90 backdrop-blur-xl px-6 border-r border-gray-800">
<div className="relative flex h-16 shrink-0 items-center">
<img src="/claude-mem-logo.webp" alt="claude-mem" className="h-10 w-auto" />
</div>
<nav className="relative flex flex-1 flex-col">
<div className="space-y-6">
<div className="relative">
<div className="absolute -inset-2 bg-gradient-to-r from-blue-600/10 via-purple-600/10 to-emerald-600/10 rounded-xl blur-xl" />
<div className="relative bg-gray-800/50 backdrop-blur-sm rounded-xl p-4 border border-gray-700/50">
<h3 className="text-xs font-bold text-blue-400 mb-3 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-400" />
STATISTICS
</h3>
<div className="space-y-2.5">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-400">Total</span>
<span className="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-300 to-blue-500">{stats.total}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-400">New</span>
<span className="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-emerald-300 to-emerald-500">{stats.new}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-400">Sessions</span>
<span className="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-300 to-purple-500">{stats.sessions}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-400">Projects</span>
<span className="text-lg font-bold text-gray-300">{stats.projects}</span>
</div>
</div>
</div>
</div>
<div className="relative">
<div className="absolute -inset-2 bg-gradient-to-r from-purple-600/10 via-blue-600/10 to-purple-600/10 rounded-xl blur-xl" />
<div className="relative bg-gray-800/50 backdrop-blur-sm rounded-xl p-4 border border-gray-700/50">
<h3 className="text-xs font-bold text-purple-400 mb-3 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-400" />
TAG CLOUD
</h3>
<div className="flex flex-wrap gap-2">
{sortedTags.slice(0, 20).map((tag) => (
<span
key={tag}
onClick={() => {
setSelectedTag(selectedTag === tag ? null : tag);
setCurrentIndex(0);
}}
className={classNames(
"px-2.5 py-1 rounded-lg border text-xs font-medium transition-all cursor-pointer",
selectedTag === tag
? "bg-purple-500/30 border-purple-400/60 text-purple-200 shadow-lg shadow-purple-500/20"
: "bg-purple-500/10 border-purple-400/30 text-purple-300 hover:bg-purple-500/20"
)}
>
{tag} ({tagCounts[tag]})
</span>
))}
</div>
</div>
</div>
</div>
</nav>
</div>
</DialogPanel>
</div>
</Dialog>
{/* Desktop sidebar */}
<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-80 xl:flex-col">
<div className="relative flex grow flex-col gap-y-5 overflow-y-auto bg-gray-900/90 backdrop-blur-xl px-6 border-r border-gray-800">
<div className="flex h-16 shrink-0 items-center">
<img src="/claude-mem-logo.webp" alt="claude-mem" className="h-10 w-auto" />
</div>
<nav className="flex flex-1 flex-col">
<div className="space-y-6">
<div className="relative">
<div className="absolute -inset-2 bg-gradient-to-r from-blue-600/10 via-purple-600/10 to-emerald-600/10 rounded-xl blur-xl" />
<div className="relative bg-gray-800/50 backdrop-blur-sm rounded-xl p-4 border border-gray-700/50">
<h3 className="text-xs font-bold text-blue-400 mb-3 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-400" />
STATISTICS
</h3>
<div className="space-y-2.5">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-400">Total</span>
<span className="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-300 to-blue-500">{stats.total}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-400">New</span>
<span className="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-emerald-300 to-emerald-500">{stats.new}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-400">Sessions</span>
<span className="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-300 to-purple-500">{stats.sessions}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-400">Projects</span>
<span className="text-lg font-bold text-gray-300">{stats.projects}</span>
</div>
</div>
</div>
</div>
<div className="relative">
<div className="absolute -inset-2 bg-gradient-to-r from-purple-600/10 via-blue-600/10 to-purple-600/10 rounded-xl blur-xl" />
<div className="relative bg-gray-800/50 backdrop-blur-sm rounded-xl p-4 border border-gray-700/50">
<h3 className="text-xs font-bold text-purple-400 mb-3 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-400" />
TAG CLOUD
</h3>
<div className="flex flex-wrap gap-2">
{sortedTags.slice(0, 20).map((tag) => (
<span
key={tag}
onClick={() => {
setSelectedTag(selectedTag === tag ? null : tag);
setCurrentIndex(0);
}}
className={classNames(
"px-2.5 py-1 rounded-lg border text-xs font-medium transition-all cursor-pointer",
selectedTag === tag
? "bg-purple-500/30 border-purple-400/60 text-purple-200 shadow-lg shadow-purple-500/20"
: "bg-purple-500/10 border-purple-400/30 text-purple-300 hover:bg-purple-500/20"
)}
>
{tag} ({tagCounts[tag]})
</span>
))}
</div>
</div>
</div>
</div>
</nav>
</div>
</div>
<div className="xl:pl-80">
{/* Fixed search header */}
<div className="fixed top-0 left-0 right-0 xl:left-80 z-40 flex h-16 shrink-0 items-center gap-x-6 border-b border-gray-800 bg-gray-900/90 backdrop-blur-xl px-4 sm:px-6 lg:px-8">
<button
type="button"
onClick={() => setSidebarOpen(true)}
className="-m-2.5 p-2.5 text-gray-300 xl:hidden hover:text-white transition-colors"
>
<span className="sr-only">Open sidebar</span>
<Bars3Icon aria-hidden="true" className="size-5" />
</button>
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
<form action="#" method="GET" className="grid flex-1 grid-cols-1 relative">
<input
name="search"
placeholder="Search memories..."
aria-label="Search"
className="col-start-1 row-start-1 block size-full bg-gray-800/50 rounded-lg pl-10 pr-4 text-base text-gray-100 border border-gray-700 focus:border-blue-500/50 outline-none placeholder:text-gray-500 sm:text-sm/6 transition-colors"
/>
<MagnifyingGlassIcon
aria-hidden="true"
className="pointer-events-none col-start-1 row-start-1 size-5 self-center ml-3 text-gray-500"
/>
</form>
</div>
<div className="flex items-center gap-3">
{connected && (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-gradient-to-r from-purple-500/20 to-blue-500/20 border border-purple-400/30">
<div className="w-2 h-2 bg-purple-400 rounded-full animate-pulse shadow-lg shadow-purple-400/50" />
<span className="text-xs font-bold text-purple-300 tracking-wide">LIVE</span>
</div>
)}
<button
onClick={() => setDebugOverviewCard(!debugOverviewCard)}
className={`px-3 py-1.5 rounded-full text-xs font-bold transition-all ${
debugOverviewCard
? 'bg-gradient-to-r from-blue-500/30 to-purple-500/30 border border-blue-400/60 text-blue-300'
: 'bg-gray-800/50 border border-gray-700 text-gray-400 hover:border-gray-600'
}`}
>
DEBUG
</button>
</div>
<button
type="button"
onClick={() => setOverviewsOpen(true)}
className="-m-2.5 p-2.5 text-gray-300 xl:hidden hover:text-white transition-colors"
>
<span className="sr-only">Open overviews</span>
<Bars3Icon aria-hidden="true" className="size-5" />
</button>
</div>
<main className="pt-16">
{/* Activity Indicator Bar */}
<div className="h-1 fixed top-16 left-0 right-0 xl:left-80 z-30" style={{
background: 'linear-gradient(90deg, transparent, #3b82f6, #8b5cf6, #10b981, transparent)',
animation: isProcessing ? 'scan 3s ease-in-out infinite' : 'none',
opacity: isProcessing ? 1 : 0,
boxShadow: isProcessing ? '0 0 20px rgba(59, 130, 246, 0.8)' : 'none'
}} />
{/* Debug Overview Card Mode */}
{debugOverviewCard && (
<OverviewCard debugMode={true} initialState="empty" />
)}
{/* Normal Memory Stream View */}
{!debugOverviewCard && (
<div className="px-4 sm:px-6 lg:px-8 py-6">
{!connected && (
<div className="max-w-3xl mx-auto mb-12">
<div className="relative group">
<div className="absolute -inset-1 bg-gradient-to-r from-blue-600 via-purple-600 to-emerald-600 rounded-2xl blur opacity-25 animate-pulse" />
<div className="relative bg-gray-900/90 backdrop-blur-xl rounded-2xl p-8 border border-gray-800">
<div className="text-center">
<div className="relative inline-block mb-4">
<div className="absolute inset-0 bg-blue-500/20 blur-3xl animate-pulse" />
<div className="relative text-6xl">📡</div>
</div>
<h2 className="text-2xl font-bold mb-2 bg-gradient-to-r from-blue-300 to-purple-300 bg-clip-text text-transparent">
{status === 'connecting' ? 'Connecting to Memory Stream' : 'Reconnecting...'}
</h2>
<p className="text-gray-400">~/.claude-mem/claude-mem.db</p>
</div>
</div>
</div>
</div>
)}
{connected && filteredMemories.length === 0 && (
<div className="max-w-4xl mx-auto text-center py-20">
<div className="relative inline-block">
<div className="absolute inset-0 bg-purple-500/20 blur-3xl animate-pulse" />
<div className="relative text-6xl mb-4">💭</div>
</div>
<h3 className="text-2xl font-bold text-gray-300 mb-2">No Memories Found</h3>
<p className="text-gray-500">
{selectedProject === 'all'
? 'No memories with titles in database'
: `No memories for project: ${selectedProject}`}
</p>
</div>
)}
{filteredMemories.length > 0 && (
<div className="mb-8 max-w-6xl mx-auto relative z-50">
<div className="flex items-center gap-4">
<select
value={selectedProject}
onChange={(e) => {
setSelectedProject(e.target.value);
setCurrentIndex(0);
}}
className="px-4 py-2 rounded-lg bg-gray-800/50 border border-gray-700 text-gray-300 font-mono text-sm cursor-pointer hover:border-gray-600 focus:outline-none focus:border-blue-500/50 transition-colors"
>
{projects.map(project => (
<option key={project} value={project}>
{project === 'all' ? 'All Projects' : project}
</option>
))}
</select>
<button
onClick={() => setCurrentIndex(i => (i - 1 + filteredMemories.length) % filteredMemories.length)}
className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-600/20 to-purple-600/20 border border-blue-400/30 hover:border-blue-400/60 flex items-center justify-center transition-all duration-300 hover:scale-110 group"
>
<span className="text-blue-300 text-lg group-hover:text-blue-200"></span>
</button>
<div className="flex items-center gap-3 flex-1">
<div className="flex-1 h-1.5 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-blue-500 via-purple-500 to-emerald-500 transition-all duration-300"
style={{ width: `${((currentIndex + 1) / filteredMemories.length) * 100}%` }}
/>
</div>
<div className="text-sm font-mono text-gray-500 min-w-[80px] text-center">
{currentIndex + 1} / {filteredMemories.length}
</div>
</div>
<button
onClick={() => setCurrentIndex(i => (i + 1) % filteredMemories.length)}
className="w-10 h-10 rounded-full bg-gradient-to-br from-purple-600/20 to-blue-600/20 border border-purple-400/30 hover:border-purple-400/60 flex items-center justify-center transition-all duration-300 hover:scale-110 group"
>
<span className="text-purple-300 text-lg group-hover:text-purple-200"></span>
</button>
</div>
</div>
)}
{filteredMemories.length > 0 && (
<div className="max-w-6xl mx-auto">
<div key={memory.id} className="relative" style={{
animation: 'slideIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)'
}}>
<div className="absolute -inset-4 bg-gradient-to-r from-blue-600/20 via-purple-600/20 to-emerald-600/20 rounded-3xl blur-2xl" />
<div className="relative bg-gradient-to-br from-gray-900/90 to-gray-950/90 backdrop-blur-xl rounded-3xl p-12 border border-gray-800">
<div className="mb-8">
<div className="flex items-center gap-3 mb-4 flex-wrap">
<span className="px-4 py-1.5 rounded-full text-xs font-bold bg-gradient-to-r from-blue-500/20 to-blue-500/10 border border-blue-400/30 text-blue-300">
#{memory.id}
</span>
<span className="px-4 py-1.5 rounded-full text-xs font-bold bg-gradient-to-r from-purple-500/20 to-purple-500/10 border border-purple-400/30 text-purple-300">
{memory.project}
</span>
{memory.origin && (
<span className="px-4 py-1.5 rounded-full text-xs font-bold bg-gradient-to-r from-emerald-500/20 to-emerald-500/10 border border-emerald-400/30 text-emerald-300">
{memory.origin}
</span>
)}
<span className="ml-auto text-xs font-mono text-gray-500">
{formatTimestamp(memory.created_at)}
</span>
</div>
<h1 className="text-4xl font-black text-transparent bg-clip-text bg-gradient-to-r from-blue-300 via-purple-300 to-emerald-300 mb-4 leading-tight">
{memory.title}
</h1>
{memory.subtitle && (
<p className="text-xl text-gray-400 leading-relaxed">
{memory.subtitle}
</p>
)}
</div>
{memory.facts?.length > 0 && (
<div className="mb-8">
<h3 className="text-sm font-bold text-blue-400 mb-4 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-400" />
FACTS EXTRACTED
</h3>
<div className="space-y-3">
{memory.facts.map((fact, i) => (
<div key={i} className="flex gap-3 text-gray-300 leading-relaxed" style={{
animation: 'fadeInUp 0.5s ease-out',
animationDelay: `${i * 0.1}s`,
animationFillMode: 'both'
}}>
<span className="text-blue-400 font-mono text-xs mt-1"></span>
<span>{fact}</span>
</div>
))}
</div>
</div>
)}
{memory.concepts?.length > 0 && (
<div className="mb-8">
<h3 className="text-sm font-bold text-purple-400 mb-4 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-400" />
CONCEPTS
</h3>
<div className="flex flex-wrap gap-2">
{memory.concepts.map((concept, i) => (
<span key={i} className="px-3 py-1.5 rounded-lg bg-purple-500/10 border border-purple-400/30 text-purple-300 text-sm font-medium" style={{
animation: 'fadeInUp 0.5s ease-out',
animationDelay: `${i * 0.05}s`,
animationFillMode: 'both'
}}>
{concept}
</span>
))}
</div>
</div>
)}
{memory.files_touched?.length > 0 && (
<div>
<h3 className="text-sm font-bold text-emerald-400 mb-4 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
FILES TOUCHED
</h3>
<div className="space-y-2">
{memory.files_touched.map((file, i) => (
<div key={i} className="flex items-center gap-2 text-sm font-mono text-emerald-300/80" style={{
animation: 'fadeInUp 0.5s ease-out',
animationDelay: `${i * 0.1}s`,
animationFillMode: 'both'
}}>
<span>📄</span>
<span>{file}</span>
</div>
))}
</div>
</div>
)}
<div className="mt-8 pt-6 border-t border-gray-800 flex items-center justify-between">
<div className="text-xs font-mono text-gray-600">
session: {memory.session_id?.substring(0, 8)}...{memory.session_id?.slice(-4)}
</div>
</div>
</div>
</div>
<div className="mt-6 text-center text-xs text-gray-600">
<p> arrow keys to navigate</p>
</div>
</div>
)}
</div>
)}
</main>
{/* Mobile overviews drawer */}
<Dialog open={overviewsOpen} onClose={setOverviewsOpen} className="relative z-50 xl:hidden">
<DialogBackdrop
transition
className="fixed inset-0 bg-gray-900/80 transition-opacity duration-300 ease-linear data-[closed]:opacity-0"
/>
<div className="fixed inset-0 flex justify-end">
<DialogPanel
transition
className="relative ml-16 flex w-full max-w-xs flex-1 transform transition duration-300 ease-in-out data-[closed]:translate-x-full"
>
<TransitionChild>
<div className="absolute right-full top-0 flex w-16 justify-center pt-5 duration-300 ease-in-out data-[closed]:opacity-0">
<button type="button" onClick={() => setOverviewsOpen(false)} className="-m-2.5 p-2.5">
<span className="sr-only">Close overviews</span>
<XMarkIcon aria-hidden="true" className="size-6 text-white" />
</button>
</div>
</TransitionChild>
<div className="relative flex grow flex-col overflow-y-auto bg-gray-900/90 backdrop-blur-xl border-l border-gray-800">
<header className="flex items-center justify-between border-b border-gray-800 px-4 py-4 sm:px-6">
<h2 className="text-base/7 font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-300 to-purple-300">Session Overviews</h2>
<span className="text-sm font-mono text-gray-500">{filteredOverviews.length}</span>
</header>
<ul role="list" className="divide-y divide-gray-800">
{filteredOverviews.length === 0 && (
<li className="px-4 py-12 text-center">
<div className="relative inline-block">
<div className="absolute inset-0 bg-purple-500/10 blur-2xl" />
<div className="relative text-4xl mb-3 opacity-50">📋</div>
</div>
<p className="text-sm text-gray-500">No overviews yet</p>
</li>
)}
{filteredOverviews.map((overview) => (
<li key={overview.id} className="px-4 py-4 sm:px-6 hover:bg-gray-800/30 transition-colors">
<div className="flex items-start gap-3 mb-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="px-2 py-0.5 rounded text-xs font-bold bg-purple-500/10 border border-purple-400/30 text-purple-300">
#{overview.id}
</span>
{overview.isNew && (
<span className="px-2 py-0.5 rounded text-xs font-bold bg-blue-500/20 border border-blue-400/40 text-blue-300 animate-pulse">
NEW
</span>
)}
</div>
<div className="text-xs font-mono text-gray-500 truncate">
{overview.project}
</div>
</div>
<div className="text-xs text-gray-500">
{formatTimestamp(overview.created_at)}
</div>
</div>
{overview.promptTitle && (
<div className="mb-3">
<h3 className="text-sm font-bold text-blue-300 mb-1 leading-snug">
{overview.promptTitle}
</h3>
{overview.promptSubtitle && (
<p className="text-xs text-gray-400 leading-relaxed">
{overview.promptSubtitle}
</p>
)}
</div>
)}
<p className="text-sm text-gray-300 leading-relaxed line-clamp-6">
{overview.content}
</p>
<div className="mt-2 pt-2 border-t border-gray-800">
<div className="text-xs font-mono text-gray-600 truncate">
session: {overview.session_id?.substring(0, 8)}...{overview.session_id?.slice(-4)}
</div>
</div>
</li>
))}
</ul>
</div>
</DialogPanel>
</div>
</Dialog>
{/* Desktop overviews sidebar */}
<aside className="hidden xl:block bg-gray-900/90 backdrop-blur-xl xl:fixed xl:bottom-0 xl:right-0 xl:top-16 xl:w-96 xl:overflow-y-auto xl:border-l xl:border-gray-800">
<header className="flex items-center justify-between border-b border-gray-800 px-4 py-4 sm:px-6 lg:px-8">
<h2 className="text-base/7 font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-300 to-purple-300">Session Overviews</h2>
<span className="text-sm font-mono text-gray-500">{filteredOverviews.length}</span>
</header>
<ul role="list" className="divide-y divide-gray-800">
{filteredOverviews.length === 0 && (
<li className="px-4 py-12 text-center">
<div className="relative inline-block">
<div className="absolute inset-0 bg-purple-500/10 blur-2xl" />
<div className="relative text-4xl mb-3 opacity-50">📋</div>
</div>
<p className="text-sm text-gray-500">No overviews yet</p>
</li>
)}
{filteredOverviews.map((overview) => (
<li key={overview.id} className="px-4 py-4 sm:px-6 lg:px-8 hover:bg-gray-800/30 transition-colors">
<div className="flex items-start gap-3 mb-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="px-2 py-0.5 rounded text-xs font-bold bg-purple-500/10 border border-purple-400/30 text-purple-300">
#{overview.id}
</span>
{overview.isNew && (
<span className="px-2 py-0.5 rounded text-xs font-bold bg-blue-500/20 border border-blue-400/40 text-blue-300 animate-pulse">
NEW
</span>
)}
</div>
<div className="text-xs font-mono text-gray-500 truncate">
{overview.project}
</div>
</div>
<div className="text-xs text-gray-500">
{formatTimestamp(overview.created_at)}
</div>
</div>
{overview.promptTitle && (
<div className="mb-3">
<h3 className="text-sm font-bold text-blue-300 mb-1 leading-snug">
{overview.promptTitle}
</h3>
{overview.promptSubtitle && (
<p className="text-xs text-gray-400 leading-relaxed">
{overview.promptSubtitle}
</p>
)}
</div>
)}
<p className="text-sm text-gray-300 leading-relaxed line-clamp-6">
{overview.content}
</p>
<div className="mt-2 pt-2 border-t border-gray-800">
<div className="text-xs font-mono text-gray-600 truncate">
session: {overview.session_id?.substring(0, 8)}...{overview.session_id?.slice(-4)}
</div>
</div>
</li>
))}
</ul>
</aside>
</div>
</div>
<style>{`
@keyframes scan {
0%, 100% {
transform: translateX(-100%);
opacity: 0;
}
50% {
transform: translateX(100%);
opacity: 1;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`}</style>
</>
);
}
-101
View File
@@ -1,101 +0,0 @@
# Memory Stream - Live Memory Viewer
A real-time slideshow viewer for claude-mem memories with SSE (Server-Sent Events) support.
## Features
- 📡 **Live streaming** - Automatically displays new memories as they're created
- 🎬 **Auto-slideshow** - Cycles through memories every 5 seconds
- ⏸️ **Pause/Resume** - Space bar or button controls
- ⌨️ **Keyboard navigation** - Arrow keys to navigate
- 🎨 **Beautiful UI** - Cyberpunk-themed neural network aesthetic
## Setup
### 1. Start the SSE server
```bash
node src/ui/memory-stream/server.js
# or use the package script:
npm run memory-stream:server
```
This will:
- Watch `~/.claude-mem/claude-mem.db-wal` for changes
- Serve SSE events on `http://localhost:3001/stream`
- Automatically detect and broadcast new memories
### 2. Start your React dev server
```bash
# In your React app directory
npm run dev
# or
bun dev
```
### 3. Open the viewer
Navigate to your React app (usually `http://localhost:5173`)
## Usage
### Live Mode (Recommended)
1. Click **"CONNECT LIVE STREAM"**
2. Server must be running (`node memory-stream-server.js`)
3. New memories appear automatically as they're created
4. Perfect for real-time monitoring during Claude Code sessions
### Presentation Mode (Alternative)
1. Click **"START PRESENTATION"**
2. Select your `~/.claude-mem/claude-mem.db` file
3. Static slideshow of existing memories
4. No server required
## Controls
- **Space** - Pause/Resume slideshow
- **←** - Previous memory
- **→** - Next memory
- **Click buttons** - Same as keyboard controls
## How It Works
### SSE Server
- Uses `better-sqlite3` with WAL mode (already enabled in claude-mem)
- Watches the `-wal` file for changes using `fs.watch()`
- Queries for new memories when WAL changes detected
- Broadcasts to all connected clients via Server-Sent Events
### React Client
- Connects to SSE endpoint via `EventSource`
- Auto-reconnects on connection loss
- Appends new memories to the slideshow in real-time
- No polling, pure event-driven updates
## Technical Details
**Database**: SQLite with WAL (Write-Ahead Logging) mode
**Change Detection**: `fs.watch()` on `claude-mem.db-wal`
**Transport**: Server-Sent Events (SSE)
**Auto-reconnect**: 2-second retry on connection loss
**CORS**: Enabled for local development
## Troubleshooting
**"Connection lost"**
- Ensure server is running: `node src/ui/memory-stream/server.js`
- Check port 3001 is available
- Look for server console output
**No memories showing**
- Verify memories exist with `title` field
- Check database path: `~/.claude-mem/claude-mem.db`
- Try "START PRESENTATION" mode to verify database access
**WAL file not found**
- WAL mode auto-enabled by claude-mem
- File created automatically on first write
- Check database exists at expected path
Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

-22
View File
@@ -1,22 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "",
"css": "index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Memory Stream - Claude Mem</title>
<script type="module" crossorigin src="/assets/index-BjZoir4u.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-5_3SV7cT.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
-120
View File
@@ -1,120 +0,0 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
: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);
--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);
}
.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);
--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);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
-12
View File
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Memory Stream - Claude Mem</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.jsx"></script>
</body>
</html>
-1
View File
@@ -1 +0,0 @@
export { default } from './MemoryStream.jsx';
-8
View File
@@ -1,8 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
-604
View File
@@ -1,604 +0,0 @@
'use client'
import { useState } from 'react'
import {
Dialog,
DialogBackdrop,
DialogPanel,
Menu,
MenuButton,
MenuItem,
MenuItems,
TransitionChild,
} from '@headlessui/react'
import {
ChartBarSquareIcon,
Cog6ToothIcon,
FolderIcon,
GlobeAltIcon,
ServerIcon,
SignalIcon,
XMarkIcon,
} from '@heroicons/react/24/outline'
import { Bars3Icon, ChevronRightIcon, ChevronUpDownIcon, MagnifyingGlassIcon } from '@heroicons/react/20/solid'
const navigation = [
{ name: 'Projects', href: '#', icon: FolderIcon, current: false },
{ name: 'Deployments', href: '#', icon: ServerIcon, current: true },
{ name: 'Activity', href: '#', icon: SignalIcon, current: false },
{ name: 'Domains', href: '#', icon: GlobeAltIcon, current: false },
{ name: 'Usage', href: '#', icon: ChartBarSquareIcon, current: false },
{ name: 'Settings', href: '#', icon: Cog6ToothIcon, current: false },
]
const teams = [
{ id: 1, name: 'Planetaria', href: '#', initial: 'P', current: false },
{ id: 2, name: 'Protocol', href: '#', initial: 'P', current: false },
{ id: 3, name: 'Tailwind Labs', href: '#', initial: 'T', current: false },
]
const statuses = {
offline: 'text-gray-400 bg-gray-100 dark:text-gray-500 dark:bg-gray-100/10',
online: 'text-green-500 bg-green-500/10 dark:text-green-400 dark:bg-green-400/10',
error: 'text-rose-500 bg-rose-500/10 dark:text-rose-400 dark:bg-rose-400/10',
}
const environments = {
Preview: 'text-gray-500 bg-gray-50 ring-gray-200 dark:text-gray-400 dark:bg-gray-400/10 dark:ring-gray-400/20',
Production:
'text-indigo-500 bg-indigo-50 ring-indigo-200 dark:text-indigo-400 dark:bg-indigo-400/10 dark:ring-indigo-400/30',
}
const deployments = [
{
id: 1,
href: '#',
projectName: 'ios-app',
teamName: 'Planetaria',
status: 'offline',
statusText: 'Initiated 1m 32s ago',
description: 'Deploys from GitHub',
environment: 'Preview',
},
{
id: 2,
href: '#',
projectName: 'mobile-api',
teamName: 'Planetaria',
status: 'online',
statusText: 'Deployed 3m ago',
description: 'Deploys from GitHub',
environment: 'Production',
},
{
id: 3,
href: '#',
projectName: 'tailwindcss.com',
teamName: 'Tailwind Labs',
status: 'offline',
statusText: 'Deployed 3h ago',
description: 'Deploys from GitHub',
environment: 'Preview',
},
{
id: 4,
href: '#',
projectName: 'company-website',
teamName: 'Tailwind Labs',
status: 'online',
statusText: 'Deployed 1d ago',
description: 'Deploys from GitHub',
environment: 'Preview',
},
{
id: 5,
href: '#',
projectName: 'relay-service',
teamName: 'Protocol',
status: 'online',
statusText: 'Deployed 1d ago',
description: 'Deploys from GitHub',
environment: 'Production',
},
{
id: 6,
href: '#',
projectName: 'android-app',
teamName: 'Planetaria',
status: 'online',
statusText: 'Deployed 5d ago',
description: 'Deploys from GitHub',
environment: 'Preview',
},
{
id: 7,
href: '#',
projectName: 'api.protocol.chat',
teamName: 'Protocol',
status: 'error',
statusText: 'Failed to deploy 6d ago',
description: 'Deploys from GitHub',
environment: 'Preview',
},
{
id: 8,
href: '#',
projectName: 'planetaria.tech',
teamName: 'Planetaria',
status: 'online',
statusText: 'Deployed 6d ago',
description: 'Deploys from GitHub',
environment: 'Preview',
},
]
const activityItems = [
{
user: {
name: 'Michael Foster',
imageUrl:
'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
},
projectName: 'ios-app',
commit: '2d89f0c8',
branch: 'main',
date: '1h',
dateTime: '2023-01-23T11:00',
},
{
user: {
name: 'Lindsay Walton',
imageUrl:
'https://images.unsplash.com/photo-1517841905240-472988babdf9?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
},
projectName: 'mobile-api',
commit: '249df660',
branch: 'main',
date: '3h',
dateTime: '2023-01-23T09:00',
},
{
user: {
name: 'Courtney Henry',
imageUrl:
'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
},
projectName: 'ios-app',
commit: '11464223',
branch: 'main',
date: '12h',
dateTime: '2023-01-23T00:00',
},
{
user: {
name: 'Courtney Henry',
imageUrl:
'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
},
projectName: 'company-website',
commit: 'dad28e95',
branch: 'main',
date: '2d',
dateTime: '2023-01-21T13:00',
},
{
user: {
name: 'Michael Foster',
imageUrl:
'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
},
projectName: 'relay-service',
commit: '624bc94c',
branch: 'main',
date: '5d',
dateTime: '2023-01-18T12:34',
},
{
user: {
name: 'Courtney Henry',
imageUrl:
'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
},
projectName: 'api.protocol.chat',
commit: 'e111f80e',
branch: 'main',
date: '1w',
dateTime: '2023-01-16T15:54',
},
{
user: {
name: 'Michael Foster',
imageUrl:
'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
},
projectName: 'api.protocol.chat',
commit: '5e136005',
branch: 'main',
date: '1w',
dateTime: '2023-01-16T11:31',
},
{
user: {
name: 'Whitney Francis',
imageUrl:
'https://images.unsplash.com/photo-1517365830460-955ce3ccd263?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
},
projectName: 'ios-app',
commit: '5c1fd07f',
branch: 'main',
date: '2w',
dateTime: '2023-01-09T08:45',
},
]
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default function Example() {
const [sidebarOpen, setSidebarOpen] = useState(false)
return (
<>
{/*
This example requires updating your template:
```
<html class="h-full bg-white dark:bg-gray-900">
<body class="h-full">
```
*/}
<div>
<Dialog open={sidebarOpen} onClose={setSidebarOpen} className="relative z-50 xl:hidden">
<DialogBackdrop
transition
className="fixed inset-0 bg-gray-900/80 transition-opacity duration-300 ease-linear data-[closed]:opacity-0"
/>
<div className="fixed inset-0 flex">
<DialogPanel
transition
className="relative mr-16 flex w-full max-w-xs flex-1 transform transition duration-300 ease-in-out data-[closed]:-translate-x-full"
>
<TransitionChild>
<div className="absolute left-full top-0 flex w-16 justify-center pt-5 duration-300 ease-in-out data-[closed]:opacity-0">
<button type="button" onClick={() => setSidebarOpen(false)} className="-m-2.5 p-2.5">
<span className="sr-only">Close sidebar</span>
<XMarkIcon aria-hidden="true" className="size-6 text-white" />
</button>
</div>
</TransitionChild>
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="relative flex grow flex-col gap-y-5 overflow-y-auto bg-gray-50 px-6 dark:bg-gray-900 dark:ring dark:ring-white/10 dark:before:pointer-events-none dark:before:absolute dark:before:inset-0 dark:before:bg-black/10">
<div className="relative flex h-16 shrink-0 items-center">
<img
alt="Your Company"
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=600"
className="h-8 w-auto dark:hidden"
/>
<img
alt="Your Company"
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=500"
className="hidden h-8 w-auto dark:block"
/>
</div>
<nav className="relative flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
<li key={item.name}>
<a
href={item.href}
className={classNames(
item.current
? 'bg-gray-100 text-indigo-600 dark:bg-white/5 dark:text-white'
: 'text-gray-700 hover:bg-gray-100 hover:text-indigo-600 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white',
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold',
)}
>
<item.icon
aria-hidden="true"
className={classNames(
item.current
? 'text-indigo-600 dark:text-white'
: 'text-gray-400 group-hover:text-indigo-600 dark:group-hover:text-white',
'size-6 shrink-0',
)}
/>
{item.name}
</a>
</li>
))}
</ul>
</li>
<li>
<div className="text-xs/6 font-semibold text-gray-400">Your teams</div>
<ul role="list" className="-mx-2 mt-2 space-y-1">
{teams.map((team) => (
<li key={team.name}>
<a
href={team.href}
className={classNames(
team.current
? 'bg-gray-100 text-indigo-600 dark:bg-white/5 dark:text-white'
: 'text-gray-700 hover:bg-gray-100 hover:text-indigo-600 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white',
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold',
)}
>
<span
className={classNames(
team.current
? 'border-indigo-600 text-indigo-600 dark:border-white/20 dark:text-white'
: 'border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600 dark:border-white/10 dark:group-hover:border-white/20 dark:group-hover:text-white',
'flex size-6 shrink-0 items-center justify-center rounded-lg border bg-white text-[0.625rem] font-medium dark:bg-white/5',
)}
>
{team.initial}
</span>
<span className="truncate">{team.name}</span>
</a>
</li>
))}
</ul>
</li>
<li className="-mx-6 mt-auto">
<a
href="#"
className="flex items-center gap-x-4 px-6 py-3 text-sm/6 font-semibold text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-white/5"
>
<img
alt=""
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
className="size-8 rounded-full bg-gray-100 outline outline-1 -outline-offset-1 outline-black/5 dark:bg-gray-800 dark:outline-white/10"
/>
<span className="sr-only">Your profile</span>
<span aria-hidden="true">Tom Cook</span>
</a>
</li>
</ul>
</nav>
</div>
</DialogPanel>
</div>
</Dialog>
{/* Static sidebar for desktop */}
<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col dark:bg-gray-900">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-50 px-6 ring-1 ring-gray-200 dark:bg-black/10 dark:ring-white/5">
<div className="flex h-16 shrink-0 items-center">
<img
alt="Your Company"
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=600"
className="h-8 w-auto dark:hidden"
/>
<img
alt="Your Company"
src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=500"
className="hidden h-8 w-auto dark:block"
/>
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
<li>
<ul role="list" className="-mx-2 space-y-1">
{navigation.map((item) => (
<li key={item.name}>
<a
href={item.href}
className={classNames(
item.current
? 'bg-gray-100 text-indigo-600 dark:bg-white/5 dark:text-white'
: 'text-gray-700 hover:bg-gray-100 hover:text-indigo-600 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white',
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold',
)}
>
<item.icon
aria-hidden="true"
className={classNames(
item.current
? 'text-indigo-600 dark:text-white'
: 'text-gray-400 group-hover:text-indigo-600 dark:group-hover:text-white',
'size-6 shrink-0',
)}
/>
{item.name}
</a>
</li>
))}
</ul>
</li>
<li>
<div className="text-xs/6 font-semibold text-gray-500 dark:text-gray-400">Your teams</div>
<ul role="list" className="-mx-2 mt-2 space-y-1">
{teams.map((team) => (
<li key={team.name}>
<a
href={team.href}
className={classNames(
team.current
? 'bg-gray-100 text-indigo-600 dark:bg-white/5 dark:text-white'
: 'text-gray-700 hover:bg-gray-100 hover:text-indigo-600 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-white',
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold',
)}
>
<span
className={classNames(
team.current
? 'border-indigo-600 text-indigo-600 dark:border-white/20 dark:text-white'
: 'border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600 dark:border-white/10 dark:group-hover:border-white/20 dark:group-hover:text-white',
'flex size-6 shrink-0 items-center justify-center rounded-lg border bg-white text-[0.625rem] font-medium dark:bg-white/5',
)}
>
{team.initial}
</span>
<span className="truncate">{team.name}</span>
</a>
</li>
))}
</ul>
</li>
<li className="-mx-6 mt-auto">
<a
href="#"
className="flex items-center gap-x-4 px-6 py-3 text-sm/6 font-semibold text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-white/5"
>
<img
alt=""
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
className="size-8 rounded-full bg-gray-100 outline outline-1 -outline-offset-1 outline-black/5 dark:bg-gray-800 dark:outline-white/10"
/>
<span className="sr-only">Your profile</span>
<span aria-hidden="true">Tom Cook</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
<div className="xl:pl-72">
{/* Sticky search header */}
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-6 border-b border-gray-200 bg-white px-4 shadow-sm sm:px-6 lg:px-8 dark:border-white/5 dark:bg-gray-900 dark:shadow-none">
<button
type="button"
onClick={() => setSidebarOpen(true)}
className="-m-2.5 p-2.5 text-gray-900 xl:hidden dark:text-white"
>
<span className="sr-only">Open sidebar</span>
<Bars3Icon aria-hidden="true" className="size-5" />
</button>
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
<form action="#" method="GET" className="grid flex-1 grid-cols-1">
<input
name="search"
placeholder="Search"
aria-label="Search"
className="col-start-1 row-start-1 block size-full bg-transparent pl-8 text-base text-gray-900 outline-none placeholder:text-gray-400 sm:text-sm/6 dark:text-white dark:placeholder:text-gray-500"
/>
<MagnifyingGlassIcon
aria-hidden="true"
className="pointer-events-none col-start-1 row-start-1 size-5 self-center text-gray-400 dark:text-gray-500"
/>
</form>
</div>
</div>
<main className="lg:pr-96">
<header className="flex items-center justify-between border-b border-gray-200 px-4 py-4 sm:px-6 sm:py-6 lg:px-8 dark:border-white/5">
<h1 className="text-base/7 font-semibold text-gray-900 dark:text-white">Deployments</h1>
{/* Sort dropdown */}
<Menu as="div" className="relative">
<MenuButton className="flex items-center gap-x-1 text-sm/6 font-medium text-gray-900 dark:text-white">
Sort by
<ChevronUpDownIcon aria-hidden="true" className="size-5 text-gray-500" />
</MenuButton>
<MenuItems
transition
className="absolute right-0 z-10 mt-2.5 w-40 origin-top-right rounded-md bg-white py-2 shadow-lg outline outline-1 outline-gray-900/5 transition data-[closed]:scale-95 data-[closed]:transform data-[closed]:opacity-0 data-[enter]:duration-100 data-[leave]:duration-75 data-[enter]:ease-out data-[leave]:ease-in dark:bg-gray-800 dark:shadow-none dark:-outline-offset-1 dark:outline-white/10"
>
<MenuItem>
<a
href="#"
className="block px-3 py-1 text-sm/6 text-gray-900 data-[focus]:bg-gray-50 data-[focus]:outline-none dark:text-white dark:data-[focus]:bg-white/5"
>
Name
</a>
</MenuItem>
<MenuItem>
<a
href="#"
className="block px-3 py-1 text-sm/6 text-gray-900 data-[focus]:bg-gray-50 data-[focus]:outline-none dark:text-white dark:data-[focus]:bg-white/5"
>
Date updated
</a>
</MenuItem>
<MenuItem>
<a
href="#"
className="block px-3 py-1 text-sm/6 text-gray-900 data-[focus]:bg-gray-50 data-[focus]:outline-none dark:text-white dark:data-[focus]:bg-white/5"
>
Environment
</a>
</MenuItem>
</MenuItems>
</Menu>
</header>
{/* Deployment list */}
<ul role="list" className="divide-y divide-gray-100 dark:divide-white/5">
{deployments.map((deployment) => (
<li key={deployment.id} className="relative flex items-center space-x-4 px-4 py-4 sm:px-6 lg:px-8">
<div className="min-w-0 flex-auto">
<div className="flex items-center gap-x-3">
<div className={classNames(statuses[deployment.status], 'flex-none rounded-full p-1')}>
<div className="size-2 rounded-full bg-current" />
</div>
<h2 className="min-w-0 text-sm/6 font-semibold text-gray-900 dark:text-white">
<a href={deployment.href} className="flex gap-x-2">
<span className="truncate">{deployment.teamName}</span>
<span className="text-gray-400">/</span>
<span className="whitespace-nowrap">{deployment.projectName}</span>
<span className="absolute inset-0" />
</a>
</h2>
</div>
<div className="mt-3 flex items-center gap-x-2.5 text-xs/5 text-gray-500 dark:text-gray-400">
<p className="truncate">{deployment.description}</p>
<svg viewBox="0 0 2 2" className="size-0.5 flex-none fill-gray-300 dark:fill-gray-500">
<circle r={1} cx={1} cy={1} />
</svg>
<p className="whitespace-nowrap">{deployment.statusText}</p>
</div>
</div>
<div
className={classNames(
environments[deployment.environment],
'flex-none rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset',
)}
>
{deployment.environment}
</div>
<ChevronRightIcon aria-hidden="true" className="size-5 flex-none text-gray-400" />
</li>
))}
</ul>
</main>
{/* Activity feed */}
<aside className="bg-gray-50 lg:fixed lg:bottom-0 lg:right-0 lg:top-16 lg:w-96 lg:overflow-y-auto lg:border-l lg:border-gray-200 dark:bg-black/10 dark:lg:border-white/5">
<header className="flex items-center justify-between border-b border-gray-200 px-4 py-4 sm:px-6 sm:py-6 lg:px-8 dark:border-white/5">
<h2 className="text-base/7 font-semibold text-gray-900 dark:text-white">Activity feed</h2>
<a href="#" className="text-sm/6 font-semibold text-indigo-600 dark:text-indigo-400">
View all
</a>
</header>
<ul role="list" className="divide-y divide-gray-100 dark:divide-white/5">
{activityItems.map((item) => (
<li key={item.commit} className="px-4 py-4 sm:px-6 lg:px-8">
<div className="flex items-center gap-x-3">
<img
alt=""
src={item.user.imageUrl}
className="size-6 flex-none rounded-full bg-gray-100 outline outline-1 -outline-offset-1 outline-black/5 dark:bg-gray-800 dark:outline-white/10"
/>
<h3 className="flex-auto truncate text-sm/6 font-semibold text-gray-900 dark:text-white">
{item.user.name}
</h3>
<time dateTime={item.dateTime} className="flex-none text-xs text-gray-500 dark:text-gray-600">
{item.date}
</time>
</div>
<p className="mt-3 truncate text-sm text-gray-500">
Pushed to <span className="text-gray-700 dark:text-gray-400">{item.projectName}</span> (
<span className="font-mono text-gray-700 dark:text-gray-400">{item.commit}</span> on{' '}
<span className="text-gray-700 dark:text-gray-400">{item.branch}</span>)
</p>
</li>
))}
</ul>
</aside>
</div>
</div>
</>
)
}
-10
View File
@@ -1,10 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import MemoryStream from './MemoryStream.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<MemoryStream />
</React.StrictMode>,
)
File diff suppressed because it is too large Load Diff
-34
View File
@@ -1,34 +0,0 @@
{
"name": "memory-stream-ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.544.0",
"ogl": "^1.0.11",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^3.3.1",
"three": "^0.180.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.14",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"vite": "^5.0.8"
}
}
-232
View File
@@ -1,232 +0,0 @@
#!/usr/bin/env node
import { watch, existsSync, readFileSync } from 'fs';
import { createServer } from 'http';
import { homedir } from 'os';
import { join } from 'path';
import Database from 'better-sqlite3';
const DB_PATH = join(homedir(), '.claude-mem/claude-mem.db');
const SESSIONS_DIR = join(homedir(), '.claude-mem/sessions');
const PORT = 3001;
let clients = [];
let lastMaxId = 0;
let lastOverviewId = 0;
function safeJsonParse(jsonString) {
if (!jsonString) return [];
try {
return JSON.parse(jsonString);
} catch {
return [];
}
}
function getMemories(minId = 0) {
const db = new Database(DB_PATH, { readonly: true });
const memories = db.prepare(`
SELECT id, session_id, created_at, project, origin, title, subtitle, facts, concepts, files_touched
FROM memories
WHERE id > ? AND title IS NOT NULL
ORDER BY id DESC
`).all(minId);
db.close();
return memories.map(m => ({
...m,
facts: safeJsonParse(m.facts),
concepts: safeJsonParse(m.concepts),
files_touched: safeJsonParse(m.files_touched)
}));
}
function getOverviews(minId = 0) {
const db = new Database(DB_PATH, { readonly: true });
const overviews = db.prepare(`
SELECT id, session_id, content, created_at, project, origin
FROM overviews
WHERE id > ?
ORDER BY id DESC
`).all(minId);
db.close();
// Enrich overviews with session titles/subtitles from session JSON files
return overviews.map(overview => {
const sessionFile = join(SESSIONS_DIR, `${overview.project}_streaming.json`);
let promptTitle = null;
let promptSubtitle = null;
try {
if (existsSync(sessionFile)) {
const sessionData = JSON.parse(readFileSync(sessionFile, 'utf8'));
// Only attach title/subtitle if it's from the same Claude session
if (sessionData.claudeSessionId === overview.session_id) {
promptTitle = sessionData.promptTitle || null;
promptSubtitle = sessionData.promptSubtitle || null;
}
}
} catch (e) {
// Ignore errors reading session file
}
return {
...overview,
promptTitle,
promptSubtitle
};
});
}
function getSessions() {
const db = new Database(DB_PATH, { readonly: true });
// Get unique sessions from overviews
const sessions = db.prepare(`
SELECT DISTINCT
o.session_id,
o.project,
o.created_at,
o.content as overview_content
FROM overviews o
ORDER BY o.created_at DESC
LIMIT 50
`).all();
db.close();
return sessions;
}
function getSessionData(sessionId) {
const db = new Database(DB_PATH, { readonly: true });
const overview = db.prepare(`
SELECT id, session_id, content, created_at, project, origin
FROM overviews
WHERE session_id = ?
LIMIT 1
`).get(sessionId);
const memories = db.prepare(`
SELECT id, session_id, created_at, project, origin, title, subtitle, facts, concepts, files_touched
FROM memories
WHERE session_id = ? AND title IS NOT NULL
ORDER BY id ASC
`).all(sessionId);
db.close();
return {
overview,
memories: memories.map(m => ({
...m,
facts: safeJsonParse(m.facts),
concepts: safeJsonParse(m.concepts),
files_touched: safeJsonParse(m.files_touched)
}))
};
}
function broadcast(type, data) {
const message = `data: ${JSON.stringify({ type, ...data })}\n\n`;
clients.forEach(client => client.write(message));
}
function broadcastSessionState(eventType, project) {
const message = `data: ${JSON.stringify({ type: eventType, project })}\n\n`;
clients.forEach(client => client.write(message));
console.log(`📡 Broadcasting ${eventType} for project: ${project}`);
}
const server = createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
if (req.url === '/stream') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
clients.push(res);
console.log(`🔌 Client connected (${clients.length} total)`);
const allMemories = getMemories(-1);
lastMaxId = allMemories.length > 0 ? Math.max(...allMemories.map(m => m.id)) : 0;
const allOverviews = getOverviews(-1);
lastOverviewId = allOverviews.length > 0 ? Math.max(...allOverviews.map(o => o.id)) : 0;
console.log(`📦 Sending ${allMemories.length} memories and ${allOverviews.length} overviews to new client`);
broadcast('initial_load', { memories: allMemories, overviews: allOverviews });
req.on('close', () => {
clients = clients.filter(client => client !== res);
console.log(`🔌 Client disconnected (${clients.length} remaining)`);
});
} else if (req.url === '/api/sessions') {
res.writeHead(200, { 'Content-Type': 'application/json' });
const sessions = getSessions();
res.end(JSON.stringify(sessions));
} else if (req.url.startsWith('/api/session/')) {
const sessionId = req.url.replace('/api/session/', '');
res.writeHead(200, { 'Content-Type': 'application/json' });
const sessionData = getSessionData(sessionId);
res.end(JSON.stringify(sessionData));
} else {
res.writeHead(404);
res.end();
}
});
watch(DB_PATH, (eventType) => {
const newMemories = getMemories(lastMaxId);
if (newMemories.length > 0) {
lastMaxId = Math.max(...newMemories.map(m => m.id));
console.log(`✨ Broadcasting ${newMemories.length} new memories`);
broadcast('new_memories', { memories: newMemories });
}
const newOverviews = getOverviews(lastOverviewId);
if (newOverviews.length > 0) {
lastOverviewId = Math.max(...newOverviews.map(o => o.id));
console.log(`✨ Broadcasting ${newOverviews.length} new overviews`);
broadcast('new_overviews', { overviews: newOverviews });
}
});
watch(SESSIONS_DIR, (eventType, filename) => {
if (!filename || !filename.endsWith('_streaming.json')) return;
const project = filename.replace('_streaming.json', '');
const sessionPath = join(SESSIONS_DIR, filename);
if (eventType === 'rename') {
// Check if file exists to determine if it was created or deleted
if (existsSync(sessionPath)) {
broadcastSessionState('session_start', project);
} else {
broadcastSessionState('session_end', project);
}
}
});
server.listen(PORT, () => {
console.log(`🚀 Memory Stream Server running on http://localhost:${PORT}`);
console.log(`📡 SSE endpoint: http://localhost:${PORT}/stream`);
});
process.on('SIGINT', () => {
clients.forEach(client => client.end());
server.close();
process.exit(0);
});
@@ -1,570 +0,0 @@
// Component ported and enhanced from https://codepen.io/JuanFuentes/pen/eYEeoyE
import { useRef, useEffect } from 'react';
import * as THREE from 'three';
const vertexShader = `
varying vec2 vUv;
uniform float uTime;
uniform float mouse;
uniform float uEnableWaves;
void main() {
vUv = uv;
float time = uTime * 5.;
float waveFactor = uEnableWaves;
vec3 transformed = position;
transformed.x += sin(time + position.y) * 0.5 * waveFactor;
transformed.y += cos(time + position.z) * 0.15 * waveFactor;
transformed.z += sin(time + position.x) * waveFactor;
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`;
const fragmentShader = `
varying vec2 vUv;
uniform float mouse;
uniform float uTime;
uniform sampler2D uTexture;
void main() {
float time = uTime;
vec2 pos = vUv;
float move = sin(time + mouse) * 0.01;
float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;
float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;
float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;
float a = texture2D(uTexture, pos).a;
gl_FragColor = vec4(r, g, b, a);
}
`;
function map(n, start, stop, start2, stop2) {
return ((n - start) / (stop - start)) * (stop2 - start2) + start2;
}
const PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;
class AsciiFilter {
width = 0;
height = 0;
center = { x: 0, y: 0 };
mouse = { x: 0, y: 0 };
cols = 0;
rows = 0;
constructor(renderer, {
fontSize,
fontFamily,
charset,
invert
} = {}) {
this.renderer = renderer;
this.domElement = document.createElement('div');
this.domElement.style.position = 'absolute';
this.domElement.style.top = '0';
this.domElement.style.left = '0';
this.domElement.style.width = '100%';
this.domElement.style.height = '100%';
this.pre = document.createElement('pre');
this.domElement.appendChild(this.pre);
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d');
this.domElement.appendChild(this.canvas);
this.deg = 0;
this.invert = invert ?? true;
this.fontSize = fontSize ?? 12;
this.fontFamily = fontFamily ?? "'Courier New', monospace";
this.charset = charset ?? ' .\'`^",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';
if (this.context) {
this.context.imageSmoothingEnabled = false;
this.context.imageSmoothingEnabled = false;
}
this.onMouseMove = this.onMouseMove.bind(this);
document.addEventListener('mousemove', this.onMouseMove);
}
setSize(width, height) {
this.width = width;
this.height = height;
this.renderer.setSize(width, height);
this.reset();
this.center = { x: width / 2, y: height / 2 };
this.mouse = { x: this.center.x, y: this.center.y };
}
reset() {
if (this.context) {
this.context.font = `${this.fontSize}px ${this.fontFamily}`;
const charWidth = this.context.measureText('A').width;
this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));
this.rows = Math.floor(this.height / this.fontSize);
this.canvas.width = this.cols;
this.canvas.height = this.rows;
this.pre.style.fontFamily = this.fontFamily;
this.pre.style.fontSize = `${this.fontSize}px`;
this.pre.style.margin = '0';
this.pre.style.padding = '0';
this.pre.style.lineHeight = '1em';
this.pre.style.position = 'absolute';
this.pre.style.left = '50%';
this.pre.style.top = '50%';
this.pre.style.transform = 'translate(-50%, -50%)';
this.pre.style.zIndex = '9';
this.pre.style.backgroundAttachment = 'fixed';
this.pre.style.mixBlendMode = 'difference';
}
}
render(scene, camera) {
this.renderer.render(scene, camera);
const w = this.canvas.width;
const h = this.canvas.height;
if (this.context) {
this.context.clearRect(0, 0, w, h);
if (this.context && w && h) {
this.context.drawImage(this.renderer.domElement, 0, 0, w, h);
}
this.asciify(this.context, w, h);
this.hue();
}
}
onMouseMove(e) {
this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO };
}
get dx() {
return this.mouse.x - this.center.x;
}
get dy() {
return this.mouse.y - this.center.y;
}
hue() {
const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI;
this.deg += (deg - this.deg) * 0.075;
this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`;
}
asciify(ctx, w, h) {
if (w && h) {
const imgData = ctx.getImageData(0, 0, w, h).data;
let str = '';
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const i = x * 4 + y * 4 * w;
const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];
if (a === 0) {
str += ' ';
continue;
}
let gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;
let idx = Math.floor((1 - gray) * (this.charset.length - 1));
if (this.invert) idx = this.charset.length - idx - 1;
str += this.charset[idx];
}
str += '\n';
}
this.pre.innerHTML = str;
}
}
dispose() {
document.removeEventListener('mousemove', this.onMouseMove);
}
}
class CanvasTxt {
constructor(txt, {
fontSize = 200,
fontFamily = 'Arial',
color = '#fdf9f3'
} = {}) {
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d');
this.txt = txt;
this.fontSize = fontSize;
this.fontFamily = fontFamily;
this.color = color;
this.font = `600 ${this.fontSize}px ${this.fontFamily}`;
}
resize() {
if (this.context) {
this.context.font = this.font;
// Split text into lines
const lines = this.txt.split('\n');
// Measure all lines to find max width
let maxWidth = 0;
for (const line of lines) {
const metrics = this.context.measureText(line);
maxWidth = Math.max(maxWidth, metrics.width);
}
// Calculate total height (first line metrics for line height)
const firstMetrics = this.context.measureText(lines[0] || 'A');
const lineHeight = Math.ceil(firstMetrics.actualBoundingBoxAscent + firstMetrics.actualBoundingBoxDescent);
const textWidth = Math.ceil(maxWidth) + 20;
const textHeight = lineHeight * lines.length + 20;
this.canvas.width = textWidth;
this.canvas.height = textHeight;
}
}
render() {
if (this.context) {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.context.fillStyle = this.color;
this.context.font = this.font;
// Split text into lines and render each
const lines = this.txt.split('\n');
const metrics = this.context.measureText(lines[0] || 'A');
const lineHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent);
lines.forEach((line, index) => {
const yPos = 10 + metrics.actualBoundingBoxAscent + (index * lineHeight);
this.context.fillText(line, 10, yPos);
});
}
}
get width() {
return this.canvas.width;
}
get height() {
return this.canvas.height;
}
get texture() {
return this.canvas;
}
}
class CanvAscii {
animationFrameId = 0;
constructor(
{
text,
asciiFontSize,
textFontSize,
textColor,
planeBaseHeight,
enableWaves,
enableMouseRotation
},
containerElem,
width,
height
) {
this.textString = text;
this.asciiFontSize = asciiFontSize;
this.textFontSize = textFontSize;
this.textColor = textColor;
this.planeBaseHeight = planeBaseHeight;
this.container = containerElem;
this.width = width;
this.height = height;
this.enableWaves = enableWaves;
this.enableMouseRotation = enableMouseRotation;
this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);
this.camera.position.z = 30;
this.scene = new THREE.Scene();
this.mouse = { x: 0, y: 0 };
this.onMouseMove = this.onMouseMove.bind(this);
this.setMesh();
this.setRenderer();
}
setMesh() {
this.textCanvas = new CanvasTxt(this.textString, {
fontSize: this.textFontSize,
fontFamily: 'IBM Plex Mono',
color: this.textColor
});
this.textCanvas.resize();
this.textCanvas.render();
this.texture = new THREE.CanvasTexture(this.textCanvas.texture);
this.texture.minFilter = THREE.NearestFilter;
const textAspect = this.textCanvas.width / this.textCanvas.height;
const baseH = this.planeBaseHeight;
const planeW = baseH * textAspect;
const planeH = baseH;
this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);
this.material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
transparent: true,
uniforms: {
uTime: { value: 0 },
mouse: { value: 1.0 },
uTexture: { value: this.texture },
uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }
}
});
this.mesh = new THREE.Mesh(this.geometry, this.material);
this.scene.add(this.mesh);
}
setRenderer() {
this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
this.renderer.setPixelRatio(1);
this.renderer.setClearColor(0x000000, 0);
this.filter = new AsciiFilter(this.renderer, {
fontFamily: 'IBM Plex Mono',
fontSize: this.asciiFontSize,
invert: true
});
this.container.appendChild(this.filter.domElement);
this.setSize(this.width, this.height);
this.container.addEventListener('mousemove', this.onMouseMove);
this.container.addEventListener('touchmove', this.onMouseMove);
}
setSize(w, h) {
this.width = w;
this.height = h;
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
this.filter.setSize(w, h);
this.center = { x: w / 2, y: h / 2 };
}
load() {
this.animate();
}
onMouseMove(evt) {
const e = (evt).touches ? (evt).touches[0] : (evt);
const bounds = this.container.getBoundingClientRect();
const x = e.clientX - bounds.left;
const y = e.clientY - bounds.top;
this.mouse = { x, y };
}
animate() {
const animateFrame = () => {
this.animationFrameId = requestAnimationFrame(animateFrame);
this.render();
};
animateFrame();
}
render() {
const time = new Date().getTime() * 0.001;
this.textCanvas.render();
this.texture.needsUpdate = true;
(this.mesh.material).uniforms.uTime.value = Math.sin(time);
this.updateRotation();
this.filter.render(this.scene, this.camera);
}
updateRotation() {
if (!this.enableMouseRotation) return;
const x = map(this.mouse.y, 0, this.height, 0.5, -0.5);
const y = map(this.mouse.x, 0, this.width, -0.5, 0.5);
this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05;
this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;
}
clear() {
this.scene.traverse(object => {
const obj = object;
if (!obj.isMesh) return;
[obj.material].flat().forEach(material => {
material.dispose();
Object.keys(material).forEach(key => {
const matProp = material[key];
if (matProp && typeof matProp === 'object' && 'dispose' in matProp && typeof matProp.dispose === 'function') {
matProp.dispose();
}
});
});
obj.geometry.dispose();
});
this.scene.clear();
}
dispose() {
cancelAnimationFrame(this.animationFrameId);
this.filter.dispose();
this.container.removeChild(this.filter.domElement);
this.container.removeEventListener('mousemove', this.onMouseMove);
this.container.removeEventListener('touchmove', this.onMouseMove);
this.clear();
this.renderer.dispose();
}
}
export default function ASCIIText({
text = 'David!',
asciiFontSize = 8,
textFontSize = 200,
textColor = '#fdf9f3',
planeBaseHeight = 8,
enableWaves = true,
enableMouseRotation = true
}) {
const containerRef = useRef(null);
const asciiRef = useRef(null);
useEffect(() => {
if (!containerRef.current) return;
const { width, height } = containerRef.current.getBoundingClientRect();
if (width === 0 || height === 0) {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) {
const { width: w, height: h } = entry.boundingClientRect;
asciiRef.current = new CanvAscii({
text,
asciiFontSize,
textFontSize,
textColor,
planeBaseHeight,
enableWaves,
enableMouseRotation
}, containerRef.current, w, h);
asciiRef.current.load();
observer.disconnect();
}
}, { threshold: 0.1 });
observer.observe(containerRef.current);
return () => {
observer.disconnect();
if (asciiRef.current) {
asciiRef.current.dispose();
}
};
}
asciiRef.current = new CanvAscii({
text,
asciiFontSize,
textFontSize,
textColor,
planeBaseHeight,
enableWaves,
enableMouseRotation
}, containerRef.current, width, height);
asciiRef.current.load();
const ro = new ResizeObserver(entries => {
if (!entries[0] || !asciiRef.current) return;
const { width: w, height: h } = entries[0].contentRect;
if (w > 0 && h > 0) {
asciiRef.current.setSize(w, h);
}
});
ro.observe(containerRef.current);
return () => {
ro.disconnect();
if (asciiRef.current) {
asciiRef.current.dispose();
}
};
}, [text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves, enableMouseRotation]);
return (
<div
ref={containerRef}
className="ascii-text-container"
style={{
position: 'absolute',
width: '100%',
height: '100%'
}}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500&display=swap');
body {
margin: 0;
padding: 0;
}
.ascii-text-container canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
image-rendering: optimizeSpeed;
image-rendering: -moz-crisp-edges;
image-rendering: -o-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: optimize-contrast;
image-rendering: crisp-edges;
image-rendering: pixelated;
}
.ascii-text-container pre {
margin: 0;
user-select: none;
padding: 0;
line-height: 1em;
text-align: left;
position: absolute;
left: 0;
top: 0;
background-image: radial-gradient(circle, #ff6188 0%, #fc9867 50%, #ffd866 100%);
background-attachment: fixed;
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
z-index: 9;
mix-blend-mode: difference;
}
`}</style>
</div>
);
}
-274
View File
@@ -1,274 +0,0 @@
import { useEffect, useRef } from 'react';
import { Renderer, Program, Mesh, Triangle, Vec3 } from 'ogl';
export default function Orb({
hue = 0,
hoverIntensity = 0.2,
rotateOnHover = true,
forceHoverState = false
}) {
const ctnDom = useRef(null);
const vert = /* glsl */ `
precision highp float;
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const frag = /* glsl */ `
precision highp float;
uniform float iTime;
uniform vec3 iResolution;
uniform float hue;
uniform float hover;
uniform float rot;
uniform float hoverIntensity;
varying vec2 vUv;
vec3 rgb2yiq(vec3 c) {
float y = dot(c, vec3(0.299, 0.587, 0.114));
float i = dot(c, vec3(0.596, -0.274, -0.322));
float q = dot(c, vec3(0.211, -0.523, 0.312));
return vec3(y, i, q);
}
vec3 yiq2rgb(vec3 c) {
float r = c.x + 0.956 * c.y + 0.621 * c.z;
float g = c.x - 0.272 * c.y - 0.647 * c.z;
float b = c.x - 1.106 * c.y + 1.703 * c.z;
return vec3(r, g, b);
}
vec3 adjustHue(vec3 color, float hueDeg) {
float hueRad = hueDeg * 3.14159265 / 180.0;
vec3 yiq = rgb2yiq(color);
float cosA = cos(hueRad);
float sinA = sin(hueRad);
float i = yiq.y * cosA - yiq.z * sinA;
float q = yiq.y * sinA + yiq.z * cosA;
yiq.y = i;
yiq.z = q;
return yiq2rgb(yiq);
}
vec3 hash33(vec3 p3) {
p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787));
p3 += dot(p3, p3.yxz + 19.19);
return -1.0 + 2.0 * fract(vec3(
p3.x + p3.y,
p3.x + p3.z,
p3.y + p3.z
) * p3.zyx);
}
float snoise3(vec3 p) {
const float K1 = 0.333333333;
const float K2 = 0.166666667;
vec3 i = floor(p + (p.x + p.y + p.z) * K1);
vec3 d0 = p - (i - (i.x + i.y + i.z) * K2);
vec3 e = step(vec3(0.0), d0 - d0.yzx);
vec3 i1 = e * (1.0 - e.zxy);
vec3 i2 = 1.0 - e.zxy * (1.0 - e);
vec3 d1 = d0 - (i1 - K2);
vec3 d2 = d0 - (i2 - K1);
vec3 d3 = d0 - 0.5;
vec4 h = max(0.6 - vec4(
dot(d0, d0),
dot(d1, d1),
dot(d2, d2),
dot(d3, d3)
), 0.0);
vec4 n = h * h * h * h * vec4(
dot(d0, hash33(i)),
dot(d1, hash33(i + i1)),
dot(d2, hash33(i + i2)),
dot(d3, hash33(i + 1.0))
);
return dot(vec4(31.316), n);
}
vec4 extractAlpha(vec3 colorIn) {
float a = max(max(colorIn.r, colorIn.g), colorIn.b);
return vec4(colorIn.rgb / (a + 1e-5), a);
}
const vec3 baseColor1 = vec3(0.611765, 0.262745, 0.996078);
const vec3 baseColor2 = vec3(0.298039, 0.760784, 0.913725);
const vec3 baseColor3 = vec3(0.062745, 0.078431, 0.600000);
const float innerRadius = 0.6;
const float noiseScale = 0.65;
float light1(float intensity, float attenuation, float dist) {
return intensity / (1.0 + dist * attenuation);
}
float light2(float intensity, float attenuation, float dist) {
return intensity / (1.0 + dist * dist * attenuation);
}
vec4 draw(vec2 uv) {
vec3 color1 = adjustHue(baseColor1, hue);
vec3 color2 = adjustHue(baseColor2, hue);
vec3 color3 = adjustHue(baseColor3, hue);
float ang = atan(uv.y, uv.x);
float len = length(uv);
float invLen = len > 0.0 ? 1.0 / len : 0.0;
float n0 = snoise3(vec3(uv * noiseScale, iTime * 0.5)) * 0.5 + 0.5;
float r0 = mix(mix(innerRadius, 1.0, 0.4), mix(innerRadius, 1.0, 0.6), n0);
float d0 = distance(uv, (r0 * invLen) * uv);
float v0 = light1(1.0, 10.0, d0);
v0 *= smoothstep(r0 * 1.05, r0, len);
float cl = cos(ang + iTime * 2.0) * 0.5 + 0.5;
float a = iTime * -1.0;
vec2 pos = vec2(cos(a), sin(a)) * r0;
float d = distance(uv, pos);
float v1 = light2(1.5, 5.0, d);
v1 *= light1(1.0, 50.0, d0);
float v2 = smoothstep(1.0, mix(innerRadius, 1.0, n0 * 0.5), len);
float v3 = smoothstep(innerRadius, mix(innerRadius, 1.0, 0.5), len);
vec3 col = mix(color1, color2, cl);
col = mix(color3, col, v0);
col = (col + v1) * v2 * v3;
col = clamp(col, 0.0, 1.0);
return extractAlpha(col);
}
vec4 mainImage(vec2 fragCoord) {
vec2 center = iResolution.xy * 0.5;
float size = min(iResolution.x, iResolution.y);
vec2 uv = (fragCoord - center) / size * 2.0;
float angle = rot;
float s = sin(angle);
float c = cos(angle);
uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);
uv.x += hover * hoverIntensity * 0.1 * sin(uv.y * 10.0 + iTime);
uv.y += hover * hoverIntensity * 0.1 * sin(uv.x * 10.0 + iTime);
return draw(uv);
}
void main() {
vec2 fragCoord = vUv * iResolution.xy;
vec4 col = mainImage(fragCoord);
gl_FragColor = vec4(col.rgb * col.a, col.a);
}
`;
useEffect(() => {
const container = ctnDom.current;
if (!container) return;
const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });
const gl = renderer.gl;
gl.clearColor(0, 0, 0, 0);
container.appendChild(gl.canvas);
const geometry = new Triangle(gl);
const program = new Program(gl, {
vertex: vert,
fragment: frag,
uniforms: {
iTime: { value: 0 },
iResolution: {
value: new Vec3(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)
},
hue: { value: hue },
hover: { value: 0 },
rot: { value: 0 },
hoverIntensity: { value: hoverIntensity }
}
});
const mesh = new Mesh(gl, { geometry, program });
function resize() {
if (!container) return;
const dpr = window.devicePixelRatio || 1;
const width = container.clientWidth;
const height = container.clientHeight;
renderer.setSize(width * dpr, height * dpr);
gl.canvas.style.width = width + 'px';
gl.canvas.style.height = height + 'px';
program.uniforms.iResolution.value.set(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height);
}
window.addEventListener('resize', resize);
resize();
let targetHover = 0;
let lastTime = 0;
let currentRot = 0;
const rotationSpeed = 0.3;
const handleMouseMove = (e) => {
const rect = container.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const width = rect.width;
const height = rect.height;
const size = Math.min(width, height);
const centerX = width / 2;
const centerY = height / 2;
const uvX = ((x - centerX) / size) * 2.0;
const uvY = ((y - centerY) / size) * 2.0;
if (Math.sqrt(uvX * uvX + uvY * uvY) < 0.8) {
targetHover = 1;
} else {
targetHover = 0;
}
};
const handleMouseLeave = () => {
targetHover = 0;
};
container.addEventListener('mousemove', handleMouseMove);
container.addEventListener('mouseleave', handleMouseLeave);
let rafId;
const update = (t) => {
rafId = requestAnimationFrame(update);
const dt = (t - lastTime) * 0.001;
lastTime = t;
program.uniforms.iTime.value = t * 0.001;
program.uniforms.hue.value = hue;
program.uniforms.hoverIntensity.value = hoverIntensity;
const effectiveHover = forceHoverState ? 1 : targetHover;
program.uniforms.hover.value += (effectiveHover - program.uniforms.hover.value) * 0.1;
if (rotateOnHover && effectiveHover > 0.5) {
currentRot += dt * rotationSpeed;
}
program.uniforms.rot.value = currentRot;
renderer.render({ scene: mesh });
};
rafId = requestAnimationFrame(update);
return () => {
cancelAnimationFrame(rafId);
window.removeEventListener('resize', resize);
container.removeEventListener('mousemove', handleMouseMove);
container.removeEventListener('mouseleave', handleMouseLeave);
container.removeChild(gl.canvas);
gl.getExtension('WEBGL_lose_context')?.loseContext();
};
}, [hue, hoverIntensity, rotateOnHover, forceHoverState]);
return <div ref={ctnDom} className="w-full h-full" />;
}
@@ -1,987 +0,0 @@
import { useState, useEffect } from 'react';
import Orb from './Orb';
import ASCIIText from './ASCIIText';
const DUMMY_DATA = {
title: 'Session Memory Processing',
subtitle: 'Compressing conversation context into semantic memories',
memories: [
{
id: 1,
title: 'First Memory',
subtitle: 'Initial context capture',
facts: ['Fact 1', 'Fact 2', 'Fact 3'],
concepts: ['concept1', 'concept2']
},
{
id: 2,
title: 'Second Memory',
subtitle: 'Additional context',
facts: ['Fact A', 'Fact B'],
concepts: ['concept3']
},
{
id: 3,
title: 'Third Memory',
subtitle: 'More context',
facts: ['Fact X', 'Fact Y', 'Fact Z'],
concepts: ['concept4', 'concept5', 'concept6']
}
],
overview: 'This session involved implementing a progressive UI visualization system for memory processing. The user requested a session card component with four distinct states showing the evolution from empty state through memory accumulation to final overview completion.'
};
export default function OverviewCard({
debugMode = true,
initialState = 'empty',
sessionData = null // { overview, memories }
}) {
const [uiState, setUiState] = useState(initialState);
const [orbOpacity, setOrbOpacity] = useState(0);
const [titleOpacity, setTitleOpacity] = useState(0);
const [asciiFontSize, setAsciiFontSize] = useState(64);
const [cardOpacity, setCardOpacity] = useState(0);
const [titlePosition, setTitlePosition] = useState('center'); // 'center' or 'top'
const [visibleMemories, setVisibleMemories] = useState(0);
const [overviewOpacity, setOverviewOpacity] = useState(0);
const [expandedMemoryId, setExpandedMemoryId] = useState(null); // null = show overview, number = show expanded memory
const [selectedSessionId, setSelectedSessionId] = useState(null);
const [sessions, setSessions] = useState([]);
const [loadedSessionData, setLoadedSessionData] = useState(null);
// Use provided sessionData or loaded session data or fallback to dummy data
const data = sessionData || loadedSessionData || DUMMY_DATA;
// Orb parameters
const [orbHue, setOrbHue] = useState(0);
const [orbHoverIntensity, setOrbHoverIntensity] = useState(0.05);
const [orbRotateOnHover, setOrbRotateOnHover] = useState(false);
const [orbForceHoverState, setOrbForceHoverState] = useState(false);
// Load settings from localStorage or use defaults
const loadSetting = (key, defaultValue) => {
const saved = localStorage.getItem(`overviewCard_${key}`);
return saved !== null ? JSON.parse(saved) : defaultValue;
};
// ASCIIText parameters - Title
const [asciiText, setAsciiText] = useState(() => loadSetting('asciiText', DUMMY_DATA.title));
const [asciiTitleFontSize, setAsciiTitleFontSize] = useState(() => loadSetting('asciiTitleFontSize', 12));
const [asciiTitleTextFontSize, setAsciiTitleTextFontSize] = useState(() => loadSetting('asciiTitleTextFontSize', 200));
const [asciiTitleColor, setAsciiTitleColor] = useState(() => loadSetting('asciiTitleColor', '#60a5fa'));
const [asciiTitlePlaneHeight, setAsciiTitlePlaneHeight] = useState(() => loadSetting('asciiTitlePlaneHeight', 8));
const [asciiTitleEnableWaves, setAsciiTitleEnableWaves] = useState(() => loadSetting('asciiTitleEnableWaves', false));
const [asciiTitleEnableMouseRotation, setAsciiTitleEnableMouseRotation] = useState(() => loadSetting('asciiTitleEnableMouseRotation', false));
const [asciiTitleOffsetY, setAsciiTitleOffsetY] = useState(() => loadSetting('asciiTitleOffsetY', 0));
// ASCIIText parameters - Subtitle
const [asciiSubtitle, setAsciiSubtitle] = useState(() => loadSetting('asciiSubtitle', DUMMY_DATA.subtitle));
const [asciiSubtitleFontSize, setAsciiSubtitleFontSize] = useState(() => loadSetting('asciiSubtitleFontSize', 6));
const [asciiSubtitleTextFontSize, setAsciiSubtitleTextFontSize] = useState(() => loadSetting('asciiSubtitleTextFontSize', 120));
const [asciiSubtitleColor, setAsciiSubtitleColor] = useState(() => loadSetting('asciiSubtitleColor', '#60a5fa'));
const [asciiSubtitlePlaneHeight, setAsciiSubtitlePlaneHeight] = useState(() => loadSetting('asciiSubtitlePlaneHeight', 4.8));
const [asciiSubtitleEnableWaves, setAsciiSubtitleEnableWaves] = useState(() => loadSetting('asciiSubtitleEnableWaves', false));
const [asciiSubtitleEnableMouseRotation, setAsciiSubtitleEnableMouseRotation] = useState(() => loadSetting('asciiSubtitleEnableMouseRotation', false));
const [asciiSubtitleOffsetY, setAsciiSubtitleOffsetY] = useState(() => loadSetting('asciiSubtitleOffsetY', 0));
// Debug panel section expansion state
const [sectionsExpanded, setSectionsExpanded] = useState({
animation: true,
orb: false,
asciiTitle: false,
asciiSubtitle: false
});
// Save to localStorage whenever settings change
useEffect(() => {
localStorage.setItem('overviewCard_asciiText', JSON.stringify(asciiText));
}, [asciiText]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiTitleFontSize', JSON.stringify(asciiTitleFontSize));
}, [asciiTitleFontSize]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiTitleTextFontSize', JSON.stringify(asciiTitleTextFontSize));
}, [asciiTitleTextFontSize]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiTitleColor', JSON.stringify(asciiTitleColor));
}, [asciiTitleColor]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiTitlePlaneHeight', JSON.stringify(asciiTitlePlaneHeight));
}, [asciiTitlePlaneHeight]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiTitleEnableWaves', JSON.stringify(asciiTitleEnableWaves));
}, [asciiTitleEnableWaves]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiTitleEnableMouseRotation', JSON.stringify(asciiTitleEnableMouseRotation));
}, [asciiTitleEnableMouseRotation]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiTitleOffsetY', JSON.stringify(asciiTitleOffsetY));
}, [asciiTitleOffsetY]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiSubtitle', JSON.stringify(asciiSubtitle));
}, [asciiSubtitle]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiSubtitleFontSize', JSON.stringify(asciiSubtitleFontSize));
}, [asciiSubtitleFontSize]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiSubtitleTextFontSize', JSON.stringify(asciiSubtitleTextFontSize));
}, [asciiSubtitleTextFontSize]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiSubtitleColor', JSON.stringify(asciiSubtitleColor));
}, [asciiSubtitleColor]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiSubtitlePlaneHeight', JSON.stringify(asciiSubtitlePlaneHeight));
}, [asciiSubtitlePlaneHeight]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiSubtitleEnableWaves', JSON.stringify(asciiSubtitleEnableWaves));
}, [asciiSubtitleEnableWaves]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiSubtitleEnableMouseRotation', JSON.stringify(asciiSubtitleEnableMouseRotation));
}, [asciiSubtitleEnableMouseRotation]);
useEffect(() => {
localStorage.setItem('overviewCard_asciiSubtitleOffsetY', JSON.stringify(asciiSubtitleOffsetY));
}, [asciiSubtitleOffsetY]);
// Fetch available sessions
useEffect(() => {
if (debugMode) {
fetch('http://localhost:3001/api/sessions')
.then(res => res.json())
.then(data => setSessions(data))
.catch(err => console.error('Failed to fetch sessions:', err));
}
}, [debugMode]);
// Load session data when selected
useEffect(() => {
if (selectedSessionId && debugMode) {
fetch(`http://localhost:3001/api/session/${selectedSessionId}`)
.then(res => res.json())
.then(data => {
// Transform data to match expected format
const formattedData = {
title: data.overview?.content?.split('.')[0] || 'Session Overview',
subtitle: data.overview?.content?.substring(0, 100) || '',
overview: data.overview?.content || '',
memories: data.memories || []
};
setLoadedSessionData(formattedData);
// Auto-transition to complete state to show the data
if (data.memories?.length > 0) {
setUiState('complete');
setVisibleMemories(data.memories.length);
}
})
.catch(err => console.error('Failed to fetch session data:', err));
}
}, [selectedSessionId, debugMode]);
// State transition effects
useEffect(() => {
switch (uiState) {
case 'empty':
// Reset everything
setOrbOpacity(0);
setTitleOpacity(0);
setAsciiFontSize(64);
setCardOpacity(0);
setTitlePosition('center');
setVisibleMemories(0);
setOverviewOpacity(0);
setAsciiText(DUMMY_DATA.title);
setAsciiSubtitle(DUMMY_DATA.subtitle);
// Fade in orb and title
setTimeout(() => setOrbOpacity(1), 100);
setTimeout(() => {
setTitleOpacity(1);
// Start animating font size down
let size = 64;
const interval = setInterval(() => {
size -= 2;
if (size <= 12) {
size = 12;
clearInterval(interval);
}
setAsciiFontSize(size);
}, 30);
}, 200);
break;
case 'first-memory':
// Card fades in, title moves to top
setCardOpacity(1);
setTitlePosition('top');
setVisibleMemories(1);
break;
case 'accumulating':
// Show all memories
setVisibleMemories(data.memories?.length || DUMMY_DATA.memories.length);
break;
case 'complete':
// Overview fades in, orb fades out, card becomes solid
setOverviewOpacity(1);
setOrbOpacity(0);
// Make card fully opaque by increasing opacity even more
setCardOpacity(1);
break;
default:
break;
}
}, [uiState]);
return (
<div className="relative w-full min-h-screen">
{/* Debug Controls */}
{debugMode && (
<div className="fixed bottom-4 right-4 z-50 bg-gray-900/95 backdrop-blur-xl border border-gray-700 rounded-xl w-96 max-h-[85vh] flex flex-col">
{/* Header */}
<div className="p-4 border-b border-gray-700">
<h3 className="text-sm font-bold text-blue-400 mb-3">Debug Controls</h3>
{/* Session Selector */}
<div className="mb-3">
<label className="text-xs text-gray-400 mb-1 block">Load Real Session</label>
<select
value={selectedSessionId || ''}
onChange={(e) => setSelectedSessionId(e.target.value || null)}
className="w-full px-2 py-1.5 rounded bg-gray-800 border border-gray-700 text-xs text-gray-300"
>
<option value="">-- Dummy Data --</option>
{sessions.map((session) => (
<option key={session.session_id} value={session.session_id}>
{session.project} - {new Date(session.created_at).toLocaleDateString()}
</option>
))}
</select>
</div>
{/* State Buttons - 2x2 Grid */}
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => setUiState('empty')}
className={`px-2 py-1.5 rounded text-xs font-medium transition-all ${
uiState === 'empty'
? 'bg-blue-500/30 border border-blue-400/60 text-blue-200'
: 'bg-gray-800/50 border border-gray-700 text-gray-300 hover:bg-gray-700/50'
}`}
>
1. Empty
</button>
<button
onClick={() => setUiState('first-memory')}
className={`px-2 py-1.5 rounded text-xs font-medium transition-all ${
uiState === 'first-memory'
? 'bg-blue-500/30 border border-blue-400/60 text-blue-200'
: 'bg-gray-800/50 border border-gray-700 text-gray-300 hover:bg-gray-700/50'
}`}
>
2. First
</button>
<button
onClick={() => setUiState('accumulating')}
className={`px-2 py-1.5 rounded text-xs font-medium transition-all ${
uiState === 'accumulating'
? 'bg-blue-500/30 border border-blue-400/60 text-blue-200'
: 'bg-gray-800/50 border border-gray-700 text-gray-300 hover:bg-gray-700/50'
}`}
>
3. Accum
</button>
<button
onClick={() => setUiState('complete')}
className={`px-2 py-1.5 rounded text-xs font-medium transition-all ${
uiState === 'complete'
? 'bg-blue-500/30 border border-blue-400/60 text-blue-200'
: 'bg-gray-800/50 border border-gray-700 text-gray-300 hover:bg-gray-700/50'
}`}
>
4. Complete
</button>
</div>
</div>
{/* Scrollable Content */}
<div className="overflow-y-auto flex-1 p-4 space-y-2">
{/* Animation State Section */}
<div className="border border-gray-700 rounded-lg overflow-hidden">
<button
onClick={() => setSectionsExpanded(s => ({ ...s, animation: !s.animation }))}
className="w-full px-3 py-2 bg-gray-800/30 hover:bg-gray-800/50 transition-colors flex items-center justify-between text-left"
>
<span className="text-xs font-bold text-purple-400">Animation State</span>
<span className="text-xs text-gray-500">{sectionsExpanded.animation ? '▼' : '▶'}</span>
</button>
{sectionsExpanded.animation && (
<div className="p-3 space-y-2 bg-gray-800/10">
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Orb Opacity</label>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="1"
step="0.01"
value={orbOpacity}
onChange={(e) => setOrbOpacity(parseFloat(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{orbOpacity.toFixed(2)}</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Title Opacity</label>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="1"
step="0.01"
value={titleOpacity}
onChange={(e) => setTitleOpacity(parseFloat(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{titleOpacity.toFixed(2)}</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Card Opacity</label>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="1"
step="0.01"
value={cardOpacity}
onChange={(e) => setCardOpacity(parseFloat(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{cardOpacity.toFixed(2)}</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Overview Opacity</label>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="1"
step="0.01"
value={overviewOpacity}
onChange={(e) => setOverviewOpacity(parseFloat(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{overviewOpacity.toFixed(2)}</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Title Position</label>
<select
value={titlePosition}
onChange={(e) => setTitlePosition(e.target.value)}
className="px-2 py-1 rounded bg-gray-800 border border-gray-700 text-xs text-gray-300"
>
<option value="center">Center</option>
<option value="top">Top</option>
</select>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Visible Memories</label>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max={data.memories?.length || 0}
step="1"
value={visibleMemories}
onChange={(e) => setVisibleMemories(parseInt(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{visibleMemories}/{data.memories?.length || 0}</span>
</div>
</div>
</div>
)}
</div>
{/* Orb Parameters Section */}
<div className="border border-gray-700 rounded-lg overflow-hidden">
<button
onClick={() => setSectionsExpanded(s => ({ ...s, orb: !s.orb }))}
className="w-full px-3 py-2 bg-gray-800/30 hover:bg-gray-800/50 transition-colors flex items-center justify-between text-left"
>
<span className="text-xs font-bold text-blue-400">Orb Parameters</span>
<span className="text-xs text-gray-500">{sectionsExpanded.orb ? '▼' : '▶'}</span>
</button>
{sectionsExpanded.orb && (
<div className="p-3 space-y-2 bg-gray-800/10">
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Hue</label>
<div className="flex items-center gap-2">
<input
type="range"
min="-180"
max="180"
step="1"
value={orbHue}
onChange={(e) => setOrbHue(parseFloat(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{orbHue}°</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Hover Intensity</label>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="1"
step="0.01"
value={orbHoverIntensity}
onChange={(e) => setOrbHoverIntensity(parseFloat(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{orbHoverIntensity.toFixed(2)}</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400 flex items-center gap-2">
<input
type="checkbox"
checked={orbRotateOnHover}
onChange={(e) => setOrbRotateOnHover(e.target.checked)}
className="w-4 h-4"
/>
Rotate On Hover
</label>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400 flex items-center gap-2">
<input
type="checkbox"
checked={orbForceHoverState}
onChange={(e) => setOrbForceHoverState(e.target.checked)}
className="w-4 h-4"
/>
Force Hover State
</label>
</div>
</div>
)}
</div>
{/* ASCII Title Parameters Section */}
<div className="border border-gray-700 rounded-lg overflow-hidden">
<button
onClick={() => setSectionsExpanded(s => ({ ...s, asciiTitle: !s.asciiTitle }))}
className="w-full px-3 py-2 bg-gray-800/30 hover:bg-gray-800/50 transition-colors flex items-center justify-between text-left"
>
<span className="text-xs font-bold text-emerald-400">ASCII Title</span>
<span className="text-xs text-gray-500">{sectionsExpanded.asciiTitle ? '▼' : '▶'}</span>
</button>
{sectionsExpanded.asciiTitle && (
<div className="p-3 space-y-2 bg-gray-800/10">
<div>
<label className="text-xs text-gray-400 mb-1 block">Text</label>
<textarea
value={asciiText}
onChange={(e) => setAsciiText(e.target.value)}
rows={2}
className="w-full px-2 py-1 rounded bg-gray-800 border border-gray-700 text-xs text-gray-300 resize-none"
/>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">ASCII Font Size</label>
<div className="flex items-center gap-2">
<input
type="range"
min="4"
max="64"
step="1"
value={asciiTitleFontSize}
onChange={(e) => setAsciiTitleFontSize(parseInt(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{asciiTitleFontSize}px</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Text Font Size</label>
<div className="flex items-center gap-2">
<input
type="range"
min="50"
max="400"
step="10"
value={asciiTitleTextFontSize}
onChange={(e) => setAsciiTitleTextFontSize(parseInt(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{asciiTitleTextFontSize}px</span>
</div>
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Color</label>
<div className="flex gap-2 items-center">
<input
type="color"
value={asciiTitleColor}
onChange={(e) => setAsciiTitleColor(e.target.value)}
className="w-8 h-8 rounded border border-gray-700 bg-gray-800 cursor-pointer"
/>
<input
type="text"
value={asciiTitleColor}
onChange={(e) => setAsciiTitleColor(e.target.value)}
className="flex-1 px-2 py-1 rounded bg-gray-800 border border-gray-700 text-xs text-gray-300"
/>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Plane Height</label>
<div className="flex items-center gap-2">
<input
type="range"
min="1"
max="20"
step="0.5"
value={asciiTitlePlaneHeight}
onChange={(e) => setAsciiTitlePlaneHeight(parseFloat(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{asciiTitlePlaneHeight}</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Y Offset</label>
<div className="flex items-center gap-2">
<input
type="range"
min="-500"
max="500"
step="10"
value={asciiTitleOffsetY}
onChange={(e) => setAsciiTitleOffsetY(parseInt(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{asciiTitleOffsetY}px</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400 flex items-center gap-2">
<input
type="checkbox"
checked={asciiTitleEnableWaves}
onChange={(e) => setAsciiTitleEnableWaves(e.target.checked)}
className="w-4 h-4"
/>
Enable Waves
</label>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400 flex items-center gap-2">
<input
type="checkbox"
checked={asciiTitleEnableMouseRotation}
onChange={(e) => setAsciiTitleEnableMouseRotation(e.target.checked)}
className="w-4 h-4"
/>
Mouse Rotation
</label>
</div>
</div>
)}
</div>
{/* ASCII Subtitle Parameters Section */}
<div className="border border-gray-700 rounded-lg overflow-hidden">
<button
onClick={() => setSectionsExpanded(s => ({ ...s, asciiSubtitle: !s.asciiSubtitle }))}
className="w-full px-3 py-2 bg-gray-800/30 hover:bg-gray-800/50 transition-colors flex items-center justify-between text-left"
>
<span className="text-xs font-bold text-amber-400">ASCII Subtitle</span>
<span className="text-xs text-gray-500">{sectionsExpanded.asciiSubtitle ? '▼' : '▶'}</span>
</button>
{sectionsExpanded.asciiSubtitle && (
<div className="p-3 space-y-2 bg-gray-800/10">
<div>
<label className="text-xs text-gray-400 mb-1 block">Text</label>
<textarea
value={asciiSubtitle}
onChange={(e) => setAsciiSubtitle(e.target.value)}
rows={2}
className="w-full px-2 py-1 rounded bg-gray-800 border border-gray-700 text-xs text-gray-300 resize-none"
/>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">ASCII Font Size</label>
<div className="flex items-center gap-2">
<input
type="range"
min="4"
max="64"
step="1"
value={asciiSubtitleFontSize}
onChange={(e) => setAsciiSubtitleFontSize(parseInt(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{asciiSubtitleFontSize}px</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Text Font Size</label>
<div className="flex items-center gap-2">
<input
type="range"
min="50"
max="400"
step="10"
value={asciiSubtitleTextFontSize}
onChange={(e) => setAsciiSubtitleTextFontSize(parseInt(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{asciiSubtitleTextFontSize}px</span>
</div>
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Color</label>
<div className="flex gap-2 items-center">
<input
type="color"
value={asciiSubtitleColor}
onChange={(e) => setAsciiSubtitleColor(e.target.value)}
className="w-8 h-8 rounded border border-gray-700 bg-gray-800 cursor-pointer"
/>
<input
type="text"
value={asciiSubtitleColor}
onChange={(e) => setAsciiSubtitleColor(e.target.value)}
className="flex-1 px-2 py-1 rounded bg-gray-800 border border-gray-700 text-xs text-gray-300"
/>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Plane Height</label>
<div className="flex items-center gap-2">
<input
type="range"
min="1"
max="20"
step="0.5"
value={asciiSubtitlePlaneHeight}
onChange={(e) => setAsciiSubtitlePlaneHeight(parseFloat(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{asciiSubtitlePlaneHeight}</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Y Offset</label>
<div className="flex items-center gap-2">
<input
type="range"
min="-500"
max="500"
step="10"
value={asciiSubtitleOffsetY}
onChange={(e) => setAsciiSubtitleOffsetY(parseInt(e.target.value))}
className="w-32"
/>
<span className="text-xs text-gray-500 w-10 text-right">{asciiSubtitleOffsetY}px</span>
</div>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400 flex items-center gap-2">
<input
type="checkbox"
checked={asciiSubtitleEnableWaves}
onChange={(e) => setAsciiSubtitleEnableWaves(e.target.checked)}
className="w-4 h-4"
/>
Enable Waves
</label>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400 flex items-center gap-2">
<input
type="checkbox"
checked={asciiSubtitleEnableMouseRotation}
onChange={(e) => setAsciiSubtitleEnableMouseRotation(e.target.checked)}
className="w-4 h-4"
/>
Mouse Rotation
</label>
</div>
</div>
)}
</div>
</div>
</div>
)}
{/* Orb Background Overlay */}
<div
className="fixed inset-0 pointer-events-none transition-opacity duration-500"
style={{ opacity: orbOpacity }}
>
<Orb
hue={orbHue}
hoverIntensity={orbHoverIntensity}
rotateOnHover={orbRotateOnHover}
forceHoverState={orbForceHoverState}
/>
</div>
{/* Floating Title (State 1: Empty) */}
{titlePosition === 'center' && (
<div
className="fixed inset-0 flex items-center justify-center pointer-events-none transition-opacity duration-500"
style={{ opacity: titleOpacity }}
>
<div className="relative w-full flex flex-col items-center">
<div
className="relative w-full h-64"
style={{ transform: `translateY(${asciiTitleOffsetY}px)` }}
>
<ASCIIText
text={asciiText}
asciiFontSize={asciiTitleFontSize}
textFontSize={asciiTitleTextFontSize}
textColor={asciiTitleColor}
planeBaseHeight={asciiTitlePlaneHeight}
enableWaves={asciiTitleEnableWaves}
enableMouseRotation={asciiTitleEnableMouseRotation}
/>
</div>
<div
className="relative w-full h-32"
style={{ transform: `translateY(${asciiSubtitleOffsetY}px)` }}
>
<ASCIIText
text={asciiSubtitle}
asciiFontSize={asciiSubtitleFontSize}
textFontSize={asciiSubtitleTextFontSize}
textColor={asciiSubtitleColor}
planeBaseHeight={asciiSubtitlePlaneHeight}
enableWaves={asciiSubtitleEnableWaves}
enableMouseRotation={asciiSubtitleEnableMouseRotation}
/>
</div>
</div>
</div>
)}
{/* Session Card (States 2-4) */}
<div
className="max-w-6xl mx-auto px-4 py-20 transition-opacity duration-500"
style={{ opacity: cardOpacity }}
>
<div className="relative">
{/* Blur background effect */}
<div className="absolute -inset-4 bg-gradient-to-r from-blue-600/20 via-purple-600/20 to-emerald-600/20 rounded-3xl blur-2xl" />
{/* Card with backdrop blur */}
<div
className="relative rounded-3xl p-12 border border-gray-800 transition-all duration-500"
style={{
backgroundColor: uiState === 'complete'
? 'rgba(10, 10, 15, 0.95)'
: 'rgba(10, 10, 15, 0.7)',
backdropFilter: 'blur(20px)'
}}
>
{/* Title at top of card (States 2-4) */}
{titlePosition === 'top' && (
<div className="mb-8">
<h1 className="text-4xl font-black text-transparent bg-clip-text bg-gradient-to-r from-blue-300 via-purple-300 to-emerald-300 mb-4 leading-tight">
{data.title || 'Session Overview'}
</h1>
<p className="text-xl text-gray-400 leading-relaxed">
{data.subtitle || ''}
</p>
</div>
)}
{/* Overview Section (State 4: Complete) */}
{uiState === 'complete' && data.overview && (
<div
className="mb-8 pb-8 border-b border-gray-800 transition-opacity duration-500"
style={{ opacity: overviewOpacity }}
>
<h3 className="text-sm font-bold text-emerald-400 mb-4 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
SESSION OVERVIEW
</h3>
<p className="text-gray-300 leading-relaxed">
{data.overview}
</p>
</div>
)}
{/* Expanded Memory View */}
{expandedMemoryId !== null && (
<div>
{/* Back Button */}
<button
onClick={() => setExpandedMemoryId(null)}
className="flex items-center gap-2 mb-6 px-4 py-2 rounded-lg bg-gray-800/50 border border-gray-700 text-gray-300 hover:bg-gray-700/50 hover:border-gray-600 transition-all"
>
<span className="text-lg"></span>
<span className="text-sm font-medium">Back to Overview</span>
</button>
{/* Full Memory Card */}
{(() => {
const memory = data.memories?.find(m => m.id === expandedMemoryId);
if (!memory) return null;
return (
<div>
<div className="mb-8">
<h2 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-300 to-purple-300 mb-4">
{memory.title}
</h2>
<p className="text-xl text-gray-400">
{memory.subtitle}
</p>
</div>
{memory.facts && memory.facts.length > 0 && (
<div className="mb-8">
<h3 className="text-sm font-bold text-blue-400 mb-4 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-400" />
FACTS EXTRACTED
</h3>
<div className="space-y-3">
{memory.facts.map((fact, i) => (
<div key={i} className="flex gap-3 text-gray-300 leading-relaxed">
<span className="text-blue-400 font-mono text-xs mt-1"></span>
<span>{fact}</span>
</div>
))}
</div>
</div>
)}
{memory.concepts && memory.concepts.length > 0 && (
<div>
<h3 className="text-sm font-bold text-purple-400 mb-4 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-400" />
CONCEPTS
</h3>
<div className="flex flex-wrap gap-2">
{memory.concepts.map((concept, i) => (
<span
key={i}
className="px-3 py-1.5 rounded-lg bg-purple-500/10 border border-purple-400/30 text-purple-300 text-sm font-medium"
>
{concept}
</span>
))}
</div>
</div>
)}
</div>
);
})()}
</div>
)}
{/* Memory Mini-cards (Overview) */}
{expandedMemoryId === null && (
<div className="grid grid-cols-3 gap-4">
{(data.memories || []).slice(0, visibleMemories).map((memory, index) => (
<div
key={memory.id}
onClick={() => setExpandedMemoryId(memory.id)}
className="border border-gray-700/50 rounded-xl p-4 bg-gray-900/30 cursor-pointer hover:bg-gray-800/40 hover:border-gray-600/50 transition-all"
style={{
animation: 'fadeInUp 0.5s ease-out',
animationDelay: `${index * 0.1}s`,
animationFillMode: 'both'
}}
>
<h3 className="text-base font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-300 to-purple-300 mb-2">
{memory.title}
</h3>
<p className="text-xs text-gray-400 line-clamp-2 mb-3">
{memory.subtitle}
</p>
{/* Preview badges */}
<div className="flex gap-2">
{memory.facts && memory.facts.length > 0 && (
<span className="px-2 py-0.5 rounded text-xs bg-blue-500/10 border border-blue-400/30 text-blue-300">
{memory.facts.length} facts
</span>
)}
{memory.concepts && memory.concepts.length > 0 && (
<span className="px-2 py-0.5 rounded text-xs bg-purple-500/10 border border-purple-400/30 text-purple-300">
{memory.concepts.length} concepts
</span>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
<style>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`}</style>
</div>
);
}
-6
View File
@@ -1,6 +0,0 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
-19
View File
@@ -1,19 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
server: {
port: 5173
}
})