diff --git a/phd-advisor-frontend/src/App.js b/phd-advisor-frontend/src/App.js index 9d03459c..897f05e5 100644 --- a/phd-advisor-frontend/src/App.js +++ b/phd-advisor-frontend/src/App.js @@ -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'); }; @@ -74,7 +77,9 @@ function App() {
{currentView === 'home' && ( )} @@ -82,9 +87,10 @@ function App() { )} {currentView === 'canvas' && isAuthenticated && ( - diff --git a/phd-advisor-frontend/src/components/AppHeader.js b/phd-advisor-frontend/src/components/AppHeader.js new file mode 100644 index 00000000..fa96fd72 --- /dev/null +++ b/phd-advisor-frontend/src/components/AppHeader.js @@ -0,0 +1,102 @@ +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-' 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 ( +
+
+ {onMobileMenu && ( + + )} + +
+
+ +
+
+

{config?.app?.title || 'Advisory'}

+

{config?.app?.subtitle || 'AI-Powered Guidance'}

+
+
+
+ + {/* Hide the view pill bar on the home page — home is a landing page, + not part of the chat ↔ canvas surface. */} + {!isOnHome && ( +
+ + + +
+ )} + + {/* Compact mobile dropdown — appears in place of the pill bar at narrow widths */} + {!isOnHome && ( + + )} + +
+ {children} + +
+
+ ); +}; + +export default AppHeader; diff --git a/phd-advisor-frontend/src/components/canvas/CanvasCriticWidgets.js b/phd-advisor-frontend/src/components/canvas/CanvasCriticWidgets.js new file mode 100644 index 00000000..0f947963 --- /dev/null +++ b/phd-advisor-frontend/src/components/canvas/CanvasCriticWidgets.js @@ -0,0 +1,430 @@ +import React, { useState } from 'react'; +import Icon from './CanvasIcon'; + +const fireToast = (msg, kind = 'success') => + window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg, kind } })); + +// Critic widgets are scripted in canvas; the *real* critique should happen in +// the main chat so message history lives in one place (per Daniel's review). +// "Open in chat" stashes a draft prompt + persona hint then asks CanvasPage to +// navigate to chat — the chat page can read `canvas-chat-handoff` from localStorage. +const handoffToChat = (persona, prompt, context = {}) => { + try { + localStorage.setItem('canvas-chat-handoff', JSON.stringify({ + at: Date.now(), persona, prompt, ...context, + })); + } catch { /* ignore */ } + window.dispatchEvent(new CustomEvent('canvas-open-in-chat', { detail: { persona, prompt } })); + fireToast(`Opening ${persona} in chat — full history will be there.`); +}; + +// ---------- Reviewer 2 widget ---------- +export function Reviewer2Widget({ state, setState, openModal }) { + return ( + <> +
+ "Paste a draft paragraph, abstract, or section. I'll respond as the most uncharitable but technically competent reviewer your work will ever see." +
+ {state.lastReview ? ( +
+ Last critique · {state.lastReview.severity}/10 severity +
Major: {state.lastReview.major}
+
+{state.lastReview.minorCount} minor issues, {state.lastReview.suggestionCount} suggestions
+
+ ) : ( +
+ No drafts reviewed yet. +
+ )} +
+ tone +
+ harsh +
+
+ + +
+ + ); +} + +// ---------- Devil's Advocate widget ---------- +export function DevilsAdvocateWidget({ state, setState, openModal }) { + return ( + <> +
Your claim
+
"{state.claim}"
+
+ Strongest counters · {state.counters.length} +
+
+ {state.counters.slice(0, 3).map((c, i) => ( +
+
{c.lbl}
+
{c.text}
+
+ ))} +
+
+ + +
+ + ); +} + +// ---------- Scope realism widget ---------- +export function ScopeRealismWidget({ state, openModal }) { + return ( +
+
+
+
{state.score.toFixed(1)}
+
+
Feasibility
+
{state.label}
+
+
+
+ target: {state.target} +
+
+
+ {state.factors.map(f => ( +
+ {f.label} + + {f.val} +
+ ))} +
+
+ + +
+
+ ); +} + +// =================================================================== +// Critic modals +// =================================================================== + +const REVIEW_TEMPLATES = [ + { + severity: 8, + major: 'The hypothesis is presented before its operationalization. You write that L2/3 spiking "encodes" prediction error without specifying what spike-pattern feature you will measure (rate? latency? variance?), what range of values would count as "encoding," or what would falsify the claim.', + minor: [ + 'Sample size of n=4 animals is described as "preliminary" without a power calculation or a stopping rule.', + 'GLM with history kernel is presented as the analysis but no mention of how its outputs map to the proposed predictive-coding interpretation.', + 'No engagement with adaptation as a confound — the obvious alternative explanation for any oddball-driven decrease in firing.', + 'Reference to "predictive coding" is loose. Rao & Ballard, Bastos, and Friston make different commitments. Which one are you testing?', + ], + suggestions: [ + 'Add a single sentence specifying the measurable signature you predict, with directionality.', + 'Either control for arousal/pupil or acknowledge it as a limit upfront.', + 'Add a stopping rule and target effect size before scaling beyond n=4.', + ], + }, + { + severity: 7, + major: 'You claim the GLM analyses are "consistent with" the hypothesis. This phrase is doing too much work. Consistency with PE encoding is also consistency with at least three alternative explanations (adaptation, arousal, attention). Without a positive test that PE encoding predicts but the alternatives do not, "consistent with" is unfalsifiable.', + minor: [ + 'Mouse V1 is justified by convention rather than by what makes it the right model for this question.', + 'No statement of what would change your mind.', + 'Figure-free abstract for an empirical claim is a red flag for reviewers.', + ], + suggestions: [ + 'Replace "consistent with" with a specific signed prediction the data either matches or doesn\'t.', + 'List 1-2 results that, if observed, would refute the hypothesis.', + ], + }, + { + severity: 9, + major: 'This reads like an introduction, not an abstract. There is no result. There is no number. The strongest claim is that your "preliminary analyses are consistent" with your hypothesis — which is the lowest possible bar in empirical neuroscience. If the actual finding is interesting, lead with the finding, not with the framing.', + minor: [ + 'Word "preliminary" appears three times in three sentences. Cut two.', + '"Oddball stimulus paradigm" is jargon-without-citation; one sentence of definition or one citation, not zero.', + 'No mention of layer-specificity, despite L2/3 being the core claim.', + ], + suggestions: [ + 'Lead sentence: "We find that " — even if the effect is small, name it.', + 'Cut "we hypothesize that" entirely. Hypotheses go in the intro of the paper, not the abstract.', + ], + }, +]; + +export function ReviewerModal({ data, onClose }) { + const [draft, setDraft] = useState(data.initial || ''); + const [running, setRunning] = useState(false); + const [review, setReview] = useState(null); + + const run = () => { + if (!draft.trim()) return; + setRunning(true); + setReview(null); + setTimeout(() => { + const idx = (draft.length + draft.split(' ').length) % REVIEW_TEMPLATES.length; + const r = REVIEW_TEMPLATES[idx]; + setReview(r); + setRunning(false); + }, 900); + }; + + const accept = () => { + if (!review) return; + data.onComplete({ + draft, + severity: review.severity, + major: review.major, + minorCount: review.minor.length, + suggestionCount: review.suggestions.length, + }); + fireToast('Critique saved · severity ' + review.severity + '/10', 'critic'); + onClose(); + }; + + return ( +
e.stopPropagation()}> +
+
+
+
Reviewer 2 Simulator
+
Paste a draft. Get the harshest competent peer review you'll ever read — before a real reviewer does.
+
+ +
+
+
+ +