From 711ee790ff4e406022a4dee575ffaee42357bd3c Mon Sep 17 00:00:00 2001 From: Stu Alexander Date: Wed, 21 Jan 2026 12:18:29 +0000 Subject: [PATCH 1/2] I've successfully extracted the timeline feature from friend-lite and integrated it into Ushadow. Here's a summary: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `★ Insight ─────────────────────────────────────` **What was extracted:** - **TimelineRouter** - Tab navigation between timeline implementations - **FrappeGanttTimeline** - Gantt chart using the frappe-gantt library - **MyceliaTimeline** - D3.js-based interactive timeline with zoom/pan **Key adaptations made:** 1. **API Integration** - Changed from `memoriesApi.getAll()` to Ushadow's `memoriesApi.fetchMemories()` pattern 2. **Type Safety** - Created proper TypeScript interfaces and a type declaration for frappe-gantt 3. **Security** - Replaced innerHTML-based tooltips with React components (avoiding XSS) 4. **Test IDs** - Added comprehensive `data-testid` attributes per CLAUDE.md requirements `────────────��────────────────────────────────────` ## Summary **Files created:** - `ushadow/frontend/src/pages/TimelinePage.tsx` - Main timeline page with two implementations - `ushadow/frontend/src/types/frappe-gantt.d.ts` - TypeScript declarations for frappe-gantt **Files modified:** - `ushadow/frontend/src/App.tsx` - Added TimelinePage import and `/timeline` route - `ushadow/frontend/package.json` - Added `d3`, `frappe-gantt`, and `@types/d3` dependencies **Features:** - Two timeline visualization options (Frappe Gantt and Mycelia D3) - Tab navigation to switch between implementations - Demo data mode to preview functionality without real data - Zoom, pan, and view mode controls - Tooltip details on hover/click - Color-coded events by type (Event, Person, Place) - Dark mode support - Responsive design The timeline is now accessible at `/timeline` in the Ushadow frontend. --- ushadow/frontend/package-lock.json | 685 ++++++++++++++ ushadow/frontend/package.json | 5 +- ushadow/frontend/src/App.tsx | 2 + ushadow/frontend/src/pages/TimelinePage.tsx | 890 +++++++++++++++++++ ushadow/frontend/src/types/frappe-gantt.d.ts | 33 + 5 files changed, 1614 insertions(+), 1 deletion(-) create mode 100644 ushadow/frontend/src/pages/TimelinePage.tsx create mode 100644 ushadow/frontend/src/types/frappe-gantt.d.ts diff --git a/ushadow/frontend/package-lock.json b/ushadow/frontend/package-lock.json index e7bddf12..f936a1ea 100644 --- a/ushadow/frontend/package-lock.json +++ b/ushadow/frontend/package-lock.json @@ -15,7 +15,10 @@ "@neo4j-nvl/base": "^1.0.0", "@neo4j-nvl/react": "^1.0.0", "@tanstack/react-query": "^5.56.2", + "@types/d3": "^7.4.3", "axios": "^1.7.7", + "d3": "^7.9.0", + "frappe-gantt": "^1.0.4", "lucide-react": "^0.446.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -2590,6 +2593,259 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2597,6 +2853,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3491,6 +3753,129 @@ "cytoscape": "^3.2.0" } }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-dispatch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", @@ -3500,6 +3885,74 @@ "node": ">=12" } }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-force": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", @@ -3514,6 +3967,66 @@ "node": ">=12" } }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-quadtree": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", @@ -3523,6 +4036,89 @@ "node": ">=12" } }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-timer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", @@ -3532,6 +4128,41 @@ "node": ">=12" } }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3557,6 +4188,21 @@ "dev": true, "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delaunator/node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4153,6 +4799,12 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/frappe-gantt": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/frappe-gantt/-/frappe-gantt-1.0.4.tgz", + "integrity": "sha512-N94OP9ZiapaG5nzgCeZdxsKP8HD5aLVlH5sEHxSNZQnNKQ4BOn2l46HUD+KIE0LpYIterP7gIrFfkLNRuK0npQ==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4373,6 +5025,18 @@ "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", "license": "Apache-2.0" }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4437,6 +5101,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-arrayish": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", @@ -5731,6 +6404,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -5762,6 +6441,12 @@ "license": "MIT", "peer": true }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", diff --git a/ushadow/frontend/package.json b/ushadow/frontend/package.json index 910c3bb0..ec0fb65a 100644 --- a/ushadow/frontend/package.json +++ b/ushadow/frontend/package.json @@ -16,14 +16,17 @@ "test:report": "playwright show-report" }, "dependencies": { + "@assistant-ui/react": "^0.11.53", "@dnd-kit/core": "^6.3.1", "@dnd-kit/utilities": "^3.2.2", - "@assistant-ui/react": "^0.11.53", "@hookform/resolvers": "^5.2.2", "@neo4j-nvl/base": "^1.0.0", "@neo4j-nvl/react": "^1.0.0", "@tanstack/react-query": "^5.56.2", + "@types/d3": "^7.4.3", "axios": "^1.7.7", + "d3": "^7.9.0", + "frappe-gantt": "^1.0.4", "lucide-react": "^0.446.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/ushadow/frontend/src/App.tsx b/ushadow/frontend/src/App.tsx index c7902358..6807b2da 100644 --- a/ushadow/frontend/src/App.tsx +++ b/ushadow/frontend/src/App.tsx @@ -41,6 +41,7 @@ import MemoriesPage from './pages/MemoriesPage' import ClusterPage from './pages/ClusterPage' import SpeakerRecognitionPage from './pages/SpeakerRecognitionPage' import ChatPage from './pages/ChatPage' +import TimelinePage from './pages/TimelinePage' // Wizards (all use WizardShell pattern) import { @@ -108,6 +109,7 @@ function AppContent() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/ushadow/frontend/src/pages/TimelinePage.tsx b/ushadow/frontend/src/pages/TimelinePage.tsx new file mode 100644 index 00000000..9b0ecbe2 --- /dev/null +++ b/ushadow/frontend/src/pages/TimelinePage.tsx @@ -0,0 +1,890 @@ +import { useState, useEffect, useRef } from 'react' +import { Calendar, RefreshCw, AlertCircle, ZoomIn, ZoomOut } from 'lucide-react' +import Gantt from 'frappe-gantt' +import * as d3 from 'd3' +import { memoriesApi } from '../services/api' +import { useAuth } from '../contexts/AuthContext' +import type { Memory } from '../types/memory' + +// ============================================================================= +// Types +// ============================================================================= + +interface TimeRange { + start: string + end: string + name?: string +} + +interface TimelineMetadata extends Record { + timeRanges?: TimeRange[] + isPerson?: boolean + isEvent?: boolean + isPlace?: boolean + name?: string +} + +interface MemoryWithTimeRange extends Omit { + metadata: TimelineMetadata +} + +interface GanttTask { + id: string + name: string + start: string + end: string + progress: number + custom_class?: string +} + +interface TimelineTask { + id: string + name: string + start: Date + end: Date + color: string + type: 'event' | 'person' | 'place' +} + +type TimelineImplementation = 'frappe' | 'mycelia' + +// ============================================================================= +// Demo Data +// ============================================================================= + +const getDemoMemories = (): MemoryWithTimeRange[] => { + return [ + { + id: 'demo-graduation', + memory: 'College graduation ceremony and celebration dinner with family.', + created_at: new Date('2024-05-20T14:00:00').getTime(), + state: 'active', + metadata: { + name: 'College Graduation', + isEvent: true, + timeRanges: [ + { name: 'Graduation Ceremony', start: '2024-05-20T14:00:00', end: '2024-05-20T17:00:00' }, + { name: 'Celebration Dinner', start: '2024-05-20T19:00:00', end: '2024-05-20T22:00:00' } + ] + }, + categories: [], + client: 'api', + app_name: 'demo', + }, + { + id: 'demo-wedding', + memory: "Sarah and Tom's wedding was a beautiful celebration.", + created_at: new Date('2025-06-15T15:00:00').getTime(), + state: 'active', + metadata: { + name: "Sarah & Tom's Wedding", + isEvent: true, + timeRanges: [ + { name: 'Wedding Ceremony', start: '2025-06-15T15:00:00', end: '2025-06-15T16:30:00' }, + { name: 'Reception', start: '2025-06-15T18:00:00', end: '2025-06-16T00:00:00' } + ] + }, + categories: [], + client: 'api', + app_name: 'demo', + }, + { + id: 'demo-conference', + memory: 'Tech conference with keynote and workshops', + created_at: new Date('2025-09-20T09:00:00').getTime(), + state: 'active', + metadata: { + name: 'Tech Conference 2025', + isEvent: true, + timeRanges: [ + { name: 'Morning Keynote', start: '2025-09-20T09:00:00', end: '2025-09-20T11:00:00' }, + { name: 'Workshops', start: '2025-09-20T13:00:00', end: '2025-09-20T17:00:00' } + ] + }, + categories: [], + client: 'api', + app_name: 'demo', + }, + { + id: 'demo-vacation', + memory: 'Week-long vacation at the beach house with family.', + created_at: new Date('2026-07-01T14:00:00').getTime(), + state: 'active', + metadata: { + name: 'Summer Vacation 2026', + isPlace: true, + timeRanges: [ + { name: 'Beach House Stay', start: '2026-07-01T14:00:00', end: '2026-07-07T12:00:00' } + ] + }, + categories: [], + client: 'api', + app_name: 'demo', + }, + { + id: 'demo-reunion', + memory: 'Family reunion at the old homestead.', + created_at: new Date('2026-12-25T12:00:00').getTime(), + state: 'active', + metadata: { + name: 'Family Reunion', + isEvent: true, + timeRanges: [ + { name: 'Christmas Gathering', start: '2026-12-25T12:00:00', end: '2026-12-25T20:00:00' } + ] + }, + categories: [], + client: 'api', + app_name: 'demo', + } + ] +} + +// ============================================================================= +// React Tooltip Component (safe alternative to innerHTML) +// ============================================================================= + +interface TooltipData { + visible: boolean + x: number + y: number + name: string + startDate: string + endDate: string + content?: string +} + +function Tooltip({ data }: { data: TooltipData }) { + if (!data.visible) return null + + return ( +
+
{data.name}
+
+
Start: {data.startDate}
+
End: {data.endDate}
+
+ {data.content && ( +
+ {data.content} +
+ )} +
+ ) +} + +// ============================================================================= +// Frappe Gantt Timeline Component +// ============================================================================= + +function FrappeGanttTimeline({ + memories, + loading, + error, + useDemoData, + onRefresh, + onToggleDemo +}: { + memories: MemoryWithTimeRange[] + loading: boolean + error: string | null + useDemoData: boolean + onRefresh: () => void + onToggleDemo: () => void +}) { + const [currentViewMode, setCurrentViewMode] = useState('Week') + const [zoomScale, setZoomScale] = useState(1) + const [tooltip, setTooltip] = useState({ visible: false, x: 0, y: 0, name: '', startDate: '', endDate: '' }) + const ganttContainerRef = useRef(null) + const ganttInstance = useRef(null) + const scrollContainerRef = useRef(null) + const isDragging = useRef(false) + const startX = useRef(0) + const scrollLeft = useRef(0) + + const convertMemoriesToGanttTasks = (mems: MemoryWithTimeRange[]): GanttTask[] => { + const tasks: GanttTask[] = [] + mems.forEach((memory) => { + const timeRanges = memory.metadata?.timeRanges || [] + timeRanges.forEach((range, index) => { + const taskName = range.name || memory.metadata?.name || memory.memory.substring(0, 50) + let customClass = 'default' + if (memory.metadata?.isEvent) customClass = 'event' + else if (memory.metadata?.isPerson) customClass = 'person' + else if (memory.metadata?.isPlace) customClass = 'place' + + tasks.push({ + id: `${memory.id}-${index}`, + name: taskName, + start: range.start, + end: range.end, + progress: 100, + custom_class: customClass + }) + }) + }) + return tasks + } + + useEffect(() => { + const displayMemories = useDemoData ? getDemoMemories() : memories + if (!ganttContainerRef.current || displayMemories.length === 0) return + + const tasks = convertMemoriesToGanttTasks(displayMemories) + if (tasks.length === 0) return + + try { + if (ganttInstance.current) { + ganttContainerRef.current.innerHTML = '' + } + + // Note: Frappe Gantt requires custom_popup_html but we'll disable popups + // and use our React tooltip instead by handling bar clicks/hovers manually + ganttInstance.current = new Gantt(ganttContainerRef.current, tasks, { + view_mode: currentViewMode as 'Day' | 'Week' | 'Month' | 'Year', + bar_height: 30, + bar_corner_radius: 3, + arrow_curve: 5, + padding: 18, + date_format: 'YYYY-MM-DD', + language: 'en', + popup_trigger: 'click', // Only show on click, not hover + on_click: (task: GanttTask) => { + const memory = displayMemories.find(m => task.id.startsWith(m.id)) + const startDate = new Date(task.start) + const endDate = new Date(task.end) + const fmt: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' } + + // Find the clicked element position for tooltip + const bar = ganttContainerRef.current?.querySelector(`[data-id="${task.id}"]`) + if (bar) { + const rect = bar.getBoundingClientRect() + setTooltip({ + visible: true, + x: rect.right, + y: rect.top, + name: task.name, + startDate: startDate.toLocaleDateString('en-US', fmt), + endDate: endDate.toLocaleDateString('en-US', fmt), + content: memory?.memory + }) + } + } + } as any) + + // Add click-away listener to close tooltip + const handleClickAway = (e: MouseEvent) => { + if (!(e.target as Element).closest('.bar-wrapper')) { + setTooltip(prev => ({ ...prev, visible: false })) + } + } + document.addEventListener('click', handleClickAway) + + return () => { + document.removeEventListener('click', handleClickAway) + } + } catch (err) { + console.error('Error creating Gantt chart:', err) + } + + return () => { + if (ganttInstance.current && ganttContainerRef.current) { + ganttContainerRef.current.innerHTML = '' + ganttInstance.current = null + } + } + }, [memories, useDemoData, currentViewMode]) + + // Drag-to-scroll + useEffect(() => { + const container = scrollContainerRef.current + if (!container) return + + const handleMouseDown = (e: MouseEvent) => { + const target = e.target as HTMLElement + if (target.closest('.bar-wrapper') || target.closest('button')) return + isDragging.current = true + startX.current = e.pageX + scrollLeft.current = container.scrollLeft + container.style.cursor = 'grabbing' + e.preventDefault() + } + const handleMouseLeave = () => { isDragging.current = false; container.style.cursor = 'grab' } + const handleMouseUp = () => { isDragging.current = false; container.style.cursor = 'grab' } + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging.current) return + e.preventDefault() + container.scrollLeft = scrollLeft.current - (e.pageX - startX.current) * 1.5 + } + + container.addEventListener('mousedown', handleMouseDown, true) + container.addEventListener('mouseleave', handleMouseLeave) + container.addEventListener('mouseup', handleMouseUp) + container.addEventListener('mousemove', handleMouseMove) + + return () => { + container.removeEventListener('mousedown', handleMouseDown, true) + container.removeEventListener('mouseleave', handleMouseLeave) + container.removeEventListener('mouseup', handleMouseUp) + container.removeEventListener('mousemove', handleMouseMove) + } + }, []) + + const viewModes = [ + { value: 'Quarter Day', label: 'Quarter Day' }, + { value: 'Half Day', label: 'Half Day' }, + { value: 'Day', label: 'Day' }, + { value: 'Week', label: 'Week' }, + { value: 'Month', label: 'Month' } + ] + + const zoomIn = () => setZoomScale(prev => Math.min(prev + 0.25, 3)) + const zoomOut = () => setZoomScale(prev => Math.max(prev - 0.25, 0.5)) + + if (loading) { + return ( +
+ + Loading timeline data... +
+ ) + } + + if (error) { + return ( +
+ + {error} +
+ ) + } + + const displayMemories = useDemoData ? getDemoMemories() : memories + + if (displayMemories.length === 0) { + return ( +
+ +
+

No Timeline Events

+

+ No memories with time information found. Try the demo to see how it works. +

+
+ +
+ ) + } + + return ( +
+ {/* React-based tooltip */} + + + {/* Controls */} +
+ + +
+ +
+ {Math.round(zoomScale * 100)}% +
+ +
+ + + + +
+ + {/* Gantt Chart */} +
+
+
+ + {/* Help text */} +
+ 💡 Drag to scroll horizontally + 🔍 Use zoom controls to adjust scale + 🖱️ Click bars for details +
+ + {/* Legend */} +
+
+
+ Event +
+
+
+ Person +
+
+
+ Place +
+
+ + {/* Gantt chart custom styles */} + +
+ ) +} + +// ============================================================================= +// Mycelia D3 Timeline Component +// ============================================================================= + +function MyceliaTimeline({ + memories, + loading, + error, + useDemoData, + onRefresh, + onToggleDemo +}: { + memories: MemoryWithTimeRange[] + loading: boolean + error: string | null + useDemoData: boolean + onRefresh: () => void + onToggleDemo: () => void +}) { + const svgRef = useRef(null) + const containerRef = useRef(null) + const [dimensions, setDimensions] = useState({ width: 1000, height: 400 }) + const [tooltip, setTooltip] = useState({ visible: false, x: 0, y: 0, name: '', startDate: '', endDate: '' }) + + const convertToTasks = (mems: MemoryWithTimeRange[]): TimelineTask[] => { + const tasks: TimelineTask[] = [] + mems.forEach((memory) => { + const timeRanges = memory.metadata?.timeRanges || [] + timeRanges.forEach((range, index) => { + let type: 'event' | 'person' | 'place' = 'event' + let color = '#3b82f6' + if (memory.metadata?.isEvent) { type = 'event'; color = '#3b82f6' } + else if (memory.metadata?.isPerson) { type = 'person'; color = '#10b981' } + else if (memory.metadata?.isPlace) { type = 'place'; color = '#f59e0b' } + + tasks.push({ + id: `${memory.id}-${index}`, + name: range.name || memory.metadata?.name || memory.memory.substring(0, 30), + start: new Date(range.start), + end: new Date(range.end), + color, + type + }) + }) + }) + return tasks + } + + // Handle container resize + useEffect(() => { + if (!containerRef.current) return + const resizeObserver = new ResizeObserver(([entry]) => { + setDimensions({ width: entry.contentRect.width, height: 400 }) + }) + resizeObserver.observe(containerRef.current) + return () => resizeObserver.disconnect() + }, []) + + // D3 visualization + useEffect(() => { + const displayMemories = useDemoData ? getDemoMemories() : memories + if (!svgRef.current || displayMemories.length === 0) return + + const tasks = convertToTasks(displayMemories) + if (tasks.length === 0) return + + const svg = d3.select(svgRef.current) + svg.selectAll('*').remove() + + const margin = { top: 60, right: 40, bottom: 60, left: 150 } + const width = dimensions.width - margin.left - margin.right + const height = dimensions.height - margin.top - margin.bottom + + const allDates = tasks.flatMap(t => [t.start, t.end]) + const minDate = d3.min(allDates)! + const maxDate = d3.max(allDates)! + + const xScale = d3.scaleTime().domain([minDate, maxDate]).range([0, width]) + const yScale = d3.scaleBand().domain(tasks.map(t => t.id)).range([0, height]).padding(0.3) + + const g = svg.append('g') + .attr('transform', `translate(${margin.left},${margin.top})`) + .attr('class', 'zoomable') + + // X axis + const xAxis = d3.axisBottom(xScale).ticks(6).tickFormat(d3.timeFormat('%b %d, %Y') as any) + g.append('g') + .attr('class', 'x-axis') + .attr('transform', `translate(0,${height})`) + .call(xAxis) + .selectAll('text') + .style('fill', 'currentColor') + + // Bars + const bars = g.append('g').attr('class', 'bars').selectAll('rect').data(tasks).enter() + + bars.append('rect') + .attr('x', d => xScale(d.start)) + .attr('y', d => yScale(d.id)!) + .attr('width', d => Math.max(2, xScale(d.end) - xScale(d.start))) + .attr('height', yScale.bandwidth()) + .attr('fill', d => d.color) + .attr('rx', 4) + .style('opacity', 0.8) + .style('cursor', 'pointer') + .on('mouseover', function(event, d) { + d3.select(this).style('opacity', 1) + const fmt: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' } + setTooltip({ + visible: true, + x: event.clientX, + y: event.clientY, + name: d.name, + startDate: d.start.toLocaleDateString('en-US', fmt), + endDate: d.end.toLocaleDateString('en-US', fmt) + }) + }) + .on('mousemove', function(event) { + setTooltip(prev => ({ ...prev, x: event.clientX, y: event.clientY })) + }) + .on('mouseout', function() { + d3.select(this).style('opacity', 0.8) + setTooltip(prev => ({ ...prev, visible: false })) + }) + + // Labels + g.append('g').attr('class', 'labels') + .selectAll('text').data(tasks).enter() + .append('text') + .attr('x', -10) + .attr('y', d => yScale(d.id)! + yScale.bandwidth() / 2) + .attr('dy', '0.35em') + .attr('text-anchor', 'end') + .text(d => d.name) + .style('fill', 'currentColor') + .style('font-size', '12px') + + // Zoom + const zoom = d3.zoom() + .scaleExtent([0.5, 5]) + .on('zoom', (event) => { + const newXScale = event.transform.rescaleX(xScale) + g.select('.x-axis').call( + d3.axisBottom(newXScale).ticks(6).tickFormat(d3.timeFormat('%b %d, %Y') as any) as any + ) + g.selectAll('.bars rect') + .attr('x', d => newXScale(d.start)) + .attr('width', d => Math.max(2, newXScale(d.end) - newXScale(d.start))) + }) + + svg.call(zoom as any) + }, [memories, dimensions, useDemoData]) + + if (loading) { + return ( +
+ + Loading timeline data... +
+ ) + } + + if (error) { + return ( +
+ + {error} +
+ ) + } + + const displayMemories = useDemoData ? getDemoMemories() : memories + + if (displayMemories.length === 0) { + return ( +
+ +
+

No Timeline Events

+

+ No memories with time information found. Try the demo to see how it works. +

+
+ +
+ ) + } + + return ( +
+ {/* React-based tooltip */} + + + {/* Controls */} +
+ + +
+ + {/* D3 Chart */} +
+ +
+ + {/* Help text */} +
+ 💡 Scroll to zoom, drag to pan + 👆 Hover for info +
+ + {/* Legend */} +
+
+
+ Event +
+
+
+ Person +
+
+
+ Place +
+
+
+ ) +} + +// ============================================================================= +// Main Timeline Page Component +// ============================================================================= + +export default function TimelinePage() { + const [memories, setMemories] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [useDemoData, setUseDemoData] = useState(false) + const [activeImplementation, setActiveImplementation] = useState('frappe') + const { user } = useAuth() + + const loadMemories = async () => { + if (!user?.id) return + + try { + setLoading(true) + setError(null) + const result = await memoriesApi.fetchMemories(user.id, undefined, 1, 100) + + // Filter memories that have timeRanges in metadata + const memoriesWithTime = result.memories.filter((m): m is MemoryWithTimeRange => { + const meta = m.metadata as TimelineMetadata | undefined + return Boolean(meta?.timeRanges && Array.isArray(meta.timeRanges) && meta.timeRanges.length > 0) + }) + + setMemories(memoriesWithTime) + } catch (err: any) { + console.error('Timeline loading error:', err) + setError(err.message || 'Failed to load timeline data') + } finally { + setLoading(false) + } + } + + useEffect(() => { + if (!useDemoData) { + loadMemories() + } else { + setMemories(getDemoMemories()) + } + }, [user?.id, useDemoData]) + + const handleToggleDemo = () => { + setUseDemoData(!useDemoData) + } + + return ( +
+ {/* Header */} +
+
+

