Spaces:
Running
Running
Neon:ryan commited on
Commit ·
2dc7022
1
Parent(s): abcb14f
Refactor Canvas components and enhance user experience
Browse files- Updated AppHeader to rename "Deliverables" tab to "Documents" for clarity.
- Enhanced Sidebar functionality by adding support for insights sections and improved search capabilities.
- Introduced new PhD Journey and PhD Resources widgets to assist users in managing their academic progress and resources.
- Improved CanvasDeliverables with additional document templates and updated UI elements for better usability.
- Enhanced CSS styles for consistency and improved visual appeal across the Canvas interface.
These changes collectively enhance the usability and functionality of the Canvas application, making it more intuitive for users to navigate and manage their academic tasks.
- phd-advisor-frontend/src/components/AppHeader.js +2 -2
- phd-advisor-frontend/src/components/Sidebar.js +36 -6
- phd-advisor-frontend/src/components/canvas/CanvasCriticWidgets.js +70 -28
- phd-advisor-frontend/src/components/canvas/CanvasDeliverables.js +69 -3
- phd-advisor-frontend/src/components/canvas/CanvasModals.js +2 -1
- phd-advisor-frontend/src/components/canvas/CanvasWelcomeTour.js +2 -1
- phd-advisor-frontend/src/components/canvas/CanvasWidgets.js +182 -1
- phd-advisor-frontend/src/components/canvas/canvasData.js +53 -0
- phd-advisor-frontend/src/components/canvas/platform.js +5 -0
- phd-advisor-frontend/src/pages/CanvasPage.js +519 -44
- phd-advisor-frontend/src/styles/CanvasPage.css +562 -0
phd-advisor-frontend/src/components/AppHeader.js
CHANGED
|
@@ -71,7 +71,7 @@ const AppHeader = ({
|
|
| 71 |
<button className={`tab ${isOnChat ? 'active' : ''}`} onClick={onNavigateToChat}>Chat</button>
|
| 72 |
<button className={`tab ${tabActive('insights') ? 'active' : ''}`} onClick={() => goToCanvas('insights')}>Insights</button>
|
| 73 |
<button className={`tab ${tabActive('workspace') ? 'active' : ''}`} onClick={() => goToCanvas('workspace')}>Workspace</button>
|
| 74 |
-
<button className={`tab ${tabActive('deliverables') ? 'active' : ''}`} onClick={() => goToCanvas('deliverables')}>
|
| 75 |
</div>
|
| 76 |
)}
|
| 77 |
|
|
@@ -89,7 +89,7 @@ const AppHeader = ({
|
|
| 89 |
<option value="chat">Chat</option>
|
| 90 |
<option value="insights">Insights</option>
|
| 91 |
<option value="workspace">Workspace</option>
|
| 92 |
-
<option value="deliverables">
|
| 93 |
</select>
|
| 94 |
)}
|
| 95 |
|
|
|
|
| 71 |
<button className={`tab ${isOnChat ? 'active' : ''}`} onClick={onNavigateToChat}>Chat</button>
|
| 72 |
<button className={`tab ${tabActive('insights') ? 'active' : ''}`} onClick={() => goToCanvas('insights')}>Insights</button>
|
| 73 |
<button className={`tab ${tabActive('workspace') ? 'active' : ''}`} onClick={() => goToCanvas('workspace')}>Workspace</button>
|
| 74 |
+
<button className={`tab ${tabActive('deliverables') ? 'active' : ''}`} onClick={() => goToCanvas('deliverables')}>Documents</button>
|
| 75 |
</div>
|
| 76 |
)}
|
| 77 |
|
|
|
|
| 89 |
<option value="chat">Chat</option>
|
| 90 |
<option value="insights">Insights</option>
|
| 91 |
<option value="workspace">Workspace</option>
|
| 92 |
+
<option value="deliverables">Documents</option>
|
| 93 |
</select>
|
| 94 |
)}
|
| 95 |
|
phd-advisor-frontend/src/components/Sidebar.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
| 1 |
import React, { useState, useEffect } from 'react';
|
| 2 |
import {
|
| 3 |
MessageSquare,
|
| 4 |
-
Plus,
|
| 5 |
SquarePen,
|
| 6 |
Search,
|
| 7 |
MoreVertical,
|
| 8 |
Trash2,
|
| 9 |
-
Edit3,
|
| 10 |
LogOut,
|
| 11 |
User,
|
| 12 |
Settings,
|
|
@@ -28,7 +26,6 @@ const Sidebar = ({
|
|
| 28 |
onSidebarToggle,
|
| 29 |
isMobileOpen = false,
|
| 30 |
onMobileToggle,
|
| 31 |
-
onNavigateToCanvas,
|
| 32 |
refreshTrigger,
|
| 33 |
onCurrentSessionDeleted,
|
| 34 |
pageContext = 'chat',
|
|
@@ -36,6 +33,7 @@ const Sidebar = ({
|
|
| 36 |
canvasSubview = 'workspace',
|
| 37 |
widgetGroups = [],
|
| 38 |
deliverableProjects = [],
|
|
|
|
| 39 |
}) => {
|
| 40 |
const isOnCanvas = pageContext === 'canvas';
|
| 41 |
const [expanded, setExpanded] = useState(() => {
|
|
@@ -277,7 +275,9 @@ const Sidebar = ({
|
|
| 277 |
type="text"
|
| 278 |
placeholder={
|
| 279 |
isOnCanvas
|
| 280 |
-
? (canvasSubview === 'deliverables' ? 'Search drafts...'
|
|
|
|
|
|
|
| 281 |
: 'Search chats...'
|
| 282 |
}
|
| 283 |
value={searchTerm}
|
|
@@ -312,7 +312,7 @@ const Sidebar = ({
|
|
| 312 |
if (projects.length === 0) {
|
| 313 |
return (
|
| 314 |
<div className="no-sessions">
|
| 315 |
-
{searchTerm ? 'No drafts match' : 'No drafts yet — create one in
|
| 316 |
</div>
|
| 317 |
);
|
| 318 |
}
|
|
@@ -400,7 +400,37 @@ const Sidebar = ({
|
|
| 400 |
});
|
| 401 |
}
|
| 402 |
|
| 403 |
-
// ---------- INSIGHTS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>;
|
|
|
|
| 1 |
import React, { useState, useEffect } from 'react';
|
| 2 |
import {
|
| 3 |
MessageSquare,
|
|
|
|
| 4 |
SquarePen,
|
| 5 |
Search,
|
| 6 |
MoreVertical,
|
| 7 |
Trash2,
|
|
|
|
| 8 |
LogOut,
|
| 9 |
User,
|
| 10 |
Settings,
|
|
|
|
| 26 |
onSidebarToggle,
|
| 27 |
isMobileOpen = false,
|
| 28 |
onMobileToggle,
|
|
|
|
| 29 |
refreshTrigger,
|
| 30 |
onCurrentSessionDeleted,
|
| 31 |
pageContext = 'chat',
|
|
|
|
| 33 |
canvasSubview = 'workspace',
|
| 34 |
widgetGroups = [],
|
| 35 |
deliverableProjects = [],
|
| 36 |
+
insightSections = [],
|
| 37 |
}) => {
|
| 38 |
const isOnCanvas = pageContext === 'canvas';
|
| 39 |
const [expanded, setExpanded] = useState(() => {
|
|
|
|
| 275 |
type="text"
|
| 276 |
placeholder={
|
| 277 |
isOnCanvas
|
| 278 |
+
? (canvasSubview === 'deliverables' ? 'Search drafts...'
|
| 279 |
+
: canvasSubview === 'insights' ? 'Search sections...'
|
| 280 |
+
: 'Search widgets...')
|
| 281 |
: 'Search chats...'
|
| 282 |
}
|
| 283 |
value={searchTerm}
|
|
|
|
| 312 |
if (projects.length === 0) {
|
| 313 |
return (
|
| 314 |
<div className="no-sessions">
|
| 315 |
+
{searchTerm ? 'No drafts match' : 'No drafts yet — create one in Documents'}
|
| 316 |
</div>
|
| 317 |
);
|
| 318 |
}
|
|
|
|
| 400 |
});
|
| 401 |
}
|
| 402 |
|
| 403 |
+
// ---------- INSIGHTS: section list with confidence badges ----------
|
| 404 |
+
if (canvasSubview === 'insights') {
|
| 405 |
+
const sections = insightSections.filter(s => !q || s.name.toLowerCase().includes(q));
|
| 406 |
+
if (sections.length === 0) {
|
| 407 |
+
return <div className="no-sessions">{searchTerm ? 'No sections match' : 'No insights yet'}</div>;
|
| 408 |
+
}
|
| 409 |
+
return (
|
| 410 |
+
<div className="csm-group">
|
| 411 |
+
<div className="csm-group-head" style={{ cursor: 'default' }}>
|
| 412 |
+
<span className="csm-group-name">Sections</span>
|
| 413 |
+
<span className="csm-group-count">{sections.length}</span>
|
| 414 |
+
</div>
|
| 415 |
+
<div className="csm-group-body">
|
| 416 |
+
{sections.map(s => {
|
| 417 |
+
const complete = s.taskCount > 0 && s.doneCount === s.taskCount;
|
| 418 |
+
return (
|
| 419 |
+
<button key={s.id} className={`csm-row ${complete ? 'csm-row-done' : ''}`} onClick={s.onClick}>
|
| 420 |
+
<span className="csm-row-bullet"/>
|
| 421 |
+
<span className="csm-row-label">{s.name}</span>
|
| 422 |
+
{s.taskCount > 0 && (
|
| 423 |
+
<span className="csm-row-meta">{s.doneCount}/{s.taskCount}</span>
|
| 424 |
+
)}
|
| 425 |
+
</button>
|
| 426 |
+
);
|
| 427 |
+
})}
|
| 428 |
+
</div>
|
| 429 |
+
</div>
|
| 430 |
+
);
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
// ---------- Fallback: flat list (legacy) ----------
|
| 434 |
const items = canvasItems.filter(it => !q || it.label.toLowerCase().includes(q));
|
| 435 |
if (items.length === 0) {
|
| 436 |
return <div className="no-sessions">{searchTerm ? 'No matches' : 'Nothing here yet'}</div>;
|
phd-advisor-frontend/src/components/canvas/CanvasCriticWidgets.js
CHANGED
|
@@ -4,6 +4,20 @@ import Icon from './CanvasIcon';
|
|
| 4 |
const fireToast = (msg, kind = 'success') =>
|
| 5 |
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg, kind } }));
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
// ---------- Reviewer 2 widget ----------
|
| 8 |
export function Reviewer2Widget({ state, setState, openModal }) {
|
| 9 |
return (
|
|
@@ -27,16 +41,26 @@ export function Reviewer2Widget({ state, setState, openModal }) {
|
|
| 27 |
<div className="bar"><i style={{ width: '92%' }}/></div>
|
| 28 |
<span style={{ color: 'var(--canvas-critic)' }}>harsh</span>
|
| 29 |
</div>
|
| 30 |
-
<
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
</>
|
| 41 |
);
|
| 42 |
}
|
|
@@ -58,17 +82,26 @@ export function DevilsAdvocateWidget({ state, setState, openModal }) {
|
|
| 58 |
</div>
|
| 59 |
))}
|
| 60 |
</div>
|
| 61 |
-
<
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
</>
|
| 73 |
);
|
| 74 |
}
|
|
@@ -98,13 +131,22 @@ export function ScopeRealismWidget({ state, openModal }) {
|
|
| 98 |
</div>
|
| 99 |
))}
|
| 100 |
</div>
|
| 101 |
-
<
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
</div>
|
| 109 |
);
|
| 110 |
}
|
|
|
|
| 4 |
const fireToast = (msg, kind = 'success') =>
|
| 5 |
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg, kind } }));
|
| 6 |
|
| 7 |
+
// Critic widgets are scripted in canvas; the *real* critique should happen in
|
| 8 |
+
// the main chat so message history lives in one place (per Daniel's review).
|
| 9 |
+
// "Open in chat" stashes a draft prompt + persona hint then asks CanvasPage to
|
| 10 |
+
// navigate to chat — the chat page can read `canvas-chat-handoff` from localStorage.
|
| 11 |
+
const handoffToChat = (persona, prompt, context = {}) => {
|
| 12 |
+
try {
|
| 13 |
+
localStorage.setItem('canvas-chat-handoff', JSON.stringify({
|
| 14 |
+
at: Date.now(), persona, prompt, ...context,
|
| 15 |
+
}));
|
| 16 |
+
} catch { /* ignore */ }
|
| 17 |
+
window.dispatchEvent(new CustomEvent('canvas-open-in-chat', { detail: { persona, prompt } }));
|
| 18 |
+
fireToast(`Opening ${persona} in chat — full history will be there.`);
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
// ---------- Reviewer 2 widget ----------
|
| 22 |
export function Reviewer2Widget({ state, setState, openModal }) {
|
| 23 |
return (
|
|
|
|
| 41 |
<div className="bar"><i style={{ width: '92%' }}/></div>
|
| 42 |
<span style={{ color: 'var(--canvas-critic)' }}>harsh</span>
|
| 43 |
</div>
|
| 44 |
+
<div style={{ display: 'flex', gap: 6, alignSelf: 'flex-start' }}>
|
| 45 |
+
<button
|
| 46 |
+
className="btn btn-critic"
|
| 47 |
+
onClick={() => openModal('reviewer-2', {
|
| 48 |
+
initial: state.lastDraft,
|
| 49 |
+
onComplete: (review) => setState({ ...state, lastDraft: review.draft, lastReview: review }),
|
| 50 |
+
})}
|
| 51 |
+
>
|
| 52 |
+
<Icon name="gavel" size={13}/>Get critique
|
| 53 |
+
</button>
|
| 54 |
+
<button
|
| 55 |
+
className="btn"
|
| 56 |
+
title="Open Reviewer 2 in the main chat (history lives there)"
|
| 57 |
+
onClick={() => handoffToChat('Reviewer 2', state.lastDraft
|
| 58 |
+
? `Critique this draft as Reviewer 2: "${state.lastDraft}"`
|
| 59 |
+
: 'Open Reviewer 2 and ready a critique.')}
|
| 60 |
+
>
|
| 61 |
+
<Icon name="message" size={13}/>Open in chat
|
| 62 |
+
</button>
|
| 63 |
+
</div>
|
| 64 |
</>
|
| 65 |
);
|
| 66 |
}
|
|
|
|
| 82 |
</div>
|
| 83 |
))}
|
| 84 |
</div>
|
| 85 |
+
<div style={{ display: 'flex', gap: 6, alignSelf: 'flex-start' }}>
|
| 86 |
+
<button
|
| 87 |
+
className="btn btn-critic"
|
| 88 |
+
onClick={() => openModal('devils-advocate', {
|
| 89 |
+
claim: state.claim,
|
| 90 |
+
counters: state.counters,
|
| 91 |
+
onUpdate: (next) => setState({ ...state, ...next }),
|
| 92 |
+
})}
|
| 93 |
+
>
|
| 94 |
+
<Icon name="scale" size={13}/>Push harder
|
| 95 |
+
</button>
|
| 96 |
+
<button
|
| 97 |
+
className="btn"
|
| 98 |
+
title="Open Devil's Advocate in the main chat (history lives there)"
|
| 99 |
+
onClick={() => handoffToChat("Devil's Advocate",
|
| 100 |
+
`Take the position of devil's advocate on my claim: "${state.claim || 'my current hypothesis'}". Be ruthless.`)}
|
| 101 |
+
>
|
| 102 |
+
<Icon name="message" size={13}/>Open in chat
|
| 103 |
+
</button>
|
| 104 |
+
</div>
|
| 105 |
</>
|
| 106 |
);
|
| 107 |
}
|
|
|
|
| 131 |
</div>
|
| 132 |
))}
|
| 133 |
</div>
|
| 134 |
+
<div style={{ display: 'flex', gap: 6, alignSelf: 'flex-start' }}>
|
| 135 |
+
<button
|
| 136 |
+
className="btn btn-critic"
|
| 137 |
+
onClick={() => openModal('scope-realism', { state })}
|
| 138 |
+
>
|
| 139 |
+
<Icon name="bullseye" size={13}/>Read full verdict
|
| 140 |
+
</button>
|
| 141 |
+
<button
|
| 142 |
+
className="btn"
|
| 143 |
+
title="Open Scope Realism in the main chat (history lives there)"
|
| 144 |
+
onClick={() => handoffToChat('Scope Realism',
|
| 145 |
+
`Run a brutal feasibility check on my goal: "${state.target || 'my current research scope'}". Be specific about what's at risk.`)}
|
| 146 |
+
>
|
| 147 |
+
<Icon name="message" size={13}/>Open in chat
|
| 148 |
+
</button>
|
| 149 |
+
</div>
|
| 150 |
</div>
|
| 151 |
);
|
| 152 |
}
|
phd-advisor-frontend/src/components/canvas/CanvasDeliverables.js
CHANGED
|
@@ -11,6 +11,7 @@ import remarkMath from 'remark-math';
|
|
| 11 |
import rehypeKatex from 'rehype-katex';
|
| 12 |
import 'katex/dist/katex.min.css';
|
| 13 |
import Icon from './CanvasIcon';
|
|
|
|
| 14 |
|
| 15 |
// Markdown plugins shared across all rendered blocks. remark-math + rehype-katex
|
| 16 |
// give us real LaTeX math (`$...$` inline, `$$...$$` block) inside any preview.
|
|
@@ -140,6 +141,71 @@ export const TEMPLATES = [
|
|
| 140 |
{ id: 'close', name: 'Close', target: 60, hint: 'Thanks + next step + signature.', checks: [] },
|
| 141 |
],
|
| 142 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
{
|
| 144 |
id: 'research-statement',
|
| 145 |
name: 'Research Statement',
|
|
@@ -397,7 +463,7 @@ const DeliverablesView = ({ allStates }) => {
|
|
| 397 |
<>
|
| 398 |
<div className="page-header">
|
| 399 |
<div>
|
| 400 |
-
<h1 className="page-title">
|
| 401 |
<div className="page-sub">
|
| 402 |
Your one-stop deliverable center. Drafts auto-save. Versions kept for rollback.
|
| 403 |
{projectList.length > 0 && ` · ${projectList.length} draft${projectList.length === 1 ? '' : 's'} in flight.`}
|
|
@@ -858,8 +924,8 @@ const lineWrap = (text, prefix) =>
|
|
| 858 |
(text ? text.split('\n').map(line => line ? `${prefix}${line}` : line).join('\n') : `${prefix}`);
|
| 859 |
|
| 860 |
const TOOLBAR = [
|
| 861 |
-
{ id: 'bold', icon: 'pencil', label:
|
| 862 |
-
{ id: 'italic', icon: 'pencil', label:
|
| 863 |
{ id: 'code', icon: 'flask', label: 'Inline code', run: (sel) => wrap(sel, '`') },
|
| 864 |
{ id: 'h2', icon: 'list', label: 'Heading', run: (sel) => `## ${sel || 'Heading'}` },
|
| 865 |
{ id: 'h3', icon: 'list', label: 'Subheading', run: (sel) => `### ${sel || 'Subheading'}` },
|
|
|
|
| 11 |
import rehypeKatex from 'rehype-katex';
|
| 12 |
import 'katex/dist/katex.min.css';
|
| 13 |
import Icon from './CanvasIcon';
|
| 14 |
+
import { MOD } from './platform';
|
| 15 |
|
| 16 |
// Markdown plugins shared across all rendered blocks. remark-math + rehype-katex
|
| 17 |
// give us real LaTeX math (`$...$` inline, `$$...$$` block) inside any preview.
|
|
|
|
| 141 |
{ id: 'close', name: 'Close', target: 60, hint: 'Thanks + next step + signature.', checks: [] },
|
| 142 |
],
|
| 143 |
},
|
| 144 |
+
{
|
| 145 |
+
id: 'irb-protocol',
|
| 146 |
+
name: 'IRB Protocol',
|
| 147 |
+
desc: 'Standard sections for human-subjects research approval.',
|
| 148 |
+
icon: 'shield',
|
| 149 |
+
mode: 'document',
|
| 150 |
+
sections: [
|
| 151 |
+
{ id: 'overview', name: 'Study overview', target: 200, hint: 'One-paragraph summary of purpose and procedures.', checks: [] },
|
| 152 |
+
{ id: 'background', name: 'Background & significance', target: 400, hint: 'Why this study? What gap does it close?', checks: ['hasGap', 'hasCitation'] },
|
| 153 |
+
{ id: 'aims', name: 'Specific aims & hypotheses', target: 250, hint: '2–3 aims. Each falsifiable.', checks: ['hasHypothesis'] },
|
| 154 |
+
{ id: 'participants', name: 'Participants & recruitment', target: 300, hint: 'Inclusion/exclusion criteria, sample size, recruitment plan.', checks: ['hasNumber'] },
|
| 155 |
+
{ id: 'procedures', name: 'Procedures', target: 500, hint: 'Step-by-step what subjects experience. Time burden in minutes.', checks: ['hasNumber'] },
|
| 156 |
+
{ id: 'risks', name: 'Risks & mitigation', target: 200, hint: 'Anticipated risks (physical, psychological, privacy) + mitigations.', checks: ['hasLimit'] },
|
| 157 |
+
{ id: 'benefits', name: 'Benefits', target: 100, hint: 'Direct + societal benefits. Be honest about minimal direct benefits.', checks: [] },
|
| 158 |
+
{ id: 'consent', name: 'Consent process', target: 200, hint: 'Who consents, when, written or verbal, capacity considerations.', checks: [] },
|
| 159 |
+
{ id: 'data', name: 'Data handling & confidentiality', target: 200, hint: 'Storage, access, identifiers, retention period.', checks: [] },
|
| 160 |
+
],
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
id: 'meeting-prep',
|
| 164 |
+
name: 'Advisor Meeting Prep',
|
| 165 |
+
desc: 'Bring this to your 1:1 — agenda, updates, decisions needed, follow-ups.',
|
| 166 |
+
icon: 'message',
|
| 167 |
+
mode: 'document',
|
| 168 |
+
sections: [
|
| 169 |
+
{ id: 'agenda', name: 'Agenda', target: 80, hint: '3–5 bullets ranked by priority.', checks: [] },
|
| 170 |
+
{ id: 'progress', name: 'Progress since last meeting', target: 200, hint: 'What you actually did, with numbers when possible.', checks: ['hasNumber'] },
|
| 171 |
+
{ id: 'blockers', name: 'Blockers', target: 150, hint: 'What you need from them to move forward.', checks: ['hasLimit'] },
|
| 172 |
+
{ id: 'decisions', name: 'Decisions needed', target: 200, hint: 'Frame as A/B options with your recommendation.', checks: [] },
|
| 173 |
+
{ id: 'questions', name: 'Questions', target: 150, hint: 'Open questions you genuinely want their take on.', checks: [] },
|
| 174 |
+
{ id: 'followup', name: 'Action items (post-meeting)', target: 100, hint: 'Fill in during/after. Owner + due date for each.', checks: [] },
|
| 175 |
+
],
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
id: 'dissertation-formatting',
|
| 179 |
+
name: 'Dissertation Formatting Checklist',
|
| 180 |
+
desc: 'Catch-everything pass before ProQuest submission.',
|
| 181 |
+
icon: 'shield',
|
| 182 |
+
mode: 'document',
|
| 183 |
+
sections: [
|
| 184 |
+
{ id: 'frontmatter', name: 'Front matter', target: 0, hint: 'Title page, copyright, abstract, dedication, acknowledgements, ToC, list of figures/tables.', checks: [] },
|
| 185 |
+
{ id: 'margins', name: 'Margins & spacing', target: 0, hint: 'Verify school requirements. Most: 1" margins, double-spaced body, single-spaced quotes/captions.', checks: ['hasNumber'] },
|
| 186 |
+
{ id: 'fonts', name: 'Fonts & typography', target: 0, hint: 'One body font (Times/Garamond/Cambria) at 12pt. Captions 10–11pt. Headings consistent.', checks: ['hasNumber'] },
|
| 187 |
+
{ id: 'pagenumbers', name: 'Page numbering', target: 0, hint: 'Roman for front matter, Arabic from Intro onward. Check section breaks.', checks: [] },
|
| 188 |
+
{ id: 'figures', name: 'Figures & tables', target: 0, hint: 'Captions below figures, above tables. Numbered. Cited in text before they appear.', checks: ['hasFigure'] },
|
| 189 |
+
{ id: 'citations', name: 'Citations & references', target: 0, hint: 'Consistent style throughout. Every cite has a reference; every reference is cited.', checks: ['hasCitation'] },
|
| 190 |
+
{ id: 'appendices', name: 'Appendices', target: 0, hint: 'Lettered (A, B, C). Each cited in the body at least once.', checks: [] },
|
| 191 |
+
{ id: 'proquest', name: 'ProQuest submission', target: 0, hint: 'PDF/A format, embedded fonts, no broken links, abstract under word limit.', checks: [] },
|
| 192 |
+
],
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
id: 'faculty-hunt',
|
| 196 |
+
name: 'Faculty / Advisor Hunt',
|
| 197 |
+
desc: 'For prospective PhDs or finding committee members — research the people.',
|
| 198 |
+
icon: 'user',
|
| 199 |
+
mode: 'document',
|
| 200 |
+
sections: [
|
| 201 |
+
{ id: 'criteria', name: 'What you\'re looking for', target: 150, hint: 'Research area, methodology, working style, mentorship reputation.', checks: [] },
|
| 202 |
+
{ id: 'shortlist', name: 'Shortlist (5–10 names)', target: 400, hint: 'For each: name, institution, 2–3 representative papers, why they fit.', checks: ['hasCitation'] },
|
| 203 |
+
{ id: 'pubs', name: 'Recent publications', target: 300, hint: 'What have they published in the last 2 years? Drop @keys from Bibliography.', checks: ['hasCitation'] },
|
| 204 |
+
{ id: 'students', name: 'Current/recent students', target: 200, hint: 'Lab size, time-to-defense, where students go after.', checks: ['hasNumber'] },
|
| 205 |
+
{ id: 'reachout', name: 'Outreach plan', target: 200, hint: 'When to email, what to send, who to mention.', checks: [] },
|
| 206 |
+
{ id: 'notes', name: 'Conversation notes', target: 0, hint: 'After meetings/emails — vibes, fit signals, red flags.', checks: [] },
|
| 207 |
+
],
|
| 208 |
+
},
|
| 209 |
{
|
| 210 |
id: 'research-statement',
|
| 211 |
name: 'Research Statement',
|
|
|
|
| 463 |
<>
|
| 464 |
<div className="page-header">
|
| 465 |
<div>
|
| 466 |
+
<h1 className="page-title">Documents</h1>
|
| 467 |
<div className="page-sub">
|
| 468 |
Your one-stop deliverable center. Drafts auto-save. Versions kept for rollback.
|
| 469 |
{projectList.length > 0 && ` · ${projectList.length} draft${projectList.length === 1 ? '' : 's'} in flight.`}
|
|
|
|
| 924 |
(text ? text.split('\n').map(line => line ? `${prefix}${line}` : line).join('\n') : `${prefix}`);
|
| 925 |
|
| 926 |
const TOOLBAR = [
|
| 927 |
+
{ id: 'bold', icon: 'pencil', label: `Bold (${MOD}+B)`, run: (sel) => wrap(sel, '**') },
|
| 928 |
+
{ id: 'italic', icon: 'pencil', label: `Italic (${MOD}+I)`, run: (sel) => wrap(sel, '*') },
|
| 929 |
{ id: 'code', icon: 'flask', label: 'Inline code', run: (sel) => wrap(sel, '`') },
|
| 930 |
{ id: 'h2', icon: 'list', label: 'Heading', run: (sel) => `## ${sel || 'Heading'}` },
|
| 931 |
{ id: 'h3', icon: 'list', label: 'Subheading', run: (sel) => `### ${sel || 'Subheading'}` },
|
phd-advisor-frontend/src/components/canvas/CanvasModals.js
CHANGED
|
@@ -871,7 +871,8 @@ export function PaletteModal({ data, onClose }) {
|
|
| 871 |
<div className="pi-title">
|
| 872 |
{w.name}
|
| 873 |
{w.critic && <span className="widget-tag" style={{ fontSize: 8.5 }}>WEDGE</span>}
|
| 874 |
-
{w.
|
|
|
|
| 875 |
{w.stub && <span style={{ fontSize: 9, color: 'var(--canvas-text-4)', fontFamily: 'var(--canvas-mono)', marginLeft: 4 }}>SOON</span>}
|
| 876 |
</div>
|
| 877 |
<div className="pi-desc">{w.desc}</div>
|
|
|
|
| 871 |
<div className="pi-title">
|
| 872 |
{w.name}
|
| 873 |
{w.critic && <span className="widget-tag" style={{ fontSize: 8.5 }}>WEDGE</span>}
|
| 874 |
+
{w.critic && <span className="widget-tag widget-tag-chat" style={{ fontSize: 8.5 }} title="Real critique happens in the main chat — this widget is a scratchpad">BEST IN CHAT</span>}
|
| 875 |
+
{w.enhanced && !w.stub && !w.critic && <span className="widget-tag widget-tag-enhanced" style={{ fontSize: 8.5 }}>ENHANCED</span>}
|
| 876 |
{w.stub && <span style={{ fontSize: 9, color: 'var(--canvas-text-4)', fontFamily: 'var(--canvas-mono)', marginLeft: 4 }}>SOON</span>}
|
| 877 |
</div>
|
| 878 |
<div className="pi-desc">{w.desc}</div>
|
phd-advisor-frontend/src/components/canvas/CanvasWelcomeTour.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import React, { useState, useEffect } from 'react';
|
| 2 |
import Icon from './CanvasIcon';
|
|
|
|
| 3 |
|
| 4 |
const TOUR_KEY = 'canvas-tour-seen-v1';
|
| 5 |
|
|
@@ -12,7 +13,7 @@ const STEPS = [
|
|
| 12 |
{
|
| 13 |
title: 'Add widgets from the palette',
|
| 14 |
icon: 'plus',
|
| 15 |
-
body:
|
| 16 |
},
|
| 17 |
{
|
| 18 |
title: 'Make it yours',
|
|
|
|
| 1 |
import React, { useState, useEffect } from 'react';
|
| 2 |
import Icon from './CanvasIcon';
|
| 3 |
+
import { MOD } from './platform';
|
| 4 |
|
| 5 |
const TOUR_KEY = 'canvas-tour-seen-v1';
|
| 6 |
|
|
|
|
| 13 |
{
|
| 14 |
title: 'Add widgets from the palette',
|
| 15 |
icon: 'plus',
|
| 16 |
+
body: `Click "Add widget" on the Workspace view, or hit ${MOD}+K and search. There are 30+ widgets — bibliography, kanban, pomodoro, writing tracker, plus three "anti-yes-man" widgets that push back on your thinking.`,
|
| 17 |
},
|
| 18 |
{
|
| 19 |
title: 'Make it yours',
|
phd-advisor-frontend/src/components/canvas/CanvasWidgets.js
CHANGED
|
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo, useRef } from 'react';
|
|
| 2 |
import ReactMarkdown from 'react-markdown';
|
| 3 |
import remarkGfm from 'remark-gfm';
|
| 4 |
import Icon from './CanvasIcon';
|
|
|
|
| 5 |
|
| 6 |
const fireToast = (msg, kind = 'success') =>
|
| 7 |
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg, kind } }));
|
|
@@ -1431,13 +1432,193 @@ export function DocumenterWidget({ state, setState }) {
|
|
| 1431 |
</div>
|
| 1432 |
))}
|
| 1433 |
{entries.length === 0 && (
|
| 1434 |
-
<EmptyState icon="pencil" title="No entries yet" hint=
|
| 1435 |
)}
|
| 1436 |
</div>
|
| 1437 |
</>
|
| 1438 |
);
|
| 1439 |
}
|
| 1440 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1441 |
// ===== Stub — roadmap preview card =====
|
| 1442 |
// Shows what's coming for this widget type so adding it from the palette
|
| 1443 |
// doesn't feel like a dead end.
|
|
|
|
| 2 |
import ReactMarkdown from 'react-markdown';
|
| 3 |
import remarkGfm from 'remark-gfm';
|
| 4 |
import Icon from './CanvasIcon';
|
| 5 |
+
import { MOD } from './platform';
|
| 6 |
|
| 7 |
const fireToast = (msg, kind = 'success') =>
|
| 8 |
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg, kind } }));
|
|
|
|
| 1432 |
</div>
|
| 1433 |
))}
|
| 1434 |
{entries.length === 0 && (
|
| 1435 |
+
<EmptyState icon="pencil" title="No entries yet" hint={`Drop a line about today. Hit ${MOD}+↵ to log it. Tap Weekly summary anytime.`}/>
|
| 1436 |
)}
|
| 1437 |
</div>
|
| 1438 |
</>
|
| 1439 |
);
|
| 1440 |
}
|
| 1441 |
|
| 1442 |
+
// ===== PhD Journey — standard milestones with status + notes =====
|
| 1443 |
+
// Captures the "General Journey" list: courses, prelim, lit review, IRB,
|
| 1444 |
+
// committee, topic, comps, defense, ProQuest. Each milestone has a status
|
| 1445 |
+
// (open/in-progress/completed) and an inline note.
|
| 1446 |
+
const PHD_MILESTONES = [
|
| 1447 |
+
{ id: 'courses', label: 'Course selection & dissertation credits', hint: 'Plan term-by-term. Check funding constraints.' },
|
| 1448 |
+
{ id: 'prelim', label: 'Preliminary exam', hint: 'Department format varies — consult your handbook.' },
|
| 1449 |
+
{ id: 'lit-review', label: 'Literature review', hint: 'Coverage + critique. Bibliography widget pairs well here.' },
|
| 1450 |
+
{ id: 'topic', label: 'Pick dissertation topic', hint: 'Narrow until your advisor pushes back.' },
|
| 1451 |
+
{ id: 'committee', label: 'Select committee', hint: 'Pros/cons of a co-chair: more buy-in, more scheduling.' },
|
| 1452 |
+
{ id: 'irb', label: 'IRB approval', hint: 'Allow 6–12 weeks. Pre-fill paperwork early.' },
|
| 1453 |
+
{ id: 'data', label: 'Data collection', hint: 'Pilot first. Plan for the inevitable instrument failure.' },
|
| 1454 |
+
{ id: 'comps', label: 'Comprehensive exam', hint: 'Department format varies.' },
|
| 1455 |
+
{ id: 'analysis', label: 'Data analysis & visualization', hint: 'Make the figures before the prose.' },
|
| 1456 |
+
{ id: 'writing', label: 'Write dissertation', hint: 'One chapter at a time. Aim for "good enough to defend".' },
|
| 1457 |
+
{ id: 'defense', label: 'Oral defense', hint: 'Slides + practice Q&A. Use the Defense Slides template.' },
|
| 1458 |
+
{ id: 'proquest', label: 'Final admin (ProQuest upload)', hint: 'Read the formatting checklist before you start formatting.' },
|
| 1459 |
+
];
|
| 1460 |
+
|
| 1461 |
+
export function PhdJourneyWidget({ state, setState }) {
|
| 1462 |
+
const statuses = state.statuses || {};
|
| 1463 |
+
const notes = state.notes || {};
|
| 1464 |
+
const [editingId, setEditingId] = useState(null);
|
| 1465 |
+
const completed = PHD_MILESTONES.filter(m => statuses[m.id] === 'completed').length;
|
| 1466 |
+
const total = PHD_MILESTONES.length;
|
| 1467 |
+
const pct = Math.round((completed / total) * 100);
|
| 1468 |
+
|
| 1469 |
+
const cycleStatus = (id) => {
|
| 1470 |
+
const cur = statuses[id] || 'open';
|
| 1471 |
+
const next = cur === 'open' ? 'in-progress' : cur === 'in-progress' ? 'completed' : 'open';
|
| 1472 |
+
setState({ ...state, statuses: { ...statuses, [id]: next } });
|
| 1473 |
+
};
|
| 1474 |
+
const updateNote = (id, text) => setState({ ...state, notes: { ...notes, [id]: text } });
|
| 1475 |
+
|
| 1476 |
+
return (
|
| 1477 |
+
<>
|
| 1478 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 12, color: 'var(--canvas-text-2)' }}>
|
| 1479 |
+
<span style={{ fontFamily: 'var(--canvas-mono)', fontWeight: 700, fontSize: 16, color: 'var(--canvas-text)' }}>
|
| 1480 |
+
{completed}<span style={{ color: 'var(--canvas-text-3)', fontWeight: 500 }}>/{total}</span>
|
| 1481 |
+
</span>
|
| 1482 |
+
<div className="progress" style={{ flex: 1, height: 6 }}>
|
| 1483 |
+
<i style={{ width: pct + '%' }}/>
|
| 1484 |
+
</div>
|
| 1485 |
+
<span style={{ fontFamily: 'var(--canvas-mono)', fontSize: 11, color: 'var(--canvas-text-3)' }}>{pct}%</span>
|
| 1486 |
+
</div>
|
| 1487 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
| 1488 |
+
{PHD_MILESTONES.map(m => {
|
| 1489 |
+
const status = statuses[m.id] || 'open';
|
| 1490 |
+
const note = notes[m.id] || '';
|
| 1491 |
+
return (
|
| 1492 |
+
<div key={m.id} className={`phd-milestone milestone-${status}`}>
|
| 1493 |
+
<button
|
| 1494 |
+
className="phd-milestone-check"
|
| 1495 |
+
onClick={() => cycleStatus(m.id)}
|
| 1496 |
+
title={`Status: ${status} (click to advance)`}
|
| 1497 |
+
>
|
| 1498 |
+
{status === 'completed' && <Icon name="check" size={11}/>}
|
| 1499 |
+
{status === 'in-progress' && <span className="task-check-dot"/>}
|
| 1500 |
+
</button>
|
| 1501 |
+
<div className="phd-milestone-body" onClick={() => setEditingId(editingId === m.id ? null : m.id)}>
|
| 1502 |
+
<div className="phd-milestone-label">{m.label}</div>
|
| 1503 |
+
{(note || editingId === m.id) ? (
|
| 1504 |
+
editingId === m.id ? (
|
| 1505 |
+
<input
|
| 1506 |
+
className="inline-input"
|
| 1507 |
+
autoFocus
|
| 1508 |
+
defaultValue={note}
|
| 1509 |
+
placeholder={m.hint}
|
| 1510 |
+
onBlur={(e) => { updateNote(m.id, e.target.value); setEditingId(null); }}
|
| 1511 |
+
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); if (e.key === 'Escape') setEditingId(null); }}
|
| 1512 |
+
onClick={(e) => e.stopPropagation()}
|
| 1513 |
+
style={{ marginTop: 2, fontSize: 11.5 }}
|
| 1514 |
+
/>
|
| 1515 |
+
) : (
|
| 1516 |
+
<div className="phd-milestone-note">{note}</div>
|
| 1517 |
+
)
|
| 1518 |
+
) : (
|
| 1519 |
+
<div className="phd-milestone-hint">{m.hint}</div>
|
| 1520 |
+
)}
|
| 1521 |
+
</div>
|
| 1522 |
+
</div>
|
| 1523 |
+
);
|
| 1524 |
+
})}
|
| 1525 |
+
</div>
|
| 1526 |
+
</>
|
| 1527 |
+
);
|
| 1528 |
+
}
|
| 1529 |
+
|
| 1530 |
+
// ===== PhD Resources — curated links + open-source apps =====
|
| 1531 |
+
// Static curated list of useful PhD tools and resources, plus user-added links.
|
| 1532 |
+
const PHD_RESOURCE_GROUPS = [
|
| 1533 |
+
{
|
| 1534 |
+
label: 'Open-source PhD tools',
|
| 1535 |
+
items: [
|
| 1536 |
+
{ name: 'Zotero', href: 'https://www.zotero.org/', desc: 'Reference manager — free and open source' },
|
| 1537 |
+
{ name: 'Obsidian', href: 'https://obsidian.md/', desc: 'Local-first knowledge graph for notes' },
|
| 1538 |
+
{ name: 'JabRef', href: 'https://www.jabref.org/', desc: 'BibTeX-native reference manager' },
|
| 1539 |
+
{ name: 'Pandoc', href: 'https://pandoc.org/', desc: 'Universal document converter' },
|
| 1540 |
+
{ name: 'Quarto', href: 'https://quarto.org/', desc: 'Scientific publishing with R/Python/Julia' },
|
| 1541 |
+
],
|
| 1542 |
+
},
|
| 1543 |
+
{
|
| 1544 |
+
label: 'Writing & formatting',
|
| 1545 |
+
items: [
|
| 1546 |
+
{ name: 'Overleaf', href: 'https://www.overleaf.com/', desc: 'Browser LaTeX editor with templates' },
|
| 1547 |
+
{ name: 'LaTeX Templates', href: 'https://www.latextemplates.com/', desc: 'Thesis, CV, poster templates' },
|
| 1548 |
+
{ name: 'Hemingway Editor', href: 'https://hemingwayapp.com/', desc: 'Plain-language readability check' },
|
| 1549 |
+
],
|
| 1550 |
+
},
|
| 1551 |
+
{
|
| 1552 |
+
label: 'Community & career',
|
| 1553 |
+
items: [
|
| 1554 |
+
{ name: 'Academic Twitter / #PhDChat', href: 'https://twitter.com/search?q=%23PhDChat', desc: 'Peers + advisors discussing the grind' },
|
| 1555 |
+
{ name: 'ORCID', href: 'https://orcid.org/', desc: 'Permanent researcher ID for citations + grants' },
|
| 1556 |
+
{ name: 'Conferences & CFPs (WikiCFP)', href: 'http://www.wikicfp.com/', desc: 'Upcoming deadlines across fields' },
|
| 1557 |
+
],
|
| 1558 |
+
},
|
| 1559 |
+
];
|
| 1560 |
+
|
| 1561 |
+
export function PhdResourcesWidget({ state, setState }) {
|
| 1562 |
+
const customLinks = state.customLinks || [];
|
| 1563 |
+
const [name, setName] = useState('');
|
| 1564 |
+
const [href, setHref] = useState('');
|
| 1565 |
+
|
| 1566 |
+
const addLink = () => {
|
| 1567 |
+
if (!name.trim() || !href.trim()) return;
|
| 1568 |
+
setState({ ...state, customLinks: [...customLinks, { id: 'r' + Date.now(), name: name.trim(), href: href.trim() }] });
|
| 1569 |
+
setName(''); setHref('');
|
| 1570 |
+
fireToast('Resource added');
|
| 1571 |
+
};
|
| 1572 |
+
const removeLink = (id) => setState({ ...state, customLinks: customLinks.filter(l => l.id !== id) });
|
| 1573 |
+
|
| 1574 |
+
return (
|
| 1575 |
+
<>
|
| 1576 |
+
{PHD_RESOURCE_GROUPS.map(g => (
|
| 1577 |
+
<div key={g.label}>
|
| 1578 |
+
<div style={{ fontSize: 10, color: 'var(--canvas-text-4)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600, marginBottom: 6 }}>
|
| 1579 |
+
{g.label}
|
| 1580 |
+
</div>
|
| 1581 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 10 }}>
|
| 1582 |
+
{g.items.map(it => (
|
| 1583 |
+
<a key={it.name} href={it.href} target="_blank" rel="noopener noreferrer" className="phd-resource-link">
|
| 1584 |
+
<span className="phd-resource-name">{it.name}</span>
|
| 1585 |
+
<span className="phd-resource-desc">{it.desc}</span>
|
| 1586 |
+
</a>
|
| 1587 |
+
))}
|
| 1588 |
+
</div>
|
| 1589 |
+
</div>
|
| 1590 |
+
))}
|
| 1591 |
+
{customLinks.length > 0 && (
|
| 1592 |
+
<div>
|
| 1593 |
+
<div style={{ fontSize: 10, color: 'var(--canvas-text-4)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600, marginBottom: 6 }}>
|
| 1594 |
+
Your links
|
| 1595 |
+
</div>
|
| 1596 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 10 }}>
|
| 1597 |
+
{customLinks.map(l => (
|
| 1598 |
+
<div key={l.id} className="phd-resource-link" style={{ position: 'relative' }}>
|
| 1599 |
+
<a href={l.href} target="_blank" rel="noopener noreferrer" style={{ display: 'flex', flexDirection: 'column', flex: 1, textDecoration: 'none', color: 'inherit' }}>
|
| 1600 |
+
<span className="phd-resource-name">{l.name}</span>
|
| 1601 |
+
<span className="phd-resource-desc">{l.href}</span>
|
| 1602 |
+
</a>
|
| 1603 |
+
<button className="icon-btn" onClick={() => removeLink(l.id)} style={{ position: 'absolute', right: 4, top: 4, width: 20, height: 20 }}>
|
| 1604 |
+
<Icon name="x" size={11}/>
|
| 1605 |
+
</button>
|
| 1606 |
+
</div>
|
| 1607 |
+
))}
|
| 1608 |
+
</div>
|
| 1609 |
+
</div>
|
| 1610 |
+
)}
|
| 1611 |
+
<div style={{ display: 'flex', gap: 4 }}>
|
| 1612 |
+
<input className="input" placeholder="Resource name" value={name} onChange={e => setName(e.target.value)} style={{ fontSize: 12, padding: '5px 8px' }}/>
|
| 1613 |
+
<input className="input" placeholder="https://…" value={href} onChange={e => setHref(e.target.value)} style={{ fontSize: 12, padding: '5px 8px', flex: 2 }}/>
|
| 1614 |
+
<button className="btn" onClick={addLink} disabled={!name.trim() || !href.trim()}>
|
| 1615 |
+
<Icon name="plus" size={12}/>Add
|
| 1616 |
+
</button>
|
| 1617 |
+
</div>
|
| 1618 |
+
</>
|
| 1619 |
+
);
|
| 1620 |
+
}
|
| 1621 |
+
|
| 1622 |
// ===== Stub — roadmap preview card =====
|
| 1623 |
// Shows what's coming for this widget type so adding it from the palette
|
| 1624 |
// doesn't feel like a dead end.
|
phd-advisor-frontend/src/components/canvas/canvasData.js
CHANGED
|
@@ -10,6 +10,7 @@ export const INSIGHTS = [
|
|
| 10 |
id: 'i-progress',
|
| 11 |
title: 'Research progress',
|
| 12 |
icon: 'graph',
|
|
|
|
| 13 |
confidence: 78,
|
| 14 |
summary: 'Primary recordings from 4 of 6 planned animals are complete. Remaining two scheduled for May 18 and May 25. Analysis pipeline working on existing data; first results draft expected June.',
|
| 15 |
bullets: [
|
|
@@ -18,11 +19,18 @@ export const INSIGHTS = [
|
|
| 18 |
'<strong>Risk:</strong> M3 fixation drift suspected; need re-review with adv.',
|
| 19 |
],
|
| 20 |
pinned: true,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
},
|
| 22 |
{
|
| 23 |
id: 'i-method',
|
| 24 |
title: 'Methodology',
|
| 25 |
icon: 'flask',
|
|
|
|
| 26 |
confidence: 64,
|
| 27 |
summary: 'GLM with spike-history kernel + visual drive is your declared model. You\'ve resisted committing to a specific predictive-coding formulation; this comes up in every advisor meeting.',
|
| 28 |
bullets: [
|
|
@@ -30,11 +38,18 @@ export const INSIGHTS = [
|
|
| 30 |
'Open: which predictive-coding variant — Rao & Ballard vs. Bastos top-down',
|
| 31 |
'Open: how to operationalize "prediction error" from extracellular spikes',
|
| 32 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
},
|
| 34 |
{
|
| 35 |
id: 'i-lit',
|
| 36 |
title: 'Literature review',
|
| 37 |
icon: 'book',
|
|
|
|
| 38 |
confidence: 71,
|
| 39 |
summary: 'Strong on canonical predictive coding (Rao & Ballard 1999, Bastos 2012, Keller & Mrsic-Flogel 2018). Thin on recent feedback-circuit anatomy and on counter-evidence — this is showing up as a critique gap.',
|
| 40 |
bullets: [
|
|
@@ -42,11 +57,18 @@ export const INSIGHTS = [
|
|
| 42 |
'<strong>Gap:</strong> sparse on L5b feedback anatomy (Harris/Shepherd lab)',
|
| 43 |
'<strong>Gap:</strong> no engagement with anti-PC critiques (e.g. Heeger 2017)',
|
| 44 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
},
|
| 46 |
{
|
| 47 |
id: 'i-questions',
|
| 48 |
title: 'Open research questions',
|
| 49 |
icon: 'sparkles',
|
|
|
|
| 50 |
confidence: 58,
|
| 51 |
summary: 'Three live threads. Question 1 (does L2/3 spiking encode prediction error?) is the dissertation core. Q2 and Q3 are scoped to specific aims.',
|
| 52 |
bullets: [
|
|
@@ -54,11 +76,18 @@ export const INSIGHTS = [
|
|
| 54 |
'<strong>Q2:</strong> How does this depend on context length (1 vs. 4 vs. 16 trials)?',
|
| 55 |
'<strong>Q3:</strong> Is the signal sharpened by feedback from V2/RSC?',
|
| 56 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
},
|
| 58 |
{
|
| 59 |
id: 'i-next',
|
| 60 |
title: 'Next steps',
|
| 61 |
icon: 'arrow',
|
|
|
|
| 62 |
confidence: 82,
|
| 63 |
summary: 'Concrete, near-term actions. Two of these have been on the list for 3+ weeks.',
|
| 64 |
bullets: [
|
|
@@ -67,17 +96,30 @@ export const INSIGHTS = [
|
|
| 67 |
'Read Heeger 2017 + Aitchison & Lengyel 2017',
|
| 68 |
'Schedule pilot with M5 (May 18)',
|
| 69 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
},
|
| 71 |
{
|
| 72 |
id: 'i-blockers',
|
| 73 |
title: 'Blockers & risks',
|
| 74 |
icon: 'alert',
|
|
|
|
| 75 |
confidence: 70,
|
| 76 |
summary: 'One technical, one structural. The structural one is more important and you are deferring it.',
|
| 77 |
bullets: [
|
| 78 |
'<strong>Technical:</strong> Drift on M3 — may lose 1 animal of data',
|
| 79 |
'<strong>Structural:</strong> No clear predictive-coding theory commitment yet → hard to define what counts as evidence',
|
| 80 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
},
|
| 82 |
];
|
| 83 |
|
|
@@ -103,6 +145,8 @@ export const WIDGET_CATALOG = [
|
|
| 103 |
{ type: 'calendar', name: 'Calendar', desc: 'Month grid with deadlines and writing days', icon: 'calendar', cat: 'project', defaultSize: 'M', enhanced: true },
|
| 104 |
{ type: 'activity', name: 'Activity Feed', desc: 'Chronological log of edits across widgets', icon: 'graph', cat: 'project', defaultSize: 'M', enhanced: true },
|
| 105 |
{ type: 'documenter', name: 'Daily Documenter', desc: 'Date-stamped journal · AI weekly summary (LLM stub)', icon: 'pencil', cat: 'project', defaultSize: 'M', enhanced: true },
|
|
|
|
|
|
|
| 106 |
|
| 107 |
{ type: 'mood', name: 'Mood / Burnout Check-in', desc: 'Daily slider, trend graph', icon: 'smile', cat: 'wellness', defaultSize: 'S', stub: true },
|
| 108 |
{ type: 'sleep', name: 'Sleep & Energy', desc: 'Correlate with productive days', icon: 'heart', cat: 'wellness', defaultSize: 'S', stub: true },
|
|
@@ -245,5 +289,14 @@ export const EMPTY_STATE = {
|
|
| 245 |
calendar: { viewMonth: new Date().toISOString().slice(0, 7) },
|
| 246 |
activity: {},
|
| 247 |
documenter: { entries: [], lastSummary: null },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
};
|
| 249 |
|
|
|
|
| 10 |
id: 'i-progress',
|
| 11 |
title: 'Research progress',
|
| 12 |
icon: 'graph',
|
| 13 |
+
category: 'progress',
|
| 14 |
confidence: 78,
|
| 15 |
summary: 'Primary recordings from 4 of 6 planned animals are complete. Remaining two scheduled for May 18 and May 25. Analysis pipeline working on existing data; first results draft expected June.',
|
| 16 |
bullets: [
|
|
|
|
| 19 |
'<strong>Risk:</strong> M3 fixation drift suspected; need re-review with adv.',
|
| 20 |
],
|
| 21 |
pinned: true,
|
| 22 |
+
sources: 12,
|
| 23 |
+
updatedMinutesAgo: 3,
|
| 24 |
+
quotes: [
|
| 25 |
+
'"Animal M4 recording finished today, sorting completes tomorrow." — May 6 lab notes',
|
| 26 |
+
'"Pipeline is happy with M1, M2; M3 looks drifty." — chat with Reineke advisor',
|
| 27 |
+
],
|
| 28 |
},
|
| 29 |
{
|
| 30 |
id: 'i-method',
|
| 31 |
title: 'Methodology',
|
| 32 |
icon: 'flask',
|
| 33 |
+
category: 'theory',
|
| 34 |
confidence: 64,
|
| 35 |
summary: 'GLM with spike-history kernel + visual drive is your declared model. You\'ve resisted committing to a specific predictive-coding formulation; this comes up in every advisor meeting.',
|
| 36 |
bullets: [
|
|
|
|
| 38 |
'Open: which predictive-coding variant — Rao & Ballard vs. Bastos top-down',
|
| 39 |
'Open: how to operationalize "prediction error" from extracellular spikes',
|
| 40 |
],
|
| 41 |
+
sources: 8,
|
| 42 |
+
updatedMinutesAgo: 12,
|
| 43 |
+
quotes: [
|
| 44 |
+
'"Need to pick a PC formulation by next 1:1." — meeting notes May 2',
|
| 45 |
+
'"Bastos lets you predict laminar profile; Rao&Ballard does not." — methodologist chat',
|
| 46 |
+
],
|
| 47 |
},
|
| 48 |
{
|
| 49 |
id: 'i-lit',
|
| 50 |
title: 'Literature review',
|
| 51 |
icon: 'book',
|
| 52 |
+
category: 'literature',
|
| 53 |
confidence: 71,
|
| 54 |
summary: 'Strong on canonical predictive coding (Rao & Ballard 1999, Bastos 2012, Keller & Mrsic-Flogel 2018). Thin on recent feedback-circuit anatomy and on counter-evidence — this is showing up as a critique gap.',
|
| 55 |
bullets: [
|
|
|
|
| 57 |
'<strong>Gap:</strong> sparse on L5b feedback anatomy (Harris/Shepherd lab)',
|
| 58 |
'<strong>Gap:</strong> no engagement with anti-PC critiques (e.g. Heeger 2017)',
|
| 59 |
],
|
| 60 |
+
sources: 47,
|
| 61 |
+
updatedMinutesAgo: 22,
|
| 62 |
+
quotes: [
|
| 63 |
+
'"Have you read Heeger 2017? It changes a lot." — lit-review aide',
|
| 64 |
+
'"L5b feedback anatomy is your weak spot." — devil\'s advocate',
|
| 65 |
+
],
|
| 66 |
},
|
| 67 |
{
|
| 68 |
id: 'i-questions',
|
| 69 |
title: 'Open research questions',
|
| 70 |
icon: 'sparkles',
|
| 71 |
+
category: 'theory',
|
| 72 |
confidence: 58,
|
| 73 |
summary: 'Three live threads. Question 1 (does L2/3 spiking encode prediction error?) is the dissertation core. Q2 and Q3 are scoped to specific aims.',
|
| 74 |
bullets: [
|
|
|
|
| 76 |
'<strong>Q2:</strong> How does this depend on context length (1 vs. 4 vs. 16 trials)?',
|
| 77 |
'<strong>Q3:</strong> Is the signal sharpened by feedback from V2/RSC?',
|
| 78 |
],
|
| 79 |
+
sources: 6,
|
| 80 |
+
updatedMinutesAgo: 38,
|
| 81 |
+
quotes: [
|
| 82 |
+
'"Q1 is what the whole dissertation rests on." — methodologist',
|
| 83 |
+
'"Q3 is exciting but probably out of scope for the thesis." — Reineke',
|
| 84 |
+
],
|
| 85 |
},
|
| 86 |
{
|
| 87 |
id: 'i-next',
|
| 88 |
title: 'Next steps',
|
| 89 |
icon: 'arrow',
|
| 90 |
+
category: 'action',
|
| 91 |
confidence: 82,
|
| 92 |
summary: 'Concrete, near-term actions. Two of these have been on the list for 3+ weeks.',
|
| 93 |
bullets: [
|
|
|
|
| 96 |
'Read Heeger 2017 + Aitchison & Lengyel 2017',
|
| 97 |
'Schedule pilot with M5 (May 18)',
|
| 98 |
],
|
| 99 |
+
sources: 5,
|
| 100 |
+
updatedMinutesAgo: 8,
|
| 101 |
+
quotes: [
|
| 102 |
+
'"Aim 2 draft has to land by May 22 or quals slip." — Reineke',
|
| 103 |
+
'"M3 review keeps getting punted." — last 3 advisor meetings',
|
| 104 |
+
],
|
| 105 |
},
|
| 106 |
{
|
| 107 |
id: 'i-blockers',
|
| 108 |
title: 'Blockers & risks',
|
| 109 |
icon: 'alert',
|
| 110 |
+
category: 'risk',
|
| 111 |
confidence: 70,
|
| 112 |
summary: 'One technical, one structural. The structural one is more important and you are deferring it.',
|
| 113 |
bullets: [
|
| 114 |
'<strong>Technical:</strong> Drift on M3 — may lose 1 animal of data',
|
| 115 |
'<strong>Structural:</strong> No clear predictive-coding theory commitment yet → hard to define what counts as evidence',
|
| 116 |
],
|
| 117 |
+
sources: 4,
|
| 118 |
+
updatedMinutesAgo: 18,
|
| 119 |
+
quotes: [
|
| 120 |
+
'"If M3 is unusable you\'re at n=5 — still publishable but tight." — methodologist',
|
| 121 |
+
'"Without a theory commitment you can\'t falsify anything." — devil\'s advocate',
|
| 122 |
+
],
|
| 123 |
},
|
| 124 |
];
|
| 125 |
|
|
|
|
| 145 |
{ type: 'calendar', name: 'Calendar', desc: 'Month grid with deadlines and writing days', icon: 'calendar', cat: 'project', defaultSize: 'M', enhanced: true },
|
| 146 |
{ type: 'activity', name: 'Activity Feed', desc: 'Chronological log of edits across widgets', icon: 'graph', cat: 'project', defaultSize: 'M', enhanced: true },
|
| 147 |
{ type: 'documenter', name: 'Daily Documenter', desc: 'Date-stamped journal · AI weekly summary (LLM stub)', icon: 'pencil', cat: 'project', defaultSize: 'M', enhanced: true },
|
| 148 |
+
{ type: 'phd-journey', name: 'PhD Journey', desc: 'Standard PhD milestones — courses → defense → ProQuest', icon: 'flag', cat: 'project', defaultSize: 'M', enhanced: true },
|
| 149 |
+
{ type: 'phd-resources', name: 'PhD Resources', desc: 'Curated open-source tools, handbooks, and community links', icon: 'star', cat: 'research', defaultSize: 'M', enhanced: true },
|
| 150 |
|
| 151 |
{ type: 'mood', name: 'Mood / Burnout Check-in', desc: 'Daily slider, trend graph', icon: 'smile', cat: 'wellness', defaultSize: 'S', stub: true },
|
| 152 |
{ type: 'sleep', name: 'Sleep & Energy', desc: 'Correlate with productive days', icon: 'heart', cat: 'wellness', defaultSize: 'S', stub: true },
|
|
|
|
| 289 |
calendar: { viewMonth: new Date().toISOString().slice(0, 7) },
|
| 290 |
activity: {},
|
| 291 |
documenter: { entries: [], lastSummary: null },
|
| 292 |
+
'phd-journey': {
|
| 293 |
+
// Status per milestone: 'open' | 'in-progress' | 'completed'
|
| 294 |
+
// Milestones come from the standard PhD journey (course selection → ProQuest)
|
| 295 |
+
statuses: {},
|
| 296 |
+
notes: {},
|
| 297 |
+
},
|
| 298 |
+
'phd-resources': {
|
| 299 |
+
customLinks: [],
|
| 300 |
+
},
|
| 301 |
};
|
| 302 |
|
phd-advisor-frontend/src/components/canvas/platform.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Tiny helper so keyboard shortcut hints adapt to the user's OS.
|
| 2 |
+
// Detect once at module load — we don't expect platform to change mid-session.
|
| 3 |
+
const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
| 4 |
+
export const MOD = isMac ? '⌘' : 'Ctrl';
|
| 5 |
+
export const MOD_LABEL = isMac ? 'Cmd' : 'Ctrl';
|
phd-advisor-frontend/src/pages/CanvasPage.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
| 14 |
HabitsWidget, GoalsWidget, MeetingsWidget,
|
| 15 |
OutlineWidget, HighlightsWidget, LatexWidget,
|
| 16 |
CalendarWidget, DocumenterWidget, ActivityWidget,
|
|
|
|
| 17 |
StubWidget,
|
| 18 |
} from '../components/canvas/CanvasWidgets';
|
| 19 |
import {
|
|
@@ -28,6 +29,7 @@ import {
|
|
| 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';
|
|
@@ -57,6 +59,8 @@ function renderWidget(type, state, setState, openModal, allStates) {
|
|
| 57 |
case 'calendar': return <CalendarWidget {...props}/>;
|
| 58 |
case 'documenter': return <DocumenterWidget {...props}/>;
|
| 59 |
case 'activity': return <ActivityWidget {...props}/>;
|
|
|
|
|
|
|
| 60 |
default: {
|
| 61 |
const meta = WIDGET_CATALOG.find(w => w.type === type);
|
| 62 |
return <StubWidget meta={meta}/>;
|
|
@@ -100,15 +104,108 @@ function CanvasWidget({ widget, isDragging, isDragOver, onDragStart, onDragOver,
|
|
| 100 |
);
|
| 101 |
}
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
const togglePin = (id) => {
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
};
|
| 110 |
|
| 111 |
-
// Strip HTML, take first 80 chars for the kanban card title.
|
| 112 |
const insightToTaskTitle = (ins) => {
|
| 113 |
const plain = (ins.bullets[0] || ins.summary || ins.title).replace(/<[^>]+>/g, '');
|
| 114 |
return plain.length > 80 ? plain.slice(0, 77) + '…' : plain;
|
|
@@ -128,54 +225,404 @@ function InsightsView({ widgetStates, setWidgetStates }) {
|
|
| 128 |
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg: 'Sent to Kanban (To Do)', kind: 'success' } }));
|
| 129 |
};
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
return (
|
| 132 |
<>
|
| 133 |
<div className="page-header">
|
| 134 |
<div>
|
| 135 |
<h1 className="page-title">Insights</h1>
|
| 136 |
-
<div className="page-sub">AI-synthesized from your research conversations
|
| 137 |
</div>
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
| 142 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
</div>
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
<button
|
| 168 |
-
|
| 169 |
-
|
|
|
|
| 170 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
</div>
|
| 173 |
-
)
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
</>
|
| 176 |
);
|
| 177 |
}
|
| 178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
function PresetPicker({ onPick }) {
|
| 180 |
return (
|
| 181 |
<div className="canvas-presets">
|
|
@@ -432,6 +879,13 @@ const CanvasPage = ({ user, authToken, onNavigateToHome, onNavigateToChat, onSig
|
|
| 432 |
openModal('global-search', { states: widgetStates });
|
| 433 |
}, [openModal, widgetStates]);
|
| 434 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
// Esc closes modal, ⌘K opens command palette, ⌘/ opens global content search,
|
| 436 |
// ? opens the welcome tour for help (matches the icon in the topbar).
|
| 437 |
useEffect(() => {
|
|
@@ -484,6 +938,26 @@ const CanvasPage = ({ user, authToken, onNavigateToHome, onNavigateToChat, onSig
|
|
| 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 {
|
|
@@ -538,6 +1012,7 @@ const CanvasPage = ({ user, authToken, onNavigateToHome, onNavigateToChat, onSig
|
|
| 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">
|
|
@@ -551,15 +1026,15 @@ const CanvasPage = ({ user, authToken, onNavigateToHome, onNavigateToChat, onSig
|
|
| 551 |
<button className="icon-btn" onClick={() => setTourForceShow(n => n + 1)} title="Show tour">
|
| 552 |
<HelpCircle size={18}/>
|
| 553 |
</button>
|
| 554 |
-
<button className="icon-btn" onClick={openGlobalSearch} title=
|
| 555 |
<Icon name="search" size={16}/>
|
| 556 |
</button>
|
| 557 |
-
<button className="icon-btn" onClick={openCommandPalette} title=
|
| 558 |
<Icon name="zap" size={16}/>
|
| 559 |
</button>
|
| 560 |
</AppHeader>
|
| 561 |
<div className="canvas-content">
|
| 562 |
-
{view === 'insights' && <InsightsView widgetStates={widgetStates} setWidgetStates={setWidgetStates}/>}
|
| 563 |
{view === 'workspace' && <WorkspaceView openModal={openModal} layout={layout} setLayout={setLayout} widgetStates={widgetStates} setWidgetStates={setWidgetStates}/>}
|
| 564 |
{view === 'deliverables' && <DeliverablesView allStates={widgetStates}/>}
|
| 565 |
</div>
|
|
@@ -587,8 +1062,8 @@ function ShortcutHint() {
|
|
| 587 |
onMouseEnter={() => setVisible(true)}
|
| 588 |
onMouseLeave={() => setVisible(false)}
|
| 589 |
>
|
| 590 |
-
<span><kbd>
|
| 591 |
-
<span><kbd>
|
| 592 |
<span><kbd>?</kbd> help</span>
|
| 593 |
</div>
|
| 594 |
);
|
|
|
|
| 14 |
HabitsWidget, GoalsWidget, MeetingsWidget,
|
| 15 |
OutlineWidget, HighlightsWidget, LatexWidget,
|
| 16 |
CalendarWidget, DocumenterWidget, ActivityWidget,
|
| 17 |
+
PhdJourneyWidget, PhdResourcesWidget,
|
| 18 |
StubWidget,
|
| 19 |
} from '../components/canvas/CanvasWidgets';
|
| 20 |
import {
|
|
|
|
| 29 |
} from '../components/canvas/CanvasModals';
|
| 30 |
import CanvasWelcomeTour from '../components/canvas/CanvasWelcomeTour';
|
| 31 |
import DeliverablesView, { TEMPLATES as DELIVERABLE_TEMPLATES } from '../components/canvas/CanvasDeliverables';
|
| 32 |
+
import { MOD } from '../components/canvas/platform';
|
| 33 |
import '../styles/CanvasPage.css';
|
| 34 |
|
| 35 |
const LAYOUT_KEY = 'canvas-layout-v2';
|
|
|
|
| 59 |
case 'calendar': return <CalendarWidget {...props}/>;
|
| 60 |
case 'documenter': return <DocumenterWidget {...props}/>;
|
| 61 |
case 'activity': return <ActivityWidget {...props}/>;
|
| 62 |
+
case 'phd-journey': return <PhdJourneyWidget {...props}/>;
|
| 63 |
+
case 'phd-resources': return <PhdResourcesWidget {...props}/>;
|
| 64 |
default: {
|
| 65 |
const meta = WIDGET_CATALOG.find(w => w.type === type);
|
| 66 |
return <StubWidget meta={meta}/>;
|
|
|
|
| 104 |
);
|
| 105 |
}
|
| 106 |
|
| 107 |
+
// ============================================================================
|
| 108 |
+
// Insights view — AI-synthesized highlights with stats bar, filters, and
|
| 109 |
+
// click-to-expand source quotes.
|
| 110 |
+
// ============================================================================
|
| 111 |
+
const INSIGHT_CATEGORIES = [
|
| 112 |
+
{ id: 'all', label: 'All' },
|
| 113 |
+
{ id: 'open', label: 'Open' },
|
| 114 |
+
{ id: 'in-progress', label: 'In progress' },
|
| 115 |
+
{ id: 'completed', label: 'Completed' },
|
| 116 |
+
{ id: 'abandoned', label: 'Abandoned' },
|
| 117 |
+
{ id: 'pinned', label: 'Pinned' },
|
| 118 |
+
{ id: 'high', label: 'High confidence' },
|
| 119 |
+
{ id: 'progress', label: 'Progress' },
|
| 120 |
+
{ id: 'theory', label: 'Theory' },
|
| 121 |
+
{ id: 'literature', label: 'Literature' },
|
| 122 |
+
{ id: 'action', label: 'Actions' },
|
| 123 |
+
{ id: 'risk', label: 'Risks' },
|
| 124 |
+
];
|
| 125 |
+
const CATEGORY_TINT = {
|
| 126 |
+
progress: 'rgba(16, 185, 129, 0.12)',
|
| 127 |
+
theory: 'rgba(99, 102, 241, 0.12)',
|
| 128 |
+
literature: 'rgba(245, 158, 11, 0.12)',
|
| 129 |
+
action: 'rgba(59, 130, 246, 0.12)',
|
| 130 |
+
risk: 'rgba(220, 38, 38, 0.12)',
|
| 131 |
+
};
|
| 132 |
+
const CATEGORY_FG = {
|
| 133 |
+
progress: '#10B981',
|
| 134 |
+
theory: '#818CF8',
|
| 135 |
+
literature: '#F59E0B',
|
| 136 |
+
action: '#3B82F6',
|
| 137 |
+
risk: '#DC2626',
|
| 138 |
+
};
|
| 139 |
+
const confidenceTier = (c) => c >= 75 ? 'high' : c >= 60 ? 'med' : 'low';
|
| 140 |
+
|
| 141 |
+
// Task statuses live on each individual bullet within an insight, not on the
|
| 142 |
+
// whole card — Daniel's feedback: "each card is a discrete task, not a set of tasks".
|
| 143 |
+
const TASK_STATUSES = [
|
| 144 |
+
{ id: 'open', label: 'Open', color: 'var(--canvas-text-3)', icon: 'sparkles' },
|
| 145 |
+
{ id: 'in-progress', label: 'In progress', color: '#3B82F6', icon: 'graph' },
|
| 146 |
+
{ id: 'completed', label: 'Completed', color: '#10B981', icon: 'check' },
|
| 147 |
+
{ id: 'abandoned', label: 'Abandoned', color: 'var(--canvas-text-4)', icon: 'x' },
|
| 148 |
+
];
|
| 149 |
+
const TASK_STATUS_KEY = 'canvas-task-status-v1';
|
| 150 |
+
const taskKey = (insId, idx) => `${insId}::${idx}`;
|
| 151 |
+
|
| 152 |
+
function InsightsView({ widgetStates, setWidgetStates, onNavigateToChat }) {
|
| 153 |
+
const [pinned, setPinned] = useState(() => new Set(INSIGHTS.filter(i => i.pinned).map(i => i.id)));
|
| 154 |
+
const [taskStatuses, setTaskStatuses] = useState(() => {
|
| 155 |
+
try { return JSON.parse(localStorage.getItem(TASK_STATUS_KEY) || '{}'); } catch { return {}; }
|
| 156 |
+
});
|
| 157 |
+
const [filter, setFilter] = useState('all');
|
| 158 |
+
const [sortBy, setSortBy] = useState('confidence');
|
| 159 |
+
const [expanded, setExpanded] = useState(new Set());
|
| 160 |
+
const [refreshing, setRefreshing] = useState(false);
|
| 161 |
+
const [openStatusMenu, setOpenStatusMenu] = useState(null);
|
| 162 |
+
// 'cards' = current cards-of-tasks layout, 'tasks' = flat task list per Daniel's
|
| 163 |
+
// "Sections in sidebar, Tasks in the main view" suggestion.
|
| 164 |
+
const [viewMode, setViewMode] = useState(() => localStorage.getItem('canvas-insights-view') || 'cards');
|
| 165 |
+
useEffect(() => { localStorage.setItem('canvas-insights-view', viewMode); }, [viewMode]);
|
| 166 |
+
|
| 167 |
+
useEffect(() => {
|
| 168 |
+
localStorage.setItem(TASK_STATUS_KEY, JSON.stringify(taskStatuses));
|
| 169 |
+
}, [taskStatuses]);
|
| 170 |
+
|
| 171 |
+
const taskStatusOf = (insId, idx) => taskStatuses[taskKey(insId, idx)] || 'open';
|
| 172 |
+
const setTaskStatus = (insId, idx, status) => {
|
| 173 |
+
setTaskStatuses(prev => ({ ...prev, [taskKey(insId, idx)]: status }));
|
| 174 |
+
const lbl = TASK_STATUSES.find(s => s.id === status)?.label || status;
|
| 175 |
+
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg: `Task marked ${lbl}`, kind: 'success' } }));
|
| 176 |
+
};
|
| 177 |
+
|
| 178 |
+
// Roll up to a card-level status: completed if all tasks done, abandoned if all abandoned,
|
| 179 |
+
// in-progress if any are in-progress, otherwise open.
|
| 180 |
+
const insightRollup = (ins) => {
|
| 181 |
+
const states = ins.bullets.map((_, idx) => taskStatusOf(ins.id, idx));
|
| 182 |
+
const total = states.length;
|
| 183 |
+
if (total === 0) return { state: 'open', done: 0, total: 0, pct: 0 };
|
| 184 |
+
const done = states.filter(s => s === 'completed').length;
|
| 185 |
+
const inProg = states.filter(s => s === 'in-progress').length;
|
| 186 |
+
const abandoned = states.filter(s => s === 'abandoned').length;
|
| 187 |
+
let state = 'open';
|
| 188 |
+
if (done === total) state = 'completed';
|
| 189 |
+
else if (abandoned === total) state = 'abandoned';
|
| 190 |
+
else if (inProg > 0 || done > 0) state = 'in-progress';
|
| 191 |
+
return { state, done, total, inProg, abandoned, pct: Math.round((done / total) * 100) };
|
| 192 |
+
};
|
| 193 |
+
|
| 194 |
const togglePin = (id) => {
|
| 195 |
+
setPinned(prev => {
|
| 196 |
+
const n = new Set(prev);
|
| 197 |
+
if (n.has(id)) n.delete(id); else n.add(id);
|
| 198 |
+
return n;
|
| 199 |
+
});
|
| 200 |
+
};
|
| 201 |
+
const toggleExpand = (id) => {
|
| 202 |
+
setExpanded(prev => {
|
| 203 |
+
const n = new Set(prev);
|
| 204 |
+
if (n.has(id)) n.delete(id); else n.add(id);
|
| 205 |
+
return n;
|
| 206 |
+
});
|
| 207 |
};
|
| 208 |
|
|
|
|
| 209 |
const insightToTaskTitle = (ins) => {
|
| 210 |
const plain = (ins.bullets[0] || ins.summary || ins.title).replace(/<[^>]+>/g, '');
|
| 211 |
return plain.length > 80 ? plain.slice(0, 77) + '…' : plain;
|
|
|
|
| 225 |
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg: 'Sent to Kanban (To Do)', kind: 'success' } }));
|
| 226 |
};
|
| 227 |
|
| 228 |
+
// TODO(LLM): real refresh hits the orchestrator and re-synthesizes insights.
|
| 229 |
+
const handleRefresh = () => {
|
| 230 |
+
setRefreshing(true);
|
| 231 |
+
setTimeout(() => setRefreshing(false), 900);
|
| 232 |
+
window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg: 'Refreshing insights… (stub)', kind: 'success' } }));
|
| 233 |
+
};
|
| 234 |
+
|
| 235 |
+
// "Ask follow-up": stash a draft prompt + insight context that the chat page
|
| 236 |
+
// can pick up on next navigation. Backend hookup TODO: include source-conversation IDs.
|
| 237 |
+
const askFollowUp = (ins) => {
|
| 238 |
+
const plain = (ins.bullets[0] || ins.summary || '').replace(/<[^>]+>/g, '');
|
| 239 |
+
const prompt = `Follow up on the insight "${ins.title}": ${plain}`;
|
| 240 |
+
try {
|
| 241 |
+
localStorage.setItem('canvas-chat-handoff', JSON.stringify({
|
| 242 |
+
at: Date.now(),
|
| 243 |
+
prompt,
|
| 244 |
+
insightId: ins.id,
|
| 245 |
+
insightTitle: ins.title,
|
| 246 |
+
}));
|
| 247 |
+
} catch { /* ignore */ }
|
| 248 |
+
window.dispatchEvent(new CustomEvent('canvas-toast', {
|
| 249 |
+
detail: { msg: `Follow-up drafted: "${ins.title}" — opening chat`, kind: 'success' },
|
| 250 |
+
}));
|
| 251 |
+
// Use the existing navigation prop if present; falls back to the global event.
|
| 252 |
+
if (onNavigateToChat) onNavigateToChat();
|
| 253 |
+
};
|
| 254 |
+
|
| 255 |
+
const filtered = useMemo(() => {
|
| 256 |
+
let r = INSIGHTS;
|
| 257 |
+
if (filter === 'pinned') r = r.filter(i => pinned.has(i.id));
|
| 258 |
+
else if (filter === 'high') r = r.filter(i => i.confidence >= 75);
|
| 259 |
+
else if (['open', 'in-progress', 'completed', 'abandoned'].includes(filter)) r = r.filter(i => insightRollup(i).state === filter);
|
| 260 |
+
else if (filter !== 'all') r = r.filter(i => i.category === filter);
|
| 261 |
+
if (sortBy === 'confidence') r = [...r].sort((a, b) => b.confidence - a.confidence);
|
| 262 |
+
if (sortBy === 'recent') r = [...r].sort((a, b) => (a.updatedMinutesAgo || 0) - (b.updatedMinutesAgo || 0));
|
| 263 |
+
if (sortBy === 'progress') r = [...r].sort((a, b) => insightRollup(b).pct - insightRollup(a).pct);
|
| 264 |
+
return r;
|
| 265 |
+
}, [filter, sortBy, pinned, taskStatuses]); // eslint-disable-line react-hooks/exhaustive-deps
|
| 266 |
+
|
| 267 |
+
// Aggregate stats over individual TASKS, not insights (Daniel's framing)
|
| 268 |
+
const stats = useMemo(() => {
|
| 269 |
+
const allTasks = INSIGHTS.flatMap(i => i.bullets.map((_, idx) => taskStatusOf(i.id, idx)));
|
| 270 |
+
const taskTotal = allTasks.length;
|
| 271 |
+
const completed = allTasks.filter(s => s === 'completed').length;
|
| 272 |
+
const inProgress = allTasks.filter(s => s === 'in-progress').length;
|
| 273 |
+
const abandoned = allTasks.filter(s => s === 'abandoned').length;
|
| 274 |
+
const open = taskTotal - completed - inProgress - abandoned;
|
| 275 |
+
const totalSources = INSIGHTS.reduce((s, i) => s + (i.sources || 0), 0);
|
| 276 |
+
const avgConf = Math.round(INSIGHTS.reduce((s, i) => s + i.confidence, 0) / INSIGHTS.length);
|
| 277 |
+
return {
|
| 278 |
+
sections: INSIGHTS.length, taskTotal, completed, inProgress, abandoned, open,
|
| 279 |
+
totalSources, avgConf, pinnedCount: pinned.size,
|
| 280 |
+
};
|
| 281 |
+
}, [pinned, taskStatuses]); // eslint-disable-line react-hooks/exhaustive-deps
|
| 282 |
+
|
| 283 |
+
const lastUpdated = Math.min(...INSIGHTS.map(i => i.updatedMinutesAgo || 0));
|
| 284 |
+
|
| 285 |
+
// Empty state — defensive (current data is hardcoded but a real backend could send [])
|
| 286 |
+
if (INSIGHTS.length === 0) {
|
| 287 |
+
return (
|
| 288 |
+
<>
|
| 289 |
+
<div className="page-header">
|
| 290 |
+
<div>
|
| 291 |
+
<h1 className="page-title">Insights</h1>
|
| 292 |
+
<div className="page-sub">AI-synthesized from your research conversations.</div>
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
<div className="empty-cell">
|
| 296 |
+
<Icon name="sparkles" size={32} style={{ color: 'var(--canvas-text-4)' }}/>
|
| 297 |
+
<div style={{ fontSize: 14, color: 'var(--canvas-text-2)', fontWeight: 500 }}>No insights yet</div>
|
| 298 |
+
<div>Have a conversation with your advisors and insights will appear here.</div>
|
| 299 |
+
</div>
|
| 300 |
+
</>
|
| 301 |
+
);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
return (
|
| 305 |
<>
|
| 306 |
<div className="page-header">
|
| 307 |
<div>
|
| 308 |
<h1 className="page-title">Insights</h1>
|
| 309 |
+
<div className="page-sub">AI-synthesized from your research conversations.</div>
|
| 310 |
</div>
|
| 311 |
+
</div>
|
| 312 |
+
|
| 313 |
+
{/* Stats bar */}
|
| 314 |
+
<div className="insights-stats">
|
| 315 |
+
<div className="insights-stat">
|
| 316 |
+
<span className="insights-stat-value">{stats.completed}/{stats.taskTotal}</span>
|
| 317 |
+
<span className="insights-stat-label">tasks done</span>
|
| 318 |
</div>
|
| 319 |
+
<div className="insights-stat insights-stat-progress">
|
| 320 |
+
<div className="insights-progress-bar">
|
| 321 |
+
<i className="ip-completed" style={{ width: `${(stats.completed / Math.max(1, stats.taskTotal)) * 100}%` }}/>
|
| 322 |
+
<i className="ip-inprogress" style={{ width: `${(stats.inProgress / Math.max(1, stats.taskTotal)) * 100}%` }}/>
|
| 323 |
+
<i className="ip-abandoned" style={{ width: `${(stats.abandoned / Math.max(1, stats.taskTotal)) * 100}%` }}/>
|
| 324 |
+
</div>
|
| 325 |
+
<span className="insights-stat-label">
|
| 326 |
+
{stats.completed} done · {stats.inProgress} in progress · {stats.open} open
|
| 327 |
+
{stats.abandoned > 0 && ` · ${stats.abandoned} abandoned`}
|
| 328 |
+
</span>
|
| 329 |
+
</div>
|
| 330 |
+
<div className="insights-stat">
|
| 331 |
+
<span className="insights-stat-value">{stats.sections}</span>
|
| 332 |
+
<span className="insights-stat-label">sections</span>
|
| 333 |
+
</div>
|
| 334 |
+
<div className="insights-stat">
|
| 335 |
+
<span className="insights-stat-value">{stats.avgConf}%</span>
|
| 336 |
+
<span className="insights-stat-label">avg confidence</span>
|
| 337 |
+
</div>
|
| 338 |
+
<span style={{ flex: 1 }}/>
|
| 339 |
+
<span className="insights-stat-update">
|
| 340 |
+
<span className="dot"/>
|
| 341 |
+
updated {lastUpdated} min ago
|
| 342 |
+
</span>
|
| 343 |
+
<button className="btn btn-ghost" onClick={handleRefresh} disabled={refreshing}>
|
| 344 |
+
{refreshing ? <div className="spinner"/> : <Icon name="refresh" size={13}/>}
|
| 345 |
+
Refresh
|
| 346 |
+
</button>
|
| 347 |
</div>
|
| 348 |
+
|
| 349 |
+
{/* View toggle: Cards (sections with their tasks) vs Tasks (flat list) */}
|
| 350 |
+
<div className="insights-view-toggle">
|
| 351 |
+
<button className={viewMode === 'cards' ? 'active' : ''} onClick={() => setViewMode('cards')}>
|
| 352 |
+
<Icon name="layout" size={12}/>Cards
|
| 353 |
+
</button>
|
| 354 |
+
<button className={viewMode === 'tasks' ? 'active' : ''} onClick={() => setViewMode('tasks')}>
|
| 355 |
+
<Icon name="task" size={12}/>Tasks
|
| 356 |
+
</button>
|
| 357 |
+
</div>
|
| 358 |
+
|
| 359 |
+
{/* Filter + sort */}
|
| 360 |
+
<div className="insights-filters">
|
| 361 |
+
<div className="palette-cats" style={{ marginBottom: 0 }}>
|
| 362 |
+
{INSIGHT_CATEGORIES.map(c => {
|
| 363 |
+
const count =
|
| 364 |
+
c.id === 'all' ? INSIGHTS.length :
|
| 365 |
+
c.id === 'pinned' ? pinned.size :
|
| 366 |
+
c.id === 'high' ? INSIGHTS.filter(i => i.confidence >= 75).length :
|
| 367 |
+
['open', 'in-progress', 'completed', 'abandoned'].includes(c.id) ? INSIGHTS.filter(i => insightRollup(i).state === c.id).length :
|
| 368 |
+
INSIGHTS.filter(i => i.category === c.id).length;
|
| 369 |
+
if (count === 0 && c.id !== 'all') return null;
|
| 370 |
+
return (
|
| 371 |
+
<button key={c.id}
|
| 372 |
+
className={`palette-cat ${filter === c.id ? 'active' : ''}`}
|
| 373 |
+
onClick={() => setFilter(c.id)}>
|
| 374 |
+
{c.label}<span style={{ marginLeft: 6, opacity: 0.6 }}>{count}</span>
|
| 375 |
</button>
|
| 376 |
+
);
|
| 377 |
+
})}
|
| 378 |
+
</div>
|
| 379 |
+
<select className="select" style={{ width: 'auto', padding: '4px 8px', fontSize: 11, fontFamily: 'var(--canvas-mono)' }}
|
| 380 |
+
value={sortBy} onChange={e => setSortBy(e.target.value)}>
|
| 381 |
+
<option value="confidence">↓ confidence</option>
|
| 382 |
+
<option value="recent">↓ recent</option>
|
| 383 |
+
<option value="progress">↓ progress</option>
|
| 384 |
+
</select>
|
| 385 |
+
</div>
|
| 386 |
+
|
| 387 |
+
{/* Pinned strip — only when there are pins and we're not already filtering by pinned */}
|
| 388 |
+
{pinned.size > 0 && filter !== 'pinned' && (
|
| 389 |
+
<div className="insights-pinned-strip">
|
| 390 |
+
<span style={{ fontSize: 11, color: 'var(--canvas-text-4)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>
|
| 391 |
+
Pinned
|
| 392 |
+
</span>
|
| 393 |
+
{INSIGHTS.filter(i => pinned.has(i.id)).map(ins => (
|
| 394 |
+
<button key={ins.id} className="insights-pinned-pill" onClick={() => {
|
| 395 |
+
const el = document.getElementById(`insight-${ins.id}`);
|
| 396 |
+
if (el) {
|
| 397 |
+
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
| 398 |
+
el.style.boxShadow = '0 0 0 2px var(--canvas-accent), 0 0 24px var(--canvas-accent-glow)';
|
| 399 |
+
setTimeout(() => { el.style.boxShadow = ''; }, 1400);
|
| 400 |
+
}
|
| 401 |
+
}}>
|
| 402 |
+
<Icon name={ins.icon} size={11}/>{ins.title}
|
| 403 |
+
</button>
|
| 404 |
+
))}
|
| 405 |
+
</div>
|
| 406 |
+
)}
|
| 407 |
+
|
| 408 |
+
{/* TASKS view — flat list of every bullet across insights */}
|
| 409 |
+
{viewMode === 'tasks' && (() => {
|
| 410 |
+
const allTasks = filtered.flatMap(ins => ins.bullets.map((b, idx) => ({
|
| 411 |
+
ins, idx, text: b, status: taskStatusOf(ins.id, idx),
|
| 412 |
+
})));
|
| 413 |
+
const statusOrder = { open: 0, 'in-progress': 1, completed: 2, abandoned: 3 };
|
| 414 |
+
const sorted = [...allTasks].sort((a, b) => statusOrder[a.status] - statusOrder[b.status]);
|
| 415 |
+
if (sorted.length === 0) {
|
| 416 |
+
return (
|
| 417 |
+
<div className="empty-cell">
|
| 418 |
+
<Icon name="task" size={28} style={{ color: 'var(--canvas-text-4)' }}/>
|
| 419 |
+
<div style={{ fontSize: 14, color: 'var(--canvas-text-2)', fontWeight: 500 }}>No tasks match this filter</div>
|
| 420 |
+
<button className="btn btn-ghost" onClick={() => setFilter('all')}>Show all</button>
|
| 421 |
</div>
|
| 422 |
+
);
|
| 423 |
+
}
|
| 424 |
+
return (
|
| 425 |
+
<div className="insights-tasks-list">
|
| 426 |
+
{sorted.map(({ ins, idx, text, status }) => {
|
| 427 |
+
const meta = TASK_STATUSES.find(s => s.id === status);
|
| 428 |
+
const menuKey = `tv::${ins.id}::${idx}`;
|
| 429 |
+
const menuOpen = openStatusMenu === menuKey;
|
| 430 |
+
return (
|
| 431 |
+
<div key={menuKey} className={`insights-task-row task-${status}`}>
|
| 432 |
+
<button
|
| 433 |
+
className="insight-task-check"
|
| 434 |
+
style={{ color: meta.color, borderColor: meta.color + '60' }}
|
| 435 |
+
onClick={() => setOpenStatusMenu(menuOpen ? null : menuKey)}
|
| 436 |
+
title={`Status: ${meta.label}`}
|
| 437 |
+
>
|
| 438 |
+
{status === 'completed' && <Icon name="check" size={11}/>}
|
| 439 |
+
{status === 'in-progress' && <span className="task-check-dot"/>}
|
| 440 |
+
{status === 'abandoned' && <Icon name="x" size={10}/>}
|
| 441 |
+
</button>
|
| 442 |
+
<div className="insights-task-body">
|
| 443 |
+
<button
|
| 444 |
+
className="insights-task-section"
|
| 445 |
+
onClick={() => {
|
| 446 |
+
setViewMode('cards');
|
| 447 |
+
setTimeout(() => {
|
| 448 |
+
const el = document.getElementById(`insight-${ins.id}`);
|
| 449 |
+
if (el) el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
| 450 |
+
}, 60);
|
| 451 |
+
}}
|
| 452 |
+
title="Jump to this section"
|
| 453 |
+
>
|
| 454 |
+
<Icon name={ins.icon} size={10}/>{ins.title}
|
| 455 |
+
</button>
|
| 456 |
+
<span className="insights-task-text" dangerouslySetInnerHTML={{ __html: text }}/>
|
| 457 |
+
</div>
|
| 458 |
+
<button className="chip" onClick={() => sendToKanban(ins)} title="Send this section's open tasks to Kanban">
|
| 459 |
+
<Icon name="task" size={11}/>Kanban
|
| 460 |
+
</button>
|
| 461 |
+
{menuOpen && (
|
| 462 |
+
<div className="insight-status-menu" onMouseLeave={() => setOpenStatusMenu(null)}>
|
| 463 |
+
{TASK_STATUSES.map(s => (
|
| 464 |
+
<button key={s.id}
|
| 465 |
+
className={status === s.id ? 'active' : ''}
|
| 466 |
+
onClick={() => { setTaskStatus(ins.id, idx, s.id); setOpenStatusMenu(null); }}>
|
| 467 |
+
<Icon name={s.icon} size={11} style={{ color: s.color }}/>
|
| 468 |
+
{s.label}
|
| 469 |
+
</button>
|
| 470 |
+
))}
|
| 471 |
+
</div>
|
| 472 |
+
)}
|
| 473 |
+
</div>
|
| 474 |
+
);
|
| 475 |
+
})}
|
| 476 |
</div>
|
| 477 |
+
);
|
| 478 |
+
})()}
|
| 479 |
+
|
| 480 |
+
{/* CARDS view (default) */}
|
| 481 |
+
{viewMode === 'cards' && (filtered.length === 0 ? (
|
| 482 |
+
<div className="empty-cell">
|
| 483 |
+
<Icon name="search" size={28} style={{ color: 'var(--canvas-text-4)' }}/>
|
| 484 |
+
<div style={{ fontSize: 14, color: 'var(--canvas-text-2)', fontWeight: 500 }}>No insights match this filter</div>
|
| 485 |
+
<button className="btn btn-ghost" onClick={() => setFilter('all')}>Show all</button>
|
| 486 |
+
</div>
|
| 487 |
+
) : (
|
| 488 |
+
<div className="insight-grid">
|
| 489 |
+
{filtered.map(ins => {
|
| 490 |
+
const isExpanded = expanded.has(ins.id);
|
| 491 |
+
const isPinned = pinned.has(ins.id);
|
| 492 |
+
const tier = confidenceTier(ins.confidence);
|
| 493 |
+
const tint = CATEGORY_TINT[ins.category] || 'var(--canvas-surface-2)';
|
| 494 |
+
const fg = CATEGORY_FG[ins.category] || 'var(--canvas-accent)';
|
| 495 |
+
const rollup = insightRollup(ins);
|
| 496 |
+
return (
|
| 497 |
+
<div
|
| 498 |
+
key={ins.id}
|
| 499 |
+
id={`insight-${ins.id}`}
|
| 500 |
+
className={`insight ${isPinned ? 'is-pinned' : ''} tier-${tier} status-${rollup.state}`}
|
| 501 |
+
>
|
| 502 |
+
<div className="insight-head">
|
| 503 |
+
<div className="insight-icon" style={{ background: tint, color: fg }}>
|
| 504 |
+
<Icon name={ins.icon} size={16}/>
|
| 505 |
+
</div>
|
| 506 |
+
<div className="insight-title">{ins.title}</div>
|
| 507 |
+
<ConfidenceRing value={ins.confidence}/>
|
| 508 |
+
</div>
|
| 509 |
+
|
| 510 |
+
{/* Per-card progress (rolls up the bullet tasks) */}
|
| 511 |
+
{rollup.total > 0 && (
|
| 512 |
+
<div className="insight-progress">
|
| 513 |
+
<div className="insight-progress-meta">
|
| 514 |
+
<span className="insight-progress-count">{rollup.done}/{rollup.total} tasks</span>
|
| 515 |
+
{rollup.state === 'completed' && <span className="insight-progress-badge done">✓ Resolved</span>}
|
| 516 |
+
{rollup.state === 'abandoned' && <span className="insight-progress-badge abandoned">Abandoned</span>}
|
| 517 |
+
{rollup.state === 'in-progress' && <span className="insight-progress-badge inprog">In progress</span>}
|
| 518 |
+
</div>
|
| 519 |
+
<div className="insight-progress-bar">
|
| 520 |
+
<i className="ip-completed" style={{ width: `${(rollup.done / rollup.total) * 100}%` }}/>
|
| 521 |
+
{rollup.inProg > 0 && <i className="ip-inprogress" style={{ width: `${(rollup.inProg / rollup.total) * 100}%` }}/>}
|
| 522 |
+
{rollup.abandoned > 0 && <i className="ip-abandoned" style={{ width: `${(rollup.abandoned / rollup.total) * 100}%` }}/>}
|
| 523 |
+
</div>
|
| 524 |
+
</div>
|
| 525 |
+
)}
|
| 526 |
+
|
| 527 |
+
<div className="insight-body">
|
| 528 |
+
<div>{ins.summary}</div>
|
| 529 |
+
<ul className="insight-tasks">
|
| 530 |
+
{ins.bullets.map((b, idx) => {
|
| 531 |
+
const status = taskStatusOf(ins.id, idx);
|
| 532 |
+
const meta = TASK_STATUSES.find(s => s.id === status);
|
| 533 |
+
const menuOpen = openStatusMenu === `${ins.id}::${idx}`;
|
| 534 |
+
return (
|
| 535 |
+
<li key={idx} className={`insight-task task-${status}`}>
|
| 536 |
+
<button
|
| 537 |
+
className="insight-task-check"
|
| 538 |
+
style={{ color: meta.color, borderColor: meta.color + '60' }}
|
| 539 |
+
onClick={() => setOpenStatusMenu(menuOpen ? null : `${ins.id}::${idx}`)}
|
| 540 |
+
title={`Status: ${meta.label}`}
|
| 541 |
+
>
|
| 542 |
+
{status === 'completed' && <Icon name="check" size={10}/>}
|
| 543 |
+
{status === 'in-progress' && <span className="task-check-dot"/>}
|
| 544 |
+
{status === 'abandoned' && <Icon name="x" size={9}/>}
|
| 545 |
+
</button>
|
| 546 |
+
<span className="insight-task-text" dangerouslySetInnerHTML={{ __html: b }}/>
|
| 547 |
+
{menuOpen && (
|
| 548 |
+
<div className="insight-status-menu" onMouseLeave={() => setOpenStatusMenu(null)}>
|
| 549 |
+
{TASK_STATUSES.map(s => (
|
| 550 |
+
<button key={s.id}
|
| 551 |
+
className={status === s.id ? 'active' : ''}
|
| 552 |
+
onClick={() => { setTaskStatus(ins.id, idx, s.id); setOpenStatusMenu(null); }}>
|
| 553 |
+
<Icon name={s.icon} size={11} style={{ color: s.color }}/>
|
| 554 |
+
{s.label}
|
| 555 |
+
</button>
|
| 556 |
+
))}
|
| 557 |
+
</div>
|
| 558 |
+
)}
|
| 559 |
+
</li>
|
| 560 |
+
);
|
| 561 |
+
})}
|
| 562 |
+
</ul>
|
| 563 |
+
</div>
|
| 564 |
+
|
| 565 |
+
{/* Detail panel — quotes from sources, only when expanded */}
|
| 566 |
+
{isExpanded && ins.quotes && (
|
| 567 |
+
<div className="insight-detail">
|
| 568 |
+
<div className="insight-detail-head">Source quotes · {ins.sources} {ins.sources === 1 ? 'source' : 'sources'}</div>
|
| 569 |
+
{ins.quotes.map((q, i) => (
|
| 570 |
+
<div key={i} className="insight-quote">{q}</div>
|
| 571 |
+
))}
|
| 572 |
+
</div>
|
| 573 |
+
)}
|
| 574 |
+
|
| 575 |
+
<div className="insight-foot">
|
| 576 |
+
<span className="insight-foot-meta">
|
| 577 |
+
<Icon name="message" size={10}/> {ins.sources}
|
| 578 |
+
<span className="insight-dot"/>
|
| 579 |
+
updated {ins.updatedMinutesAgo}m ago
|
| 580 |
+
</span>
|
| 581 |
+
</div>
|
| 582 |
+
|
| 583 |
+
<div className="insight-actions">
|
| 584 |
+
<button className="chip" onClick={() => askFollowUp(ins)} title="Open this insight in a new chat session">
|
| 585 |
+
<Icon name="message" size={11}/>Ask follow-up
|
| 586 |
+
</button>
|
| 587 |
+
<button className="chip" onClick={() => sendToKanban(ins)} title="Add all open tasks from this insight to your Kanban (To Do)">
|
| 588 |
+
<Icon name="task" size={11}/>Add to Kanban
|
| 589 |
+
</button>
|
| 590 |
+
<button className="chip" onClick={() => toggleExpand(ins.id)}>
|
| 591 |
+
<Icon name="expand" size={11}/>{isExpanded ? 'Collapse' : 'Source quotes'}
|
| 592 |
+
</button>
|
| 593 |
+
<button className={`chip ${isPinned ? 'pinned' : ''}`} onClick={() => togglePin(ins.id)}>
|
| 594 |
+
<Icon name="pin" size={11}/>{isPinned ? 'Pinned' : 'Pin'}
|
| 595 |
+
</button>
|
| 596 |
+
</div>
|
| 597 |
+
</div>
|
| 598 |
+
);
|
| 599 |
+
})}
|
| 600 |
+
</div>
|
| 601 |
+
))}
|
| 602 |
</>
|
| 603 |
);
|
| 604 |
}
|
| 605 |
|
| 606 |
+
// Small SVG ring used for the confidence indicator
|
| 607 |
+
function ConfidenceRing({ value }) {
|
| 608 |
+
const r = 14;
|
| 609 |
+
const c = 2 * Math.PI * r;
|
| 610 |
+
const dash = c * (1 - value / 100);
|
| 611 |
+
const tier = confidenceTier(value);
|
| 612 |
+
const color = tier === 'high' ? '#10B981' : tier === 'med' ? '#F59E0B' : '#DC2626';
|
| 613 |
+
return (
|
| 614 |
+
<div className={`confidence-ring tier-${tier}`} title={`${value}% confidence (${tier})`}>
|
| 615 |
+
<svg width="32" height="32" viewBox="0 0 32 32">
|
| 616 |
+
<circle cx="16" cy="16" r={r} fill="none" stroke="var(--canvas-surface-3)" strokeWidth="3"/>
|
| 617 |
+
<circle cx="16" cy="16" r={r} fill="none" stroke={color} strokeWidth="3"
|
| 618 |
+
strokeDasharray={c} strokeDashoffset={dash} strokeLinecap="round"
|
| 619 |
+
transform="rotate(-90 16 16)"/>
|
| 620 |
+
</svg>
|
| 621 |
+
<span style={{ color }}>{value}</span>
|
| 622 |
+
</div>
|
| 623 |
+
);
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
function PresetPicker({ onPick }) {
|
| 627 |
return (
|
| 628 |
<div className="canvas-presets">
|
|
|
|
| 879 |
openModal('global-search', { states: widgetStates });
|
| 880 |
}, [openModal, widgetStates]);
|
| 881 |
|
| 882 |
+
// Critic widgets dispatch `canvas-open-in-chat` when the user wants real LLM history.
|
| 883 |
+
useEffect(() => {
|
| 884 |
+
const handler = () => onNavigateToChat && onNavigateToChat();
|
| 885 |
+
window.addEventListener('canvas-open-in-chat', handler);
|
| 886 |
+
return () => window.removeEventListener('canvas-open-in-chat', handler);
|
| 887 |
+
}, [onNavigateToChat]);
|
| 888 |
+
|
| 889 |
// Esc closes modal, ⌘K opens command palette, ⌘/ opens global content search,
|
| 890 |
// ? opens the welcome tour for help (matches the icon in the topbar).
|
| 891 |
useEffect(() => {
|
|
|
|
| 938 |
return order.map(c => groups[c]).filter(Boolean);
|
| 939 |
}, [layout]);
|
| 940 |
|
| 941 |
+
// Insights: list of sections — Daniel's feedback said sidebar should show sections here
|
| 942 |
+
const insightSections = useMemo(() => {
|
| 943 |
+
let taskMap = {};
|
| 944 |
+
try { taskMap = JSON.parse(localStorage.getItem(TASK_STATUS_KEY) || '{}'); } catch { /* ignore */ }
|
| 945 |
+
return INSIGHTS.map(ins => {
|
| 946 |
+
const states = ins.bullets.map((_, idx) => taskMap[taskKey(ins.id, idx)] || 'open');
|
| 947 |
+
const done = states.filter(s => s === 'completed').length;
|
| 948 |
+
return {
|
| 949 |
+
id: ins.id,
|
| 950 |
+
name: ins.title,
|
| 951 |
+
icon: ins.icon,
|
| 952 |
+
category: ins.category,
|
| 953 |
+
confidence: ins.confidence,
|
| 954 |
+
taskCount: ins.bullets.length,
|
| 955 |
+
doneCount: done,
|
| 956 |
+
onClick: () => flashScrollTo(`#insight-${ins.id}`),
|
| 957 |
+
};
|
| 958 |
+
});
|
| 959 |
+
}, [view, layout, widgetStates]); // eslint-disable-line react-hooks/exhaustive-deps
|
| 960 |
+
|
| 961 |
// Deliverables: list of projects with sections + history actions
|
| 962 |
const deliverableProjects = useMemo(() => {
|
| 963 |
try {
|
|
|
|
| 1012 |
canvasSubview={view}
|
| 1013 |
widgetGroups={widgetGroups}
|
| 1014 |
deliverableProjects={deliverableProjects}
|
| 1015 |
+
insightSections={insightSections}
|
| 1016 |
/>
|
| 1017 |
<div className={`canvas-main-area ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
|
| 1018 |
<div className="canvas-app-shell">
|
|
|
|
| 1026 |
<button className="icon-btn" onClick={() => setTourForceShow(n => n + 1)} title="Show tour">
|
| 1027 |
<HelpCircle size={18}/>
|
| 1028 |
</button>
|
| 1029 |
+
<button className="icon-btn" onClick={openGlobalSearch} title={`Search canvas content (${MOD}+/)`}>
|
| 1030 |
<Icon name="search" size={16}/>
|
| 1031 |
</button>
|
| 1032 |
+
<button className="icon-btn" onClick={openCommandPalette} title={`Commands (${MOD}+K)`}>
|
| 1033 |
<Icon name="zap" size={16}/>
|
| 1034 |
</button>
|
| 1035 |
</AppHeader>
|
| 1036 |
<div className="canvas-content">
|
| 1037 |
+
{view === 'insights' && <InsightsView widgetStates={widgetStates} setWidgetStates={setWidgetStates} onNavigateToChat={onNavigateToChat}/>}
|
| 1038 |
{view === 'workspace' && <WorkspaceView openModal={openModal} layout={layout} setLayout={setLayout} widgetStates={widgetStates} setWidgetStates={setWidgetStates}/>}
|
| 1039 |
{view === 'deliverables' && <DeliverablesView allStates={widgetStates}/>}
|
| 1040 |
</div>
|
|
|
|
| 1062 |
onMouseEnter={() => setVisible(true)}
|
| 1063 |
onMouseLeave={() => setVisible(false)}
|
| 1064 |
>
|
| 1065 |
+
<span><kbd>{MOD}</kbd><kbd>K</kbd> commands</span>
|
| 1066 |
+
<span><kbd>{MOD}</kbd><kbd>/</kbd> search</span>
|
| 1067 |
<span><kbd>?</kbd> help</span>
|
| 1068 |
</div>
|
| 1069 |
);
|
phd-advisor-frontend/src/styles/CanvasPage.css
CHANGED
|
@@ -398,6 +398,483 @@
|
|
| 398 |
box-shadow: 0 0 8px var(--canvas-ok);
|
| 399 |
}
|
| 400 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
/* Insights */
|
| 402 |
.canvas-page-with-sidebar .insight-grid {
|
| 403 |
display: grid;
|
|
@@ -590,6 +1067,10 @@
|
|
| 590 |
background: rgba(16, 185, 129, 0.15);
|
| 591 |
color: #10B981;
|
| 592 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
.canvas-page-with-sidebar .widget-actions { display: flex; gap: 1px; opacity: 0; transition: opacity .12s; }
|
| 594 |
.canvas-page-with-sidebar .widget:hover .widget-actions { opacity: 1; }
|
| 595 |
.canvas-page-with-sidebar .widget-actions .icon-btn { width: 24px; height: 24px; }
|
|
@@ -2454,6 +2935,87 @@ body[data-canvas-theme="light"] .canvas-modal-backdrop .critic-meter .bar { back
|
|
| 2454 |
background: rgba(0, 0, 0, 0.04);
|
| 2455 |
}
|
| 2456 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2457 |
/* "Coming soon" stub widget — roadmap-preview card, not a dead end */
|
| 2458 |
.canvas-page-with-sidebar .widget-stub {
|
| 2459 |
flex: 1;
|
|
|
|
| 398 |
box-shadow: 0 0 8px var(--canvas-ok);
|
| 399 |
}
|
| 400 |
|
| 401 |
+
/* ----- Insights view ----- */
|
| 402 |
+
.canvas-page-with-sidebar .insights-stats {
|
| 403 |
+
display: flex;
|
| 404 |
+
gap: 18px;
|
| 405 |
+
align-items: center;
|
| 406 |
+
padding: 14px 18px;
|
| 407 |
+
background: var(--canvas-surface);
|
| 408 |
+
border: 1px solid var(--canvas-border);
|
| 409 |
+
border-radius: 10px;
|
| 410 |
+
margin-bottom: 14px;
|
| 411 |
+
flex-wrap: wrap;
|
| 412 |
+
}
|
| 413 |
+
.canvas-page-with-sidebar[data-canvas-theme="light"] .insights-stats {
|
| 414 |
+
background: #ffffff;
|
| 415 |
+
border-color: rgba(15, 15, 15, 0.06);
|
| 416 |
+
}
|
| 417 |
+
.canvas-page-with-sidebar .insights-stat {
|
| 418 |
+
display: flex;
|
| 419 |
+
flex-direction: column;
|
| 420 |
+
align-items: flex-start;
|
| 421 |
+
gap: 1px;
|
| 422 |
+
min-width: 70px;
|
| 423 |
+
}
|
| 424 |
+
.canvas-page-with-sidebar .insights-stat-value {
|
| 425 |
+
font-family: var(--canvas-mono);
|
| 426 |
+
font-size: 22px;
|
| 427 |
+
font-weight: 700;
|
| 428 |
+
color: var(--canvas-text);
|
| 429 |
+
letter-spacing: -0.02em;
|
| 430 |
+
line-height: 1;
|
| 431 |
+
}
|
| 432 |
+
.canvas-page-with-sidebar .insights-stat-label {
|
| 433 |
+
font-size: 11px;
|
| 434 |
+
color: var(--canvas-text-3);
|
| 435 |
+
text-transform: uppercase;
|
| 436 |
+
letter-spacing: 0.06em;
|
| 437 |
+
font-weight: 500;
|
| 438 |
+
}
|
| 439 |
+
.canvas-page-with-sidebar .insights-stat-update {
|
| 440 |
+
font-size: 11px;
|
| 441 |
+
color: var(--canvas-text-3);
|
| 442 |
+
font-family: var(--canvas-mono);
|
| 443 |
+
display: inline-flex;
|
| 444 |
+
align-items: center;
|
| 445 |
+
gap: 6px;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.canvas-page-with-sidebar .insights-filters {
|
| 449 |
+
display: flex;
|
| 450 |
+
gap: 12px;
|
| 451 |
+
align-items: center;
|
| 452 |
+
margin-bottom: 14px;
|
| 453 |
+
flex-wrap: wrap;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.canvas-page-with-sidebar .insights-pinned-strip {
|
| 457 |
+
display: flex;
|
| 458 |
+
align-items: center;
|
| 459 |
+
gap: 8px;
|
| 460 |
+
padding: 10px 14px;
|
| 461 |
+
margin-bottom: 14px;
|
| 462 |
+
background: var(--canvas-accent-glow);
|
| 463 |
+
border: 1px solid rgba(99, 102, 241, 0.18);
|
| 464 |
+
border-radius: 8px;
|
| 465 |
+
flex-wrap: wrap;
|
| 466 |
+
}
|
| 467 |
+
.canvas-page-with-sidebar .insights-pinned-pill {
|
| 468 |
+
display: inline-flex;
|
| 469 |
+
align-items: center;
|
| 470 |
+
gap: 5px;
|
| 471 |
+
padding: 4px 10px;
|
| 472 |
+
background: var(--canvas-surface);
|
| 473 |
+
border: 1px solid var(--canvas-border);
|
| 474 |
+
border-radius: 999px;
|
| 475 |
+
font-family: inherit;
|
| 476 |
+
font-size: 12px;
|
| 477 |
+
color: var(--canvas-text);
|
| 478 |
+
cursor: pointer;
|
| 479 |
+
transition: all .12s;
|
| 480 |
+
}
|
| 481 |
+
.canvas-page-with-sidebar .insights-pinned-pill:hover {
|
| 482 |
+
border-color: var(--canvas-accent);
|
| 483 |
+
color: var(--canvas-accent);
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
/* Cards vs Tasks view toggle on the Insights view */
|
| 487 |
+
.canvas-page-with-sidebar .insights-view-toggle {
|
| 488 |
+
display: inline-flex;
|
| 489 |
+
align-items: center;
|
| 490 |
+
gap: 2px;
|
| 491 |
+
margin-bottom: 12px;
|
| 492 |
+
background: var(--canvas-surface);
|
| 493 |
+
border: 1px solid var(--canvas-border);
|
| 494 |
+
border-radius: 7px;
|
| 495 |
+
padding: 3px;
|
| 496 |
+
}
|
| 497 |
+
.canvas-page-with-sidebar .insights-view-toggle button {
|
| 498 |
+
background: transparent;
|
| 499 |
+
border: none;
|
| 500 |
+
padding: 5px 12px;
|
| 501 |
+
border-radius: 5px;
|
| 502 |
+
font-family: inherit;
|
| 503 |
+
font-size: 12px;
|
| 504 |
+
color: var(--canvas-text-3);
|
| 505 |
+
display: inline-flex;
|
| 506 |
+
align-items: center;
|
| 507 |
+
gap: 6px;
|
| 508 |
+
cursor: pointer;
|
| 509 |
+
transition: background .12s, color .12s;
|
| 510 |
+
}
|
| 511 |
+
.canvas-page-with-sidebar .insights-view-toggle button:hover { color: var(--canvas-text); }
|
| 512 |
+
.canvas-page-with-sidebar .insights-view-toggle button.active {
|
| 513 |
+
background: var(--canvas-surface-3);
|
| 514 |
+
color: var(--canvas-text);
|
| 515 |
+
box-shadow: inset 0 0 0 1px var(--canvas-border-2);
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
/* Flat task list view */
|
| 519 |
+
.canvas-page-with-sidebar .insights-tasks-list {
|
| 520 |
+
display: flex;
|
| 521 |
+
flex-direction: column;
|
| 522 |
+
gap: 4px;
|
| 523 |
+
}
|
| 524 |
+
.canvas-page-with-sidebar .insights-task-row {
|
| 525 |
+
display: flex;
|
| 526 |
+
align-items: flex-start;
|
| 527 |
+
gap: 10px;
|
| 528 |
+
padding: 10px 12px;
|
| 529 |
+
background: var(--canvas-surface);
|
| 530 |
+
border: 1px solid var(--canvas-border);
|
| 531 |
+
border-radius: 7px;
|
| 532 |
+
position: relative;
|
| 533 |
+
transition: background .12s, border-color .12s;
|
| 534 |
+
}
|
| 535 |
+
.canvas-page-with-sidebar .insights-task-row:hover {
|
| 536 |
+
border-color: var(--canvas-border-2);
|
| 537 |
+
}
|
| 538 |
+
.canvas-page-with-sidebar .insights-task-row.task-completed { opacity: 0.7; }
|
| 539 |
+
.canvas-page-with-sidebar .insights-task-row.task-abandoned { opacity: 0.5; }
|
| 540 |
+
.canvas-page-with-sidebar .insights-task-row.task-completed:hover,
|
| 541 |
+
.canvas-page-with-sidebar .insights-task-row.task-abandoned:hover { opacity: 0.95; }
|
| 542 |
+
.canvas-page-with-sidebar .insights-task-row .insight-task-check { margin-top: 4px; }
|
| 543 |
+
.canvas-page-with-sidebar .insights-task-body {
|
| 544 |
+
flex: 1;
|
| 545 |
+
min-width: 0;
|
| 546 |
+
display: flex;
|
| 547 |
+
flex-direction: column;
|
| 548 |
+
gap: 4px;
|
| 549 |
+
}
|
| 550 |
+
.canvas-page-with-sidebar .insights-task-section {
|
| 551 |
+
background: transparent;
|
| 552 |
+
border: none;
|
| 553 |
+
color: var(--canvas-text-3);
|
| 554 |
+
font-family: var(--canvas-mono);
|
| 555 |
+
font-size: 10.5px;
|
| 556 |
+
text-transform: uppercase;
|
| 557 |
+
letter-spacing: 0.06em;
|
| 558 |
+
padding: 0;
|
| 559 |
+
display: inline-flex;
|
| 560 |
+
align-items: center;
|
| 561 |
+
gap: 5px;
|
| 562 |
+
cursor: pointer;
|
| 563 |
+
align-self: flex-start;
|
| 564 |
+
}
|
| 565 |
+
.canvas-page-with-sidebar .insights-task-section:hover { color: var(--canvas-accent); }
|
| 566 |
+
.canvas-page-with-sidebar .insights-task-text {
|
| 567 |
+
font-size: 13px;
|
| 568 |
+
color: var(--canvas-text);
|
| 569 |
+
line-height: 1.5;
|
| 570 |
+
}
|
| 571 |
+
.canvas-page-with-sidebar .insights-task-row.task-completed .insights-task-text,
|
| 572 |
+
.canvas-page-with-sidebar .insights-task-row.task-abandoned .insights-task-text {
|
| 573 |
+
text-decoration: line-through;
|
| 574 |
+
color: var(--canvas-text-3);
|
| 575 |
+
}
|
| 576 |
+
.canvas-page-with-sidebar .insights-task-text strong { color: var(--canvas-text); }
|
| 577 |
+
|
| 578 |
+
/* Per-card progress bar — replaces the old single status pill */
|
| 579 |
+
.canvas-page-with-sidebar .insight-progress {
|
| 580 |
+
display: flex;
|
| 581 |
+
flex-direction: column;
|
| 582 |
+
gap: 4px;
|
| 583 |
+
padding: 0 0 4px;
|
| 584 |
+
}
|
| 585 |
+
.canvas-page-with-sidebar .insight-progress-meta {
|
| 586 |
+
display: flex;
|
| 587 |
+
align-items: center;
|
| 588 |
+
gap: 8px;
|
| 589 |
+
font-family: var(--canvas-mono);
|
| 590 |
+
font-size: 10.5px;
|
| 591 |
+
color: var(--canvas-text-3);
|
| 592 |
+
}
|
| 593 |
+
.canvas-page-with-sidebar .insight-progress-count { font-weight: 600; }
|
| 594 |
+
.canvas-page-with-sidebar .insight-progress-badge {
|
| 595 |
+
font-family: var(--canvas-mono);
|
| 596 |
+
font-size: 9px;
|
| 597 |
+
text-transform: uppercase;
|
| 598 |
+
letter-spacing: 0.08em;
|
| 599 |
+
font-weight: 700;
|
| 600 |
+
padding: 1px 6px;
|
| 601 |
+
border-radius: 3px;
|
| 602 |
+
}
|
| 603 |
+
.canvas-page-with-sidebar .insight-progress-badge.done { background: rgba(16,185,129,0.15); color: #10B981; }
|
| 604 |
+
.canvas-page-with-sidebar .insight-progress-badge.inprog { background: rgba(59,130,246,0.15); color: #3B82F6; }
|
| 605 |
+
.canvas-page-with-sidebar .insight-progress-badge.abandoned { background: var(--canvas-surface-2); color: var(--canvas-text-4); }
|
| 606 |
+
|
| 607 |
+
.canvas-page-with-sidebar .insight-progress-bar {
|
| 608 |
+
height: 5px;
|
| 609 |
+
background: var(--canvas-surface-2);
|
| 610 |
+
border-radius: 3px;
|
| 611 |
+
overflow: hidden;
|
| 612 |
+
display: flex;
|
| 613 |
+
}
|
| 614 |
+
.canvas-page-with-sidebar .insight-progress-bar > i {
|
| 615 |
+
display: block;
|
| 616 |
+
height: 100%;
|
| 617 |
+
transition: width .25s;
|
| 618 |
+
}
|
| 619 |
+
.canvas-page-with-sidebar .insight-progress-bar .ip-completed { background: #10B981; }
|
| 620 |
+
.canvas-page-with-sidebar .insight-progress-bar .ip-inprogress { background: #3B82F6; }
|
| 621 |
+
.canvas-page-with-sidebar .insight-progress-bar .ip-abandoned { background: var(--canvas-text-4); }
|
| 622 |
+
|
| 623 |
+
/* Per-bullet tasks inside the body */
|
| 624 |
+
.canvas-page-with-sidebar .insight-tasks {
|
| 625 |
+
list-style: none;
|
| 626 |
+
padding: 0;
|
| 627 |
+
margin: 6px 0 0;
|
| 628 |
+
display: flex;
|
| 629 |
+
flex-direction: column;
|
| 630 |
+
gap: 6px;
|
| 631 |
+
}
|
| 632 |
+
.canvas-page-with-sidebar .insight-task {
|
| 633 |
+
display: flex;
|
| 634 |
+
align-items: flex-start;
|
| 635 |
+
gap: 8px;
|
| 636 |
+
padding: 2px 0;
|
| 637 |
+
font-size: 13px;
|
| 638 |
+
color: var(--canvas-text-2);
|
| 639 |
+
line-height: 1.5;
|
| 640 |
+
position: relative;
|
| 641 |
+
}
|
| 642 |
+
.canvas-page-with-sidebar .insight-task.task-completed .insight-task-text {
|
| 643 |
+
text-decoration: line-through;
|
| 644 |
+
color: var(--canvas-text-3);
|
| 645 |
+
opacity: 0.7;
|
| 646 |
+
}
|
| 647 |
+
.canvas-page-with-sidebar .insight-task.task-abandoned .insight-task-text {
|
| 648 |
+
text-decoration: line-through;
|
| 649 |
+
color: var(--canvas-text-4);
|
| 650 |
+
opacity: 0.5;
|
| 651 |
+
}
|
| 652 |
+
.canvas-page-with-sidebar .insight-task.task-in-progress .insight-task-text {
|
| 653 |
+
color: var(--canvas-text);
|
| 654 |
+
font-weight: 500;
|
| 655 |
+
}
|
| 656 |
+
.canvas-page-with-sidebar .insight-task-text { flex: 1; }
|
| 657 |
+
.canvas-page-with-sidebar .insight-task-text strong { color: var(--canvas-text); }
|
| 658 |
+
.canvas-page-with-sidebar .insight-task-check {
|
| 659 |
+
flex-shrink: 0;
|
| 660 |
+
width: 18px;
|
| 661 |
+
height: 18px;
|
| 662 |
+
border-radius: 4px;
|
| 663 |
+
border: 1.5px solid currentColor;
|
| 664 |
+
background: transparent;
|
| 665 |
+
cursor: pointer;
|
| 666 |
+
display: grid;
|
| 667 |
+
place-items: center;
|
| 668 |
+
margin-top: 2px;
|
| 669 |
+
transition: background .12s;
|
| 670 |
+
}
|
| 671 |
+
.canvas-page-with-sidebar .insight-task-check:hover {
|
| 672 |
+
background: var(--canvas-surface-2);
|
| 673 |
+
}
|
| 674 |
+
.canvas-page-with-sidebar .insight-task.task-completed .insight-task-check {
|
| 675 |
+
background: #10B981;
|
| 676 |
+
color: #fff;
|
| 677 |
+
}
|
| 678 |
+
.canvas-page-with-sidebar .insight-task.task-abandoned .insight-task-check {
|
| 679 |
+
background: var(--canvas-text-4);
|
| 680 |
+
color: #fff;
|
| 681 |
+
}
|
| 682 |
+
.canvas-page-with-sidebar .task-check-dot {
|
| 683 |
+
width: 6px;
|
| 684 |
+
height: 6px;
|
| 685 |
+
border-radius: 50%;
|
| 686 |
+
background: currentColor;
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
/* Sidebar row for a fully-done section gets struck through */
|
| 690 |
+
.sidebar .csm-row-done .csm-row-label {
|
| 691 |
+
text-decoration: line-through;
|
| 692 |
+
color: var(--text-tertiary, #9CA3AF);
|
| 693 |
+
}
|
| 694 |
+
.sidebar .csm-row-done .csm-row-meta {
|
| 695 |
+
color: #10B981;
|
| 696 |
+
font-weight: 600;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
/* Insight status pill + menu */
|
| 700 |
+
.canvas-page-with-sidebar .insight-status-wrap {
|
| 701 |
+
position: relative;
|
| 702 |
+
}
|
| 703 |
+
.canvas-page-with-sidebar .insight-status-pill {
|
| 704 |
+
display: inline-flex;
|
| 705 |
+
align-items: center;
|
| 706 |
+
gap: 5px;
|
| 707 |
+
padding: 3px 9px;
|
| 708 |
+
border-radius: 999px;
|
| 709 |
+
border: 1px solid transparent;
|
| 710 |
+
background: transparent;
|
| 711 |
+
font-family: inherit;
|
| 712 |
+
font-size: 10.5px;
|
| 713 |
+
font-weight: 600;
|
| 714 |
+
text-transform: uppercase;
|
| 715 |
+
letter-spacing: 0.04em;
|
| 716 |
+
cursor: pointer;
|
| 717 |
+
transition: background .12s;
|
| 718 |
+
}
|
| 719 |
+
.canvas-page-with-sidebar .insight-status-pill:hover {
|
| 720 |
+
background: var(--canvas-surface-2);
|
| 721 |
+
}
|
| 722 |
+
.canvas-page-with-sidebar .insight-status-menu {
|
| 723 |
+
position: absolute;
|
| 724 |
+
top: calc(100% + 4px);
|
| 725 |
+
right: 0;
|
| 726 |
+
z-index: 5;
|
| 727 |
+
background: var(--canvas-surface);
|
| 728 |
+
border: 1px solid var(--canvas-border-2);
|
| 729 |
+
border-radius: 7px;
|
| 730 |
+
box-shadow: var(--canvas-shadow-lg);
|
| 731 |
+
padding: 4px;
|
| 732 |
+
min-width: 150px;
|
| 733 |
+
display: flex;
|
| 734 |
+
flex-direction: column;
|
| 735 |
+
}
|
| 736 |
+
.canvas-page-with-sidebar .insight-status-menu button {
|
| 737 |
+
display: flex;
|
| 738 |
+
align-items: center;
|
| 739 |
+
gap: 8px;
|
| 740 |
+
background: transparent;
|
| 741 |
+
border: none;
|
| 742 |
+
padding: 6px 10px;
|
| 743 |
+
border-radius: 4px;
|
| 744 |
+
font-family: inherit;
|
| 745 |
+
font-size: 12px;
|
| 746 |
+
color: var(--canvas-text);
|
| 747 |
+
cursor: pointer;
|
| 748 |
+
text-align: left;
|
| 749 |
+
}
|
| 750 |
+
.canvas-page-with-sidebar .insight-status-menu button:hover {
|
| 751 |
+
background: var(--canvas-surface-2);
|
| 752 |
+
}
|
| 753 |
+
.canvas-page-with-sidebar .insight-status-menu button.active {
|
| 754 |
+
background: var(--canvas-accent-glow);
|
| 755 |
+
color: var(--canvas-accent);
|
| 756 |
+
font-weight: 600;
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
/* Card-level status mutes the body when completed/abandoned */
|
| 760 |
+
.canvas-page-with-sidebar .insight.status-completed .insight-body,
|
| 761 |
+
.canvas-page-with-sidebar .insight.status-completed .insight-foot {
|
| 762 |
+
opacity: 0.65;
|
| 763 |
+
}
|
| 764 |
+
.canvas-page-with-sidebar .insight.status-abandoned {
|
| 765 |
+
opacity: 0.5;
|
| 766 |
+
}
|
| 767 |
+
.canvas-page-with-sidebar .insight.status-abandoned:hover { opacity: 0.85; }
|
| 768 |
+
|
| 769 |
+
/* Progress bar in stats — segmented for status mix */
|
| 770 |
+
.canvas-page-with-sidebar .insights-stat-progress {
|
| 771 |
+
flex: 1;
|
| 772 |
+
min-width: 180px;
|
| 773 |
+
max-width: 280px;
|
| 774 |
+
gap: 4px;
|
| 775 |
+
}
|
| 776 |
+
.canvas-page-with-sidebar .insights-progress-bar {
|
| 777 |
+
height: 6px;
|
| 778 |
+
width: 100%;
|
| 779 |
+
background: var(--canvas-surface-2);
|
| 780 |
+
border-radius: 3px;
|
| 781 |
+
overflow: hidden;
|
| 782 |
+
display: flex;
|
| 783 |
+
}
|
| 784 |
+
.canvas-page-with-sidebar .insights-progress-bar > i {
|
| 785 |
+
display: block;
|
| 786 |
+
height: 100%;
|
| 787 |
+
transition: width .3s;
|
| 788 |
+
}
|
| 789 |
+
.canvas-page-with-sidebar .insights-progress-bar .ip-completed { background: #10B981; }
|
| 790 |
+
.canvas-page-with-sidebar .insights-progress-bar .ip-inprogress { background: #3B82F6; }
|
| 791 |
+
.canvas-page-with-sidebar .insights-progress-bar .ip-abandoned { background: var(--canvas-text-4); }
|
| 792 |
+
|
| 793 |
+
/* Confidence ring */
|
| 794 |
+
.canvas-page-with-sidebar .confidence-ring {
|
| 795 |
+
position: relative;
|
| 796 |
+
width: 32px;
|
| 797 |
+
height: 32px;
|
| 798 |
+
display: grid;
|
| 799 |
+
place-items: center;
|
| 800 |
+
flex-shrink: 0;
|
| 801 |
+
}
|
| 802 |
+
.canvas-page-with-sidebar .confidence-ring span {
|
| 803 |
+
position: absolute;
|
| 804 |
+
font-family: var(--canvas-mono);
|
| 805 |
+
font-size: 9.5px;
|
| 806 |
+
font-weight: 700;
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
/* Insight card refinements */
|
| 810 |
+
.canvas-page-with-sidebar .insight {
|
| 811 |
+
transition: border-color .15s, transform .15s, box-shadow .15s;
|
| 812 |
+
}
|
| 813 |
+
.canvas-page-with-sidebar .insight:hover {
|
| 814 |
+
transform: translateY(-1px);
|
| 815 |
+
box-shadow: 0 6px 18px rgba(0,0,0,0.08);
|
| 816 |
+
}
|
| 817 |
+
.canvas-page-with-sidebar .insight.is-pinned {
|
| 818 |
+
border-color: var(--canvas-accent);
|
| 819 |
+
background: var(--canvas-surface);
|
| 820 |
+
box-shadow: 0 0 0 1px var(--canvas-accent-glow);
|
| 821 |
+
}
|
| 822 |
+
.canvas-page-with-sidebar[data-canvas-theme="light"] .insight.is-pinned {
|
| 823 |
+
background: #ffffff;
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
/* Insight footer (source count + updated time) */
|
| 827 |
+
.canvas-page-with-sidebar .insight-foot {
|
| 828 |
+
display: flex;
|
| 829 |
+
justify-content: space-between;
|
| 830 |
+
align-items: center;
|
| 831 |
+
padding: 6px 0 0;
|
| 832 |
+
font-size: 11px;
|
| 833 |
+
color: var(--canvas-text-3);
|
| 834 |
+
}
|
| 835 |
+
.canvas-page-with-sidebar .insight-foot-meta {
|
| 836 |
+
display: inline-flex;
|
| 837 |
+
align-items: center;
|
| 838 |
+
gap: 6px;
|
| 839 |
+
font-family: var(--canvas-mono);
|
| 840 |
+
}
|
| 841 |
+
.canvas-page-with-sidebar .insight-dot {
|
| 842 |
+
width: 3px;
|
| 843 |
+
height: 3px;
|
| 844 |
+
border-radius: 50%;
|
| 845 |
+
background: var(--canvas-text-4);
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
/* Expanded source-quotes panel */
|
| 849 |
+
.canvas-page-with-sidebar .insight-detail {
|
| 850 |
+
margin-top: 8px;
|
| 851 |
+
padding: 10px 12px;
|
| 852 |
+
background: var(--canvas-bg-2);
|
| 853 |
+
border-left: 2px solid var(--canvas-accent);
|
| 854 |
+
border-radius: 0 6px 6px 0;
|
| 855 |
+
display: flex;
|
| 856 |
+
flex-direction: column;
|
| 857 |
+
gap: 6px;
|
| 858 |
+
animation: canvas-view-in 180ms var(--canvas-ease);
|
| 859 |
+
}
|
| 860 |
+
.canvas-page-with-sidebar[data-canvas-theme="light"] .insight-detail {
|
| 861 |
+
background: rgba(99, 102, 241, 0.04);
|
| 862 |
+
}
|
| 863 |
+
.canvas-page-with-sidebar .insight-detail-head {
|
| 864 |
+
font-size: 10.5px;
|
| 865 |
+
font-family: var(--canvas-mono);
|
| 866 |
+
text-transform: uppercase;
|
| 867 |
+
letter-spacing: 0.06em;
|
| 868 |
+
color: var(--canvas-accent);
|
| 869 |
+
font-weight: 600;
|
| 870 |
+
}
|
| 871 |
+
.canvas-page-with-sidebar .insight-quote {
|
| 872 |
+
font-size: 12.5px;
|
| 873 |
+
color: var(--canvas-text-2);
|
| 874 |
+
line-height: 1.5;
|
| 875 |
+
font-style: italic;
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
/* Insights */
|
| 879 |
.canvas-page-with-sidebar .insight-grid {
|
| 880 |
display: grid;
|
|
|
|
| 1067 |
background: rgba(16, 185, 129, 0.15);
|
| 1068 |
color: #10B981;
|
| 1069 |
}
|
| 1070 |
+
.canvas-modal-backdrop .widget-tag-chat {
|
| 1071 |
+
background: rgba(59, 130, 246, 0.15);
|
| 1072 |
+
color: #3B82F6;
|
| 1073 |
+
}
|
| 1074 |
.canvas-page-with-sidebar .widget-actions { display: flex; gap: 1px; opacity: 0; transition: opacity .12s; }
|
| 1075 |
.canvas-page-with-sidebar .widget:hover .widget-actions { opacity: 1; }
|
| 1076 |
.canvas-page-with-sidebar .widget-actions .icon-btn { width: 24px; height: 24px; }
|
|
|
|
| 2935 |
background: rgba(0, 0, 0, 0.04);
|
| 2936 |
}
|
| 2937 |
|
| 2938 |
+
/* PhD Journey milestone rows */
|
| 2939 |
+
.canvas-page-with-sidebar .phd-milestone {
|
| 2940 |
+
display: flex;
|
| 2941 |
+
align-items: flex-start;
|
| 2942 |
+
gap: 10px;
|
| 2943 |
+
padding: 8px 4px;
|
| 2944 |
+
border-radius: 6px;
|
| 2945 |
+
cursor: pointer;
|
| 2946 |
+
transition: background .12s;
|
| 2947 |
+
}
|
| 2948 |
+
.canvas-page-with-sidebar .phd-milestone:hover { background: var(--canvas-surface-2); }
|
| 2949 |
+
.canvas-page-with-sidebar .phd-milestone-check {
|
| 2950 |
+
flex-shrink: 0;
|
| 2951 |
+
width: 18px;
|
| 2952 |
+
height: 18px;
|
| 2953 |
+
border-radius: 4px;
|
| 2954 |
+
border: 1.5px solid var(--canvas-text-4);
|
| 2955 |
+
background: transparent;
|
| 2956 |
+
cursor: pointer;
|
| 2957 |
+
display: grid;
|
| 2958 |
+
place-items: center;
|
| 2959 |
+
margin-top: 2px;
|
| 2960 |
+
color: #fff;
|
| 2961 |
+
transition: background .12s, border-color .12s;
|
| 2962 |
+
}
|
| 2963 |
+
.canvas-page-with-sidebar .phd-milestone.milestone-in-progress .phd-milestone-check {
|
| 2964 |
+
background: #3B82F6;
|
| 2965 |
+
border-color: #3B82F6;
|
| 2966 |
+
}
|
| 2967 |
+
.canvas-page-with-sidebar .phd-milestone.milestone-completed .phd-milestone-check {
|
| 2968 |
+
background: #10B981;
|
| 2969 |
+
border-color: #10B981;
|
| 2970 |
+
}
|
| 2971 |
+
.canvas-page-with-sidebar .phd-milestone-body { flex: 1; min-width: 0; }
|
| 2972 |
+
.canvas-page-with-sidebar .phd-milestone-label {
|
| 2973 |
+
font-size: 13px;
|
| 2974 |
+
color: var(--canvas-text);
|
| 2975 |
+
font-weight: 500;
|
| 2976 |
+
line-height: 1.4;
|
| 2977 |
+
}
|
| 2978 |
+
.canvas-page-with-sidebar .phd-milestone.milestone-completed .phd-milestone-label {
|
| 2979 |
+
text-decoration: line-through;
|
| 2980 |
+
color: var(--canvas-text-3);
|
| 2981 |
+
}
|
| 2982 |
+
.canvas-page-with-sidebar .phd-milestone-hint {
|
| 2983 |
+
font-size: 11.5px;
|
| 2984 |
+
color: var(--canvas-text-4);
|
| 2985 |
+
font-style: italic;
|
| 2986 |
+
margin-top: 2px;
|
| 2987 |
+
}
|
| 2988 |
+
.canvas-page-with-sidebar .phd-milestone-note {
|
| 2989 |
+
font-size: 11.5px;
|
| 2990 |
+
color: var(--canvas-text-2);
|
| 2991 |
+
margin-top: 2px;
|
| 2992 |
+
}
|
| 2993 |
+
|
| 2994 |
+
/* PhD Resources link rows */
|
| 2995 |
+
.canvas-page-with-sidebar .phd-resource-link {
|
| 2996 |
+
display: flex;
|
| 2997 |
+
flex-direction: column;
|
| 2998 |
+
gap: 1px;
|
| 2999 |
+
padding: 6px 8px;
|
| 3000 |
+
background: transparent;
|
| 3001 |
+
border-radius: 5px;
|
| 3002 |
+
text-decoration: none;
|
| 3003 |
+
color: var(--canvas-text);
|
| 3004 |
+
transition: background .12s;
|
| 3005 |
+
}
|
| 3006 |
+
.canvas-page-with-sidebar .phd-resource-link:hover {
|
| 3007 |
+
background: var(--canvas-surface-2);
|
| 3008 |
+
}
|
| 3009 |
+
.canvas-page-with-sidebar .phd-resource-name {
|
| 3010 |
+
font-size: 12.5px;
|
| 3011 |
+
font-weight: 600;
|
| 3012 |
+
color: var(--canvas-accent);
|
| 3013 |
+
}
|
| 3014 |
+
.canvas-page-with-sidebar .phd-resource-desc {
|
| 3015 |
+
font-size: 11px;
|
| 3016 |
+
color: var(--canvas-text-3);
|
| 3017 |
+
}
|
| 3018 |
+
|
| 3019 |
/* "Coming soon" stub widget — roadmap-preview card, not a dead end */
|
| 3020 |
.canvas-page-with-sidebar .widget-stub {
|
| 3021 |
flex: 1;
|