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: 2 additions & 2 deletions client/src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ export function ChatInput({ onSend, onStop, isLoading, placeholder = "Ask about
};

return (
<form onSubmit={handleSubmit} className="flex gap-2 items-end" data-testid="form-chat">
<div className="flex-1 relative">
<form onSubmit={handleSubmit} className="flex min-w-0 items-end gap-2" data-testid="form-chat">
<div className="relative min-w-0 flex-1">
<Textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/CoachPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export function CoachPanel({ isOpen, onClose, timeline = [], isNewUser = false }
if (!isOpen) return null;

return (
<div className="flex flex-col h-full border-l bg-background">
<div className="flex h-full min-h-0 w-full flex-col overflow-hidden border-l bg-background">
<CoachPanelHeader
onClearHistory={handleClearHistory}
onClose={onClose}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/coach/CoachPanelFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function CoachPanelFooter({
isProcessing,
}: Readonly<CoachPanelFooterProps>) {
return (
<div className="flex-shrink-0 p-2 border-t space-y-2">
<div className="flex-shrink-0 space-y-2 border-t p-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))]">
<QuickActions actions={quickActions} onSelect={onQuickAction} disabled={isProcessing} />
<ChatInput onSend={onSendMessage} onStop={onStopMessage} isLoading={isProcessing} />
</div>
Expand Down
16 changes: 11 additions & 5 deletions client/src/components/coach/CoachPanelHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ export function CoachPanelHeader({

return (
<TooltipProvider>
<div className="flex items-center justify-between gap-2 p-3 border-b flex-shrink-0">
<div className="flex items-center gap-2">
<MessageSquare className="h-4 w-4 text-primary" />
<span className="font-semibold text-sm">AI Coach</span>
<Badge variant="secondary" data-testid="badge-active-style">Active style: {activeStyleLabel}</Badge>
<div className="flex flex-shrink-0 items-center justify-between gap-2 border-b p-3">
<div className="flex min-w-0 items-center gap-2">
<MessageSquare className="h-4 w-4 shrink-0 text-primary" />
<span className="shrink-0 text-sm font-semibold">AI Coach</span>
<Badge
variant="secondary"
className="min-w-0 truncate"
data-testid="badge-active-style"
>
Active style: {activeStyleLabel}
</Badge>
</div>
<div className="flex items-center gap-1">
{canClearHistory && (
Expand Down
20 changes: 13 additions & 7 deletions client/src/components/ui/responsive-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
readonly description?: React.ReactNode;
readonly children: React.ReactNode;
readonly contentClassName?: string;
readonly mobileFullHeight?: boolean;
readonly testId?: string;
}

Expand All @@ -34,6 +35,7 @@
description,
children,
contentClassName,
mobileFullHeight = false,
testId,
}: ResponsiveSheetProps) {
const isMobile = useIsMobile();
Expand All @@ -44,20 +46,24 @@
<SheetContent
side="bottom"
className={cn(
"max-h-[90vh] overflow-y-auto rounded-t-2xl px-4 pb-6 pt-5",
mobileFullHeight
? "flex h-[100dvh] max-h-[100dvh] flex-col overflow-hidden rounded-none p-0"
: "max-h-[90vh] overflow-y-auto rounded-t-2xl px-4 pb-6 pt-5",
contentClassName,
)}
data-testid={testId}
>
<div
className="mx-auto mb-2 h-1 w-10 rounded-full bg-muted-foreground/40"
aria-hidden="true"
/>
<SheetHeader className="text-left">
{!mobileFullHeight ? (

Check warning on line 56 in client/src/components/ui/responsive-sheet.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=daler91_Hyrox-Companion&issues=AZ4jVnQp_yjOFvuDQeN5&open=AZ4jVnQp_yjOFvuDQeN5&pullRequest=1171
<div
className="mx-auto mb-2 h-1 w-10 rounded-full bg-muted-foreground/40"
aria-hidden="true"
/>
) : null}
<SheetHeader className={cn("shrink-0 text-left", mobileFullHeight && "sr-only")}>
<SheetTitle>{title}</SheetTitle>
{description ? <SheetDescription>{description}</SheetDescription> : null}
</SheetHeader>
<div className="mt-4">{children}</div>
<div className={mobileFullHeight ? "min-h-0 flex-1" : "mt-4"}>{children}</div>
</SheetContent>
</Sheet>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,29 +77,29 @@ export function EmbeddedWorkoutCoachChat({
hidden={isHidden}
className={cn(
"flex w-full min-w-0 self-start flex-col rounded-lg border border-border bg-card",
isPanelView && "min-h-[65vh]",
isPanelView && "min-h-0",
className,
)}
aria-label="Coach chat about this workout"
data-testid="embedded-workout-coach-chat"
>
<header className="flex items-center gap-2 border-b border-border px-3 py-2">
<header className="flex min-w-0 shrink-0 items-center gap-2 border-b border-border px-3 py-2">
<Button
type="button"
variant="ghost"
size={backButtonText ? "sm" : "icon"}
className={cn("text-muted-foreground", backButtonText ? "h-8 px-2" : "size-7")}
className={cn("shrink-0 text-muted-foreground", backButtonText ? "h-8 px-2" : "size-7")}
onClick={onBack}
aria-label="Back to workout details"
data-testid="embedded-workout-coach-chat-back"
>
<ArrowLeft className="size-4" aria-hidden="true" />
{backButtonText ? <span>{backButtonText}</span> : null}
</Button>
<Sparkles className="size-3.5 text-primary" aria-hidden="true" />
<Sparkles className="size-3.5 shrink-0 text-primary" aria-hidden="true" />
<div className="min-w-0 text-xs font-medium uppercase text-muted-foreground">
<span className="sr-only">Asking about </span>
<span className="truncate text-foreground">{entry.focus?.trim() || "This workout"}</span>
<span className="block truncate text-foreground">{entry.focus?.trim() || "This workout"}</span>
</div>
</header>

Expand All @@ -115,7 +115,7 @@ export function EmbeddedWorkoutCoachChat({
onDismissSuggestion={noopId}
/>

<div className="border-t border-border p-2">
<div className="shrink-0 border-t border-border p-2">
<ChatInput
onSend={handleSend}
onStop={isStreaming ? cancelStream : undefined}
Expand Down
1 change: 1 addition & 0 deletions client/src/components/workout-detail/LogSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ export function LogSheet({
title={title}
description={formatScheduledDate(entry.date)}
contentClassName={coachChatOpen ? "sm:max-w-5xl" : "sm:max-w-2xl"}
mobileFullHeight={coachPanel.coachPanelOpen}
testId={`log-sheet-${entry.id}`}
>
<WorkoutCoachLayout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function ReadOnlyWorkoutDetailSheet({
title={title}
description={formatScheduledDate(entry.date)}
contentClassName={coachChatOpen ? "sm:max-w-5xl" : undefined}
mobileFullHeight={coachPanel.coachPanelOpen}
testId={sheetTestId}
>
<WorkoutCoachLayout
Expand Down
1 change: 1 addition & 0 deletions client/src/components/workout-detail/ReviewSurface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export function ReviewSurface({
}
description={formatScheduledDate(entry.date)}
contentClassName={sheetContentClassName}
mobileFullHeight={coachPanel.coachPanelOpen}
testId={`review-surface-${entry.id}`}
>
<WorkoutCoachLayout
Expand Down
22 changes: 20 additions & 2 deletions client/src/components/workout-detail/WorkoutCoachPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import { MobileCoachToggle } from "./MobileCoachToggle";

const SPLIT_WORKOUT_COACH_LAYOUT =
"grid grid-cols-1 items-start gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(320px,400px)]";
const MOBILE_WORKOUT_COACH_LAYOUT = "flex min-h-0 flex-1 flex-col";
const STACKED_WORKOUT_COACH_LAYOUT = "space-y-4";
const MOBILE_COACH_CHAT_AREA = "max-h-none flex-1";
const MOBILE_COACH_CARD =
"min-h-0 flex-1 self-stretch overflow-hidden rounded-none border-0 bg-background";
const MOBILE_COACH_CHAT_AREA = "min-h-0 max-h-none flex-1";

interface WorkoutCoachPanelStateArgs {
readonly coachChatOpen: boolean;
Expand Down Expand Up @@ -55,7 +58,7 @@ export function getWorkoutCoachPanelState({
chatHidden: showingMobileDetailsWithChat,
coachPanelOpen,
detailsHidden: coachPanelOpen,
layoutClassName: coachChatOpen && !isMobile ? SPLIT_WORKOUT_COACH_LAYOUT : STACKED_WORKOUT_COACH_LAYOUT,
layoutClassName: getWorkoutCoachLayoutClassName({ coachChatOpen, coachPanelOpen, isMobile }),
returnButtonVisible: showingMobileDetailsWithChat,
};
}
Expand Down Expand Up @@ -103,12 +106,27 @@ export function WorkoutCoachChatPanel({
onBack={getCoachBackHandler(panelState.coachPanelOpen, onShowWorkoutDetails, onCloseCoachChat)}
backButtonText={panelState.coachPanelOpen ? "Workout details" : undefined}
chatAreaClassName={panelState.coachPanelOpen ? MOBILE_COACH_CHAT_AREA : undefined}
className={panelState.coachPanelOpen ? MOBILE_COACH_CARD : undefined}
isHidden={panelState.chatHidden}
isPanelView={panelState.coachPanelOpen}
/>
);
}

function getWorkoutCoachLayoutClassName({
coachChatOpen,
coachPanelOpen,
isMobile,
}: {
readonly coachChatOpen: boolean;
readonly coachPanelOpen: boolean;
readonly isMobile: boolean;
}): string {
if (coachPanelOpen) return MOBILE_WORKOUT_COACH_LAYOUT;
if (coachChatOpen && !isMobile) return SPLIT_WORKOUT_COACH_LAYOUT;
return STACKED_WORKOUT_COACH_LAYOUT;
}

function getCoachBackHandler(
coachPanelOpen: boolean,
onShowWorkoutDetails?: () => void,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,12 @@ describe("EmbeddedWorkoutCoachChat", () => {
seedNonce={1}
onBack={onBack}
backButtonText="Workout details"
chatAreaClassName="max-h-none flex-1"
chatAreaClassName="min-h-0 max-h-none flex-1"
isPanelView
/>,
);

expect(screen.getByTestId("embedded-workout-coach-chat")).toHaveClass("min-h-[65vh]");
expect(screen.getByTestId("embedded-workout-coach-chat")).toHaveClass("min-h-0");
expect(screen.getByTestId("embedded-workout-coach-chat-back")).toHaveTextContent(
"Workout details",
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ const viewportState = vi.hoisted(() => ({ isMobile: false }));
vi.mock("@/components/ui/responsive-sheet", () => ({
ResponsiveSheet: ({
children,
mobileFullHeight,
title,
description,
}: {
children: ReactNode;
mobileFullHeight?: boolean;
title: ReactNode;
description?: ReactNode;
}) => (
<section>
<section data-testid="responsive-sheet" data-mobile-full-height={String(!!mobileFullHeight)}>
<h1>{title}</h1>
{description ? <p>{description}</p> : null}
{children}
Expand Down Expand Up @@ -106,6 +108,10 @@ describe("PreviewSheet", () => {
);

expect(screen.getByTestId("preview-details-entry-1")).toHaveAttribute("hidden");
expect(screen.getByTestId("responsive-sheet")).toHaveAttribute(
"data-mobile-full-height",
"true",
);
expect(screen.getByTestId("embedded-workout-coach-chat")).not.toHaveAttribute("hidden");
expect(screen.getByTestId("embedded-workout-coach-chat")).toHaveAttribute(
"data-panel-view",
Expand Down Expand Up @@ -137,6 +143,10 @@ describe("PreviewSheet", () => {
);

expect(screen.getByTestId("preview-details-entry-1")).not.toHaveAttribute("hidden");
expect(screen.getByTestId("responsive-sheet")).toHaveAttribute(
"data-mobile-full-height",
"false",
);
expect(screen.getByTestId("embedded-workout-coach-chat")).toHaveAttribute("hidden");

await user.click(screen.getByTestId("preview-return-to-coach-entry-1"));
Expand Down
7 changes: 2 additions & 5 deletions client/src/pages/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -767,16 +767,13 @@ export default function Timeline() {
skippedEntry ||
adhocOpen
? "hidden"
: "fixed inset-x-0 bottom-0 z-50 h-[70vh]"
: "fixed inset-0 z-50 h-[100dvh]"
}
>
<div
data-testid="coach-panel-mobile-sheet"
className="relative h-full bg-background shadow-2xl rounded-t-2xl border-t border-x"
className="relative h-full bg-background shadow-2xl"
>
<div className="flex items-center justify-center pt-2" aria-hidden="true">
<div className="h-1 w-10 rounded-full bg-muted-foreground/40" />
</div>
<FeatureErrorBoundaryWrapper featureName="Coach">
<CoachPanel
isOpen={coachOpen}
Expand Down
Loading