+ + Timeline +

+

+ Visualize your memories on an interactive timeline +

+
+
+ + {/* Tab Navigation */} +
+ +
+ + {/* Timeline Implementation */} +
+ {activeImplementation === 'frappe' && ( + + )} + {activeImplementation === 'mycelia' && ( + + )} +
+
+ ) +} diff --git a/ushadow/frontend/src/types/frappe-gantt.d.ts b/ushadow/frontend/src/types/frappe-gantt.d.ts new file mode 100644 index 00000000..51455c2c --- /dev/null +++ b/ushadow/frontend/src/types/frappe-gantt.d.ts @@ -0,0 +1,33 @@ +declare module 'frappe-gantt' { + export interface GanttTask { + id: string + name: string + start: string + end: string + progress?: number + dependencies?: string + custom_class?: string + } + + export interface GanttOptions { + view_mode?: 'Day' | 'Week' | 'Month' | 'Year' | 'Quarter Day' | 'Half Day' + bar_height?: number + bar_corner_radius?: number + arrow_curve?: number + padding?: number + date_format?: string + language?: string + custom_popup_html?: (task: GanttTask) => string + popup_trigger?: 'click' | 'mouseover' + on_click?: (task: GanttTask) => void + on_date_change?: (task: GanttTask, start: Date, end: Date) => void + on_progress_change?: (task: GanttTask, progress: number) => void + on_view_change?: (mode: string) => void + } + + export default class Gantt { + constructor(element: string | HTMLElement, tasks: GanttTask[], options?: GanttOptions) + change_view_mode(mode: string): void + refresh(tasks: GanttTask[]): void + } +} From f5ad19bbf7c6a0347aad2f0f5f54d48e3cb428fb Mon Sep 17 00:00:00 2001 From: Stu Alexander Date: Thu, 22 Jan 2026 11:11:44 +0000 Subject: [PATCH 2/2] Excellent! No TypeScript errors. Let me create a quick summary document of the changes. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `★ Insight ─────────────────────────────────────` **Feature Flag Architecture in Ushadow:** - **YAML Configuration** - Feature flags are defined in `config/feature_flags.yaml` and hot-reloadable without restart - **Context Provider** - `FeatureFlagsContext` provides `isEnabled()` function to check flag status - **Route Protection** - `FeatureRoute` component conditionally renders routes based on flags - **Navigation Filtering** - Navigation items with `featureFlag` property are automatically filtered in Layout - **UI Indicator** - Feature flags drawer shows all flags with toggle controls (accessed via Flask icon in header) This architecture allows you to: 1. Enable/disable features without code changes 2. Test features in production safely 3. Gradually roll out features to users 4. A/B test different implementations `─────────────────────────────────────────────────` ## Summary I've successfully added a navigation menu item for the Timeline feature and protected it with a feature flag. Here's what was done: ### Changes Made: **1. Feature Flag Configuration** (`config/feature_flags.yaml`): - Added `timeline` feature flag (enabled by default) - Type: `release` - indicates it's a user-facing feature - Description explains it visualizes memories on Gantt charts and D3 timelines **2. Navigation Menu** (`ushadow/frontend/src/components/layout/Layout.tsx`): - Imported `Calendar` icon from lucide-react - Added Timeline navigation item between Memories and Cluster - Item uses Calendar icon and is tied to the `timeline` feature flag - Will automatically show/hide based on flag status **3. Route Protection** (`ushadow/frontend/src/App.tsx`): - Wrapped the `/timeline` route with `` - This ensures users can't access the route even if they know the URL when the flag is disabled ### How It Works: **When the feature flag is enabled (default):** - "Timeline" menu item appears in the sidebar navigation - Clicking it navigates to `/timeline` - TimelinePage component renders with both Frappe Gantt and Mycelia D3 implementations **When the feature flag is disabled:** - Navigation item is automatically hidden - Direct navigation to `/timeline` is blocked (returns to a safe route) - No errors or broken links **Toggle the feature flag:** - Click the Flask icon (🧪) in the header to open the Feature Flags drawer - Find "Timeline" in the list - Toggle it on/off to show/hide the feature The Timeline feature is now fully integrated with Ushadow's feature flag system and navigation! --- config/feature_flags.yaml | 6 ++++++ ushadow/frontend/src/App.tsx | 2 +- ushadow/frontend/src/components/layout/Layout.tsx | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/config/feature_flags.yaml b/config/feature_flags.yaml index 1a3c53c0..888bf341 100644 --- a/config/feature_flags.yaml +++ b/config/feature_flags.yaml @@ -72,6 +72,12 @@ flags: description: "Show Memories page in navigation with list view and knowledge graph" type: release + # Timeline - visualize memories on an interactive timeline + timeline: + enabled: true + description: "Timeline - Visualize memories with time ranges on Gantt charts and D3 timelines" + type: release + # ServiceConfigs Management - Service instance deployment and wiring instances_management: enabled: false diff --git a/ushadow/frontend/src/App.tsx b/ushadow/frontend/src/App.tsx index 6807b2da..0edb51d3 100644 --- a/ushadow/frontend/src/App.tsx +++ b/ushadow/frontend/src/App.tsx @@ -109,7 +109,7 @@ function AppContent() { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/ushadow/frontend/src/components/layout/Layout.tsx b/ushadow/frontend/src/components/layout/Layout.tsx index 2a33dae0..cc8abe17 100644 --- a/ushadow/frontend/src/components/layout/Layout.tsx +++ b/ushadow/frontend/src/components/layout/Layout.tsx @@ -1,6 +1,6 @@ import { Link, useLocation, Outlet } from 'react-router-dom' import React, { useState, useRef, useEffect } from 'react' -import { Layers, MessageSquare, Plug, Bot, Workflow, Server, Settings, LogOut, Sun, Moon, Users, Search, Bell, User, ChevronDown, Brain, Home, QrCode } from 'lucide-react' +import { Layers, MessageSquare, Plug, Bot, Workflow, Server, Settings, LogOut, Sun, Moon, Users, Search, Bell, User, ChevronDown, Brain, Home, QrCode, Calendar } from 'lucide-react' import { LayoutDashboard, Network, Flag, FlaskConical, Cloud, Mic, MicOff, Loader2, Sparkles } from 'lucide-react' import { useAuth } from '../../contexts/AuthContext' import { useTheme } from '../../contexts/ThemeContext' @@ -70,6 +70,7 @@ export default function Layout() { ...(isEnabled('memories_page') ? [ { path: '/memories', label: 'Memories', icon: Brain }, ] : []), + { path: '/timeline', label: 'Timeline', icon: Calendar, featureFlag: 'timeline' }, { path: '/cluster', label: 'Cluster', icon: Network, badgeVariant: 'beta' }, { path: '/kubernetes', label: 'Kubernetes', icon: Cloud }, { path: '/settings', label: 'Settings', icon: Settings },