Skip to content
Merged
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
4 changes: 1 addition & 3 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -273,18 +273,16 @@
font-family: "Inter", "SF Pro Display", system-ui, sans-serif;
}

/* Prevent scrollbar flash when panels/modals open */
/* Ensure full height layout */
html,
body {
overflow: hidden;
height: 100%;
width: 100%;
}

#root {
height: 100%;
width: 100%;
overflow: hidden;
}

/* Smooth scrolling */
Expand Down
86 changes: 60 additions & 26 deletions frontend/src/pages/SessionDetail/ContentSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,84 @@ import React from "react";
import type { ContentSectionProps } from "./types";
import { CopyButton } from "./CopyButton";

const wordCount = (text: string) =>
text.trim().split(/\s+/).filter(Boolean).length;

export const ContentSection: React.FC<ContentSectionProps> = ({
title,
content,
copyLabel,
icon,
variant = "summary",
}) => {
const isEmpty = !content;
const words = content ? wordCount(content) : 0;

const isSummary = variant === "summary";

const accentClass = isSummary
? "from-brand-400 to-purple-500"
: "from-emerald-400 to-teal-500";

const iconColorClass = isSummary ? "text-brand-500" : "text-emerald-600";

const emptyIconBgClass = isSummary ? "bg-brand-50" : "bg-emerald-50";
const emptyIconColorClass = isSummary ? "text-brand-300" : "text-emerald-300";

const emptyHeading = isSummary
? "Summary not yet generated"
: "No transcript available";
const emptySub = isSummary
? "Will appear once the AI processes the transcript"
: "Will appear once the audio is processed";

const filename = isSummary ? "summary.txt" : "transcript.txt";

return (
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100">
<div className="flex items-center gap-2">
<span className="text-brand-500">{icon}</span>
{/* Accent top bar */}
<div className={`h-0.5 bg-linear-to-r ${accentClass}`} />

{/* Header */}
<div className="flex items-center justify-between px-6 py-4">
<div className="flex items-center gap-2.5">
<span className={iconColorClass}>{icon}</span>
<h2 className="text-base font-semibold text-gray-900">{title}</h2>
{!isEmpty && (
<span className="text-xs text-gray-400 bg-gray-50 px-2.5 py-0.5 rounded-full border border-gray-100 font-medium">
{words.toLocaleString()} {words === 1 ? "word" : "words"}
</span>
)}
</div>
{!isEmpty && <CopyButton text={content!} label={copyLabel} />}
</div>
<div className="px-6 py-5">

{/* Body */}
<div className="px-6 pb-6">
{isEmpty ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<div className="w-10 h-10 bg-gray-50 rounded-xl flex items-center justify-center mb-3">
<svg
className="w-5 h-5 text-gray-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div className="flex flex-col items-center justify-center py-10 text-center">
<div
className={`w-12 h-12 rounded-xl flex items-center justify-center mb-3 ${emptyIconBgClass}`}
>
<span className={emptyIconColorClass}>{icon}</span>
</div>
<p className="text-sm text-gray-400">Not available yet</p>
<p className="text-xs text-gray-300 mt-1">
This will appear once the session is processed
</p>
<p className="text-sm font-medium text-gray-500">{emptyHeading}</p>
<p className="text-xs text-gray-400 mt-1">{emptySub}</p>
</div>
) : (
<pre className="whitespace-pre-wrap text-sm text-gray-700 font-mono leading-relaxed max-h-96 overflow-y-auto">
{content}
</pre>
<div className="bg-gray-50 rounded-xl border border-gray-100 overflow-hidden">
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-gray-100 bg-white">
<div className="w-2.5 h-2.5 rounded-full bg-red-300" />
<div className="w-2.5 h-2.5 rounded-full bg-yellow-300" />
<div className="w-2.5 h-2.5 rounded-full bg-green-300" />
<span className="ml-2 text-xs text-gray-400 font-mono">
{filename}
</span>
</div>
<pre className="whitespace-pre-wrap text-sm text-gray-700 font-mono leading-7 p-5 max-h-130 overflow-y-auto">
{content}
</pre>
</div>
)}
</div>
</div>
Expand Down
144 changes: 94 additions & 50 deletions frontend/src/pages/SessionDetail/SessionContent.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,109 @@
import React, { useState } from "react";
import { SessionHeader } from "./SessionHeader";
import { ErrorNotice } from "./ErrorNotice";
import { ContentSection } from "./ContentSection";
import type { SummarizerSession } from "../../types/user.types";

type Tab = "summary" | "transcript";

interface SessionContentProps {
session: SummarizerSession;
onDelete: () => void;
}

const SummaryIcon = (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
);

const TranscriptIcon = (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
);

export const SessionContent: React.FC<SessionContentProps> = ({
session,
onDelete,
}) => (
<div className="space-y-6 animate-fade-in">
<SessionHeader session={session} onDelete={onDelete} />
}) => {
const [activeTab, setActiveTab] = useState<Tab>("summary");

{session.error && <ErrorNotice error={session.error} onDelete={onDelete} />}
const tabs: { id: Tab; label: string; hasContent: boolean }[] = [
{ id: "summary", label: "Summary", hasContent: !!session.summary },
{ id: "transcript", label: "Transcript", hasContent: !!session.transcript },
];

{!session.error && (
<>
<ContentSection
title="Summary"
content={session.summary}
copyLabel="Summary"
icon={
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
}
/>
<ContentSection
title="Transcript"
content={session.transcript}
copyLabel="Transcript"
icon={
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
}
/>
</>
)}
</div>
);
return (
<div className="space-y-6 animate-fade-in">
<SessionHeader session={session} onDelete={onDelete} />

{session.error && (
<ErrorNotice error={session.error} onDelete={onDelete} />
)}

{/* Tab bar */}
<div className="flex items-center gap-1 bg-gray-100 p-1 rounded-xl w-fit">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`relative flex items-center gap-2 px-5 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
activeTab === tab.id
? "bg-white text-gray-900 shadow-sm"
: "text-gray-500 hover:text-gray-700"
}`}
>
{tab.label}
<span
className={`w-1.5 h-1.5 rounded-full transition-colors duration-200 ${
tab.hasContent ? "bg-emerald-400" : "bg-gray-300"
}`}
/>
</button>
))}
</div>

{/* Active section — key forces fade-in on tab switch */}
<div key={activeTab} className="animate-fade-in">
{activeTab === "summary" ? (
<ContentSection
title="Summary"
content={session.summary}
copyLabel="Summary"
variant="summary"
icon={SummaryIcon}
/>
) : (
<ContentSection
title="Transcript"
content={session.transcript}
copyLabel="Transcript"
variant="transcript"
icon={TranscriptIcon}
/>
)}
</div>
</div>
);
};
1 change: 1 addition & 0 deletions frontend/src/pages/SessionDetail/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface ContentSectionProps {
content: string | null;
copyLabel: string;
icon: React.ReactNode;
variant?: "summary" | "transcript";
}

export interface SessionHeaderProps {
Expand Down