feat: Upload page redesigns
This commit is contained in:
parent
07ae990c95
commit
45185fb125
18 changed files with 844 additions and 784 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 `}
|
||||
/>
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 & 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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
||||
];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue