mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-02 19:55:18 +02:00
refactor: enhance UI for podcast card and audio components
This commit is contained in:
parent
3cef7ef87c
commit
8510a76e9a
2 changed files with 152 additions and 246 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { DownloadIcon, PauseIcon, PlayIcon, Volume2Icon, VolumeXIcon } from "lucide-react";
|
import { DownloadIcon, PauseIcon, PlayIcon, Volume2Icon, VolumeXIcon } from "lucide-react";
|
||||||
import Image from "next/image";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
|
@ -12,8 +11,6 @@ interface AudioProps {
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
src: string;
|
src: string;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
|
||||||
artwork?: string;
|
|
||||||
durationMs?: number;
|
durationMs?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +22,7 @@ function formatTime(seconds: number): string {
|
||||||
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Audio({ id, src, title, description, artwork, durationMs, className }: AudioProps) {
|
export function Audio({ id, src, title, durationMs, className }: AudioProps) {
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
|
@ -149,16 +146,17 @@ export function Audio({ id, src, title, description, artwork, durationMs, classN
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 sm:gap-4 rounded-xl border border-destructive/20 bg-destructive/5 p-3 sm:p-4",
|
"max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex size-12 sm:size-16 shrink-0 items-center justify-center rounded-lg bg-destructive/10">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<Volume2Icon className="size-6 sm:size-8 text-destructive" />
|
<p className="text-sm font-semibold text-destructive">Audio Error</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<p className="font-medium text-destructive text-sm sm:text-base truncate">{title}</p>
|
<div className="px-5 py-4">
|
||||||
<p className="text-destructive/70 text-xs sm:text-sm">{error}</p>
|
<p className="text-sm font-medium text-foreground truncate">{title}</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -168,49 +166,28 @@ export function Audio({ id, src, title, description, artwork, durationMs, classN
|
||||||
<div
|
<div
|
||||||
id={id}
|
id={id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group relative overflow-hidden rounded-xl border bg-gradient-to-br from-background to-muted/30 p-3 sm:p-4 shadow-sm transition-all hover:shadow-md",
|
"max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Hidden audio element */}
|
|
||||||
<audio ref={audioRef} src={src} preload="metadata">
|
<audio ref={audioRef} src={src} preload="metadata">
|
||||||
<track kind="captions" srcLang="en" label="English captions" default />
|
<track kind="captions" srcLang="en" label="English captions" default />
|
||||||
</audio>
|
</audio>
|
||||||
|
|
||||||
<div className="flex gap-3 sm:gap-4">
|
<div className="px-5 pt-5 pb-4">
|
||||||
{/* Artwork */}
|
<p className="text-sm font-semibold text-foreground line-clamp-2">{title}</p>
|
||||||
<div className="relative shrink-0">
|
|
||||||
<div className="relative size-14 sm:size-20 overflow-hidden rounded-lg bg-gradient-to-br from-primary/20 to-primary/5 shadow-inner">
|
|
||||||
{artwork ? (
|
|
||||||
<Image src={artwork} alt={title} fill className="object-cover" unoptimized />
|
|
||||||
) : (
|
|
||||||
<div className="flex size-full items-center justify-center">
|
|
||||||
<Volume2Icon className="size-6 sm:size-8 text-primary/50" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<div className="flex min-w-0 flex-1 flex-col justify-between">
|
|
||||||
{/* Title and description */}
|
|
||||||
<div className="min-w-0">
|
|
||||||
<h3 className="truncate font-semibold text-foreground text-sm sm:text-base">{title}</h3>
|
|
||||||
{description && (
|
|
||||||
<p className="mt-0.5 line-clamp-1 text-muted-foreground text-xs sm:text-sm">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress bar */}
|
<div className="px-5 pt-3 pb-4 space-y-3">
|
||||||
<div className="mt-1.5 sm:mt-2 space-y-0.5 sm:space-y-1">
|
<div className="space-y-0.5">
|
||||||
<Slider
|
<Slider
|
||||||
value={[currentTime]}
|
value={[currentTime]}
|
||||||
max={duration || 100}
|
max={duration || 100}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
onValueChange={handleSeek}
|
onValueChange={handleSeek}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer [&_[role=slider]]:border-0 [&_[role=slider]]:!bg-muted-foreground [&_[role=slider]]:h-4 [&_[role=slider]]:w-4 [&>span>span:first-child]:bg-muted-foreground/60"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between text-muted-foreground text-[10px] sm:text-xs">
|
<div className="flex justify-between text-muted-foreground text-[10px] sm:text-xs">
|
||||||
|
|
@ -218,31 +195,25 @@ export function Audio({ id, src, title, description, artwork, durationMs, classN
|
||||||
<span>{formatTime(duration)}</span>
|
<span>{formatTime(duration)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Controls */}
|
<div className="flex items-center justify-between">
|
||||||
<div className="mt-2 sm:mt-3 flex items-center justify-between border-t pt-2 sm:pt-3">
|
|
||||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||||
{/* Play/Pause button */}
|
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="secondary"
|
||||||
size="sm"
|
size="icon"
|
||||||
onClick={togglePlayPause}
|
onClick={togglePlayPause}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="gap-1.5 sm:gap-2 h-7 sm:h-8 px-2.5 sm:px-3 text-xs sm:text-sm"
|
className="size-7 sm:size-8"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="size-3 sm:size-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
<div className="size-3 sm:size-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||||
) : isPlaying ? (
|
) : isPlaying ? (
|
||||||
<PauseIcon className="size-3 sm:size-4" />
|
<PauseIcon className="size-3.5 sm:size-4" fill="currentColor" />
|
||||||
) : (
|
) : (
|
||||||
<PlayIcon className="size-3 sm:size-4" />
|
<PlayIcon className="size-3.5 sm:size-4" fill="currentColor" />
|
||||||
)}
|
)}
|
||||||
{isPlaying ? "Pause" : "Play"}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Volume control */}
|
|
||||||
<div className="flex items-center gap-1 sm:gap-1.5">
|
<div className="flex items-center gap-1 sm:gap-1.5">
|
||||||
<Button variant="ghost" size="icon" onClick={toggleMute} className="size-7 sm:size-8">
|
<Button variant="ghost" size="icon" onClick={toggleMute} className="size-7 sm:size-8">
|
||||||
{isMuted ? (
|
{isMuted ? (
|
||||||
|
|
@ -251,7 +222,6 @@ export function Audio({ id, src, title, description, artwork, durationMs, classN
|
||||||
<Volume2Icon className="size-3.5 sm:size-4" />
|
<Volume2Icon className="size-3.5 sm:size-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
{/* Custom volume bar - visually distinct from progress slider */}
|
|
||||||
<div className="relative flex h-6 w-12 sm:w-16 items-center">
|
<div className="relative flex h-6 w-12 sm:w-16 items-center">
|
||||||
<div className="relative h-1 w-full rounded-full bg-muted-foreground/20">
|
<div className="relative h-1 w-full rounded-full bg-muted-foreground/20">
|
||||||
<div
|
<div
|
||||||
|
|
@ -273,17 +243,17 @@ export function Audio({ id, src, title, description, artwork, durationMs, classN
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Download button */}
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
className="gap-1.5 sm:gap-2 h-7 sm:h-8 px-2.5 sm:px-3 text-xs sm:text-sm"
|
className="gap-1.5 sm:gap-2 h-7 sm:h-8 px-2.5 sm:px-3 text-xs sm:text-sm text-muted-foreground"
|
||||||
>
|
>
|
||||||
<DownloadIcon className="size-3 sm:size-4" />
|
<DownloadIcon className="size-3 sm:size-4" />
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||||
import { AlertCircleIcon, MicIcon } from "lucide-react";
|
|
||||||
import { useParams, usePathname } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||||
import { Audio } from "@/components/tool-ui/audio";
|
import { Audio } from "@/components/tool-ui/audio";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
import { baseApiService } from "@/lib/apis/base-api.service";
|
import { baseApiService } from "@/lib/apis/base-api.service";
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||||
import { clearActivePodcastTaskId, setActivePodcastTaskId } from "@/lib/chat/podcast-state";
|
import { clearActivePodcastTaskId, setActivePodcastTaskId } from "@/lib/chat/podcast-state";
|
||||||
|
|
@ -92,82 +97,38 @@ function parsePodcastDetails(data: unknown): { podcast_transcript?: PodcastTrans
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Loading state component shown while podcast is being generated
|
|
||||||
*/
|
|
||||||
function PodcastGeneratingState({ title }: { title: string }) {
|
function PodcastGeneratingState({ title }: { title: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 overflow-hidden rounded-xl border border-primary/20 bg-gradient-to-br from-primary/5 to-primary/10 p-4 sm:p-6">
|
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<div className="relative shrink-0">
|
<p className="text-sm font-semibold text-foreground line-clamp-2">{title}</p>
|
||||||
<div className="flex size-12 sm:size-16 items-center justify-center rounded-full bg-primary/20">
|
<TextShimmerLoader text="Generating podcast" size="sm" />
|
||||||
<MicIcon className="size-6 sm:size-8 text-primary" />
|
|
||||||
</div>
|
|
||||||
{/* Animated rings */}
|
|
||||||
<div className="absolute inset-1 animate-ping rounded-full bg-primary/20" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-semibold text-foreground text-sm sm:text-lg leading-tight">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<div className="mt-1.5 sm:mt-2 flex items-center gap-1.5 sm:gap-2 text-muted-foreground">
|
|
||||||
<Spinner size="sm" className="size-3 sm:size-4" />
|
|
||||||
<span className="text-xs sm:text-sm">
|
|
||||||
Generating podcast. This may take a few minutes.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 sm:mt-3">
|
|
||||||
<div className="h-1 sm:h-1.5 w-full overflow-hidden rounded-full bg-primary/10">
|
|
||||||
<div className="h-full w-1/3 animate-pulse rounded-full bg-primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Error state component shown when podcast generation fails
|
|
||||||
*/
|
|
||||||
function PodcastErrorState({ title, error }: { title: string; error: string }) {
|
function PodcastErrorState({ title, error }: { title: string; error: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 overflow-hidden rounded-xl border border-destructive/20 bg-destructive/5 p-4 sm:p-6">
|
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<div className="flex size-12 sm:size-16 shrink-0 items-center justify-center rounded-full bg-destructive/10">
|
<p className="text-sm font-semibold text-destructive">Podcast Generation Failed</p>
|
||||||
<AlertCircleIcon className="size-6 sm:size-8 text-destructive" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-semibold text-foreground text-sm sm:text-base leading-tight">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-destructive text-xs sm:text-sm">Failed to generate podcast</p>
|
|
||||||
<p className="mt-1.5 sm:mt-2 text-muted-foreground text-xs sm:text-sm">{error}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
|
<div className="px-5 py-4">
|
||||||
|
<p className="text-sm font-medium text-foreground line-clamp-2">{title}</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Audio loading state component
|
|
||||||
*/
|
|
||||||
function AudioLoadingState({ title }: { title: string }) {
|
function AudioLoadingState({ title }: { title: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 overflow-hidden rounded-xl border bg-muted/30 p-4 sm:p-6">
|
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||||
<div className="flex items-center gap-3 sm:gap-4">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<div className="flex size-12 sm:size-16 shrink-0 items-center justify-center rounded-full bg-primary/10">
|
<p className="text-sm font-semibold text-foreground line-clamp-2">{title}</p>
|
||||||
<MicIcon className="size-6 sm:size-8 text-primary/50" />
|
<TextShimmerLoader text="Loading audio" size="sm" />
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-semibold text-foreground text-sm sm:text-base leading-tight">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<div className="mt-1.5 sm:mt-2 flex items-center gap-1.5 sm:gap-2 text-muted-foreground">
|
|
||||||
<Spinner size="sm" className="size-3 sm:size-4" />
|
|
||||||
<span className="text-xs sm:text-sm">Loading audio...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -176,12 +137,10 @@ function AudioLoadingState({ title }: { title: string }) {
|
||||||
function PodcastPlayer({
|
function PodcastPlayer({
|
||||||
podcastId,
|
podcastId,
|
||||||
title,
|
title,
|
||||||
description,
|
|
||||||
durationMs,
|
durationMs,
|
||||||
}: {
|
}: {
|
||||||
podcastId: number;
|
podcastId: number;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
|
||||||
durationMs?: number;
|
durationMs?: number;
|
||||||
}) {
|
}) {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -289,31 +248,40 @@ function PodcastPlayer({
|
||||||
return <PodcastErrorState title={title} error={error || "Failed to load audio"} />;
|
return <PodcastErrorState title={title} error={error || "Failed to load audio"} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasTranscript = transcript && transcript.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
<Audio
|
<Audio
|
||||||
id={`podcast-${podcastId}`}
|
id={`podcast-${podcastId}`}
|
||||||
src={audioSrc}
|
src={audioSrc}
|
||||||
title={title}
|
title={title}
|
||||||
description={description}
|
|
||||||
durationMs={durationMs}
|
durationMs={durationMs}
|
||||||
className="w-full"
|
className={hasTranscript ? "rounded-b-none border-b-0" : undefined}
|
||||||
/>
|
/>
|
||||||
{/* Transcript section */}
|
{hasTranscript && (
|
||||||
{transcript && transcript.length > 0 && (
|
<div className="max-w-lg overflow-hidden rounded-b-2xl border border-t-0 bg-muted/30 select-none">
|
||||||
<details className="mt-2 sm:mt-3 rounded-lg border bg-muted/30 p-2.5 sm:p-3">
|
<div className="mx-5 h-px bg-border/50" />
|
||||||
<summary className="cursor-pointer font-medium text-muted-foreground text-xs sm:text-sm hover:text-foreground">
|
<Accordion type="single" collapsible className="px-5">
|
||||||
View transcript ({transcript.length} entries)
|
<AccordionItem value="transcript" className="border-b-0">
|
||||||
</summary>
|
<AccordionTrigger className="py-3 text-xs sm:text-sm font-medium text-muted-foreground hover:text-foreground hover:no-underline">
|
||||||
<div className="mt-2 sm:mt-3 space-y-2 sm:space-y-3 max-h-64 sm:max-h-96 overflow-y-auto">
|
View transcript
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="pb-0">
|
||||||
|
<div className="space-y-2 max-h-64 sm:max-h-96 overflow-y-auto select-text">
|
||||||
{transcript.map((entry, idx) => (
|
{transcript.map((entry, idx) => (
|
||||||
<div key={`${idx}-${entry.speaker_id}`} className="text-xs sm:text-sm">
|
<div key={`${idx}-${entry.speaker_id}`} className="text-xs sm:text-sm">
|
||||||
<span className="font-medium text-primary">Speaker {entry.speaker_id + 1}:</span>{" "}
|
<span className="font-medium text-primary">
|
||||||
|
Speaker {entry.speaker_id + 1}:
|
||||||
|
</span>{" "}
|
||||||
<span className="text-muted-foreground">{entry.dialog}</span>
|
<span className="text-muted-foreground">{entry.dialog}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -389,17 +357,7 @@ function PodcastStatusPoller({ podcastId, title }: { podcastId: number; title: s
|
||||||
|
|
||||||
// Show player when ready
|
// Show player when ready
|
||||||
if (podcastStatus.status === "ready") {
|
if (podcastStatus.status === "ready") {
|
||||||
return (
|
return <PodcastPlayer podcastId={podcastStatus.id} title={podcastStatus.title || title} />;
|
||||||
<PodcastPlayer
|
|
||||||
podcastId={podcastStatus.id}
|
|
||||||
title={podcastStatus.title || title}
|
|
||||||
description={
|
|
||||||
podcastStatus.transcript_entries
|
|
||||||
? `${podcastStatus.transcript_entries} dialogue entries`
|
|
||||||
: "SurfSense AI-generated podcast"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback
|
// Fallback
|
||||||
|
|
@ -431,12 +389,14 @@ export const GeneratePodcastToolUI = makeAssistantToolUI<
|
||||||
if (status.type === "incomplete") {
|
if (status.type === "incomplete") {
|
||||||
if (status.reason === "cancelled") {
|
if (status.reason === "cancelled") {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 rounded-xl border border-muted p-3 sm:p-4 text-muted-foreground">
|
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||||
<p className="flex items-center gap-1.5 sm:gap-2 text-xs sm:text-sm">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<MicIcon className="size-3.5 sm:size-4" />
|
<p className="text-sm font-semibold text-muted-foreground">Podcast Cancelled</p>
|
||||||
<span className="line-through">Podcast generation cancelled</span>
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Podcast generation was cancelled
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (status.reason === "error") {
|
if (status.reason === "error") {
|
||||||
|
|
@ -464,21 +424,14 @@ export const GeneratePodcastToolUI = makeAssistantToolUI<
|
||||||
// (new: "generating", legacy: "already_generating")
|
// (new: "generating", legacy: "already_generating")
|
||||||
if (result.status === "generating" || result.status === "already_generating") {
|
if (result.status === "generating" || result.status === "already_generating") {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 overflow-hidden rounded-xl border border-amber-500/20 bg-amber-500/5 p-3 sm:p-4">
|
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||||
<div className="flex items-center gap-2.5 sm:gap-3">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<div className="flex size-8 sm:size-10 shrink-0 items-center justify-center rounded-full bg-amber-500/20">
|
<p className="text-sm font-semibold text-foreground">Podcast already in progress</p>
|
||||||
<MicIcon className="size-4 sm:size-5 text-amber-500" />
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="text-amber-600 dark:text-amber-400 text-xs sm:text-sm font-medium">
|
|
||||||
Podcast already in progress
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-[10px] sm:text-xs mt-0.5">
|
|
||||||
Please wait for the current podcast to complete.
|
Please wait for the current podcast to complete.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -489,36 +442,19 @@ export const GeneratePodcastToolUI = makeAssistantToolUI<
|
||||||
|
|
||||||
// Ready with podcast_id (new: "ready", legacy: "success")
|
// Ready with podcast_id (new: "ready", legacy: "success")
|
||||||
if ((result.status === "ready" || result.status === "success") && result.podcast_id) {
|
if ((result.status === "ready" || result.status === "success") && result.podcast_id) {
|
||||||
return (
|
return <PodcastPlayer podcastId={result.podcast_id} title={result.title || title} />;
|
||||||
<PodcastPlayer
|
|
||||||
podcastId={result.podcast_id}
|
|
||||||
title={result.title || title}
|
|
||||||
description={
|
|
||||||
result.transcript_entries
|
|
||||||
? `${result.transcript_entries} dialogue entries`
|
|
||||||
: "SurfSense AI-generated podcast"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy: old chats with Celery task_id (status: "processing" or "success" without podcast_id)
|
// Legacy: old chats with Celery task_id (status: "processing" or "success" without podcast_id)
|
||||||
// These can't be recovered since the old task polling endpoint no longer exists
|
// These can't be recovered since the old task polling endpoint no longer exists
|
||||||
if (result.task_id && !result.podcast_id) {
|
if (result.task_id && !result.podcast_id) {
|
||||||
return (
|
return (
|
||||||
<div className="my-4 overflow-hidden rounded-xl border border-muted p-4">
|
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
|
||||||
<div className="flex items-center gap-3">
|
<div className="px-5 pt-5 pb-4">
|
||||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted">
|
<p className="text-sm font-semibold text-muted-foreground">Podcast Unavailable</p>
|
||||||
<MicIcon className="size-5 text-muted-foreground" />
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
</div>
|
This podcast was generated with an older version. Please generate a new one.
|
||||||
<div>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
This podcast was generated with an older version and cannot be displayed.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground text-xs mt-0.5">
|
|
||||||
Please generate a new podcast to listen.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue