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
15 changes: 3 additions & 12 deletions src/app/coaching-sessions/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import { OverarchingGoalContainer } from "@/components/ui/coaching-sessions/over
import { CoachingTabsContainer } from "@/components/ui/coaching-sessions/coaching-tabs-container";
import { EditorCacheProvider } from "@/components/ui/coaching-sessions/editor-cache-context";

import CoachingSessionSelector from "@/components/ui/coaching-session-selector";
import { useRouter, useParams, useSearchParams } from "next/navigation";
import { useCurrentCoachingRelationship } from "@/lib/hooks/use-current-coaching-relationship";
import { useCurrentCoachingSession } from "@/lib/hooks/use-current-coaching-session";
import ShareSessionLink from "@/components/ui/share-session-link";
import { MeetingControls } from "@/components/ui/coaching-sessions/meeting-controls";
import { toast } from "sonner";
import { ForbiddenError } from "@/components/ui/errors/forbidden-error";
import { EntityApiError } from "@/types/general";
Expand Down Expand Up @@ -87,11 +87,6 @@ export default function CoachingSessionsPage() {
);
}

const handleCoachingSessionSelect = (coachingSessionId: string) => {
console.debug("coachingSessionId selected: " + coachingSessionId);
router.push(`/coaching-sessions/${coachingSessionId}`);
};

const handleShareError = (error: Error) => {
toast.error("Failed to copy session link.");
};
Expand Down Expand Up @@ -122,16 +117,12 @@ export default function CoachingSessionsPage() {
locale={siteConfig.locale}
style={siteConfig.titleStyle}
/>
<div className="ml-auto flex w-full sm:max-w-sm md:max-w-md items-center gap-3 sm:justify-end md:justify-start">
<div className="ml-auto flex items-center gap-3">
<MeetingControls sessionId={currentCoachingSessionId || ""} />
<ShareSessionLink
sessionId={params.id as string}
onError={handleShareError}
/>
<CoachingSessionSelector
relationshipId={currentCoachingRelationshipId}
disabled={!currentCoachingRelationshipId}
onSelect={handleCoachingSessionSelect}
/>
</div>
</div>
</div>
Expand Down
32 changes: 32 additions & 0 deletions src/app/settings/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Metadata } from "next";
import "@/styles/globals.css";
import { siteConfig } from "@/site.config.ts";

import { SiteHeader } from "@/components/ui/site-header";
import { AppSidebar } from "@/components/ui/app-sidebar";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { Toaster } from "@/components/ui/sonner";

export const metadata: Metadata = {
title: `Settings | ${siteConfig.name}`,
description: "Manage your account and integration settings",
};

export default function SettingsLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<SidebarProvider>
<div className="flex min-h-screen min-w-full">
<AppSidebar />
<SidebarInset>
<SiteHeader />
<main className="flex-1 p-6">{children}</main>
<Toaster />
</SidebarInset>
</div>
</SidebarProvider>
);
}
12 changes: 12 additions & 0 deletions src/app/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";

import { PageContainer } from "@/components/ui/page-container";
import { SettingsContainer } from "@/components/ui/settings/settings-container";

export default function SettingsPage() {
return (
<PageContainer>
<SettingsContainer />
</PageContainer>
);
}
2 changes: 1 addition & 1 deletion src/components/ui/coaching-session-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export default function CoachingSessionSelector({
onValueChange={handleSetCoachingSession}
>
<SelectTrigger
className="w-full min-w-0 py-6 pr-2"
className="w-full min-w-0 py-6"
id="coaching-session-selector"
>
<SelectValue className="truncate" placeholder="Select coaching session">
Expand Down
8 changes: 5 additions & 3 deletions src/components/ui/coaching-sessions/actions-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,10 @@ const ActionsList: React.FC<{
</TableCell>
<TableCell className="hidden md:table-cell">
{action.due_by
.setLocale(siteConfig.locale)
.toLocaleString(DateTime.DATE_MED)}
? action.due_by
.setLocale(siteConfig.locale)
.toLocaleString(DateTime.DATE_MED)
: "—"}
</TableCell>
<TableCell className="hidden md:table-cell">
{action.created_at
Expand All @@ -330,7 +332,7 @@ const ActionsList: React.FC<{
setEditingActionId(action.id);
setNewBody(action.body ?? "");
setNewStatus(action.status);
setNewDueBy(action.due_by);
setNewDueBy(action.due_by ?? DateTime.now());
}}
>
Edit
Expand Down
207 changes: 207 additions & 0 deletions src/components/ui/coaching-sessions/ai-suggestion-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
"use client";

import { useState } from "react";
import { Check, X, Loader2, Target, Handshake, Users } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/components/lib/utils";
import { AiSuggestedItem, AiSuggestionType } from "@/types/meeting-recording";
import { useAiSuggestionMutation } from "@/lib/api/ai-suggestions";
import { toast } from "sonner";
import { Id } from "@/types/general";

interface AiSuggestionCardProps {
suggestion: AiSuggestedItem;
onAction?: () => void;
className?: string;
/** Coach user ID for displaying assignee labels */
coachId?: Id;
/** Coachee user ID for displaying assignee labels */
coacheeId?: Id;
}

/**
* Gets a display label for a user ID (Coach, Coachee, or unknown).
*/
function getUserLabel(userId: Id | null, coachId?: Id, coacheeId?: Id): string | null {
if (!userId) return null;
if (coachId && userId === coachId) return "Coach";
if (coacheeId && userId === coacheeId) return "Coachee";
return null;
}

/**
* Renders a single AI suggestion with accept/dismiss actions.
* When accepted, creates the corresponding Action or Agreement.
*/
export function AiSuggestionCard({
suggestion,
onAction,
className,
coachId,
coacheeId,
}: AiSuggestionCardProps) {
const [isAccepting, setIsAccepting] = useState(false);
const [isDismissing, setIsDismissing] = useState(false);
const { accept, dismiss } = useAiSuggestionMutation();

const isAction = suggestion.item_type === AiSuggestionType.Action;
const Icon = isAction ? Target : Handshake;
const typeLabel = isAction ? "Action" : "Agreement";

// Get display labels for stated_by and assigned_to users
const statedByLabel = getUserLabel(suggestion.stated_by_user_id, coachId, coacheeId);
const assignedToLabel = getUserLabel(suggestion.assigned_to_user_id, coachId, coacheeId);

const handleAccept = async () => {
setIsAccepting(true);
try {
const result = await accept(suggestion.id);
toast.success(`${typeLabel} added successfully`, {
description: `Created new ${result.entity_type} from AI suggestion.`,
});
onAction?.();
} catch (error) {
toast.error(`Failed to add ${typeLabel.toLowerCase()}`, {
description: error instanceof Error ? error.message : "Please try again.",
});
} finally {
setIsAccepting(false);
}
};

const handleDismiss = async () => {
setIsDismissing(true);
try {
await dismiss(suggestion.id);
toast.info("Suggestion dismissed");
onAction?.();
} catch (error) {
toast.error("Failed to dismiss suggestion", {
description: error instanceof Error ? error.message : "Please try again.",
});
} finally {
setIsDismissing(false);
}
};

const isLoading = isAccepting || isDismissing;

return (
<Card className={cn("border-l-4", isAction ? "border-l-blue-500" : "border-l-green-500", className)}>
<CardContent className="p-4">
<div className="flex items-start gap-3">
{/* Icon */}
<div className={cn(
"flex-shrink-0 p-2 rounded-full",
isAction ? "bg-blue-100 dark:bg-blue-900" : "bg-green-100 dark:bg-green-900"
)}>
<Icon className={cn(
"h-4 w-4",
isAction ? "text-blue-600 dark:text-blue-400" : "text-green-600 dark:text-green-400"
)} />
</div>

{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<Badge variant="secondary" className="text-xs">
{typeLabel}
</Badge>

{/* Assignee badges for actions */}
{isAction && assignedToLabel && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="outline" className="text-xs gap-1">
→ {assignedToLabel}
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>Assigned to {assignedToLabel}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}

{/* Mutual commitment badge for agreements */}
{!isAction && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="outline" className="text-xs gap-1 border-green-500/50 text-green-700 dark:text-green-400">
<Users className="h-3 w-3" />
Mutual
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>Mutual commitment between coach and coachee</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}

{/* Stated by badge (if known) */}
{statedByLabel && (
<span className="text-xs text-muted-foreground">
Stated by {statedByLabel}
</span>
)}

{suggestion.confidence && (
<span className="text-xs text-muted-foreground">
{Math.round(suggestion.confidence * 100)}% confident
</span>
)}
</div>
<p className="text-sm text-foreground">{suggestion.content}</p>
{suggestion.source_text && (
<p className="mt-1 text-xs text-muted-foreground italic line-clamp-2">
&ldquo;{suggestion.source_text}&rdquo;
</p>
)}
</div>

{/* Actions */}
<div className="flex-shrink-0 flex gap-2">
<Button
size="sm"
variant="outline"
onClick={handleAccept}
disabled={isLoading}
className="h-8 gap-1"
>
{isAccepting ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Check className="h-3 w-3" />
)}
Add
</Button>
<Button
size="sm"
variant="ghost"
onClick={handleDismiss}
disabled={isLoading}
className="h-8 gap-1 text-muted-foreground hover:text-destructive"
>
{isDismissing ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<X className="h-3 w-3" />
)}
</Button>
</div>
</div>
</CardContent>
</Card>
);
}
Loading