Skip to content
Closed
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
13 changes: 7 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ TESTSPRITE_GUIDE.md
# AI & LLM Agents
.gemini/

# Testing Caches
.pytest_cache/
.mypy_cache/

# Un-ignore the frontend lib folder which was caught by the Python lib/ rule
!frontend/src/lib/
# AI Agent Reports & Metadata
.agents/
impeccable.md
.impeccable.md
audit_report.md
critique_report.md
Team_T8_report_*.pdf
Binary file removed Team_T8_report_20260318.pdf
Binary file not shown.
118 changes: 52 additions & 66 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ export default function App() {
<div className="app-layout">
<Sidebar activeView={view} onViewChange={handleViewChange} user={user} onSignOut={signOut} />

<div className="main-wrapper">
<main className="main-wrapper">
<TopBar
onSearch={handleSearch}
searchQuery={searchQuery}
Expand All @@ -325,52 +325,42 @@ export default function App() {

{/* ========== DASHBOARD VIEW ========== */}
{view === 'dashboard' && (
<>
<div className="page-header">
<div className="page-header__row">
<div>
<h1>Overview</h1>
<h2>Protein Structure</h2>
</div>
<div className="page-header__actions">
<div className="page-header__date">
<span>🕐</span>
<span>24H</span>
<span style={{ marginLeft: 8 }}>📅</span>
<span>
{new Date().toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
</div>
<button
className="btn btn--primary"
onClick={() => handlePredict()}
disabled={status === 'processing'}
>
Prediction Request
<span style={{ fontSize: 18 }}>→</span>
</button>
</div>
<div className="dashboard-grid">
<div className="left-column">
<div className="mol-viewer-card">
<MolViewer pdbData={pdbData} status={status} />
<PredictionStatus status={status} error={error} />
<ConfidenceBar plddtData={plddtData} />
</div>
</div>

<div className="dashboard-grid">
<div className="left-column">
<div className="mol-viewer-card">
<MolViewer pdbData={pdbData} status={status} />
<PredictionStatus status={status} error={error} />
<ConfidenceBar plddtData={plddtData} />
<div className="right-column">
<div className="page-header">
<div className="page-header__row">
<div>
<h1>Overview</h1>
<h2>Protein Structure</h2>
</div>
<div className="page-header__actions">
<div className="page-header__date">
<span>🕐</span>
<span>24H</span>
<span style={{ marginLeft: 8 }}>📅</span>
<span>
{new Date().toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
</div>
</div>
</div>
</div>

<div className="right-column">
<SequenceInput
sequence={sequence}
setSequence={setSequence}
onPredict={() => handlePredict()}
onPredict={handlePredict}
status={status}
/>
<PldtMetrics plddtData={plddtData} sequence={sequence} />
Expand All @@ -392,7 +382,6 @@ export default function App() {
/>
</div>
</div>
</>
)}

{/* ========== DISCOVERY VIEW ========== */}
Expand Down Expand Up @@ -425,35 +414,33 @@ export default function App() {

{/* ========== ANALYSIS VIEW ========== */}
{view === 'analysis' && (
<>
<div className="page-header">
<div className="page-header__row">
<div>
<h1>{selectedProtein?.proteinName || 'Analyzing Protein…'}</h1>
<h2>
{selectedProtein
? `${selectedProtein.accession} · ${selectedProtein.organism}`
: 'Fetching protein data…'}
</h2>
</div>
<div className="page-header__actions">
<button className="btn btn--ghost" onClick={handleBackToSearch}>
← Back to Results
</button>
</div>
<div className="dashboard-grid">
<div className="left-column">
<div className="mol-viewer-card">
<MolViewer pdbData={pdbData} status={status} />
<PredictionStatus status={status} error={error} />
<ConfidenceBar plddtData={plddtData} />
</div>
</div>

<div className="dashboard-grid">
<div className="left-column">
<div className="mol-viewer-card">
<MolViewer pdbData={pdbData} status={status} />
<PredictionStatus status={status} error={error} />
<ConfidenceBar plddtData={plddtData} />
<div className="right-column">
<div className="page-header">
<div className="page-header__row">
<div>
<h1>{selectedProtein?.proteinName || 'Analyzing Protein…'}</h1>
<h2>
{selectedProtein
? `${selectedProtein.accession} · ${selectedProtein.organism}`
: 'Fetching protein data…'}
</h2>
</div>
<div className="page-header__actions">
<button className="btn btn--ghost" onClick={handleBackToSearch}>
← Back to Results
</button>
</div>
</div>
</div>

<div className="right-column">
<ProteinBio protein={selectedProtein} />
<LabReadiness metrics={labMetrics} isLoading={labLoading} />
<PldtMetrics plddtData={plddtData} sequence={sequence} />
Expand All @@ -474,9 +461,8 @@ export default function App() {
</div>
</div>
</div>
</>
)}
</div>
</main>

<Toast toasts={toasts} onDismiss={dismissToast} />
</div>
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/components/MolViewer.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { useEffect, useRef, useState, useCallback, memo } from 'react';

const VIZ_STYLES = [
{ key: 'cartoon', label: 'Cartoon', icon: '🎨' },
Expand All @@ -7,7 +7,7 @@ const VIZ_STYLES = [
{ key: 'surface', label: 'Surface', icon: '◐' },
];

export default function MolViewer({ pdbData, status }) {
export default memo(function MolViewer({ pdbData, status }) {
const containerRef = useRef(null);
const viewerRef = useRef(null);
const [vizStyle, setVizStyle] = useState('cartoon');
Expand Down Expand Up @@ -117,6 +117,7 @@ export default function MolViewer({ pdbData, status }) {
key={s.key}
className={`mol-viewer__style-btn${vizStyle === s.key ? ' mol-viewer__style-btn--active' : ''}`}
title={s.label}
aria-label={s.label}
onClick={() => {
setVizStyle(s.key);
applyStyle(s.key);
Expand All @@ -131,6 +132,7 @@ export default function MolViewer({ pdbData, status }) {
<button
className="mol-viewer__overlay-btn"
title="Fullscreen"
aria-label="Fullscreen"
onClick={() => containerRef.current?.requestFullscreen?.()}
>
<svg
Expand All @@ -152,6 +154,7 @@ export default function MolViewer({ pdbData, status }) {
<button
className="mol-viewer__overlay-btn"
title="Reset view"
aria-label="Reset view"
onClick={() => {
if (viewerRef.current) {
viewerRef.current.zoomTo();
Expand Down Expand Up @@ -180,4 +183,4 @@ export default function MolViewer({ pdbData, status }) {
)}
</div>
);
}
});
4 changes: 2 additions & 2 deletions frontend/src/components/PldtMetrics.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default function PldtMetrics({ plddtData, sequence }) {
<div className="card__title">
<span
className="card__title-icon"
style={{ background: 'rgba(27, 37, 89, 0.08)', color: 'var(--navy)' }}
style={{ color: 'var(--accent)' }}
>
❤️
</span>
Expand Down Expand Up @@ -120,7 +120,7 @@ export default function PldtMetrics({ plddtData, sequence }) {
padding: '2px 10px',
fontWeight: 700,
fontSize: 13,
color: 'var(--navy)',
color: 'var(--text-primary)',
}}
>
{mean !== null ? mean.toFixed(2) : '—'}
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/ProteinMetrics.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function ProteinMetrics({ plddtData, seqLength }) {
<div className="card__title">
<span
className="card__title-icon"
style={{ background: 'rgba(27, 37, 89, 0.08)', color: 'var(--navy)' }}
style={{ color: 'var(--accent)' }}
>
📊
</span>
Expand Down Expand Up @@ -81,7 +81,7 @@ export default function ProteinMetrics({ plddtData, seqLength }) {
cy="60"
r="50"
fill="none"
stroke="var(--navy)"
stroke="var(--text-primary)"
strokeWidth="8"
strokeLinecap="round"
strokeDasharray={`${(plddtData.mean / 100) * 314} 314`}
Expand All @@ -95,7 +95,7 @@ export default function ProteinMetrics({ plddtData, seqLength }) {
textAnchor="middle"
fontSize="22"
fontWeight="800"
fill="var(--navy)"
fill="var(--text-primary)"
>
{plddtData ? Math.round(plddtData.mean) : '—'}
</text>
Expand Down
37 changes: 26 additions & 11 deletions frontend/src/components/SequenceInput.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useState, memo } from 'react';

const defaultAminoAcids = 'A,C,D,E,F,G,H,I,K,L,M,N,P,Q,R,S,T,V,W,Y';
const aminoAcidsStr = (import.meta.env.VITE_VALID_AMINO_ACIDS || defaultAminoAcids).replace(
Expand All @@ -7,20 +7,33 @@ const aminoAcidsStr = (import.meta.env.VITE_VALID_AMINO_ACIDS || defaultAminoAci
);
const VALID_AMINO_ACIDS = new Set(aminoAcidsStr.split(''));

export default function SequenceInput({ sequence, setSequence, onPredict, status }) {
const SequenceInput = memo(function SequenceInput({ sequence: externalSeq, setSequence: setExternalSeq, onPredict, status }) {
const [localSeq, setLocalSeq] = useState(externalSeq || '');
const [prevExternalSeq, setPrevExternalSeq] = useState(externalSeq);

if (externalSeq !== prevExternalSeq) {
setPrevExternalSeq(externalSeq);
setLocalSeq(externalSeq || '');
}

const isLoading = status === 'processing';
const charCount = sequence.trim().length;
const charCount = localSeq.trim().length;

const invalidChars = useMemo(() => {
const bad = new Set();
for (const ch of sequence) {
for (const ch of localSeq) {
if (!VALID_AMINO_ACIDS.has(ch)) bad.add(ch);
}
return [...bad].sort();
}, [sequence]);
}, [localSeq]);

const hasInvalid = invalidChars.length > 0;

const handlePredictClicked = () => {
setExternalSeq(localSeq);
onPredict(localSeq);
};

return (
<div className="card" id="sequence-input-card">
<div className="card__header">
Expand All @@ -40,7 +53,7 @@ export default function SequenceInput({ sequence, setSequence, onPredict, status
onClick={async () => {
try {
const text = await navigator.clipboard.readText();
if (text) setSequence(text.toUpperCase().replace(/[^A-Z]/g, ''));
if (text) setLocalSeq(text.toUpperCase().replace(/[^A-Z]/g, ''));
} catch {
/* clipboard permission denied */
}
Expand All @@ -60,7 +73,7 @@ export default function SequenceInput({ sequence, setSequence, onPredict, status
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
</button>
<button className="card__action-btn" title="Clear" onClick={() => setSequence('')}>
<button className="card__action-btn" title="Clear" onClick={() => setLocalSeq('')}>
</button>
</div>
Expand All @@ -71,8 +84,8 @@ export default function SequenceInput({ sequence, setSequence, onPredict, status
id="sequence-textarea"
className="sequence-input__textarea"
placeholder="Paste your amino-acid sequence here (single letter codes: A, C, D, E, F, G, H, I, K, L, M, N, P, Q, R, S, T, V, W, Y)…"
value={sequence}
onChange={(e) => setSequence(e.target.value.toUpperCase().replace(/[^A-Z]/g, ''))}
value={localSeq}
onChange={(e) => setLocalSeq(e.target.value.toUpperCase().replace(/[^A-Z]/g, ''))}
spellCheck={false}
style={hasInvalid ? { borderColor: 'var(--warning)' } : undefined}
/>
Expand Down Expand Up @@ -104,7 +117,7 @@ export default function SequenceInput({ sequence, setSequence, onPredict, status
<button
id="predict-btn"
className="sequence-input__predict-btn"
onClick={onPredict}
onClick={handlePredictClicked}
disabled={isLoading || charCount < 10 || hasInvalid}
>
{isLoading ? (
Expand Down Expand Up @@ -134,4 +147,6 @@ export default function SequenceInput({ sequence, setSequence, onPredict, status
</div>
</div>
);
}
});

export default SequenceInput;
Loading
Loading