Merge pull request #482 from presenton/feat/design_improvement
feat: new design for template preview & upload page
This commit is contained in:
commit
3d0539b89a
11 changed files with 228 additions and 160 deletions
|
|
@ -5,14 +5,39 @@ import React from "react";
|
|||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
const PATHS_WITH_HEADER_BACK = [
|
||||
"/upload",
|
||||
"/outline",
|
||||
"/documents-preview",
|
||||
"/template-preview",
|
||||
] as const;
|
||||
|
||||
function pathMatches(pathname: string | null, base: string) {
|
||||
return pathname === base || pathname?.startsWith(`${base}/`) === true;
|
||||
}
|
||||
|
||||
const Header = () => {
|
||||
const pathname = usePathname();
|
||||
const showHeaderBack = PATHS_WITH_HEADER_BACK.some((p) => pathMatches(pathname, p));
|
||||
|
||||
const backToUpload =
|
||||
pathMatches(pathname, "/outline") || pathMatches(pathname, "/documents-preview");
|
||||
const backToTemplates = pathMatches(pathname, "/template-preview");
|
||||
|
||||
const backHref = backToUpload ? "/upload" : backToTemplates ? "/templates" : "/dashboard";
|
||||
const backLabel = backToUpload
|
||||
? "Back to upload"
|
||||
: backToTemplates
|
||||
? "Back to templates"
|
||||
: "Go to your dashboard";
|
||||
|
||||
return (
|
||||
<div className="w-full bg-white sticky top-0 z-50 py-7 ">
|
||||
<div className="w-full sticky top-0 z-50 py-7 ">
|
||||
<Wrapper className="px-5 sm:px-10 lg:px-20">
|
||||
<div className="flex items-center justify-between py-1">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* {(pathname !== "/upload" && pathname !== "/dashboard") && <BackBtn />} */}
|
||||
<Link href="/dashboard" onClick={() => trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/dashboard" })}>
|
||||
<img
|
||||
src="/logo-with-bg.png"
|
||||
|
|
@ -21,6 +46,20 @@ const Header = () => {
|
|||
/>
|
||||
</Link>
|
||||
</div>
|
||||
{showHeaderBack ? (
|
||||
<div>
|
||||
<Link
|
||||
href={backHref}
|
||||
className="text-[#7A5AF8] text-xs font-syne font-semibold flex items-center gap-2"
|
||||
onClick={() =>
|
||||
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: backHref })
|
||||
}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 shrink-0" aria-hidden />
|
||||
<span>{backLabel}</span>
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
</div>
|
||||
</Wrapper>
|
||||
|
|
|
|||
|
|
@ -35,14 +35,14 @@ const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }
|
|||
</div>
|
||||
<p className='text-[#3A3A3A] text-xs font-medium pb-2.5'>Select Provider</p>
|
||||
{mode === 'presenton' && <div className='space-y-2.5'>
|
||||
<button className={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'text-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#E1E1E5]'}`} onClick={() => setSelectedProvider('text-provider')}>
|
||||
<button className={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'text-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`} onClick={() => setSelectedProvider('text-provider')}>
|
||||
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
|
||||
|
||||
<img src='/providers/openai.png' className=' object-cover w-full h-full overflow-hidden' alt='google' />
|
||||
</div>
|
||||
<p className='text-[#191919] text-xs font-medium' >Text Provider</p>
|
||||
</button>
|
||||
<button className={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'image-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#E1E1E5]'}`} onClick={() => setSelectedProvider('image-provider')}>
|
||||
<button className={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'image-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`} onClick={() => setSelectedProvider('image-provider')}>
|
||||
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
|
||||
<img src='/providers/image-provider.png' className=' object-cover w-full h-full overflow-hidden' alt='google' />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -174,13 +174,15 @@ const LayoutPreview = () => {
|
|||
|
||||
const handleOpenPreview = useCallback((id: string) => router.push(`/template-preview?slug=${id}`), [router]);
|
||||
|
||||
const inbuiltTemplateCards = useMemo(
|
||||
() =>
|
||||
templates.map((template: TemplateLayoutsWithSettings) => (
|
||||
<InbuiltTemplateCard key={template.id} template={template} onOpen={handleOpenPreview} />
|
||||
)),
|
||||
[handleOpenPreview],
|
||||
);
|
||||
const { nonNeoInbuilt, neoInbuilt } = useMemo(() => {
|
||||
const nonNeo: TemplateLayoutsWithSettings[] = [];
|
||||
const neo: TemplateLayoutsWithSettings[] = [];
|
||||
for (const t of templates) {
|
||||
if (t.id.startsWith("neo")) neo.push(t);
|
||||
else nonNeo.push(t);
|
||||
}
|
||||
return { nonNeoInbuilt: nonNeo, neoInbuilt: neo };
|
||||
}, []);
|
||||
|
||||
const customTemplateCards = useMemo(
|
||||
() => customTemplates.map((template: CustomTemplates) => <CustomTemplateCard key={template.id} template={template} />),
|
||||
|
|
@ -242,12 +244,36 @@ const LayoutPreview = () => {
|
|||
>Built-in</button>
|
||||
</div>
|
||||
|
||||
{/* Inbuilt Templates Section */}
|
||||
{tab === 'default' && <section className="my-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{inbuiltTemplateCards}
|
||||
</div>
|
||||
</section>}
|
||||
{/* Inbuilt Templates Section: non-neo first, then Report (neo) */}
|
||||
{tab === 'default' && (
|
||||
<section className="my-12 space-y-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{nonNeoInbuilt.map((template) => (
|
||||
<InbuiltTemplateCard
|
||||
key={template.id}
|
||||
template={template}
|
||||
onOpen={handleOpenPreview}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{neoInbuilt.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-base font-semibold text-[#101828] mb-6 font-syne tracking-tight">
|
||||
Report
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{neoInbuilt.map((template) => (
|
||||
<InbuiltTemplateCard
|
||||
key={template.id}
|
||||
template={template}
|
||||
onOpen={handleOpenPreview}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
{tab === 'custom' && <section className="my-12">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import PresentationMode from "./PresentationMode";
|
|||
import SidePanel from "./SidePanel";
|
||||
import SlideContent from "./SlideContent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import {
|
||||
|
|
@ -32,12 +32,8 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
|
|||
const [selectedSlide, setSelectedSlide] = useState(0);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
// Ensure /app_data and /static image paths resolve through FastAPI in Electron.
|
||||
// useEffect(() => {
|
||||
// const observer = setupImageUrlConverter();
|
||||
// return () => observer?.disconnect();
|
||||
// }, []);
|
||||
|
||||
|
||||
const { presentationData, isStreaming } = useSelector(
|
||||
|
|
@ -124,7 +120,11 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
|
|||
<p className="text-center mb-4">
|
||||
We couldn't load your presentation. Please try again.
|
||||
</p>
|
||||
<Button onClick={() => { trackEvent(MixpanelEvent.PresentationPage_Refresh_Page_Button_Clicked, { pathname }); window.location.reload(); }}>Refresh Page</Button>
|
||||
<div className="flex gap-2 justify-center items-center">
|
||||
|
||||
<Button onClick={() => { trackEvent(MixpanelEvent.PresentationPage_Refresh_Page_Button_Clicked, { pathname }); window.location.reload(); }}>Refresh Page</Button>
|
||||
<Button onClick={() => { trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" }); router.push("/upload"); }}>Go to Upload</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -221,6 +221,11 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
|
|||
className="min-h-[110px] max-h-[180px] w-full resize-none rounded-xl border border-gray-200 p-3 text-sm focus-visible:ring-1 focus-visible:ring-[#5141e5]"
|
||||
disabled={isUpdating}
|
||||
onChange={(e) => setEditPrompt(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "Enter" || e.shiftKey || isUpdating) return;
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
rows={5}
|
||||
wrap="soft"
|
||||
/>
|
||||
|
|
@ -236,7 +241,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Popover open={isSpeakerPopoverOpen} onOpenChange={setIsSpeakerPopoverOpen}>
|
||||
{slide?.speaker_note && <Popover open={isSpeakerPopoverOpen} onOpenChange={setIsSpeakerPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -270,11 +275,11 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
|
|||
</div>
|
||||
<div className="space-y-3 p-4">
|
||||
<div className="max-h-[220px] min-h-[100px] overflow-auto whitespace-pre-wrap rounded-xl border border-gray-200 bg-gray-50 p-3 text-sm text-gray-800">
|
||||
{slide?.speaker_note?.trim() || "No speaker notes for this slide."}
|
||||
{slide?.speaker_note?.trim()}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Popover>}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -119,38 +119,13 @@ const GroupLayoutPreview = () => {
|
|||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
|
||||
<header className="bg-white shadow-sm border-b sticky top-0 z-30">
|
||||
<div className=" mx-auto px-6 py-6">
|
||||
<header className=" z-30">
|
||||
<div className=" mx-auto px-6 pb-[30px]">
|
||||
<div className="flex items-center justify-between mb-4 max-w-[1440px] mx-auto">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
trackEvent(MixpanelEvent.TemplatePreview_Back_Button_Clicked, { pathname });
|
||||
router.back();
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
trackEvent(MixpanelEvent.TemplatePreview_All_Groups_Button_Clicked, { pathname });
|
||||
router.push("/templates");
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
All Templates
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{isCustom && (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center justify-end ml-auto mr-0 gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
@ -170,55 +145,63 @@ const GroupLayoutPreview = () => {
|
|||
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<h1 className="text-3xl font-bold text-gray-900">{templateName}</h1>
|
||||
<h1 className="text-[64px] font-bold text-gray-900">{templateName}</h1>
|
||||
{isCustom && (
|
||||
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-sm">
|
||||
Custom
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-600">
|
||||
{layoutCount} layout{layoutCount !== 1 ? "s" : ""} •{" "}
|
||||
<p className="text-gray-600 text-xl">
|
||||
{/* {layoutCount} layout{layoutCount !== 1 ? "s" : ""} •{" "} */}
|
||||
{templateDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="mx-auto px-2 py-8" id="presentation-page">
|
||||
<div className="mx-auto h-full mb-4" >
|
||||
{!isCustom && (
|
||||
<div className="space-y-12 w-[1440px] h-[720px] aspect-video mx-auto">
|
||||
<div className="space-y-3 w-[1305px] p-2.5 bg-[#FFFFFF1A] rounded-[20px] border border-[#EDECEC] mx-auto"
|
||||
style={{
|
||||
boxShadow: "0 0 20px 0 rgba(122, 90, 248, 0.16) inset",
|
||||
|
||||
}}
|
||||
>
|
||||
{staticTemplates.map((template: any, index: number) => {
|
||||
const LayoutComponent = template.component;
|
||||
|
||||
return (
|
||||
<Card
|
||||
<div
|
||||
key={`${templateParams}-${template.layoutId}-${index}`}
|
||||
id={template.layoutId}
|
||||
className="overflow-hidden shadow-md"
|
||||
className="overflow-hidden bg-white rounded-tl-[10px] rounded-tr-[10px]"
|
||||
>
|
||||
<div className="bg-white px-6 py-4 border-b">
|
||||
<div className=" px-4 py-6 ">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
<span className="px-3 py-1 bg-[#7A5AF8] text-white font-syne rounded-full text-sm font-medium">
|
||||
{index + 1 < 10 ? `0${index + 1}` : index + 1}
|
||||
</span>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mt-3">
|
||||
{template.layoutName}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1 max-w-2xl">
|
||||
<p className="text-sm text-gray-500 mt-1 ">
|
||||
{template.layoutDescription}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="px-3 py-1 bg-gray-100 text-gray-600 rounded text-sm font-mono">
|
||||
{/* <div className="flex items-center gap-3">
|
||||
<span className="px-3 py-1 text-gray-600 rounded text-sm font-mono">
|
||||
{template.layoutId}
|
||||
</span>
|
||||
<span className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
|
||||
#{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-100 p-6 flex justify-center overflow-x-auto">
|
||||
<div className=" flex justify-center overflow-x-auto">
|
||||
<div
|
||||
className="flex-shrink-0"
|
||||
style={{ width: "1280px", height: "720px" }}
|
||||
|
|
@ -226,7 +209,7 @@ const GroupLayoutPreview = () => {
|
|||
<LayoutComponent data={template.sampleData} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -254,13 +237,13 @@ const GroupLayoutPreview = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex items-end justify-end ">
|
||||
<span className="px-3 py-1 bg-gray-100 text-gray-600 rounded text-sm font-mono">
|
||||
<span className="px-3 py-1 text-gray-600 rounded text-sm font-mono">
|
||||
{templateParams}:{layout.layoutId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-100 p-6 flex justify-center overflow-x-auto">
|
||||
<div className=" p-6 flex justify-center overflow-x-auto">
|
||||
<div
|
||||
className="flex-shrink-0"
|
||||
style={{ width: "1280px", height: "720px" }}
|
||||
|
|
@ -273,7 +256,7 @@ const GroupLayoutPreview = () => {
|
|||
})}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { LanguageType, PresentationConfig, ToneType, VerbosityType } from "../type";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Check, ChevronsUp, ChevronsUpDown, ChevronUp, GalleryVertical, Languages, SlidersHorizontal } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -46,15 +46,24 @@ const SLIDE_OPTIONS: SlideOption[] = ["5", "8", "9", "10", "11", "12", "13", "14
|
|||
const SlideCountSelect: React.FC<{
|
||||
value: string | null;
|
||||
onValueChange: (value: string) => void;
|
||||
}> = ({ value, onValueChange }) => {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}> = ({ value, onValueChange, open, onOpenChange }) => {
|
||||
const [customInput, setCustomInput] = useState(
|
||||
value && !SLIDE_OPTIONS.includes(value as SlideOption) ? value : ""
|
||||
);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (value && !SLIDE_OPTIONS.includes(value as SlideOption)) {
|
||||
// setCustomInput(value);
|
||||
// } else {
|
||||
// setCustomInput("");
|
||||
// }
|
||||
// }, [value]);
|
||||
|
||||
const sanitizeToPositiveInteger = (raw: string): string => {
|
||||
const digitsOnly = raw.replace(/\D+/g, "");
|
||||
if (!digitsOnly) return "";
|
||||
// Remove leading zeros
|
||||
const noLeadingZeros = digitsOnly.replace(/^0+/, "");
|
||||
return noLeadingZeros;
|
||||
};
|
||||
|
|
@ -63,21 +72,32 @@ const SlideCountSelect: React.FC<{
|
|||
const sanitized = sanitizeToPositiveInteger(customInput);
|
||||
if (sanitized && Number(sanitized) > 0) {
|
||||
onValueChange(sanitized);
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
const displayLabel = value ? `${value} slides` : "Select Slides";
|
||||
|
||||
return (
|
||||
<Select value={value || ""} onValueChange={onValueChange} name="slides">
|
||||
<SelectTrigger
|
||||
className="w-[140px] font-instrument_sans font-medium bg-white text-slate-700 hover:bg-slate-50 focus-visible:ring-[#5146E5]/30 flex items-center gap-2 h-10 rounded-xl px-3 ring-1 ring-inset ring-slate-200 shadow-sm"
|
||||
data-testid="slides-select"
|
||||
>
|
||||
<div className="flex items-center gap-2.5"><GalleryVertical className="w-4 h-4" /> <SelectValue placeholder="Select Slides" /></div>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="font-instrument_sans">
|
||||
{/* Sticky custom input at the top */}
|
||||
<Popover open={open} onOpenChange={onOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
name="slides"
|
||||
data-testid="slides-select"
|
||||
aria-expanded={open}
|
||||
className="w-[105px] overflow-hidden font-syne font-medium bg-[#F6F6F9] text-slate-700 hover:bg-slate-50 focus-visible:ring-[#5146E5]/30 flex justify-between items-center gap-2 h-10 rounded-full px-3.5 ring-1 ring-inset ring-slate-200 shadow-sm"
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 items-center gap-2.5">
|
||||
<span className="text-sm font-medium ">{displayLabel}</span>
|
||||
</span>
|
||||
<ChevronUp className="ml-2 h-4 w-4 shrink-0" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[140px] p-0 font-syne" align="end">
|
||||
<div
|
||||
className="sticky top-0 z-10 bg-white p-2 border-b"
|
||||
className="sticky top-0 z-10 bg-white p-2 border-b"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
|
@ -107,26 +127,35 @@ const SlideCountSelect: React.FC<{
|
|||
<span className="text-sm font-medium">slides</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hidden item to allow SelectValue to render custom selection */}
|
||||
{value && !SLIDE_OPTIONS.includes(value as SlideOption) && (
|
||||
<SelectItem value={value} className="hidden">
|
||||
{value} slides
|
||||
</SelectItem>
|
||||
)}
|
||||
|
||||
{SLIDE_OPTIONS.map((option) => (
|
||||
<SelectItem
|
||||
key={option}
|
||||
value={option}
|
||||
className="font-instrument_sans text-sm font-medium"
|
||||
role="option"
|
||||
>
|
||||
{option} slides
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Command>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
{SLIDE_OPTIONS.map((option) => (
|
||||
<CommandItem
|
||||
key={option}
|
||||
value={`${option} slides`}
|
||||
role="option"
|
||||
onSelect={() => {
|
||||
onValueChange(option);
|
||||
setCustomInput("");
|
||||
onOpenChange(false);
|
||||
}}
|
||||
className="font-syne text-sm font-medium"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
value === option ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{option} slides
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -147,17 +176,14 @@ const LanguageSelect: React.FC<{
|
|||
name="language"
|
||||
data-testid="language-select"
|
||||
aria-expanded={open}
|
||||
className="w-[180px] flex justify-between items-center gap-2 font-instrument_sans font-semibold overflow-hidden bg-white text-slate-700 h-10 rounded-xl px-3 ring-1 ring-inset ring-slate-200 shadow-sm"
|
||||
className="w-[120px] overflow-hidden flex justify-between items-center gap-2 font-syne font-semibold bg-[#F6F6F9] text-slate-700 h-10 rounded-full px-3.5 ring-1 ring-inset ring-slate-200 shadow-sm"
|
||||
>
|
||||
<span className="flex justify-center items-center gap-2.5">
|
||||
<span className="border border-slate-200 rounded-md p-1">
|
||||
<Languages className="w-4 h-4" />
|
||||
</span>
|
||||
<span className="text-sm font-medium truncate">
|
||||
<span className="min-w-[65px] flex-1 text-left">
|
||||
<span className="text-sm font-medium truncate block">
|
||||
{value || "Select language"}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronUp className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<ChevronUp className="ml-2 h-4 w-4 shrink-0" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0" align="end">
|
||||
|
|
@ -200,6 +226,7 @@ export function ConfigurationSelects({
|
|||
config,
|
||||
onConfigChange,
|
||||
}: ConfigurationSelectsProps) {
|
||||
const [openSlides, setOpenSlides] = useState(false);
|
||||
const [openLanguage, setOpenLanguage] = useState(false);
|
||||
const [openAdvanced, setOpenAdvanced] = useState(false);
|
||||
|
||||
|
|
@ -241,6 +268,8 @@ export function ConfigurationSelects({
|
|||
<SlideCountSelect
|
||||
value={config.slides}
|
||||
onValueChange={(value) => onConfigChange("slides", value)}
|
||||
open={openSlides}
|
||||
onOpenChange={setOpenSlides}
|
||||
/>
|
||||
<LanguageSelect
|
||||
value={config.language}
|
||||
|
|
@ -255,7 +284,7 @@ export function ConfigurationSelects({
|
|||
title="Advanced settings"
|
||||
type="button"
|
||||
onClick={() => handleOpenAdvancedChange(true)}
|
||||
className="ml-auto flex items-center gap-2 text-sm bg-white text-slate-700 hover:bg-slate-50 focus-visible:ring-[#5146E5]/30 h-10 rounded-xl px-3 ring-1 ring-inset ring-slate-200 shadow-sm font-instrument_sans font-medium"
|
||||
className="ml-auto flex items-center gap-2 text-sm bg-[#F6F6F9] text-slate-700 hover:bg-slate-50 focus-visible:ring-[#5146E5]/30 h-10 rounded-xl px-3 ring-1 ring-inset ring-slate-200 shadow-sm font-instrument_sans font-medium"
|
||||
data-testid="advanced-settings-button"
|
||||
>
|
||||
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
|
||||
|
|
@ -263,7 +292,7 @@ export function ConfigurationSelects({
|
|||
</ToolTip>
|
||||
|
||||
<Dialog open={openAdvanced} onOpenChange={handleOpenAdvancedChange}>
|
||||
<DialogContent className="max-w-2xl font-instrument_sans">
|
||||
<DialogContent className="max-w-2xl font-syne">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Advanced settings</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
|
|
|||
|
|
@ -15,18 +15,17 @@ export function PromptInput({ value, onChange }: PromptInputProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 font-syne">
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
value={value}
|
||||
rows={5}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder="Tell us about your presentation"
|
||||
data-testid="prompt-input"
|
||||
className={`py-4 px-5 border-2 font-medium font-instrument_sans text-base min-h-[150px] max-h-[300px] border-[#5146E5] focus-visible:ring-offset-0 focus-visible:ring-[#5146E5] overflow-y-auto custom_scrollbar `}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative font-syne">
|
||||
<Textarea
|
||||
value={value}
|
||||
rows={5}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder="Tell us about your presentation"
|
||||
data-testid="prompt-input"
|
||||
className={`py-3 px-2.5 border-2 font-medium font-instrument_sans text-base min-h-[150px] max-h-[300px] border-[#DBDBDB99] focus-visible:ring-offset-0 focus-visible:ring-[#5146E5] overflow-y-auto custom_scrollbar `}
|
||||
/>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
|
@ -153,9 +153,9 @@ const SupportingDoc = ({
|
|||
<div className="space-y-2" data-testid="attachments-uploader">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-600 font-syne">
|
||||
{hasFiles ? `${filteredFiles.length} attachment${filteredFiles.length > 1 ? 's' : ''}` : 'No attachments yet'}
|
||||
{hasFiles ? `${filteredFiles.length} attachment${filteredFiles.length > 1 ? 's' : ''}` : ''}
|
||||
</p>
|
||||
<button
|
||||
{hasFiles && <button
|
||||
type="button"
|
||||
onClick={handleClearFiles}
|
||||
disabled={!hasFiles}
|
||||
|
|
@ -164,7 +164,7 @@ const SupportingDoc = ({
|
|||
aria-disabled={!hasFiles}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</button>}
|
||||
</div>
|
||||
|
||||
<label
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ const UploadPage = () => {
|
|||
const pathname = usePathname();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// State management
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [config, setConfig] = useState<PresentationConfig>({
|
||||
slides: "5",
|
||||
|
|
@ -63,13 +62,8 @@ const UploadPage = () => {
|
|||
extra_info: "",
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the presentation configuration
|
||||
* @param key - Configuration key to update
|
||||
* @param value - New value for the configuration
|
||||
*/
|
||||
const handleConfigChange = (key: keyof PresentationConfig, value: string) => {
|
||||
setConfig((prev) => ({ ...prev, [key]: value }));
|
||||
const handleConfigChange = (key: keyof PresentationConfig, value: unknown) => {
|
||||
setConfig((prev) => ({ ...prev, [key]: value } as PresentationConfig));
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -208,10 +202,9 @@ const UploadPage = () => {
|
|||
extra_info={loadingState.extra_info}
|
||||
/>
|
||||
<div className="rounded-2xl border border-slate-200/70 bg-white/80 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-white/60" >
|
||||
<div className="flex flex-col gap-4 md:items-center md:flex-row justify-between p-4">
|
||||
<div className="flex flex-col gap-4 md:items-center md:flex-row justify-between px-4 py-5">
|
||||
<div >
|
||||
<h2 className="text-lg font-unbounded tracking-tight text-slate-900 ">Configuration</h2>
|
||||
<p className="text-sm text-slate-500 font-syne">Choose slides, tone, and language preferences.</p>
|
||||
<h2 className="text-lg font-unbounded tracking-tight text-[#191919] ">Configuration</h2>
|
||||
</div>
|
||||
<ConfigurationSelects
|
||||
config={config}
|
||||
|
|
@ -220,42 +213,36 @@ const UploadPage = () => {
|
|||
</div>
|
||||
<div className="border-t border-slate-200/70" />
|
||||
|
||||
<div className="p-4 md:p-6">
|
||||
<h3 className="text-base font-normal font-unbounded text-slate-900 mb-2">Content</h3>
|
||||
<div className="p-4 mt-2 ">
|
||||
<h3 className="text-sm font-normal font-unbounded text-[#333333] mb-2">Content</h3>
|
||||
<div className="relative">
|
||||
<PromptInput
|
||||
value={config.prompt}
|
||||
onChange={(value) => handleConfigChange("prompt", value)}
|
||||
data-testid="prompt-input"
|
||||
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-slate-200/70" />
|
||||
<div className="p-4 md:p-6">
|
||||
<h3 className="text-base font-normal font-unbounded text-slate-900 mb-2">Attachments (optional)</h3>
|
||||
|
||||
|
||||
<div className="p-4 ">
|
||||
<h3 className="text-sm font-normal font-unbounded text-[#333333] mb-2">Attachments (optional)</h3>
|
||||
<SupportingDoc
|
||||
files={[...files]}
|
||||
onFilesChange={setFiles}
|
||||
data-testid="file-upload-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-slate-200/70" />
|
||||
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="p-4">
|
||||
<Button
|
||||
onClick={handleGeneratePresentation}
|
||||
className="w-full rounded-[28px] flex items-center justify-center py-5 bg-[#5141e5] text-white font-syne font-semibold text-lg hover:bg-[#5141e5]/85 focus-visible:ring-2 focus-visible:ring-[#5141e5]/40"
|
||||
data-testid="next-button"
|
||||
style={{
|
||||
background: "linear-gradient(270deg, #D5CAFC 2.4%, #E3D2EB 27.88%, #F4DCD3 69.23%, #FDE4C2 100%)"
|
||||
}}
|
||||
className="w-fit mr-0 ml-auto rounded-[28px] flex items-center justify-center py-5 px-4 text-[#101323] font-syne font-semibold text-xs "
|
||||
>
|
||||
<span>Generate Presentation</span>
|
||||
<ChevronRight className="!w-5 !h-5 ml-1.5" />
|
||||
<span>Get Started</span>
|
||||
<ChevronRight className="!w-5 !h-5 " />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue