Spaces:
Running
Running
Neon:ryan commited on
Commit ·
abcb14f
1
Parent(s): dd07b04
Enhance Canvas sidebar and deliverables functionality
Browse files- Introduced a new sidebar structure for the Canvas page, allowing users to navigate between Insights, Workspace, and Deliverables with improved organization.
- Implemented expandable sections for deliverable projects, enabling users to view and manage project details more effectively.
- Added an auto-save indicator next to project titles to inform users of save status in real-time.
- Updated CSS styles for the sidebar and deliverables view to enhance visual consistency and user experience.
These changes significantly improve the usability and functionality of the Canvas interface, making project management more intuitive.
phd-advisor-frontend/src/components/Sidebar.js
CHANGED
|
@@ -11,7 +11,9 @@ import {
|
|
| 11 |
User,
|
| 12 |
Settings,
|
| 13 |
PanelLeft,
|
| 14 |
-
FileText
|
|
|
|
|
|
|
| 15 |
} from 'lucide-react';
|
| 16 |
import CopyrightNotice from './CopyrightNotice';
|
| 17 |
import '../styles/Sidebar.css';
|
|
@@ -30,9 +32,22 @@ const Sidebar = ({
|
|
| 30 |
refreshTrigger,
|
| 31 |
onCurrentSessionDeleted,
|
| 32 |
pageContext = 'chat',
|
| 33 |
-
canvasItems = []
|
|
|
|
|
|
|
|
|
|
| 34 |
}) => {
|
| 35 |
const isOnCanvas = pageContext === 'canvas';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
const [chatSessions, setChatSessions] = useState([]);
|
| 37 |
const [searchTerm, setSearchTerm] = useState('');
|
| 38 |
const [isLoading, setIsLoading] = useState(true);
|
|
@@ -260,7 +275,11 @@ const Sidebar = ({
|
|
| 260 |
<Search size={16} className="search-icon" />
|
| 261 |
<input
|
| 262 |
type="text"
|
| 263 |
-
placeholder={
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
value={searchTerm}
|
| 265 |
onChange={(e) => setSearchTerm(e.target.value)}
|
| 266 |
className="search-input"
|
|
@@ -279,39 +298,123 @@ const Sidebar = ({
|
|
| 279 |
</div>
|
| 280 |
)}
|
| 281 |
|
| 282 |
-
{/* Canvas
|
| 283 |
{isOnCanvas ? (
|
| 284 |
-
<div className="
|
| 285 |
-
{(() => {
|
| 286 |
-
const
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
if (
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
{!isCollapsed && (searchTerm ? 'No widgets match' : 'No widgets yet')}
|
| 293 |
-
</div>
|
| 294 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
}
|
| 296 |
return (
|
| 297 |
<div className="sessions-list">
|
| 298 |
-
{
|
| 299 |
-
<div
|
| 300 |
-
key={it.id}
|
| 301 |
-
className={`session-item ${isCollapsed ? 'collapsed' : ''}`}
|
| 302 |
-
onClick={it.onClick}
|
| 303 |
-
title={isCollapsed ? it.label : ''}
|
| 304 |
-
>
|
| 305 |
<div className="session-content">
|
| 306 |
-
<div className="session-icon">
|
| 307 |
-
|
|
|
|
|
|
|
| 308 |
</div>
|
| 309 |
-
{!isCollapsed && (
|
| 310 |
-
<div className="session-details">
|
| 311 |
-
<div className="session-title">{it.label}</div>
|
| 312 |
-
{it.sub && <div className="session-meta"><span>{it.sub}</span></div>}
|
| 313 |
-
</div>
|
| 314 |
-
)}
|
| 315 |
</div>
|
| 316 |
</div>
|
| 317 |
))}
|
|
|
|
| 11 |
User,
|
| 12 |
Settings,
|
| 13 |
PanelLeft,
|
| 14 |
+
FileText,
|
| 15 |
+
ChevronRight,
|
| 16 |
+
Clock
|
| 17 |
} from 'lucide-react';
|
| 18 |
import CopyrightNotice from './CopyrightNotice';
|
| 19 |
import '../styles/Sidebar.css';
|
|
|
|
| 32 |
refreshTrigger,
|
| 33 |
onCurrentSessionDeleted,
|
| 34 |
pageContext = 'chat',
|
| 35 |
+
canvasItems = [],
|
| 36 |
+
canvasSubview = 'workspace',
|
| 37 |
+
widgetGroups = [],
|
| 38 |
+
deliverableProjects = [],
|
| 39 |
}) => {
|
| 40 |
const isOnCanvas = pageContext === 'canvas';
|
| 41 |
+
const [expanded, setExpanded] = useState(() => {
|
| 42 |
+
try { return JSON.parse(localStorage.getItem('sidebar-expanded-v1') || '{}'); } catch { return {}; }
|
| 43 |
+
});
|
| 44 |
+
const toggleExpanded = (key) => {
|
| 45 |
+
setExpanded(prev => {
|
| 46 |
+
const next = { ...prev, [key]: !prev[key] };
|
| 47 |
+
localStorage.setItem('sidebar-expanded-v1', JSON.stringify(next));
|
| 48 |
+
return next;
|
| 49 |
+
});
|
| 50 |
+
};
|
| 51 |
const [chatSessions, setChatSessions] = useState([]);
|
| 52 |
const [searchTerm, setSearchTerm] = useState('');
|
| 53 |
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
| 275 |
<Search size={16} className="search-icon" />
|
| 276 |
<input
|
| 277 |
type="text"
|
| 278 |
+
placeholder={
|
| 279 |
+
isOnCanvas
|
| 280 |
+
? (canvasSubview === 'deliverables' ? 'Search drafts...' : 'Search widgets...')
|
| 281 |
+
: 'Search chats...'
|
| 282 |
+
}
|
| 283 |
value={searchTerm}
|
| 284 |
onChange={(e) => setSearchTerm(e.target.value)}
|
| 285 |
className="search-input"
|
|
|
|
| 298 |
</div>
|
| 299 |
)}
|
| 300 |
|
| 301 |
+
{/* Canvas sidebar — subview-aware (Insights / Workspace / Deliverables) */}
|
| 302 |
{isOnCanvas ? (
|
| 303 |
+
<div className="canvas-sidebar-menu">
|
| 304 |
+
{!isCollapsed && (() => {
|
| 305 |
+
const q = searchTerm.toLowerCase();
|
| 306 |
+
|
| 307 |
+
// ---------- DELIVERABLES: project list with expandable section dropdown ----------
|
| 308 |
+
if (canvasSubview === 'deliverables') {
|
| 309 |
+
const projects = deliverableProjects.filter(p =>
|
| 310 |
+
!q || p.name.toLowerCase().includes(q) || p.sections.some(s => s.name.toLowerCase().includes(q))
|
|
|
|
|
|
|
| 311 |
);
|
| 312 |
+
if (projects.length === 0) {
|
| 313 |
+
return (
|
| 314 |
+
<div className="no-sessions">
|
| 315 |
+
{searchTerm ? 'No drafts match' : 'No drafts yet — create one in Deliverables'}
|
| 316 |
+
</div>
|
| 317 |
+
);
|
| 318 |
+
}
|
| 319 |
+
return projects.map(p => {
|
| 320 |
+
const open = expanded[`p-${p.id}`] ?? p.isActive;
|
| 321 |
+
const totalWords = p.sections.reduce((s, x) => s + x.wc, 0);
|
| 322 |
+
return (
|
| 323 |
+
<div key={p.id} className={`csm-project ${p.isActive ? 'active' : ''}`}>
|
| 324 |
+
<button
|
| 325 |
+
className="csm-project-head"
|
| 326 |
+
onClick={() => toggleExpanded(`p-${p.id}`)}
|
| 327 |
+
>
|
| 328 |
+
<ChevronRight size={12} className={`csm-chevron ${open ? 'open' : ''}`}/>
|
| 329 |
+
<FileText size={13}/>
|
| 330 |
+
<span className="csm-project-name">{p.name}</span>
|
| 331 |
+
{p.versions > 0 && (
|
| 332 |
+
<span className="csm-versions" title={`${p.versions} version${p.versions === 1 ? '' : 's'} saved`}>
|
| 333 |
+
<Clock size={10}/>{p.versions}
|
| 334 |
+
</span>
|
| 335 |
+
)}
|
| 336 |
+
</button>
|
| 337 |
+
{open && (
|
| 338 |
+
<div className="csm-project-body">
|
| 339 |
+
<button className="csm-row" onClick={p.onOpen}>
|
| 340 |
+
<span className="csm-row-icon">📂</span>
|
| 341 |
+
<span>Open editor</span>
|
| 342 |
+
</button>
|
| 343 |
+
{p.sections.map(s => (
|
| 344 |
+
<button key={s.id} className="csm-row csm-row-section" onClick={s.onClick}>
|
| 345 |
+
<span className="csm-row-bullet"/>
|
| 346 |
+
<span className="csm-row-label">{s.name}</span>
|
| 347 |
+
{s.wc > 0 && <span className="csm-row-meta">{s.wc}</span>}
|
| 348 |
+
</button>
|
| 349 |
+
))}
|
| 350 |
+
<div className="csm-row csm-row-foot">
|
| 351 |
+
<Clock size={11}/>
|
| 352 |
+
<span>{p.versions} version{p.versions === 1 ? '' : 's'} · auto-saved</span>
|
| 353 |
+
</div>
|
| 354 |
+
<div className="csm-row csm-row-foot">
|
| 355 |
+
<span style={{ color: 'var(--text-tertiary, #9CA3AF)' }}>{totalWords} words total</span>
|
| 356 |
+
</div>
|
| 357 |
+
</div>
|
| 358 |
+
)}
|
| 359 |
+
</div>
|
| 360 |
+
);
|
| 361 |
+
});
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// ---------- WORKSPACE: widgets grouped by category ----------
|
| 365 |
+
if (canvasSubview === 'workspace') {
|
| 366 |
+
const groups = widgetGroups
|
| 367 |
+
.map(g => ({
|
| 368 |
+
...g,
|
| 369 |
+
items: g.items.filter(it => !q || it.label.toLowerCase().includes(q)),
|
| 370 |
+
}))
|
| 371 |
+
.filter(g => g.items.length > 0);
|
| 372 |
+
if (groups.length === 0) {
|
| 373 |
+
return (
|
| 374 |
+
<div className="no-sessions">
|
| 375 |
+
{searchTerm ? 'No widgets match' : 'Workspace is empty — add widgets'}
|
| 376 |
+
</div>
|
| 377 |
+
);
|
| 378 |
+
}
|
| 379 |
+
return groups.map(g => {
|
| 380 |
+
const open = expanded[`g-${g.id}`] ?? true;
|
| 381 |
+
return (
|
| 382 |
+
<div key={g.id} className="csm-group">
|
| 383 |
+
<button className="csm-group-head" onClick={() => toggleExpanded(`g-${g.id}`)}>
|
| 384 |
+
<ChevronRight size={11} className={`csm-chevron ${open ? 'open' : ''}`}/>
|
| 385 |
+
<span className="csm-group-name">{g.label}</span>
|
| 386 |
+
<span className="csm-group-count">{g.items.length}</span>
|
| 387 |
+
</button>
|
| 388 |
+
{open && (
|
| 389 |
+
<div className="csm-group-body">
|
| 390 |
+
{g.items.map(it => (
|
| 391 |
+
<button key={it.id} className={`csm-row ${it.critic ? 'critic' : ''}`} onClick={it.onClick}>
|
| 392 |
+
<span className="csm-row-bullet"/>
|
| 393 |
+
<span className="csm-row-label">{it.label}</span>
|
| 394 |
+
</button>
|
| 395 |
+
))}
|
| 396 |
+
</div>
|
| 397 |
+
)}
|
| 398 |
+
</div>
|
| 399 |
+
);
|
| 400 |
+
});
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
// ---------- INSIGHTS / fallback: keep flat list ----------
|
| 404 |
+
const items = canvasItems.filter(it => !q || it.label.toLowerCase().includes(q));
|
| 405 |
+
if (items.length === 0) {
|
| 406 |
+
return <div className="no-sessions">{searchTerm ? 'No matches' : 'Nothing here yet'}</div>;
|
| 407 |
}
|
| 408 |
return (
|
| 409 |
<div className="sessions-list">
|
| 410 |
+
{items.map((it) => (
|
| 411 |
+
<div key={it.id} className="session-item" onClick={it.onClick}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
<div className="session-content">
|
| 413 |
+
<div className="session-icon"><FileText size={16}/></div>
|
| 414 |
+
<div className="session-details">
|
| 415 |
+
<div className="session-title">{it.label}</div>
|
| 416 |
+
{it.sub && <div className="session-meta"><span>{it.sub}</span></div>}
|
| 417 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
</div>
|
| 419 |
</div>
|
| 420 |
))}
|
phd-advisor-frontend/src/components/canvas/CanvasDeliverables.js
CHANGED
|
@@ -27,7 +27,7 @@ const newId = (p) => p + Math.random().toString(36).slice(2, 8);
|
|
| 27 |
// ============================================================================
|
| 28 |
// Templates
|
| 29 |
// ============================================================================
|
| 30 |
-
const TEMPLATES = [
|
| 31 |
{
|
| 32 |
id: 'research-paper',
|
| 33 |
name: 'Research Paper',
|
|
@@ -509,11 +509,14 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 509 |
<button className="btn btn-ghost" style={{ padding: '4px 8px', fontSize: 12, marginBottom: 4, color: 'var(--canvas-text-3)' }} onClick={closeProject}>
|
| 510 |
<Icon name="back" size={12}/>All drafts
|
| 511 |
</button>
|
| 512 |
-
<
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
|
|
|
|
|
|
|
|
|
| 517 |
<div className="page-sub">
|
| 518 |
{template.name} · {totalWords} / {totalTarget} words · ~{readingMinutes(totalWords)} min read · {template.sections.length} {template.mode === 'slides' ? 'slides' : 'sections'}
|
| 519 |
</div>
|
|
@@ -804,6 +807,45 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 804 |
);
|
| 805 |
};
|
| 806 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 807 |
// ============================================================================
|
| 808 |
// RichBlock — Notion-style click-to-edit + Docs-style floating toolbar.
|
| 809 |
// • Idle: shows rendered markdown (with KaTeX math) — looks like a real doc
|
|
|
|
| 27 |
// ============================================================================
|
| 28 |
// Templates
|
| 29 |
// ============================================================================
|
| 30 |
+
export const TEMPLATES = [
|
| 31 |
{
|
| 32 |
id: 'research-paper',
|
| 33 |
name: 'Research Paper',
|
|
|
|
| 509 |
<button className="btn btn-ghost" style={{ padding: '4px 8px', fontSize: 12, marginBottom: 4, color: 'var(--canvas-text-3)' }} onClick={closeProject}>
|
| 510 |
<Icon name="back" size={12}/>All drafts
|
| 511 |
</button>
|
| 512 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
| 513 |
+
<input
|
| 514 |
+
className="page-title page-title-editable"
|
| 515 |
+
value={project.name}
|
| 516 |
+
onChange={(e) => renameProject(e.target.value)}
|
| 517 |
+
/>
|
| 518 |
+
<SaveIndicator project={project}/>
|
| 519 |
+
</div>
|
| 520 |
<div className="page-sub">
|
| 521 |
{template.name} · {totalWords} / {totalTarget} words · ~{readingMinutes(totalWords)} min read · {template.sections.length} {template.mode === 'slides' ? 'slides' : 'sections'}
|
| 522 |
</div>
|
|
|
|
| 807 |
);
|
| 808 |
};
|
| 809 |
|
| 810 |
+
// ============================================================================
|
| 811 |
+
// Auto-save indicator. Drafts persist to localStorage on every keystroke,
|
| 812 |
+
// so we show a transient "Saving…" pill when the project changes, settling
|
| 813 |
+
// to "Saved · Xs ago" while idle.
|
| 814 |
+
// ============================================================================
|
| 815 |
+
function SaveIndicator({ project }) {
|
| 816 |
+
const [savedAt, setSavedAt] = useState(Date.now());
|
| 817 |
+
const [pulse, setPulse] = useState(false);
|
| 818 |
+
const sectionsKey = JSON.stringify(project?.sections || {});
|
| 819 |
+
useEffect(() => {
|
| 820 |
+
setPulse(true);
|
| 821 |
+
const t1 = setTimeout(() => setPulse(false), 300);
|
| 822 |
+
const t2 = setTimeout(() => setSavedAt(Date.now()), 350);
|
| 823 |
+
return () => { clearTimeout(t1); clearTimeout(t2); };
|
| 824 |
+
}, [sectionsKey]);
|
| 825 |
+
|
| 826 |
+
// Tick the relative-time string
|
| 827 |
+
const [, force] = useState(0);
|
| 828 |
+
useEffect(() => {
|
| 829 |
+
const i = setInterval(() => force(n => n + 1), 5000);
|
| 830 |
+
return () => clearInterval(i);
|
| 831 |
+
}, []);
|
| 832 |
+
|
| 833 |
+
const ago = (() => {
|
| 834 |
+
const d = Math.floor((Date.now() - savedAt) / 1000);
|
| 835 |
+
if (d < 5) return 'just now';
|
| 836 |
+
if (d < 60) return `${d}s ago`;
|
| 837 |
+
if (d < 3600) return `${Math.floor(d / 60)}m ago`;
|
| 838 |
+
return `${Math.floor(d / 3600)}h ago`;
|
| 839 |
+
})();
|
| 840 |
+
|
| 841 |
+
return (
|
| 842 |
+
<span className={`save-indicator ${pulse ? 'saving' : ''}`}>
|
| 843 |
+
<span className="save-indicator-dot"/>
|
| 844 |
+
{pulse ? 'Saving…' : `Saved · ${ago}`}
|
| 845 |
+
</span>
|
| 846 |
+
);
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
// ============================================================================
|
| 850 |
// RichBlock — Notion-style click-to-edit + Docs-style floating toolbar.
|
| 851 |
// • Idle: shows rendered markdown (with KaTeX math) — looks like a real doc
|
phd-advisor-frontend/src/pages/CanvasPage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import React, { useState, useEffect, useCallback } from 'react';
|
| 2 |
import { HelpCircle } from 'lucide-react';
|
| 3 |
import { useTheme } from '../contexts/ThemeContext';
|
| 4 |
import { useAppConfig } from '../contexts/AppConfigContext';
|
|
@@ -27,7 +27,7 @@ import {
|
|
| 27 |
PaletteModal, CommandPaletteModal, GlobalSearchModal,
|
| 28 |
} from '../components/canvas/CanvasModals';
|
| 29 |
import CanvasWelcomeTour from '../components/canvas/CanvasWelcomeTour';
|
| 30 |
-
import DeliverablesView from '../components/canvas/CanvasDeliverables';
|
| 31 |
import '../styles/CanvasPage.css';
|
| 32 |
|
| 33 |
const LAYOUT_KEY = 'canvas-layout-v2';
|
|
@@ -455,22 +455,72 @@ const CanvasPage = ({ user, authToken, onNavigateToHome, onNavigateToChat, onSig
|
|
| 455 |
return () => window.removeEventListener('keydown', k);
|
| 456 |
}, [closeModal, openCommandPalette, openGlobalSearch]);
|
| 457 |
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
return (
|
| 476 |
<div className="canvas-page-with-sidebar" data-canvas-theme={theme}>
|
|
@@ -485,7 +535,9 @@ const CanvasPage = ({ user, authToken, onNavigateToHome, onNavigateToChat, onSig
|
|
| 485 |
onSelectSession={(id) => onNavigateToChat && onNavigateToChat(id)}
|
| 486 |
onNewChat={() => onNavigateToChat && onNavigateToChat()}
|
| 487 |
pageContext="canvas"
|
| 488 |
-
|
|
|
|
|
|
|
| 489 |
/>
|
| 490 |
<div className={`canvas-main-area ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
|
| 491 |
<div className="canvas-app-shell">
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
| 2 |
import { HelpCircle } from 'lucide-react';
|
| 3 |
import { useTheme } from '../contexts/ThemeContext';
|
| 4 |
import { useAppConfig } from '../contexts/AppConfigContext';
|
|
|
|
| 27 |
PaletteModal, CommandPaletteModal, GlobalSearchModal,
|
| 28 |
} from '../components/canvas/CanvasModals';
|
| 29 |
import CanvasWelcomeTour from '../components/canvas/CanvasWelcomeTour';
|
| 30 |
+
import DeliverablesView, { TEMPLATES as DELIVERABLE_TEMPLATES } from '../components/canvas/CanvasDeliverables';
|
| 31 |
import '../styles/CanvasPage.css';
|
| 32 |
|
| 33 |
const LAYOUT_KEY = 'canvas-layout-v2';
|
|
|
|
| 455 |
return () => window.removeEventListener('keydown', k);
|
| 456 |
}, [closeModal, openCommandPalette, openGlobalSearch]);
|
| 457 |
|
| 458 |
+
// Highlight a widget when picked from the sidebar
|
| 459 |
+
const flashScrollTo = (selector) => {
|
| 460 |
+
const el = document.querySelector(selector);
|
| 461 |
+
if (!el) return;
|
| 462 |
+
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
| 463 |
+
el.style.boxShadow = '0 0 0 2px var(--canvas-accent), 0 0 24px var(--canvas-accent-glow)';
|
| 464 |
+
setTimeout(() => { el.style.boxShadow = ''; }, 1400);
|
| 465 |
+
};
|
| 466 |
+
|
| 467 |
+
// Workspace: group widgets by category for the new sidebar
|
| 468 |
+
const widgetGroups = useMemo(() => {
|
| 469 |
+
const groups = {};
|
| 470 |
+
layout.forEach(w => {
|
| 471 |
+
const meta = WIDGET_CATALOG.find(m => m.type === w.type);
|
| 472 |
+
if (!meta) return;
|
| 473 |
+
const cat = meta.cat;
|
| 474 |
+
(groups[cat] ||= { id: cat, label: cat, items: [] }).items.push({
|
| 475 |
+
id: w.id,
|
| 476 |
+
label: meta.name,
|
| 477 |
+
icon: meta.icon,
|
| 478 |
+
critic: meta.critic,
|
| 479 |
+
onClick: () => flashScrollTo(`[data-widget-id="${w.id}"]`),
|
| 480 |
+
});
|
| 481 |
+
});
|
| 482 |
+
// Order: critic last
|
| 483 |
+
const order = ['research', 'writing', 'project', 'wellness', 'career', 'data', 'practical', 'critic'];
|
| 484 |
+
return order.map(c => groups[c]).filter(Boolean);
|
| 485 |
+
}, [layout]);
|
| 486 |
+
|
| 487 |
+
// Deliverables: list of projects with sections + history actions
|
| 488 |
+
const deliverableProjects = useMemo(() => {
|
| 489 |
+
try {
|
| 490 |
+
const dStore = JSON.parse(localStorage.getItem('canvas-deliverables-v2') || '{}');
|
| 491 |
+
const projects = Object.values(dStore.projects || {});
|
| 492 |
+
return projects.map(p => {
|
| 493 |
+
const t = DELIVERABLE_TEMPLATES.find(x => x.id === p.templateId);
|
| 494 |
+
return {
|
| 495 |
+
id: p.id,
|
| 496 |
+
name: p.name,
|
| 497 |
+
icon: t?.icon || 'book',
|
| 498 |
+
versions: p.versions?.length || 0,
|
| 499 |
+
isActive: p.id === dStore.activeProjectId,
|
| 500 |
+
sections: (t?.sections || []).map(s => ({
|
| 501 |
+
id: s.id,
|
| 502 |
+
name: s.name,
|
| 503 |
+
wc: ((p.sections || {})[s.id] || '').trim().split(/\s+/).filter(Boolean).length,
|
| 504 |
+
onClick: () => {
|
| 505 |
+
if (p.id !== dStore.activeProjectId) {
|
| 506 |
+
// Open this project first; section scroll happens after a tick.
|
| 507 |
+
const next = { ...dStore, activeProjectId: p.id };
|
| 508 |
+
localStorage.setItem('canvas-deliverables-v2', JSON.stringify(next));
|
| 509 |
+
window.dispatchEvent(new Event('storage'));
|
| 510 |
+
}
|
| 511 |
+
setTimeout(() => flashScrollTo(`#notion-section-${s.id}`), 80);
|
| 512 |
+
},
|
| 513 |
+
})),
|
| 514 |
+
onOpen: () => {
|
| 515 |
+
const next = { ...dStore, activeProjectId: p.id };
|
| 516 |
+
localStorage.setItem('canvas-deliverables-v2', JSON.stringify(next));
|
| 517 |
+
window.dispatchEvent(new Event('storage'));
|
| 518 |
+
},
|
| 519 |
+
};
|
| 520 |
+
});
|
| 521 |
+
} catch { return []; }
|
| 522 |
+
// re-derive when view or layout changes (layout proxy for "user did something")
|
| 523 |
+
}, [view, layout, widgetStates]); // eslint-disable-line react-hooks/exhaustive-deps
|
| 524 |
|
| 525 |
return (
|
| 526 |
<div className="canvas-page-with-sidebar" data-canvas-theme={theme}>
|
|
|
|
| 535 |
onSelectSession={(id) => onNavigateToChat && onNavigateToChat(id)}
|
| 536 |
onNewChat={() => onNavigateToChat && onNavigateToChat()}
|
| 537 |
pageContext="canvas"
|
| 538 |
+
canvasSubview={view}
|
| 539 |
+
widgetGroups={widgetGroups}
|
| 540 |
+
deliverableProjects={deliverableProjects}
|
| 541 |
/>
|
| 542 |
<div className={`canvas-main-area ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
|
| 543 |
<div className="canvas-app-shell">
|
phd-advisor-frontend/src/styles/CanvasPage.css
CHANGED
|
@@ -1609,6 +1609,159 @@ body[data-canvas-theme="light"] .canvas-modal-backdrop .critic-meter .bar { back
|
|
| 1609 |
.canvas-page-with-sidebar .paper-empty { color: #9ca3af; font-style: italic; font-size: 12px; }
|
| 1610 |
.canvas-page-with-sidebar .paper-section { margin-bottom: 4px; }
|
| 1611 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1612 |
/* ----- Deliverable project tabs (Overleaf-style multi-draft) ----- */
|
| 1613 |
.canvas-page-with-sidebar .deliverable-projects {
|
| 1614 |
display: flex;
|
|
@@ -1652,6 +1805,41 @@ body[data-canvas-theme="light"] .canvas-modal-backdrop .critic-meter .bar { back
|
|
| 1652 |
.canvas-page-with-sidebar .deliverable-new-project summary::-webkit-details-marker { display: none; }
|
| 1653 |
.canvas-page-with-sidebar .deliverable-new-project[open] summary { background: var(--canvas-surface-2); color: var(--canvas-text); }
|
| 1654 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1655 |
/* Editable project title — looks like the page-title but is an input */
|
| 1656 |
.canvas-page-with-sidebar .page-title-editable {
|
| 1657 |
background: transparent;
|
|
|
|
| 1609 |
.canvas-page-with-sidebar .paper-empty { color: #9ca3af; font-style: italic; font-size: 12px; }
|
| 1610 |
.canvas-page-with-sidebar .paper-section { margin-bottom: 4px; }
|
| 1611 |
|
| 1612 |
+
/* ----- Canvas-mode app sidebar menu (Insights / Workspace / Deliverables) ----- */
|
| 1613 |
+
/* These rules live in the *app* sidebar (not the canvas-page-with-sidebar scope)
|
| 1614 |
+
because they apply to .sidebar from components.css when the user is on canvas. */
|
| 1615 |
+
.canvas-sidebar-menu {
|
| 1616 |
+
flex: 1;
|
| 1617 |
+
overflow-y: auto;
|
| 1618 |
+
padding: 8px 8px 16px;
|
| 1619 |
+
display: flex;
|
| 1620 |
+
flex-direction: column;
|
| 1621 |
+
gap: 4px;
|
| 1622 |
+
}
|
| 1623 |
+
.canvas-sidebar-menu .no-sessions {
|
| 1624 |
+
padding: 18px 12px;
|
| 1625 |
+
font-size: 12px;
|
| 1626 |
+
color: var(--text-tertiary, #9CA3AF);
|
| 1627 |
+
text-align: center;
|
| 1628 |
+
}
|
| 1629 |
+
|
| 1630 |
+
/* --- Workspace: widget groups --- */
|
| 1631 |
+
.canvas-sidebar-menu .csm-group { display: flex; flex-direction: column; gap: 1px; margin-bottom: 4px; }
|
| 1632 |
+
.canvas-sidebar-menu .csm-group-head {
|
| 1633 |
+
display: flex;
|
| 1634 |
+
align-items: center;
|
| 1635 |
+
gap: 6px;
|
| 1636 |
+
padding: 6px 8px;
|
| 1637 |
+
background: transparent;
|
| 1638 |
+
border: none;
|
| 1639 |
+
border-radius: 5px;
|
| 1640 |
+
font-family: inherit;
|
| 1641 |
+
font-size: 11px;
|
| 1642 |
+
text-transform: uppercase;
|
| 1643 |
+
letter-spacing: 0.06em;
|
| 1644 |
+
font-weight: 600;
|
| 1645 |
+
color: var(--text-tertiary, #9CA3AF);
|
| 1646 |
+
cursor: pointer;
|
| 1647 |
+
}
|
| 1648 |
+
.canvas-sidebar-menu .csm-group-head:hover { background: var(--bg-secondary, #f3f4f6); color: var(--text-secondary, #6b7280); }
|
| 1649 |
+
.dark .canvas-sidebar-menu .csm-group-head:hover { background: var(--bg-secondary-dark, #374151); }
|
| 1650 |
+
.canvas-sidebar-menu .csm-group-name { flex: 1; text-align: left; }
|
| 1651 |
+
.canvas-sidebar-menu .csm-group-count {
|
| 1652 |
+
font-family: 'JetBrains Mono', monospace;
|
| 1653 |
+
font-size: 10px;
|
| 1654 |
+
color: var(--text-tertiary, #9CA3AF);
|
| 1655 |
+
}
|
| 1656 |
+
.canvas-sidebar-menu .csm-group-body { display: flex; flex-direction: column; gap: 1px; padding: 2px 0 6px 8px; }
|
| 1657 |
+
|
| 1658 |
+
/* --- Deliverables: project blocks --- */
|
| 1659 |
+
.canvas-sidebar-menu .csm-project {
|
| 1660 |
+
display: flex;
|
| 1661 |
+
flex-direction: column;
|
| 1662 |
+
gap: 1px;
|
| 1663 |
+
border-radius: 6px;
|
| 1664 |
+
margin-bottom: 4px;
|
| 1665 |
+
}
|
| 1666 |
+
.canvas-sidebar-menu .csm-project.active { background: rgba(99, 102, 241, 0.06); }
|
| 1667 |
+
.dark .canvas-sidebar-menu .csm-project.active { background: rgba(129, 140, 248, 0.10); }
|
| 1668 |
+
.canvas-sidebar-menu .csm-project-head {
|
| 1669 |
+
display: flex;
|
| 1670 |
+
align-items: center;
|
| 1671 |
+
gap: 7px;
|
| 1672 |
+
padding: 8px 10px;
|
| 1673 |
+
background: transparent;
|
| 1674 |
+
border: none;
|
| 1675 |
+
border-radius: 5px;
|
| 1676 |
+
font-family: inherit;
|
| 1677 |
+
font-size: 13px;
|
| 1678 |
+
font-weight: 500;
|
| 1679 |
+
color: var(--text-primary, #111827);
|
| 1680 |
+
cursor: pointer;
|
| 1681 |
+
text-align: left;
|
| 1682 |
+
}
|
| 1683 |
+
.canvas-sidebar-menu .csm-project-head:hover { background: var(--bg-secondary, #f3f4f6); }
|
| 1684 |
+
.dark .canvas-sidebar-menu .csm-project-head { color: var(--text-primary-dark, #f9fafb); }
|
| 1685 |
+
.dark .canvas-sidebar-menu .csm-project-head:hover { background: var(--bg-secondary-dark, #374151); }
|
| 1686 |
+
.canvas-sidebar-menu .csm-project.active .csm-project-name { color: var(--accent-primary, #6366F1); font-weight: 600; }
|
| 1687 |
+
.canvas-sidebar-menu .csm-project-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 1688 |
+
.canvas-sidebar-menu .csm-versions {
|
| 1689 |
+
display: inline-flex;
|
| 1690 |
+
align-items: center;
|
| 1691 |
+
gap: 3px;
|
| 1692 |
+
font-family: 'JetBrains Mono', monospace;
|
| 1693 |
+
font-size: 10px;
|
| 1694 |
+
color: var(--text-tertiary, #9CA3AF);
|
| 1695 |
+
padding: 1px 5px;
|
| 1696 |
+
border-radius: 3px;
|
| 1697 |
+
background: var(--bg-secondary, #f3f4f6);
|
| 1698 |
+
}
|
| 1699 |
+
.dark .canvas-sidebar-menu .csm-versions { background: var(--bg-secondary-dark, #374151); }
|
| 1700 |
+
.canvas-sidebar-menu .csm-project-body {
|
| 1701 |
+
padding: 2px 0 8px 22px;
|
| 1702 |
+
display: flex;
|
| 1703 |
+
flex-direction: column;
|
| 1704 |
+
gap: 1px;
|
| 1705 |
+
}
|
| 1706 |
+
|
| 1707 |
+
/* --- Shared row (used in groups and project bodies) --- */
|
| 1708 |
+
.canvas-sidebar-menu .csm-row {
|
| 1709 |
+
display: flex;
|
| 1710 |
+
align-items: center;
|
| 1711 |
+
gap: 8px;
|
| 1712 |
+
padding: 5px 10px 5px 6px;
|
| 1713 |
+
background: transparent;
|
| 1714 |
+
border: none;
|
| 1715 |
+
border-radius: 4px;
|
| 1716 |
+
font-family: inherit;
|
| 1717 |
+
font-size: 12.5px;
|
| 1718 |
+
color: var(--text-secondary, #6b7280);
|
| 1719 |
+
text-align: left;
|
| 1720 |
+
cursor: pointer;
|
| 1721 |
+
}
|
| 1722 |
+
.canvas-sidebar-menu .csm-row:hover {
|
| 1723 |
+
background: var(--bg-secondary, #f3f4f6);
|
| 1724 |
+
color: var(--text-primary, #111827);
|
| 1725 |
+
}
|
| 1726 |
+
.dark .canvas-sidebar-menu .csm-row { color: var(--text-secondary-dark, #9ca3af); }
|
| 1727 |
+
.dark .canvas-sidebar-menu .csm-row:hover { background: var(--bg-secondary-dark, #374151); color: var(--text-primary-dark, #f9fafb); }
|
| 1728 |
+
.canvas-sidebar-menu .csm-row.critic { color: #8B5CF6; }
|
| 1729 |
+
.dark .canvas-sidebar-menu .csm-row.critic { color: #A78BFA; }
|
| 1730 |
+
.canvas-sidebar-menu .csm-row-bullet {
|
| 1731 |
+
width: 4px;
|
| 1732 |
+
height: 4px;
|
| 1733 |
+
border-radius: 50%;
|
| 1734 |
+
background: currentColor;
|
| 1735 |
+
opacity: 0.45;
|
| 1736 |
+
flex-shrink: 0;
|
| 1737 |
+
margin-left: 4px;
|
| 1738 |
+
}
|
| 1739 |
+
.canvas-sidebar-menu .csm-row-icon { font-size: 11px; opacity: 0.85; }
|
| 1740 |
+
.canvas-sidebar-menu .csm-row-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 1741 |
+
.canvas-sidebar-menu .csm-row-meta {
|
| 1742 |
+
font-family: 'JetBrains Mono', monospace;
|
| 1743 |
+
font-size: 9.5px;
|
| 1744 |
+
color: var(--text-tertiary, #9CA3AF);
|
| 1745 |
+
}
|
| 1746 |
+
.canvas-sidebar-menu .csm-row-foot {
|
| 1747 |
+
display: flex;
|
| 1748 |
+
align-items: center;
|
| 1749 |
+
gap: 6px;
|
| 1750 |
+
padding: 3px 8px;
|
| 1751 |
+
font-family: 'JetBrains Mono', monospace;
|
| 1752 |
+
font-size: 9.5px;
|
| 1753 |
+
color: var(--text-tertiary, #9CA3AF);
|
| 1754 |
+
cursor: default;
|
| 1755 |
+
}
|
| 1756 |
+
.canvas-sidebar-menu .csm-row-foot:hover { background: transparent; color: var(--text-tertiary, #9CA3AF); }
|
| 1757 |
+
|
| 1758 |
+
/* Chevron rotates open/closed */
|
| 1759 |
+
.canvas-sidebar-menu .csm-chevron {
|
| 1760 |
+
transition: transform 0.15s ease;
|
| 1761 |
+
color: var(--text-tertiary, #9CA3AF);
|
| 1762 |
+
}
|
| 1763 |
+
.canvas-sidebar-menu .csm-chevron.open { transform: rotate(90deg); }
|
| 1764 |
+
|
| 1765 |
/* ----- Deliverable project tabs (Overleaf-style multi-draft) ----- */
|
| 1766 |
.canvas-page-with-sidebar .deliverable-projects {
|
| 1767 |
display: flex;
|
|
|
|
| 1805 |
.canvas-page-with-sidebar .deliverable-new-project summary::-webkit-details-marker { display: none; }
|
| 1806 |
.canvas-page-with-sidebar .deliverable-new-project[open] summary { background: var(--canvas-surface-2); color: var(--canvas-text); }
|
| 1807 |
|
| 1808 |
+
/* Auto-save indicator next to the project title */
|
| 1809 |
+
.canvas-page-with-sidebar .save-indicator {
|
| 1810 |
+
display: inline-flex;
|
| 1811 |
+
align-items: center;
|
| 1812 |
+
gap: 6px;
|
| 1813 |
+
font-size: 11.5px;
|
| 1814 |
+
color: var(--canvas-text-3);
|
| 1815 |
+
font-family: var(--canvas-mono);
|
| 1816 |
+
white-space: nowrap;
|
| 1817 |
+
padding: 4px 8px;
|
| 1818 |
+
border-radius: 999px;
|
| 1819 |
+
background: var(--canvas-surface-2);
|
| 1820 |
+
transition: background .2s, color .2s;
|
| 1821 |
+
}
|
| 1822 |
+
.canvas-page-with-sidebar .save-indicator-dot {
|
| 1823 |
+
width: 6px;
|
| 1824 |
+
height: 6px;
|
| 1825 |
+
border-radius: 50%;
|
| 1826 |
+
background: var(--canvas-ok);
|
| 1827 |
+
box-shadow: 0 0 6px rgba(16, 185, 129, 0.4);
|
| 1828 |
+
transition: background .2s;
|
| 1829 |
+
}
|
| 1830 |
+
.canvas-page-with-sidebar .save-indicator.saving {
|
| 1831 |
+
background: var(--canvas-accent-glow);
|
| 1832 |
+
color: var(--canvas-accent);
|
| 1833 |
+
}
|
| 1834 |
+
.canvas-page-with-sidebar .save-indicator.saving .save-indicator-dot {
|
| 1835 |
+
background: var(--canvas-accent);
|
| 1836 |
+
animation: save-pulse 0.6s ease infinite;
|
| 1837 |
+
}
|
| 1838 |
+
@keyframes save-pulse {
|
| 1839 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 1840 |
+
50% { opacity: 0.4; transform: scale(0.85); }
|
| 1841 |
+
}
|
| 1842 |
+
|
| 1843 |
/* Editable project title — looks like the page-title but is an input */
|
| 1844 |
.canvas-page-with-sidebar .page-title-editable {
|
| 1845 |
background: transparent;
|