Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions phd-advisor-frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ function App() {
setCurrentView('auth');
};

const navigateToCanvas = () => {
const navigateToCanvas = (canvasView) => {
if (['insights', 'workspace', 'deliverables'].includes(canvasView)) {
localStorage.setItem('canvas-view-v2', canvasView);
}
setCurrentView('canvas');
};

Expand Down Expand Up @@ -74,17 +77,20 @@ function App() {
<div className="App">
{currentView === 'home' && (
<HomePage
onNavigateToHome={navigateToHome}
onNavigateToChat={isAuthenticated ? navigateToChat : navigateToAuth}
onNavigateToCanvas={isAuthenticated ? navigateToCanvas : navigateToAuth}
isAuthenticated={isAuthenticated}
/>
)}
{currentView === 'auth' && (
<AuthPage onAuthSuccess={handleAuthSuccess} />
)}
{currentView === 'canvas' && isAuthenticated && (
<CanvasPage
<CanvasPage
user={user}
authToken={authToken}
onNavigateToHome={navigateToHome}
onNavigateToChat={navigateToChat}
onSignOut={handleSignOut}
/>
Expand Down
100 changes: 100 additions & 0 deletions phd-advisor-frontend/src/components/AppHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';
import { Home, Menu, Users } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
import { useAppConfig } from '../contexts/AppConfigContext';

/**
* Shared floating header used on every page so the app feels like one surface.
*
* Props:
* currentPage: 'home' | 'chat' | 'canvas'
* onNavigateToHome, onNavigateToChat, onNavigateToCanvas: navigation callbacks
* (onNavigateToCanvas may receive 'insights' | 'workspace' to deep-link a view)
* onMobileMenu?: () => void — when present, shows the mobile menu button
* children?: ReactNode — extra controls slotted between the tabs and the theme toggle
*/
const AppHeader = ({
currentPage = 'home',
onNavigateToHome,
onNavigateToChat,
onNavigateToCanvas,
onMobileMenu,
children,
}) => {
const { config, resolveIcon } = useAppConfig();
const BrandIcon = resolveIcon ? resolveIcon('Users') : Users;

const goToCanvas = (view) => {
if (onNavigateToCanvas) onNavigateToCanvas(view);
};

// Accept either 'canvas' (all canvas tabs highlight equally) or a more specific
// 'canvas-<subview>' from CanvasPage so only the active one highlights.
const isOnHome = currentPage === 'home';
const isOnChat = currentPage === 'chat';
const isOnCanvas = currentPage === 'canvas' || currentPage.startsWith('canvas-');
const canvasSub = currentPage.startsWith('canvas-') ? currentPage.slice(7) : null;
const tabActive = (sub) => isOnCanvas && (canvasSub === null ? false : canvasSub === sub);

return (
<header className="floating-header app-header">
<div className="header-left">
{onMobileMenu && (
<button className="mobile-menu-button" onClick={onMobileMenu}>
<Menu size={20} />
</button>
)}
<button
className="modern-home-btn"
onClick={onNavigateToHome}
title="Home"
disabled={isOnHome}
aria-disabled={isOnHome}
>
<Home size={20} />
</button>
<div className="header-brand">
<div className="brand-icon">
<BrandIcon size={24} />
</div>
<div className="brand-text">
<h1>{config?.app?.title || 'Advisory'}</h1>
<p>{config?.app?.subtitle || 'AI-Powered Guidance'}</p>
</div>
</div>
</div>

{/* Hide the view pill bar on the home page — home is a landing page,
not part of the chat ↔ canvas surface. */}
{!isOnHome && (
<div className="canvas-tabs chat-view-tabs">
<button className={`tab ${isOnChat ? 'active' : ''}`} onClick={onNavigateToChat}>Chat</button>
<button className={`tab ${tabActive('insights') ? 'active' : ''}`} onClick={() => goToCanvas('insights')}>Insights</button>
</div>
)}

{/* Compact mobile dropdown — appears in place of the pill bar at narrow widths */}
{!isOnHome && (
<select
className="canvas-tabs-mobile"
value={isOnChat ? 'chat' : (canvasSub || 'insights')}
onChange={(e) => {
const v = e.target.value;
if (v === 'chat') onNavigateToChat();
else goToCanvas(v);
}}
>
<option value="chat">Chat</option>
<option value="insights">Insights</option>
</select>
)}

<div className="header-right">
{children}
<ThemeToggle />
</div>
</header>
);
};

export default AppHeader;
90 changes: 90 additions & 0 deletions phd-advisor-frontend/src/components/canvas/CanvasIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';

const ICONS = {
sparkles: <><path d="M12 3l1.8 4.5L18 9l-4.2 1.5L12 15l-1.8-4.5L6 9l4.2-1.5z"/><path d="M19 14l.9 2.1L22 17l-2.1.9L19 20l-.9-2.1L16 17l2.1-.9z"/></>,
layout: <><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></>,
insights: <><path d="M3 3v18h18"/><path d="M7 14l3-3 3 3 5-5"/></>,
search: <><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></>,
bell: <><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></>,
settings: <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></>,
plus: <path d="M12 5v14M5 12h14"/>,
x: <path d="M18 6L6 18M6 6l12 12"/>,
check: <path d="M20 6L9 17l-5-5"/>,
refresh: <><path d="M21 12a9 9 0 0 0-15-6.7L3 8"/><path d="M3 4v4h4"/><path d="M3 12a9 9 0 0 0 15 6.7l3-2.7"/><path d="M21 20v-4h-4"/></>,
trash: <><path d="M3 6h18"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></>,
grip: <><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></>,
more: <><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></>,
pin: <><path d="M12 17v5"/><path d="M9 11V3h6v8l3 3v2H6v-2l3-3z"/></>,
link: <><path d="M10 14a5 5 0 0 0 7 0l3-3a5 5 0 1 0-7-7l-1 1"/><path d="M14 10a5 5 0 0 0-7 0l-3 3a5 5 0 1 0 7 7l1-1"/></>,
message: <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>,
task: <><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 9h6M9 13h4"/></>,
book: <><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></>,
kanban: <><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 7v10M15 7v6"/></>,
timer: <><circle cx="12" cy="13" r="8"/><path d="M9 2h6M12 8v5l3 2"/></>,
pencil: <><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 1 1 3 3L7 19l-4 1 1-4z"/></>,
calendar: <><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></>,
wallet: <><path d="M21 12V7a2 2 0 0 0-2-2H5a2 2 0 0 0 0 4h14a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7"/><circle cx="17" cy="13" r="1"/></>,
flag: <><path d="M4 22V4"/><path d="M4 4h13l-2 5 2 5H4"/></>,
gavel: <><path d="M14.5 12.5L18 16M11 9l3.5 3.5M5 13l5-5 6 6-5 5z"/><path d="M3 21h10"/></>,
scale: <><path d="M12 3v18"/><path d="M3 7h18"/><path d="M5 7l-2 6a4 4 0 0 0 8 0l-2-6"/><path d="M19 7l-2 6a4 4 0 0 0 8 0l-2-6"/></>,
brain: <><path d="M9 3a3 3 0 0 0-3 3v.5A3 3 0 0 0 4 9a3 3 0 0 0 1 5.5V15a3 3 0 0 0 4 3v-3"/><path d="M15 3a3 3 0 0 1 3 3v.5A3 3 0 0 1 20 9a3 3 0 0 1-1 5.5V15a3 3 0 0 1-4 3"/><path d="M9 18v3M9 14h6"/></>,
alert: <><path d="M10.3 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></>,
notes: <><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M8 13h8M8 17h6"/></>,
list: <><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></>,
zap: <path d="M13 2L3 14h8l-1 8 10-12h-8l1-8z"/>,
flame: <><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></>,
heart: <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>,
graph: <><circle cx="12" cy="5" r="3"/><circle cx="6" cy="19" r="3"/><circle cx="18" cy="19" r="3"/><path d="M6.7 16.7L10 8M14 8l3.3 8.7"/></>,
award: <><circle cx="12" cy="8" r="6"/><path d="M15.5 12.5L17 22l-5-3-5 3 1.5-9.5"/></>,
network: <><circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M7.5 7.5L11 16.5M16.5 7.5L13 16.5"/></>,
database: <><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v6c0 1.66 4 3 9 3s9-1.34 9-3V5"/><path d="M3 11v6c0 1.66 4 3 9 3s9-1.34 9-3v-6"/></>,
flask: <><path d="M9 3h6M10 3v7L4.5 19A2 2 0 0 0 6.3 22h11.4a2 2 0 0 0 1.8-3L14 10V3"/></>,
shield: <path d="M12 2L4 5v6.09c0 5.05 3.41 9.76 8 10.91 4.59-1.15 8-5.86 8-10.91V5l-8-3z"/>,
music: <><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></>,
bullseye: <><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5"/></>,
arrow: <><path d="M5 12h14M12 5l7 7-7 7"/></>,
copy: <><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></>,
download: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></>,
play: <path d="M5 3l14 9-14 9z"/>,
pause: <><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></>,
reset: <><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></>,
star: <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>,
shuffle: <><path d="M16 3h5v5M4 20l16.5-16.5"/><path d="M21 16v5h-5M15 15l5.5 5.5M4 4l5 5"/></>,
expand: <><path d="M3 8V4h4M21 8V4h-4M3 16v4h4M21 16v4h-4"/></>,
resize: <><path d="M21 11V3h-8M3 13v8h8"/></>,
smile: <><circle cx="12" cy="12" r="9"/><path d="M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01"/></>,
send: <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>,
chevron: <path d="M9 18l6-6-6-6"/>,
user: <><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></>,
cite: <><path d="M3 21c0-3 2-6 6-6"/><path d="M9 15v-2c0-3-2-5-5-5"/><path d="M14 21c0-3 2-6 6-6"/><path d="M20 15v-2c0-3-2-5-5-5"/></>,
pinned: <><path d="M9 4v5l-3 4h12l-3-4V4M12 13v8M9 4h6"/></>,
microscope: <><path d="M6 18h8M3 22h18"/><path d="M14 22a7 7 0 1 0 0-14h-1"/><path d="M9 14h2M9 12a2 2 0 0 1-2-2V6h4v4a2 2 0 0 1-2 2zM12 6V3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3"/></>,
sun: <><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></>,
moon: <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>,
back: <><path d="M19 12H5M12 19l-7-7 7-7"/></>,
};

const Icon = ({ name, size = 16, className = '', style }) => {
const paths = ICONS[name];
if (!paths) return null;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
style={style}
aria-hidden="true"
>
{paths}
</svg>
);
};

export default Icon;
87 changes: 87 additions & 0 deletions phd-advisor-frontend/src/components/canvas/CanvasWelcomeTour.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useState, useEffect } from 'react';
import Icon from './CanvasIcon';

const TOUR_KEY = 'canvas-tour-seen-v1';

const STEPS = [
{
title: 'Welcome to your Canvas',
icon: 'sparkles',
body: 'AI-summarized highlights from your research conversations live here. Each insight is a discrete task you can mark open, in-progress, completed, or abandoned.',
},
{
title: 'Filter, sort, pin',
icon: 'layout',
body: 'Use the filter chips to narrow by status, category, or confidence. Pin the most important sections to keep them at the top. The Tasks view flattens everything into a single to-do list.',
},
{
title: 'Ask follow-up',
icon: 'message',
body: 'Each insight has an "Ask follow-up" action that opens a fresh chat with the relevant context preloaded — useful when a synthesis raises a new question worth digging into.',
},
];

const CanvasWelcomeTour = ({ forceShow = false, onClose }) => {
const [step, setStep] = useState(0);
const [visible, setVisible] = useState(false);

useEffect(() => {
const seen = localStorage.getItem(TOUR_KEY);
if (forceShow || !seen) setVisible(true);
}, [forceShow]);

const dismiss = () => {
localStorage.setItem(TOUR_KEY, '1');
setVisible(false);
if (onClose) onClose();
};

if (!visible) return null;

const s = STEPS[step];
const isLast = step === STEPS.length - 1;

return (
<div className="canvas-modal-backdrop" onClick={(e) => { if (e.target === e.currentTarget) dismiss(); }}>
<div className="canvas-modal canvas-tour" onClick={(e) => e.stopPropagation()}>
<div className="modal-head">
<div className="modal-icon"><Icon name={s.icon} size={18}/></div>
<div style={{ flex: 1 }}>
<div className="modal-title">{s.title}</div>
<div className="modal-sub">Step {step + 1} of {STEPS.length}</div>
</div>
<button className="icon-btn" onClick={dismiss} title="Skip"><Icon name="x" size={16}/></button>
</div>
<div className="modal-body" style={{ minHeight: 80 }}>
<p style={{ margin: 0, fontSize: 13.5, lineHeight: 1.6, color: 'var(--canvas-text-2)' }}>{s.body}</p>
</div>
<div className="modal-foot" style={{ justifyContent: 'space-between' }}>
<div style={{ display: 'flex', gap: 6 }}>
{STEPS.map((_, i) => (
<span key={i}
onClick={() => setStep(i)}
style={{
width: 8, height: 8, borderRadius: '50%', cursor: 'pointer',
background: i === step ? 'var(--canvas-accent)' : 'var(--canvas-surface-3)',
transition: 'background .15s',
}}/>
))}
</div>
<div style={{ display: 'flex', gap: 8 }}>
{step > 0 && (
<button className="btn btn-ghost" onClick={() => setStep(s => s - 1)}>Back</button>
)}
{!isLast && (
<button className="btn btn-ghost" onClick={dismiss}>Skip</button>
)}
<button className="btn btn-primary" onClick={() => isLast ? dismiss() : setStep(s => s + 1)}>
{isLast ? <><Icon name="check" size={13}/>Get started</> : <>Next<Icon name="arrow" size={13}/></>}
</button>
</div>
</div>
</div>
</div>
);
};

export default CanvasWelcomeTour;
Loading
Loading