feat: Upload page redesigns

This commit is contained in:
shiva raj badu 2026-02-21 14:44:34 +05:45
parent 07ae990c95
commit 45185fb125
No known key found for this signature in database
18 changed files with 844 additions and 784 deletions

View file

@ -11,7 +11,7 @@ import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
const Header = () => {
const pathname = usePathname();
return (
<div className="bg-[#5146E5] w-full shadow-lg sticky top-0 z-50">
<div className="w-full shadow-lg sticky top-0 z-50">
<Wrapper>
<div className="flex items-center justify-between py-1">
<div className="flex items-center gap-3">

View file

@ -1,3 +1,4 @@
import { useEffect } from "react"
import { useEditor, EditorContent } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import { Markdown } from "tiptap-markdown"
@ -19,12 +20,14 @@ export default function MarkdownEditor({ content, onChange }: { content: string;
immediatelyRender: false,
});
// Update editor content when the content prop changes (for streaming)
// useEffect(() => {
// if (editor && content !== editor.storage.markdown.getMarkdown()) {
// editor.commands.setContent(content);
// }
// }, [content, editor]);
// Keep editor state in sync when parent changes content (e.g. reorder)
useEffect(() => {
if (!editor) return;
const currentMarkdown = editor.storage.markdown.getMarkdown();
if (content !== currentMarkdown) {
editor.commands.setContent(content, false);
}
}, [content, editor]);
return (
<div className="relative">

View file

@ -92,40 +92,28 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
{/* Outlines content */}
{outlines && outlines.length > 0 && (
<div>
<div className="bg-[#F9F8F8] p-7 rounded-[20px]">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
>
{isStreaming ? (
outlines.map((item, index) => (
<OutlineItem
key={`slide-${index}`}
index={index + 1}
slideOutline={item}
isStreaming={isStreaming}
isActiveStreaming={activeSlideIndex === index}
isStableStreaming={highestActiveIndex >= 0 && index < highestActiveIndex}
/>
))
) :
<SortableContext
items={outlines?.map((item, index) => ({ id: `slide-${index}` })) || []}
<SortableContext
items={outlines.map((_, index) => `slide-${index}`)}
strategy={verticalListSortingStrategy}
>
{outlines?.map((item, index) => (
{outlines.map((item, index) => (
<OutlineItem
key={`slide-${index}`}
sortableId={`slide-${index}`}
index={index + 1}
slideOutline={item}
isStreaming={isStreaming}
isActiveStreaming={false}
isStableStreaming={false}
isActiveStreaming={activeSlideIndex === index}
isStableStreaming={highestActiveIndex >= 0 && index < highestActiveIndex}
/>
))}
</SortableContext>}
</SortableContext>
</DndContext>
<Button

View file

@ -1,6 +1,6 @@
import { useSortable } from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import { Trash2 } from "lucide-react"
import { GripHorizontal, Trash, Trash2 } from "lucide-react"
import { RootState } from "@/store/store"
import { useDispatch, useSelector } from "react-redux"
import { deleteSlideOutline, setOutlines } from "@/store/slices/presentationGeneration"
@ -11,6 +11,7 @@ import { marked } from "marked"
interface OutlineItemProps {
sortableId: string
slideOutline: {
content: string,
},
@ -21,6 +22,7 @@ interface OutlineItemProps {
}
export function OutlineItem({
sortableId,
index,
slideOutline,
isStreaming,
@ -45,7 +47,7 @@ export function OutlineItem({
}
}, [outlines.length]);
const handleSlideChange = (newOutline:any) => {
const handleSlideChange = (newOutline: any) => {
if (isStreaming) return;
const newData = outlines?.map((each, idx) => {
if (idx === index - 1) {
@ -69,7 +71,7 @@ export function OutlineItem({
transform,
transition,
isDragging,
} = useSortable({ id: index })
} = useSortable({ id: sortableId, disabled: isStreaming })
const style = {
transform: CSS.Transform.toString(transform),
@ -117,30 +119,26 @@ export function OutlineItem({
}, [isStreaming, isActiveStreaming, isStableStreaming, slideOutline.content])
return (
<div className="mb-2">
{/* Main Title Row */}
<div className="mb-4 bg-white rounded-[12px] shadow-sm p-10 relative">
<div
ref={setNodeRef}
style={style}
className={`flex items-start gap-2 md:gap-4 p-2 sm:pr-4 border border-black/10 bg-purple-100/10 rounded-[8px] ${isDragging ? "opacity-50" : ""}`}
className={`flex items-start gap-3 md:gap-4 rounded-[8px] ${isDragging ? "opacity-50" : ""}`}
>
{/* Drag Handle with Number - Make it smaller on mobile */}
<div
{...attributes}
{...listeners}
className="min-w-8 sm:min-w-10 w-10 sm:w-14 h-10 sm:h-14 bg-blue-400/10 rounded-[8px] flex items-center justify-center relative cursor-grab"
className=" flex items-center justify-center relative cursor-grab"
>
<div className="grid grid-cols-2 gap-[2px]">
<div className="w-[3px] h-[3px] bg-black/80 rounded-full" />
<div className="w-[3px] h-[3px] bg-black/80 rounded-full" />
<div className="w-[3px] h-[3px] bg-black/80 rounded-full" />
<div className="w-[3px] h-[3px] bg-black/80 rounded-full" />
</div>
<span className="text-black/80 text-md sm:text-lg font-medium ml-1">{index}</span>
<GripHorizontal className="w-4 h-4 text-black/80" />
</div>
{/* Main Title Input - Add onFocus handler */}
<div id={`outline-item-${index}`} className="flex flex-col basis-full gap-2">
<p className="text-black/80 w-fit text-[10px] font-medium bg-white border border-[#EDEEEF] rounded-[80px] px-2.5">slide {index}</p>
{/* Editable Markdown Content */}
{isStreaming ? (
isActiveStreaming ? (
@ -166,15 +164,15 @@ export function OutlineItem({
</div>
{/* Action Buttons */}
<div className="flex gap-1 sm:gap-2 items-center">
<div className="absolute -top-3 -right-3 flex gap-1 sm:gap-2 items-center">
<ToolTip content="Delete Slide">
<button
onClick={handleSlideDelete}
className="p-1.5 sm:p-2 bg-gray-200/50 hover:bg-gray-200 rounded-lg transition-colors"
className="p-1.5 sm:p-2 bg-white shadow-md rounded-full transition-colors"
>
<Trash2 className="w-4 h-4 sm:w-5 sm:h-5 text-black/70" />
<Trash className="w-4 h-4 text-black/70" />
</button>
</ToolTip>
</div>

View file

@ -15,7 +15,7 @@ import { useOutlineStreaming } from "../hooks/useOutlineStreaming";
import { useOutlineManagement } from "../hooks/useOutlineManagement";
import { usePresentationGeneration } from "../hooks/usePresentationGeneration";
import TemplateSelection from "./TemplateSelection";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
const OutlinePage: React.FC = () => {
const { presentation_id, outlines } = useSelector(
@ -48,11 +48,21 @@ const OutlinePage: React.FC = () => {
/>
<Wrapper className="h-full flex flex-col w-full">
<div className="flex-grow overflow-y-hidden w-[1200px] mx-auto">
<div className="flex-grow overflow-y-hidden w-[1200px] mx-auto mt-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
<TabsList className="grid w-[50%] mx-auto my-4 grid-cols-2">
<TabsTrigger value={TABS.OUTLINE}>Outline & Content</TabsTrigger>
<TabsTrigger value={TABS.LAYOUTS}>Select Template</TabsTrigger>
<TabsList className="my-4grid h-auto w-fit grid-cols-2 rounded-full border border-[#DFDFE1] bg-[#F8F8F9] p-1.5">
<TabsTrigger
value={TABS.OUTLINE}
className="rounded-full px-5 py-2 text-xs font-medium text-[#2D2D2D] shadow-none data-[state=active]:bg-[#E9E2F8] data-[state=active]:text-[#7E3AF2] data-[state=active]:shadow-none"
>
Outline & Content
</TabsTrigger>
<TabsTrigger
value={TABS.LAYOUTS}
className="relative rounded-full px-5 py-2 text-xs font-medium text-[#2D2D2D] shadow-none before:absolute before:left-0 before:top-1/2 before:h-6 before:w-px before:-translate-y-1/2 before:bg-[#D7D7D8] data-[state=active]:bg-[#E9E2F8] data-[state=active]:text-[#7E3AF2] data-[state=active]:shadow-none"
>
Select Template
</TabsTrigger>
</TabsList>
<div className="flex-grow w-full mx-auto">

View file

@ -11,12 +11,16 @@ export const useOutlineManagement = (outlines: { content: string }[] | null) =>
if (!active || !over || !outlines) return;
if (active.id !== over.id) {
const oldIndex = outlines.findIndex((item) => item.content === active.id);
const newIndex = outlines.findIndex((item) => item.content === over.id);
const reorderedArray = arrayMove(outlines, oldIndex, newIndex);
dispatch(setOutlines(reorderedArray));
}
if (active.id === over.id) return;
const oldIndex = Number(String(active.id).replace("slide-", ""));
const newIndex = Number(String(over.id).replace("slide-", ""));
if (Number.isNaN(oldIndex) || Number.isNaN(newIndex)) return;
if (oldIndex < 0 || newIndex < 0 || oldIndex >= outlines.length || newIndex >= outlines.length) return;
const reorderedArray = arrayMove(outlines, oldIndex, newIndex);
dispatch(setOutlines(reorderedArray));
}, [outlines, dispatch]);
const handleAddSlide = useCallback(() => {

View file

@ -0,0 +1,172 @@
import ToolTip from '@/components/ToolTip'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import { SlidersHorizontal } from 'lucide-react'
import React, { useState } from 'react'
import { PresentationConfig, ToneType, VerbosityType } from '../type'
interface ConfigurationSelectsProps {
config: PresentationConfig;
onConfigChange: (key: keyof PresentationConfig, value: any) => void;
}
const AdvanceSettings = ({ config, onConfigChange }: ConfigurationSelectsProps) => {
const [openAdvanced, setOpenAdvanced] = useState(false);
const [advancedDraft, setAdvancedDraft] = useState({
tone: config.tone,
verbosity: config.verbosity,
instructions: config.instructions,
includeTableOfContents: config.includeTableOfContents,
includeTitleSlide: config.includeTitleSlide,
webSearch: config.webSearch,
});
const handleOpenAdvancedChange = (open: boolean) => {
if (open) {
setAdvancedDraft({
tone: config.tone,
verbosity: config.verbosity,
instructions: config.instructions,
includeTableOfContents: config.includeTableOfContents,
includeTitleSlide: config.includeTitleSlide,
webSearch: config.webSearch,
});
}
setOpenAdvanced(open);
};
const handleSaveAdvanced = () => {
onConfigChange("tone", advancedDraft.tone);
onConfigChange("verbosity", advancedDraft.verbosity);
onConfigChange("instructions", advancedDraft.instructions);
onConfigChange("includeTableOfContents", advancedDraft.includeTableOfContents);
onConfigChange("includeTitleSlide", advancedDraft.includeTitleSlide);
onConfigChange("webSearch", advancedDraft.webSearch);
setOpenAdvanced(false);
};
return (
<div className=''>
<ToolTip content="Advanced settings" className='w-full h-full'>
<button
aria-label="Advanced settings"
title="Advanced settings"
type="button"
onClick={() => handleOpenAdvancedChange(true)}
className=" w-full h-full flex items-center px-3 py-1 text-sm bg-[#F7F6F9] hover:bg-[#F7F6F9] border-[#EDEEEF] focus-visible:ring-[#5141E5] border-none rounded-[48px] font-instrument_sans font-medium"
>
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
</button>
</ToolTip>
<Dialog open={openAdvanced} onOpenChange={handleOpenAdvancedChange}>
<DialogContent className="max-w-2xl font-instrument_sans">
<DialogHeader>
<DialogTitle>Advanced settings</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
{/* Tone */}
<div className="w-full flex flex-col gap-2">
<label className="text-sm font-semibold text-gray-700">Tone</label>
<p className="text-xs text-gray-500">Controls the writing style (e.g., casual, professional, funny).</p>
<Select
value={advancedDraft.tone}
onValueChange={(value) => setAdvancedDraft((prev) => ({ ...prev, tone: value as ToneType }))}
>
<SelectTrigger className="w-full font-instrument_sans capitalize font-medium bg-blue-100 border-blue-200 focus-visible:ring-blue-300">
<SelectValue placeholder="Select tone" />
</SelectTrigger>
<SelectContent className="font-instrument_sans">
{Object.values(ToneType).map((tone) => (
<SelectItem key={tone} value={tone} className="text-sm font-medium capitalize">
{tone}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Verbosity */}
<div className="w-full flex flex-col gap-2">
<label className="text-sm font-semibold text-gray-700">Verbosity</label>
<p className="text-xs text-gray-500">Controls how detailed slide descriptions are: concise, standard, or text-heavy.</p>
<Select
value={advancedDraft.verbosity}
onValueChange={(value) => setAdvancedDraft((prev) => ({ ...prev, verbosity: value as VerbosityType }))}
>
<SelectTrigger className="w-full font-instrument_sans capitalize font-medium bg-blue-100 border-blue-200 focus-visible:ring-blue-300">
<SelectValue placeholder="Select verbosity" />
</SelectTrigger>
<SelectContent className="font-instrument_sans">
{Object.values(VerbosityType).map((verbosity) => (
<SelectItem key={verbosity} value={verbosity} className="text-sm font-medium capitalize">
{verbosity}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Toggles */}
<div className="w-full flex flex-col gap-2 p-3 rounded-md bg-blue-100 border-blue-200">
<div className="flex items-center justify-between">
<label className="text-sm font-semibold text-gray-700">Include table of contents</label>
<Switch
checked={advancedDraft.includeTableOfContents}
onCheckedChange={(checked) => setAdvancedDraft((prev) => ({ ...prev, includeTableOfContents: checked }))}
/>
</div>
<p className="text-xs text-gray-600">Add an index slide summarizing sections (requires 3+ slides).</p>
</div>
<div className="w-full flex flex-col gap-2 p-3 rounded-md bg-blue-100 border-blue-200">
<div className="flex items-center justify-between">
<label className="text-sm font-semibold text-gray-700">Title slide</label>
<Switch
checked={advancedDraft.includeTitleSlide}
onCheckedChange={(checked) => setAdvancedDraft((prev) => ({ ...prev, includeTitleSlide: checked }))}
/>
</div>
<p className="text-xs text-gray-600">Include a title slide as the first slide.</p>
</div>
<div className="w-full flex flex-col gap-2 p-3 rounded-md bg-blue-100 border-blue-200">
<div className="flex items-center justify-between">
<label className="text-sm font-semibold text-gray-700">Web search</label>
<Switch
checked={advancedDraft.webSearch}
onCheckedChange={(checked) => setAdvancedDraft((prev) => ({ ...prev, webSearch: checked }))}
/>
</div>
<p className="text-xs text-gray-600">Allow the model to consult the web for fresher facts.</p>
</div>
{/* Instructions */}
<div className="w-full sm:col-span-2 flex flex-col gap-2">
<label className="text-sm font-semibold text-gray-700">Instructions</label>
<p className="text-xs text-gray-500">Optional guidance for the AI. These override defaults except format constraints.</p>
<Textarea
value={advancedDraft.instructions}
rows={4}
onChange={(e) => setAdvancedDraft((prev) => ({ ...prev, instructions: e.target.value }))}
placeholder="Example: Focus on enterprise buyers, emphasize ROI and security compliance. Keep slides data-driven, avoid jargon, and include a short call-to-action on the final slide."
className="py-2 px-3 border-2 font-medium text-sm min-h-[100px] max-h-[200px] border-blue-200 focus-visible:ring-offset-0 focus-visible:ring-blue-300"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => handleOpenAdvancedChange(false)}>Cancel</Button>
<Button onClick={handleSaveAdvanced} className="bg-[#5141e5] text-white hover:bg-[#5141e5]/90">Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
export default AdvanceSettings

View file

@ -1,364 +0,0 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { LanguageType, PresentationConfig, ToneType, VerbosityType } from "../type";
import { useState } from "react";
import { Check, ChevronsUpDown, SlidersHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import ToolTip from "@/components/ToolTip";
// Types
interface ConfigurationSelectsProps {
config: PresentationConfig;
onConfigChange: (key: keyof PresentationConfig, value: any) => void;
}
type SlideOption = "5" | "8" | "9" | "10" | "11" | "12" | "13" | "14" | "15" | "16" | "17" | "18" | "19" | "20";
// Constants
const SLIDE_OPTIONS: SlideOption[] = ["5", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"];
/**
* Renders a select component for slide count
*/
const SlideCountSelect: React.FC<{
value: string | null;
onValueChange: (value: string) => void;
}> = ({ value, onValueChange }) => {
const [customInput, setCustomInput] = useState(
value && !SLIDE_OPTIONS.includes(value as SlideOption) ? 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;
};
const applyCustomValue = () => {
const sanitized = sanitizeToPositiveInteger(customInput);
if (sanitized && Number(sanitized) > 0) {
onValueChange(sanitized);
}
};
return (
<Select value={value || ""} onValueChange={onValueChange} name="slides">
<SelectTrigger
className="w-[180px] font-instrument_sans font-medium bg-blue-100 border-blue-200 focus-visible:ring-blue-300"
data-testid="slides-select"
>
<SelectValue placeholder="Select Slides" />
</SelectTrigger>
<SelectContent className="font-instrument_sans">
{/* Sticky custom input at the top */}
<div
className="sticky top-0 z-10 bg-white p-2 border-b"
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-2">
<Input
inputMode="numeric"
pattern="[0-9]*"
value={customInput}
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
onChange={(e) => {
const next = sanitizeToPositiveInteger(e.target.value);
setCustomInput(next);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
applyCustomValue();
}
}}
onBlur={applyCustomValue}
placeholder="--"
className="h-8 w-16 px-2 text-sm"
/>
<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>
);
};
/**
* Renders a language selection component with search functionality
*/
const LanguageSelect: React.FC<{
value: string | null;
onValueChange: (value: string) => void;
open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ value, onValueChange, open, onOpenChange }) => (
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
name="language"
data-testid="language-select"
aria-expanded={open}
className="w-[200px] justify-between font-instrument_sans font-semibold overflow-hidden bg-blue-100 hover:bg-blue-100 border-blue-200 focus-visible:ring-blue-300 border-none"
>
<p className="text-sm font-medium truncate">
{value || "Select language"}
</p>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="end">
<Command>
<CommandInput
placeholder="Search language..."
className="font-instrument_sans"
/>
<CommandList>
<CommandEmpty>No language found.</CommandEmpty>
<CommandGroup>
{Object.values(LanguageType).map((language) => (
<CommandItem
key={language}
value={language}
role="option"
onSelect={(currentValue) => {
onValueChange(currentValue);
onOpenChange(false);
}}
className="font-instrument_sans"
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === language ? "opacity-100" : "opacity-0"
)}
/>
{language}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
export function ConfigurationSelects({
config,
onConfigChange,
}: ConfigurationSelectsProps) {
const [openLanguage, setOpenLanguage] = useState(false);
const [openAdvanced, setOpenAdvanced] = useState(false);
const [advancedDraft, setAdvancedDraft] = useState({
tone: config.tone,
verbosity: config.verbosity,
instructions: config.instructions,
includeTableOfContents: config.includeTableOfContents,
includeTitleSlide: config.includeTitleSlide,
webSearch: config.webSearch,
});
const handleOpenAdvancedChange = (open: boolean) => {
if (open) {
setAdvancedDraft({
tone: config.tone,
verbosity: config.verbosity,
instructions: config.instructions,
includeTableOfContents: config.includeTableOfContents,
includeTitleSlide: config.includeTitleSlide,
webSearch: config.webSearch,
});
}
setOpenAdvanced(open);
};
const handleSaveAdvanced = () => {
onConfigChange("tone", advancedDraft.tone);
onConfigChange("verbosity", advancedDraft.verbosity);
onConfigChange("instructions", advancedDraft.instructions);
onConfigChange("includeTableOfContents", advancedDraft.includeTableOfContents);
onConfigChange("includeTitleSlide", advancedDraft.includeTitleSlide);
onConfigChange("webSearch", advancedDraft.webSearch);
setOpenAdvanced(false);
};
return (
<div className="flex flex-wrap order-1 gap-4 items-center">
<SlideCountSelect
value={config.slides}
onValueChange={(value) => onConfigChange("slides", value)}
/>
<LanguageSelect
value={config.language}
onValueChange={(value) => onConfigChange("language", value)}
open={openLanguage}
onOpenChange={setOpenLanguage}
/>
<ToolTip content="Advanced settings">
<button
aria-label="Advanced settings"
title="Advanced settings"
type="button"
onClick={() => handleOpenAdvancedChange(true)}
className="ml-auto flex items-center gap-2 text-sm underline underline-offset-4 bg-blue-100 hover:bg-blue-100 border-blue-200 focus-visible:ring-blue-300 border-none p-2 rounded-md font-instrument_sans font-medium"
>
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
</button>
</ToolTip>
<Dialog open={openAdvanced} onOpenChange={handleOpenAdvancedChange}>
<DialogContent className="max-w-2xl font-instrument_sans">
<DialogHeader>
<DialogTitle>Advanced settings</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
{/* Tone */}
<div className="w-full flex flex-col gap-2">
<label className="text-sm font-semibold text-gray-700">Tone</label>
<p className="text-xs text-gray-500">Controls the writing style (e.g., casual, professional, funny).</p>
<Select
value={advancedDraft.tone}
onValueChange={(value) => setAdvancedDraft((prev) => ({ ...prev, tone: value as ToneType }))}
>
<SelectTrigger className="w-full font-instrument_sans capitalize font-medium bg-blue-100 border-blue-200 focus-visible:ring-blue-300">
<SelectValue placeholder="Select tone" />
</SelectTrigger>
<SelectContent className="font-instrument_sans">
{Object.values(ToneType).map((tone) => (
<SelectItem key={tone} value={tone} className="text-sm font-medium capitalize">
{tone}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Verbosity */}
<div className="w-full flex flex-col gap-2">
<label className="text-sm font-semibold text-gray-700">Verbosity</label>
<p className="text-xs text-gray-500">Controls how detailed slide descriptions are: concise, standard, or text-heavy.</p>
<Select
value={advancedDraft.verbosity}
onValueChange={(value) => setAdvancedDraft((prev) => ({ ...prev, verbosity: value as VerbosityType }))}
>
<SelectTrigger className="w-full font-instrument_sans capitalize font-medium bg-blue-100 border-blue-200 focus-visible:ring-blue-300">
<SelectValue placeholder="Select verbosity" />
</SelectTrigger>
<SelectContent className="font-instrument_sans">
{Object.values(VerbosityType).map((verbosity) => (
<SelectItem key={verbosity} value={verbosity} className="text-sm font-medium capitalize">
{verbosity}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Toggles */}
<div className="w-full flex flex-col gap-2 p-3 rounded-md bg-blue-100 border-blue-200">
<div className="flex items-center justify-between">
<label className="text-sm font-semibold text-gray-700">Include table of contents</label>
<Switch
checked={advancedDraft.includeTableOfContents}
onCheckedChange={(checked) => setAdvancedDraft((prev) => ({ ...prev, includeTableOfContents: checked }))}
/>
</div>
<p className="text-xs text-gray-600">Add an index slide summarizing sections (requires 3+ slides).</p>
</div>
<div className="w-full flex flex-col gap-2 p-3 rounded-md bg-blue-100 border-blue-200">
<div className="flex items-center justify-between">
<label className="text-sm font-semibold text-gray-700">Title slide</label>
<Switch
checked={advancedDraft.includeTitleSlide}
onCheckedChange={(checked) => setAdvancedDraft((prev) => ({ ...prev, includeTitleSlide: checked }))}
/>
</div>
<p className="text-xs text-gray-600">Include a title slide as the first slide.</p>
</div>
<div className="w-full flex flex-col gap-2 p-3 rounded-md bg-blue-100 border-blue-200">
<div className="flex items-center justify-between">
<label className="text-sm font-semibold text-gray-700">Web search</label>
<Switch
checked={advancedDraft.webSearch}
onCheckedChange={(checked) => setAdvancedDraft((prev) => ({ ...prev, webSearch: checked }))}
/>
</div>
<p className="text-xs text-gray-600">Allow the model to consult the web for fresher facts.</p>
</div>
{/* Instructions */}
<div className="w-full sm:col-span-2 flex flex-col gap-2">
<label className="text-sm font-semibold text-gray-700">Instructions</label>
<p className="text-xs text-gray-500">Optional guidance for the AI. These override defaults except format constraints.</p>
<Textarea
value={advancedDraft.instructions}
rows={4}
onChange={(e) => setAdvancedDraft((prev) => ({ ...prev, instructions: e.target.value }))}
placeholder="Example: Focus on enterprise buyers, emphasize ROI and security compliance. Keep slides data-driven, avoid jargon, and include a short call-to-action on the final slide."
className="py-2 px-3 border-2 font-medium text-sm min-h-[100px] max-h-[200px] border-blue-200 focus-visible:ring-offset-0 focus-visible:ring-blue-300"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => handleOpenAdvancedChange(false)}>Cancel</Button>
<Button onClick={handleSaveAdvanced} className="bg-[#5141e5] text-white hover:bg-[#5141e5]/90">Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View file

@ -0,0 +1,69 @@
import { Button } from '@/components/ui/button';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Check, ChevronDown } from 'lucide-react';
import React, { useState } from 'react'
import { LanguageType } from '../type';
import { cn } from '@/lib/utils';
export const LanguageSelector: React.FC<{
value: string | null;
onValueChange: (value: string) => void;
}> = ({ value, onValueChange }) => {
const [openLanguage, setOpenLanguage] = useState(false);
return (
<Popover open={openLanguage} onOpenChange={setOpenLanguage}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
name="language"
data-testid="language-select"
aria-expanded={openLanguage}
className="px-3.5 py-1 justify-between rounded-[48px] font-instrument_sans font-semibold overflow-hidden bg-[#F7F6F9] border-[#EDEEEF] focus-visible:ring-[#5141E5] border-none"
>
<p className="text-sm font-medium truncate">
{value || "Select language"}
</p>
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="end">
<Command>
<CommandInput
placeholder="Search language..."
className="font-instrument_sans"
/>
<CommandList>
<CommandEmpty>No language found.</CommandEmpty>
<CommandGroup>
{Object.values(LanguageType).map((language) => (
<CommandItem
key={language}
value={language}
role="option"
onSelect={(currentValue) => {
onValueChange(currentValue);
setOpenLanguage(false);
}}
className="font-instrument_sans"
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === language ? "opacity-100" : "opacity-0"
)}
/>
{language}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View file

@ -0,0 +1,90 @@
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import React, { useState } from 'react'
const SLIDE_OPTIONS: string[] = ["5", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"];
const NumberOfSlide = ({ value, onValueChange }: { value: string, onValueChange: (value: string) => void }) => {
const [customInput, setCustomInput] = useState(
value && !SLIDE_OPTIONS.includes(value) ? 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;
};
const applyCustomValue = () => {
const sanitized = sanitizeToPositiveInteger(customInput);
if (sanitized && Number(sanitized) > 0) {
onValueChange(sanitized);
}
};
return (
<Select value={value || ""} onValueChange={onValueChange} name="slides">
<SelectTrigger
className="w-[180px] font-instrument_sans font-medium bg-blue-100 border-blue-200 focus-visible:ring-blue-300"
data-testid="slides-select"
>
<SelectValue placeholder="Select Slides" />
</SelectTrigger>
<SelectContent className="font-instrument_sans">
{/* Sticky custom input at the top */}
<div
className="sticky top-0 z-10 bg-white p-2 border-b"
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-2">
<Input
inputMode="numeric"
pattern="[0-9]*"
value={customInput}
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
onChange={(e) => {
const next = sanitizeToPositiveInteger(e.target.value);
setCustomInput(next);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
applyCustomValue();
}
}}
onBlur={applyCustomValue}
placeholder="--"
className="h-8 w-16 px-2 text-sm"
/>
<span className="text-sm font-medium">slides</span>
</div>
</div>
{/* Hidden item to allow SelectValue to render custom selection */}
{value && !SLIDE_OPTIONS.includes(value) && (
<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>
)
}
export default NumberOfSlide

View file

@ -13,30 +13,30 @@ export function PromptInput({
onChange,
}: PromptInputProps) {
const [showHint, setShowHint] = useState(false);
const handleChange = (value: string) => {
setShowHint(value.length > 0);
onChange(value);
};
return (
<div className="space-y-2">
<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>
<p
className={`text-sm text-gray-500 font-inter font-medium ${showHint ? "opacity-100" : "opacity-0"
}`}
>
Provide specific details about your presentation needs (e.g., topic,
style, key points) for more accurate results
</p>
</div>
<Textarea
value={value}
rows={3}
name="prompt"
id="prompt"
aria-label="Prompt"
aria-describedby="prompt-description"
aria-required="true"
aria-invalid="false"
aria-autocomplete="list"
aria-controls="prompt-list"
aria-expanded="false"
aria-haspopup="listbox"
autoFocus={true}
onChange={(e) => onChange(e.target.value)}
placeholder="Tell us about your presentation"
data-testid="prompt-input"
className={`py-3.5 px-2.5 rounded-[10px] border-none bg-[#F6F6F9] placeholder:text-[#B3B3B3] font-medium font-instrument_sans text-base max-h-[300px] focus-visible:ring-offset-0 focus-visible:ring-0 overflow-y-auto custom_scrollbar `}
/>
);
}

View file

@ -1,7 +1,7 @@
'use client'
import React, { useRef, useState } from 'react'
import { File, X, Upload } from 'lucide-react'
import { File, X, Upload, Plus } from 'lucide-react'
import { toast } from 'sonner'
import { cn } from '@/lib/utils'
@ -112,35 +112,38 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => {
return (
<div className="w-full">
<h2 className="text-[#444] font-instrument_sans pt-4 text-lg mb-4">Supporting Documents</h2>
<div className="w-full bg-[#F6F6F9] px-2.5 py-3.5 rounded-[10px] ">
<div
onClick={() => fileInputRef.current?.click()}
className={cn(
"w-full border-2 border-dashed border-gray-400 rounded-lg",
"transition-all duration-300 ease-in-out bg-white",
"min-h-[300px] flex flex-col mb-8",
"w-full border cursor-pointer border-dashed border-[#B8B8C1] rounded-lg",
"transition-all duration-300 ease-in-out ",
" flex flex-col ",
isDragging && "border-purple-400 bg-purple-50"
)}
onDragOver={(e) => handleDragEvents(e, true)}
onDragLeave={(e) => handleDragEvents(e, false)}
onDrop={handleDrop}
>
<div className="flex-1 flex flex-col items-center justify-center p-6">
<Upload className={cn(
"w-12 h-12 text-gray-400 mb-4",
isDragging && "text-purple-400"
)} />
<div className="flex-1 flex gap-2 items-center justify-center p-6">
<div className='w-[42px] h-[42px] flex justify-center items-center rounded-full bg-[#EBE9FE]' >
<div className='w-[22px] h-[22px] rounded-full bg-[#7A5AF8] flex items-center justify-center text-white'>
<Plus className='w-3 h-3' />
</div>
</div>
<div>
<p className="text-gray-600 text-center mb-2">
{isDragging
? 'Drop your file here'
: 'Drag and drop your file here or click below button'
}
</p>
<p className="text-gray-400 text-sm text-center mb-4">
Supports PDFs, Text files, PPTX, DOCX
</p>
<p className=" text-xs text-[#808080] ">
{isDragging
? <span className=' '>Drop your file here</span>
: <span className=' '> <span className=' underline underline-offset-4'>Click to Upload</span> or drag &amp; drop.</span>
}
</p>
<p className="text-gray-400 text-sm text-center mt-1 ">
Supports PDFs, Text files, PPTX, DOCX
</p>
</div>
<input
type="file"
@ -153,7 +156,7 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => {
data-testid="file-upload-input"
/>
<button
{/* <button
onClick={(e) => {
e.stopPropagation()
fileInputRef.current?.click()
@ -163,7 +166,7 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => {
font-medium text-sm"
>
Choose Files
</button>
</button> */}
</div>
{files.length > 0 && (

View file

@ -14,18 +14,20 @@ import React, { useState } from "react";
import { useRouter, usePathname } from "next/navigation";
import { useDispatch } from "react-redux";
import { clearOutlines, setPresentationId } from "@/store/slices/presentationGeneration";
import { ConfigurationSelects } from "./ConfigurationSelects";
import { PromptInput } from "./PromptInput";
import { LanguageType, PresentationConfig, ToneType, VerbosityType } from "../type";
import { LanguageType, PresentationConfig, ToneType, VerbosityType } from "../type";
import SupportingDoc from "./SupportingDoc";
import { Button } from "@/components/ui/button";
import { ChevronRight } from "lucide-react";
import { ChevronRight, GitPullRequestCreate, UploadIcon } from "lucide-react";
import { toast } from "sonner";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { OverlayLoader } from "@/components/ui/overlay-loader";
import Wrapper from "@/components/Wrapper";
import { setPptGenUploadState } from "@/store/slices/presentationGenUpload";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { LanguageSelector } from "./LanguageSelector";
import AdvanceSettings from "./AdvanceSettings";
import NumberOfSlide from "./NumberOfSlide";
// Types for loading state
interface LoadingState {
@ -202,34 +204,99 @@ const UploadPage = () => {
duration={loadingState.duration}
extra_info={loadingState.extra_info}
/>
<div className="flex flex-col gap-4 md:items-center md:flex-row justify-between py-4">
{/* <div className="flex flex-col gap-4 md:items-center md:flex-row justify-between py-4">
<p></p>
<ConfigurationSelects
config={config}
onConfigChange={handleConfigChange}
/>
</div> */}
<div className=" w-full mx-auto px-2 md:px-0 ">
<div
className='fixed z-0 md:-bottom-[36%] -bottom-[40%] left-0 w-full h-full'
style={{
height: "341px",
borderRadius: '1440px',
background: 'radial-gradient(5.92% 104.69% at 50% 100%, rgba(122, 90, 248, 0.00) 0%, rgba(255, 255, 255, 0.00) 100%), radial-gradient(50% 50% at 50% 50%, rgba(122, 90, 248, 0.80) 0%, rgba(122, 90, 248, 0.00) 100%)',
}}
/>
<div className=' w-max ml-9 rounded-tl-[28px] rounded-tr-[28px] flex items-center bg-[#FAFAFF] px-2.5 pt-2.5 pb-1'
style={{
boxShadow: '0 0 16px 0 rgba(80, 71, 230, 0.12)',
}}
>
<div className={`flex justify-center gap-1 py-2.5 pl-2 pr-3 cursor-pointer bg-white rounded-[80px] `}
style={{
boxShadow: '0 0 4px 0 rgba(0, 0, 0, 0.06)',
}}
>
<GitPullRequestCreate className={`w-4 h-4 text-[#6938EF]`} />
<p className='text-xs font-medium text-black'>Create Presentation</p>
</div>
</div>
<div className=" w-full bg-[#FAFAFF] rounded-[28px] p-2.5 "
style={{
boxShadow: '0 0 16px 0 rgba(80, 71, 230, 0.12)',
clipPath: 'inset(0px -28px -28px -28px)',
}}
>
<div className="bg-[#FEFEFF] rounded-[18px] p-2 border border-[#EDEEEF] ">
<div className="py-2.5 space-y-2.5">
<PromptInput
value={config.prompt}
onChange={(value) => handleConfigChange("prompt", value)}
data-testid="prompt-input"
/>
<SupportingDoc
files={[...files]}
onFilesChange={setFiles}
data-testid="file-upload-input"
/>
</div>
<div className="mt-2 flex justify-between gap-4">
<div className=" flex items-stretch gap-3">
<LanguageSelector
value={config.language}
onValueChange={(value) => handleConfigChange("language", value)}
/>
<AdvanceSettings
config={config}
onConfigChange={handleConfigChange}
/>
</div>
<div>
<Button
onClick={handleGeneratePresentation}
style={{
background: "linear-gradient(270deg, #D5CAFC 2.4%, #E3D2EB 27.88%, #F4DCD3 69.23%, #FDE4C2 100%)",
}}
className="w-full rounded-[32px] flex items-center justify-center px-4 py-2.5 text-[#101323] font-instrument_sans font-semibold"
data-testid="next-button"
>
<span>Next</span>
<ChevronRight className="!w-6 !h-6" />
</Button>
</div>
</div>
</div>
</div>
</div>
<div className="relative">
<PromptInput
value={config.prompt}
onChange={(value) => handleConfigChange("prompt", value)}
data-testid="prompt-input"
/>
</div>
<SupportingDoc
files={[...files]}
onFilesChange={setFiles}
data-testid="file-upload-input"
/>
<Button
onClick={handleGeneratePresentation}
className="w-full rounded-[32px] flex items-center justify-center py-6 bg-[#5141e5] text-white font-instrument_sans font-semibold text-xl hover:bg-[#5141e5]/80 transition-colors duration-300"
data-testid="next-button"
>
<span>Next</span>
<ChevronRight className="!w-6 !h-6" />
</Button>
</Wrapper>
);
};

View file

@ -46,10 +46,10 @@ const page = () => {
<div className="relative">
<Header />
<div className="flex flex-col items-center justify-center py-8">
<h1 className="text-3xl font-semibold font-instrument_sans">
Create Presentation{" "}
<h1 className="text-3xl font-semibold font-instrument_sans text-[#101323] pb-3.5">
AI Presentation
</h1>
{/* <p className='text-sm text-gray-500'>We will generate a presentation for you</p> */}
<p className="text-xl font-syne text-[#101323CC]">Choose a design, set preferences, and generate polished slides.</p>
</div>
<UploadPage />

View file

@ -176,6 +176,7 @@ import neoStandardSettings from "./neo-standard/settings.json";
import neoModernSettings from "./neo-modern/settings.json";
import neoSwiftSettings from "./neo-swift/settings.json";
// Helper to create template entry
@ -223,6 +224,7 @@ export const neoGeneralTemplates: TemplateWithData[] = [
createTemplateEntry(TitleDescriptionMultiChartGridWithMetricsLayout, TitleDescriptionMultiChartGridWithMetricsSchema, TitleDescriptionMultiChartGridWithMetricsId, TitleDescriptionMultiChartGridWithMetricsName, TitleDescriptionMultiChartGridWithMetricsDesc, "neo-general", "TitleDescriptionMultiChartGridWithMetrics"),
createTemplateEntry(TitleDescriptionMultiChartGridWithBulletsLayout, TitleDescriptionMultiChartGridWithBulletsSchema, TitleDescriptionMultiChartGridWithBulletsId, TitleDescriptionMultiChartGridWithBulletsName, TitleDescriptionMultiChartGridWithBulletsDesc, "neo-general", "TitleDescriptionMultiChartGridWithBullets"),
]
export const neoStandardTemplates: TemplateWithData[] = [
createTemplateEntry(TitleBadgeChartLayout, TitleBadgeChartSchema, TitleBadgeChartId, TitleBadgeChartName, TitleBadgeChartDesc, "neo-standard", "TitleBadgeChartLayout"),
createTemplateEntry(TitleDescriptionBulletListStandardLayout, TitleDescriptionBulletListStandardSchema, TitleDescriptionBulletListStandardId, TitleDescriptionBulletListStandardName, TitleDescriptionBulletListStandardDesc, "neo-standard", "TitleDescriptionBulletList"),
@ -351,6 +353,7 @@ export const allLayouts: TemplateWithData[] = [
...standardTemplates,
...swiftTemplates,
];

View file

@ -84,167 +84,172 @@ export default function AnthropicConfig({
};
return (
<div className="space-y-6">
<div className="space-y-6 ">
{/* API Key Input */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Anthropic API Key
</label>
<div className="relative">
<input
type="text"
value={anthropicApiKey}
onChange={(e) => onApiKeyChange(e.target.value)}
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
placeholder="Enter your Anthropic API key"
/>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
Your API key will be stored locally and never shared
</p>
</div>
{/* Extended Reasoning Toggle */}
{/* <div>
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
<label className="text-sm font-medium text-gray-700">
Extended Reasoning
</label>
<Switch
checked={extendedReasoning}
onCheckedChange={(checked) => onInputChange(checked, "extended_reasoning")}
/>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
Enable extended reasoning for more detailed and thorough responses
</p>
</div> */}
{/* Check for available models button - show when no models checked or no models found */}
{(!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
<div className="mb-4">
<button
onClick={fetchAvailableModels}
disabled={modelsLoading || !anthropicApiKey}
className={`w-full py-2.5 px-4 rounded-lg transition-all duration-200 border-2 ${modelsLoading || !anthropicApiKey
? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500"
: "bg-white border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-2 focus:ring-blue-500/20"
}`}
>
{modelsLoading ? (
<div className="flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
Checking for models...
</div>
) : (
"Check for available models"
)}
</button>
</div>
)}
{/* Show message if no models found */}
{modelsChecked && availableModels.length === 0 && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
No models found. Please make sure your API key is valid and has access to Anthropic models.
<div className="mb-4 flex items-center justify-between bg-white p-10">
<div className="">
<h3 className="text-xl font-normal text-[#191919]">Anthropic API key</h3>
<p className="mt-2 text-sm max-w-[205px] text-gray-500">
Your API key will be stored locally and never shared
</p>
</div>
)}
<div className="flex items-center gap-4">
<div className="relative w-[275px] ">
<div className="flex flex-col justify-start gap-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Anthropic API Key
</label>
<input
type="text"
value={anthropicApiKey}
onChange={(e) => onApiKeyChange(e.target.value)}
className="w-full px-2 py-3 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
placeholder="Enter your Anthropic API key"
/>
</div>
{/* Model Selection - only show if models are available */}
{modelsChecked && availableModels.length > 0 ? (
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Select Anthropic Model
</label>
<div className="w-full">
<Popover
open={openModelSelect}
onOpenChange={setOpenModelSelect}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openModelSelect}
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
>
<div className="flex gap-3 items-center">
<span className="text-sm font-medium text-gray-900">
{anthropicModel
? availableModels.find(model => model === anthropicModel) || anthropicModel
: "Select a model"}
</span>
</div>
<ChevronsUpDown className="w-4 h-4 text-gray-500" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
{/* Check for available models button - show when no models checked or no models found */}
{(!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
<button
onClick={fetchAvailableModels}
disabled={modelsLoading || !anthropicApiKey}
className={` mt-7 py-2.5 bg-[#F7F6F9] px-3.5 rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border ${modelsLoading || !anthropicApiKey
? " border-gray-300 cursor-not-allowed text-gray-500"
: " border-[#EDEEEF] text-blue-600 hover:bg-[#E8F0FF]/90 focus:ring-2 focus:ring-blue-500/20"
}`}
>
<Command>
<CommandInput placeholder="Search models..." />
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{availableModels.map((model, index) => (
<CommandItem
key={index}
value={model}
onSelect={(value) => {
onInputChange(value, "anthropic_model");
setOpenModelSelect(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
anthropicModel === model
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex gap-3 items-center">
<div className="flex flex-col space-y-1 flex-1">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-900">
{model}
</span>
</div>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{modelsLoading ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
Checking for models...
</span>
) : (
"Check for available models"
)}
</button>
)}
</div>
<div className="w-[295px]">
{/* Show message if no models found */}
{modelsChecked && availableModels.length === 0 && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
No models found. Please make sure your API key is valid and has access to Anthropic models.
</p>
</div>
)}
{/* Model Selection - only show if models are available */}
{modelsChecked && availableModels.length > 0 ? (
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Select Anthropic Model
</label>
<div className="w-full">
<Popover
open={openModelSelect}
onOpenChange={setOpenModelSelect}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openModelSelect}
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
>
<div className="flex gap-3 items-center">
<span className="text-sm font-medium text-gray-900">
{anthropicModel
? availableModels.find(model => model === anthropicModel) || anthropicModel
: "Select a model"}
</span>
</div>
<ChevronsUpDown className="w-4 h-4 text-gray-500" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
>
<Command>
<CommandInput placeholder="Search models..." />
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{availableModels.map((model, index) => (
<CommandItem
key={index}
value={model}
onSelect={(value) => {
onInputChange(value, "anthropic_model");
setOpenModelSelect(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
anthropicModel === model
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex gap-3 items-center">
<div className="flex flex-col space-y-1 flex-1">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-900">
{model}
</span>
</div>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
) : null}
</div>
</div>
) : null}
</div>
{/* Web Grounding Toggle - at the end, below models dropdown */}
<div>
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
<label className="text-sm font-medium text-gray-700">
Enable Web Grounding
</label>
<Switch
checked={!!webGrounding}
onCheckedChange={(checked) => onInputChange(checked, "web_grounding")}
/>
{/* Web Grounding Toggle - show at the end, below models dropdown */}
<div className="bg-white flex justify-between items-center p-10 rounded-[12px]">
<div>
<h4 className="text-xl font-normal text-[#191919]">Model Controls</h4>
<p className="mt-2 text-sm max-w-[205px] text-gray-500">
Configure web access and advanced AI features.
</p>
</div>
<div className="flex items-center gap-4">
<div className="w-[275px]">
<div className="flex items-center mb-4 gap-2.5 ">
<Switch
checked={!!webGrounding}
onCheckedChange={(checked) => onInputChange(checked, "web_grounding")}
/>
<label className="text-sm font-medium text-gray-700">
Enable Web Grounding
</label>
</div>
{/* Extended Reasoning Toggle */}
{/* <div className="flex items-center mb-4 gap-2.5 ">
<Switch
checked={extendedReasoning}
onCheckedChange={(checked) => onInputChange(checked, "extended_reasoning")}
/>
<label className="text-sm font-medium text-gray-700">
Extended Reasoning
</label>
</div> */}
</div>
<div className="w-[295px]"></div>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
If enabled, the model can use web search grounding when available.
</p>
</div>
</div>
);
}
}

View file

@ -81,150 +81,162 @@ export default function GoogleConfig({
};
return (
<div className="space-y-6">
<div className="space-y-6 ">
{/* API Key Input */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Google API Key
</label>
<div className="relative">
<input
type="text"
value={googleApiKey}
onChange={(e) => onApiKeyChange(e.target.value)}
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
placeholder="Enter your API key"
/>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
Your API key will be stored locally and never shared
</p>
</div>
{/* Check for available models button - show when no models checked or no models found */}
{(!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
<div className="mb-4">
<button
onClick={fetchAvailableModels}
disabled={modelsLoading || !googleApiKey}
className={`w-full py-2.5 px-4 rounded-lg transition-all duration-200 border-2 ${modelsLoading || !googleApiKey
? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500"
: "bg-white border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-2 focus:ring-blue-500/20"
}`}
>
{modelsLoading ? (
<div className="flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
Checking for models...
</div>
) : (
"Check for available models"
)}
</button>
</div>
)}
{/* Show message if no models found */}
{modelsChecked && availableModels.length === 0 && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
No models found. Please make sure your API key is valid and has access to Google models.
<div className="mb-4 flex items-center justify-between bg-white p-10">
<div className="">
<h3 className="text-xl font-normal text-[#191919]">Google API key</h3>
<p className="mt-2 text-sm max-w-[205px] text-gray-500">
Your API key will be stored locally and never shared
</p>
</div>
)}
<div className="flex items-center gap-4">
<div className="relative w-[275px] ">
<div className="flex flex-col justify-start gap-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Google API Key
</label>
<input
type="text"
value={googleApiKey}
onChange={(e) => onApiKeyChange(e.target.value)}
className="w-full px-2 py-3 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
placeholder="Enter your API key"
/>
</div>
{/* Model Selection - only show if models are available */}
{modelsChecked && availableModels.length > 0 ? (
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Select Google Model
</label>
<div className="w-full">
<Popover
open={openModelSelect}
onOpenChange={setOpenModelSelect}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openModelSelect}
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
>
<div className="flex gap-3 items-center">
<span className="text-sm font-medium text-gray-900">
{googleModel
? availableModels.find(model => model === googleModel) || googleModel
: "Select a model"}
</span>
</div>
<ChevronsUpDown className="w-4 h-4 text-gray-500" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
{/* Check for available models button - show when no models checked or no models found */}
{(!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
<button
onClick={fetchAvailableModels}
disabled={modelsLoading || !googleApiKey}
className={` mt-7 py-2.5 bg-[#F7F6F9] px-3.5 rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border ${modelsLoading || !googleApiKey
? " border-gray-300 cursor-not-allowed text-gray-500"
: " border-[#EDEEEF] text-blue-600 hover:bg-[#E8F0FF]/90 focus:ring-2 focus:ring-blue-500/20"
}`}
>
<Command>
<CommandInput placeholder="Search models..." />
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{availableModels.map((model, index) => (
<CommandItem
key={index}
value={model}
onSelect={(value) => {
onInputChange(value, "google_model");
setOpenModelSelect(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
googleModel === model
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex gap-3 items-center">
<div className="flex flex-col space-y-1 flex-1">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-900">
{model}
</span>
</div>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{modelsLoading ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
Checking for models...
</span>
) : (
"Check for available models"
)}
</button>
)}
</div>
<div className="w-[295px]">
{/* Show message if no models found */}
{modelsChecked && availableModels.length === 0 && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
No models found. Please make sure your API key is valid and has access to Google models.
</p>
</div>
)}
{/* Model Selection - only show if models are available */}
{modelsChecked && availableModels.length > 0 ? (
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Select Google Model
</label>
<div className="w-full">
<Popover
open={openModelSelect}
onOpenChange={setOpenModelSelect}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openModelSelect}
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
>
<div className="flex gap-3 items-center">
<span className="text-sm font-medium text-gray-900">
{googleModel
? availableModels.find(model => model === googleModel) || googleModel
: "Select a model"}
</span>
</div>
<ChevronsUpDown className="w-4 h-4 text-gray-500" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
>
<Command>
<CommandInput placeholder="Search models..." />
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{availableModels.map((model, index) => (
<CommandItem
key={index}
value={model}
onSelect={(value) => {
onInputChange(value, "google_model");
setOpenModelSelect(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
googleModel === model
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex gap-3 items-center">
<div className="flex flex-col space-y-1 flex-1">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-900">
{model}
</span>
</div>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
) : null}
</div>
</div>
) : null}
</div>
{/* Web Grounding Toggle - at the end, below models dropdown */}
<div>
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
<label className="text-sm font-medium text-gray-700">
Enable Web Grounding
</label>
<Switch
checked={!!webGrounding}
onCheckedChange={(checked) => onInputChange(checked, "web_grounding")}
/>
{/* Web Grounding Toggle - show at the end, below models dropdown */}
<div className="bg-white flex justify-between items-center p-10 rounded-[12px]">
<div>
<h4 className="text-xl font-normal text-[#191919]">Model Controls</h4>
<p className="mt-2 text-sm max-w-[205px] text-gray-500">
Configure web access and advanced AI features.
</p>
</div>
<div className="flex items-center gap-4">
<div className="w-[275px]">
<div className="flex items-center mb-4 gap-2.5 ">
<Switch
checked={!!webGrounding}
onCheckedChange={(checked) => onInputChange(checked, "web_grounding")}
/>
<label className="text-sm font-medium text-gray-700">
Enable Web Grounding
</label>
</div>
</div>
<div className="w-[295px]"></div>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
If enabled, the model can use web search grounding when available.
</p>
</div>
</div>
);
}
}

View file

@ -3,9 +3,9 @@ import { TooltipProvider } from '@radix-ui/react-tooltip'
import React from 'react'
import { TooltipContent, TooltipTrigger, } from './ui/tooltip'
const ToolTip = ({ children, content }: { children: React.ReactNode, content: string }) => {
const ToolTip = ({ children, content, className }: { children: React.ReactNode, content: string, className?: string }) => {
return (
<div>
<div className={className}>
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>