mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 01:36:30 +02:00
refactor(ux): Enhance podcast and chat panel features
- Updated the welcome dialog in podcast generation to reflect the correct podcast title. - Improved the Dashboard layout by adding an indicator for active chats on the researcher page. - Enhanced the breadcrumb component to fetch and display chat details dynamically. - Adjusted the chat panel width for better visibility. - Introduced animations and improved user interactions in the chat panel and podcast player components. - Updated the ConfigModal to provide clearer instructions for user input.
This commit is contained in:
parent
0835a192a2
commit
3ccb0bc7bb
7 changed files with 390 additions and 146 deletions
|
|
@ -43,7 +43,7 @@ export function ChatPanelContainer() {
|
|||
<div
|
||||
className={cn(
|
||||
"shrink-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 flex flex-col h-full transition-all",
|
||||
isChatPannelOpen ? "w-64" : "w-0"
|
||||
isChatPannelOpen ? "w-96" : "w-0"
|
||||
)}
|
||||
>
|
||||
{isChatLoading || chatError ? (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { AlertCircle, Pencil, Play, Podcast, RefreshCw } from "lucide-react";
|
||||
import { AlertCircle, Pencil, Play, Podcast, RefreshCw, Sparkles } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useCallback, useContext, useTransition } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { activeChatAtom } from "@/stores/chat/active-chat.atom";
|
||||
|
|
@ -41,23 +42,26 @@ export function ChatPanelView(props: ChatPanelViewProps) {
|
|||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"w-full cursor-pointer p-4 border-b",
|
||||
!isChatPannelOpen && "flex items-center justify-center"
|
||||
)}
|
||||
title={podcastIsStale ? "Regenerate Podcast" : "Generate Podcast"}
|
||||
>
|
||||
<div className={cn("w-full p-4", !isChatPannelOpen && "flex items-center justify-center")}>
|
||||
{isChatPannelOpen ? (
|
||||
<div className="space-y-3">
|
||||
{/* Show stale podcast warning if applicable */}
|
||||
{podcastIsStale && (
|
||||
<div className="rounded-lg p-3 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="rounded-xl p-3 bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-950/30 dark:to-orange-950/20 border border-amber-200/50 dark:border-amber-800/50 shadow-sm"
|
||||
>
|
||||
<div className="flex gap-2 items-start">
|
||||
<AlertCircle className="h-4 w-4 text-amber-600 dark:text-amber-500 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-amber-800 dark:text-amber-200">
|
||||
<p className="font-medium">Podcast is outdated</p>
|
||||
<p className="text-xs mt-1 opacity-90">
|
||||
<motion.div
|
||||
animate={{ rotate: [0, 10, -10, 0] }}
|
||||
transition={{ duration: 0.5, repeat: Infinity, repeatDelay: 3 }}
|
||||
>
|
||||
<AlertCircle className="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
|
||||
</motion.div>
|
||||
<div className="text-sm text-amber-900 dark:text-amber-100">
|
||||
<p className="font-semibold">Podcast Outdated</p>
|
||||
<p className="text-xs mt-1 opacity-80">
|
||||
{getPodcastStalenessMessage(
|
||||
chatDetails?.state_version || 0,
|
||||
podcast?.chat_state_version
|
||||
|
|
@ -65,41 +69,96 @@ export function ChatPanelView(props: ChatPanelViewProps) {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleGeneratePost}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleGeneratePost();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"w-full space-y-3 rounded-xl p-3 transition-colors",
|
||||
podcastIsStale
|
||||
? "bg-gradient-to-r from-amber-400/50 to-orange-300/50 dark:from-amber-500/30 dark:to-orange-600/30 hover:from-amber-400/60 hover:to-orange-300/60"
|
||||
: "bg-gradient-to-r from-slate-400/50 to-slate-200/50 dark:from-slate-400/30 dark:to-slate-800/60 hover:from-slate-400/60 hover:to-slate-200/60"
|
||||
)}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="w-full flex items-center justify-between">
|
||||
{podcastIsStale ? (
|
||||
<RefreshCw strokeWidth={1} className="h-5 w-5" />
|
||||
) : (
|
||||
<Podcast strokeWidth={1} className="h-5 w-5" />
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleGeneratePost}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleGeneratePost();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"relative w-full rounded-2xl p-4 transition-all duration-300 cursor-pointer group overflow-hidden",
|
||||
"border-2",
|
||||
podcastIsStale
|
||||
? "bg-gradient-to-br from-amber-500/10 via-orange-500/10 to-amber-500/10 dark:from-amber-500/20 dark:via-orange-500/20 dark:to-amber-500/20 border-amber-400/50 hover:border-amber-400 hover:shadow-lg hover:shadow-amber-500/20"
|
||||
: "bg-gradient-to-br from-primary/10 via-primary/5 to-primary/10 border-primary/30 hover:border-primary/60 hover:shadow-lg hover:shadow-primary/20"
|
||||
)}
|
||||
<ConfigModal generatePodcast={generatePodcast} />
|
||||
>
|
||||
{/* Background gradient animation */}
|
||||
<motion.div
|
||||
className={cn(
|
||||
"absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500",
|
||||
podcastIsStale
|
||||
? "bg-gradient-to-r from-transparent via-amber-400/10 to-transparent"
|
||||
: "bg-gradient-to-r from-transparent via-primary/10 to-transparent"
|
||||
)}
|
||||
animate={{
|
||||
x: ["-100%", "100%"],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
ease: "linear",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<motion.div
|
||||
className={cn(
|
||||
"p-2.5 rounded-xl",
|
||||
podcastIsStale
|
||||
? "bg-amber-500/20 dark:bg-amber-500/30"
|
||||
: "bg-primary/20 dark:bg-primary/30"
|
||||
)}
|
||||
animate={{
|
||||
rotate: podcastIsStale ? [0, 360] : 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: podcastIsStale ? Infinity : 0,
|
||||
ease: "linear",
|
||||
}}
|
||||
>
|
||||
{podcastIsStale ? (
|
||||
<RefreshCw className="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
||||
) : (
|
||||
<Sparkles className="h-5 w-5 text-primary" />
|
||||
)}
|
||||
</motion.div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold">
|
||||
{podcastIsStale ? "Regenerate Podcast" : "Generate Podcast"}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{podcastIsStale
|
||||
? "Update with latest changes"
|
||||
: "Create podcasts of your chat"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ConfigModal generatePodcast={generatePodcast} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-left">
|
||||
{podcastIsStale ? "Regenerate Podcast" : "Generate Podcast"}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
<motion.button
|
||||
title={podcastIsStale ? "Regenerate Podcast" : "Generate Podcast"}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
|
|
@ -108,37 +167,39 @@ export function ChatPanelView(props: ChatPanelViewProps) {
|
|||
isChatPannelOpen: !isChatPannelOpen,
|
||||
}))
|
||||
}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className={cn(
|
||||
"p-2 rounded-full hover:bg-muted transition-colors",
|
||||
podcastIsStale && "text-amber-600 dark:text-amber-500"
|
||||
"p-2.5 rounded-full transition-colors shadow-sm",
|
||||
podcastIsStale
|
||||
? "bg-amber-500/20 hover:bg-amber-500/30 text-amber-600 dark:text-amber-400"
|
||||
: "bg-primary/20 hover:bg-primary/30 text-primary"
|
||||
)}
|
||||
>
|
||||
{podcastIsStale ? (
|
||||
<RefreshCw strokeWidth={1} className="h-5 w-5" />
|
||||
) : (
|
||||
<Podcast strokeWidth={1} className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
{podcastIsStale ? <RefreshCw className="h-5 w-5" /> : <Sparkles className="h-5 w-5" />}
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
{podcast ? (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full border-b",
|
||||
"w-full border-t",
|
||||
!isChatPannelOpen && "flex items-center justify-center p-4"
|
||||
)}
|
||||
>
|
||||
{isChatPannelOpen ? (
|
||||
<PodcastPlayer compact podcast={podcast} />
|
||||
) : podcast ? (
|
||||
<button
|
||||
<motion.button
|
||||
title="Play Podcast"
|
||||
type="button"
|
||||
onClick={() => setChatUIState((prev) => ({ ...prev, isChatPannelOpen: true }))}
|
||||
className="p-2 rounded-full hover:bg-muted transition-colors text-green-600 dark:text-green-500"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
className="p-2.5 rounded-full bg-green-500/20 hover:bg-green-500/30 text-green-600 dark:text-green-400 transition-colors shadow-sm"
|
||||
>
|
||||
<Play strokeWidth={1} className="h-5 w-5" />
|
||||
</button>
|
||||
<Play className="h-5 w-5" />
|
||||
</motion.button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -44,8 +44,20 @@ export function ConfigModal(props: ConfigModalProps) {
|
|||
<PopoverContent onClick={(e) => e.stopPropagation()} align="end" className="bg-sidebar w-96 ">
|
||||
<form className="flex flex-col gap-3 w-full">
|
||||
<label className="text-sm font-medium" htmlFor="prompt">
|
||||
What subjects should the AI cover in this podcast ?
|
||||
Special user instructions
|
||||
</label>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Leave empty to use the default prompt
|
||||
</p>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400 space-y-1">
|
||||
<p>Examples:</p>
|
||||
<ul className="list-disc list-inside space-y-0.5">
|
||||
<li>Make hosts speak in London street language</li>
|
||||
<li>Use real-world analogies and metaphors</li>
|
||||
<li>Add dramatic pauses like a late-night radio show</li>
|
||||
<li>Include 90s pop culture references</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
name="prompt"
|
||||
|
|
|
|||
|
|
@ -202,94 +202,116 @@ export function PodcastPlayer({
|
|||
if (compact) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-3 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<motion.div
|
||||
className="w-8 h-8 bg-primary/20 rounded-md flex items-center justify-center flex-shrink-0"
|
||||
animate={{ scale: isPlaying ? [1, 1.05, 1] : 1 }}
|
||||
transition={{
|
||||
repeat: isPlaying ? Infinity : 0,
|
||||
duration: 2,
|
||||
}}
|
||||
>
|
||||
<Podcast className="h-4 w-4 text-primary" />
|
||||
</motion.div>
|
||||
<h4 className="font-medium text-xs line-clamp-1 flex-grow">{podcast.title}</h4>
|
||||
{onClose && (
|
||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="h-6 w-6 flex-shrink-0"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
{/* Audio Visualizer */}
|
||||
<motion.div
|
||||
className="relative h-1 bg-gradient-to-r from-primary/20 via-primary/40 to-primary/20 rounded-full overflow-hidden"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{isPlaying && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-primary to-transparent"
|
||||
animate={{
|
||||
x: ["-100%", "100%"],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "linear",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Progress Bar with Time */}
|
||||
<div className="space-y-2">
|
||||
<Slider
|
||||
value={[currentTime]}
|
||||
min={0}
|
||||
max={duration || 100}
|
||||
step={0.1}
|
||||
onValueChange={handleSeek}
|
||||
className="flex-grow"
|
||||
className="w-full cursor-pointer"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground whitespace-nowrap flex-shrink-0">
|
||||
{formatTime(currentTime)} / {formatTime(duration)}
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span className="font-mono">{formatTime(currentTime)}</span>
|
||||
<span className="font-mono">{formatTime(duration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={skipBackward}
|
||||
className="h-7 w-7"
|
||||
disabled={!duration}
|
||||
>
|
||||
<SkipBack className="h-3 w-3" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
{/* Controls */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Left: Volume */}
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button variant="ghost" size="icon" onClick={toggleMute} className="h-8 w-8">
|
||||
{isMuted ? (
|
||||
<VolumeX className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Volume2 className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button
|
||||
variant="default"
|
||||
size="icon"
|
||||
onClick={togglePlayPause}
|
||||
className="h-8 w-8 rounded-full"
|
||||
disabled={!duration}
|
||||
>
|
||||
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4 ml-0.5" />}
|
||||
</Button>
|
||||
</motion.div>
|
||||
{/* Center: Playback Controls */}
|
||||
<div className="flex items-center gap-1">
|
||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={skipBackward}
|
||||
className="h-9 w-9"
|
||||
disabled={!duration}
|
||||
>
|
||||
<SkipBack className="h-4 w-4" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={skipForward}
|
||||
className="h-7 w-7"
|
||||
disabled={!duration}
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
animate={
|
||||
isPlaying
|
||||
? {
|
||||
boxShadow: [
|
||||
"0 0 0 0 rgba(var(--primary), 0)",
|
||||
"0 0 0 8px rgba(var(--primary), 0.1)",
|
||||
"0 0 0 0 rgba(var(--primary), 0)",
|
||||
],
|
||||
}
|
||||
: {}
|
||||
}
|
||||
transition={{ duration: 1.5, repeat: isPlaying ? Infinity : 0 }}
|
||||
>
|
||||
<SkipForward className="h-3 w-3" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
<Button
|
||||
variant="default"
|
||||
size="icon"
|
||||
onClick={togglePlayPause}
|
||||
className="h-10 w-10 rounded-full"
|
||||
disabled={!duration}
|
||||
>
|
||||
{isPlaying ? <Pause className="h-5 w-5" /> : <Play className="h-5 w-5 ml-0.5" />}
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleMute}
|
||||
className={`h-7 w-7 ${isMuted ? "text-muted-foreground" : "text-primary"}`}
|
||||
>
|
||||
{isMuted ? <VolumeX className="h-3 w-3" /> : <Volume2 className="h-3 w-3" />}
|
||||
</Button>
|
||||
</motion.div>
|
||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={skipForward}
|
||||
className="h-9 w-9"
|
||||
disabled={!duration}
|
||||
>
|
||||
<SkipForward className="h-4 w-4" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Right: Placeholder for symmetry */}
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue