From 2f58e90a535087ef92a7806c3819a6249b9dfc49 Mon Sep 17 00:00:00 2001 From: 1shCha Date: Sun, 1 Feb 2026 12:12:30 -0500 Subject: [PATCH 1/5] pdf to workspace accomplished --- .../api/workspaces/generate-title/route.ts | 2 +- .../assistant-ui/AssistantPanel.tsx | 78 ++++++++ src/components/home/HomePromptInput.tsx | 175 +++++++++++++++++- src/hooks/workspace/use-create-workspace.ts | 4 +- src/hooks/workspace/use-pdf-upload.ts | 88 +++++++++ src/lib/ai/tools/web-search.ts | 2 +- src/lib/ai/workers/quiz-worker.ts | 2 +- 7 files changed, 340 insertions(+), 11 deletions(-) create mode 100644 src/hooks/workspace/use-pdf-upload.ts diff --git a/src/app/api/workspaces/generate-title/route.ts b/src/app/api/workspaces/generate-title/route.ts index 23079db6..97c5e29b 100644 --- a/src/app/api/workspaces/generate-title/route.ts +++ b/src/app/api/workspaces/generate-title/route.ts @@ -67,7 +67,7 @@ async function handlePOST(request: NextRequest) { } const result = await generateObject({ - model: google("gemini-2.5-flash-lite"), + model: google("gemini-flash-lite-latest"), schema: z.object({ title: z.string().describe("A short, concise workspace title (max 5-6 words)"), icon: z.string().describe("A HeroIcon name that represents the topic (must be one of the available icons)"), diff --git a/src/components/assistant-ui/AssistantPanel.tsx b/src/components/assistant-ui/AssistantPanel.tsx index 2f53bafa..abe50fbd 100644 --- a/src/components/assistant-ui/AssistantPanel.tsx +++ b/src/components/assistant-ui/AssistantPanel.tsx @@ -82,6 +82,11 @@ function WorkspaceContextWrapper({ isLoading={isLoading} setIsChatExpanded={setIsChatExpanded} /> + void; +}) { + const searchParams = useSearchParams(); + const router = useRouter(); + const aui = useAui(); + const hasAutoSentRef = useRef(false); + const timeoutIdsRef = useRef[]>([]); + + const action = searchParams.get("action"); + + useEffect(() => { + if (action !== "generate_study_materials" || !workspaceId || isLoading || hasAutoSentRef.current) return; + + setIsChatExpanded?.(true); + + const prompt = `First, process the PDF file in this workspace to read its content. + +Then, using that PDF content, update all three study cards: +1. Update "Summary Notes" with a comprehensive summary +2. Update "Quiz" with 5-10 relevant questions +3. Update "Flashcards" with key terms and concepts + +Process the PDF once, then use that same content for all three updates.`; + + let attempts = 0; + const maxAttempts = 12; + const intervalMs = 200; + const ids = timeoutIdsRef.current; + + const clearAll = () => { + ids.forEach((tid) => clearTimeout(tid)); + ids.length = 0; + }; + + const trySend = () => { + attempts += 1; + const composer = aui?.composer?.(); + if (composer) { + try { + composer.setText(prompt); + composer.send(); + hasAutoSentRef.current = true; + clearAll(); + const url = new URL(window.location.href); + url.searchParams.delete("action"); + router.replace(url.pathname + url.search); + return; + } catch { + // fall through to retry + } + } + if (attempts < maxAttempts) { + const id = setTimeout(trySend, intervalMs); + ids.push(id); + } + }; + + const id = setTimeout(trySend, 100); + ids.push(id); + + return () => clearAll(); + }, [action, workspaceId, isLoading, aui, router, setIsChatExpanded]); + + return null; +} + function WorkspaceContextWrapperContent({ workspaceId, setIsChatExpanded, diff --git a/src/components/home/HomePromptInput.tsx b/src/components/home/HomePromptInput.tsx index e7d8f8f7..a84955f5 100644 --- a/src/components/home/HomePromptInput.tsx +++ b/src/components/home/HomePromptInput.tsx @@ -4,10 +4,13 @@ import { useState, useRef, useMemo, useEffect } from "react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { useCreateWorkspaceFromPrompt } from "@/hooks/workspace/use-create-workspace"; -import { ArrowUp, Loader2 } from "lucide-react"; +import { usePdfUpload } from "@/hooks/workspace/use-pdf-upload"; +import { ArrowUp, Loader2, X, FileText, Upload } from "lucide-react"; import { cn } from "@/lib/utils"; import { Input } from "@/components/ui/input"; import TypingText from "@/components/ui/typing-text"; +import { useDropzone } from "react-dropzone"; +import type { PdfData } from "@/lib/workspace-state/types"; const PLACEHOLDER_OPTIONS = [ "Calc 3 double integrals", @@ -57,8 +60,28 @@ export function HomePromptInput({ shouldFocus }: HomePromptInputProps) { const [value, setValue] = useState(""); const inputRef = useRef(null); const typingKeyRef = useRef(0); - + const createFromPrompt = useCreateWorkspaceFromPrompt(); + const { uploadFiles, uploadedFiles, isUploading, removeFile, clearFiles } = usePdfUpload(); + + // Setup drop zone for PDF files + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + accept: { + 'application/pdf': ['.pdf'], + }, + multiple: true, + noClick: true, // Don't open file dialog on click (only on drag) + onDrop: async (acceptedFiles) => { + if (acceptedFiles.length > 0) { + try { + await uploadFiles(acceptedFiles); + toast.success(`Uploaded ${acceptedFiles.length} PDF${acceptedFiles.length > 1 ? 's' : ''}`); + } catch (error) { + toast.error("Failed to upload PDFs"); + } + } + }, + }); // Shuffle options with random start for variety const shuffledOptions = useMemo(() => { @@ -90,14 +113,111 @@ export function HomePromptInput({ shouldFocus }: HomePromptInputProps) { const prompt = value.trim(); if (!prompt || createFromPrompt.isLoading) return; + // Construct initial state with PDF cards AND empty placeholder cards if files were uploaded + let initialState = undefined; + if (uploadedFiles.length > 0) { + // Calculate total PDF height (each PDF takes 10 rows) + const pdfHeight = 10; + const totalPdfY = uploadedFiles.length * pdfHeight; + + // Create PDF card items from uploaded files (stacked vertically at top) + const pdfItems = uploadedFiles.map((file, index) => ({ + id: crypto.randomUUID(), + type: 'pdf' as const, + name: file.name, + subtitle: '', + color: '#6366F1' as const, // Indigo for PDFs + layout: { x: 0, y: index * pdfHeight, w: 4, h: pdfHeight }, + lastSource: 'user' as const, + data: { + fileUrl: file.fileUrl, + filename: file.filename, + fileSize: file.fileSize, + } as PdfData, + })); + + // Create empty placeholder cards with fixed layout and colors + const noteId = crypto.randomUUID(); + const quizId = crypto.randomUUID(); + const flashcardId = crypto.randomUUID(); + + const emptyNote = { + id: noteId, + type: 'note' as const, + name: 'Summary Notes', + subtitle: 'AI will fill this from your PDFs', + color: '#10B981' as const, // Emerald for Note + layout: { x: 0, y: totalPdfY, w: 4, h: 13 }, + lastSource: 'user' as const, + data: { + blockContent: [ + { + id: 'placeholder-block', + type: 'paragraph', + props: { backgroundColor: 'default', textColor: 'default', textAlignment: 'left' }, + content: [], + children: [], + }, + ], + field1: '', + }, + }; + + const emptyQuiz = { + id: quizId, + type: 'quiz' as const, + name: 'Quiz', + subtitle: 'AI will generate questions from your PDFs', + color: '#F59E0B' as const, // Amber for Quiz + layout: { x: 0, y: totalPdfY + 13, w: 2, h: 13 }, + lastSource: 'user' as const, + data: { + questions: [], + }, + }; + + const emptyFlashcard = { + id: flashcardId, + type: 'flashcard' as const, + name: 'Flashcards', + subtitle: 'AI will create study cards from your PDFs', + color: '#EC4899' as const, // Pink for Flashcards + layout: { x: 2, y: totalPdfY + 13, w: 2, h: 8 }, + lastSource: 'user' as const, + data: { + cards: [], + }, + }; + + const allItems = [...pdfItems, emptyNote, emptyQuiz, emptyFlashcard]; + + initialState = { + workspaceId: '', // Will be set by backend + globalTitle: '', + globalDescription: '', + items: allItems, + itemsCreated: allItems.length, + }; + } + createFromPrompt.mutate(prompt, { - template: "getting_started", // Auto-include sample content (quiz/flashcards) for home prompt (magic feeling) + template: uploadedFiles.length > 0 ? "blank" : "getting_started", // Use blank template if PDFs provided + initialState, onSuccess: (workspace) => { // Reset typing animation by changing key typingKeyRef.current += 1; - router.push( - `/workspace/${workspace.slug}?createFrom=${encodeURIComponent(prompt)}` - ); + // Clear uploaded files + clearFiles(); + const url = `/workspace/${workspace.slug}`; + const params = new URLSearchParams(); + + if (uploadedFiles.length > 0) { + params.set('action', 'generate_study_materials'); + } else { + params.set('createFrom', prompt); + } + + router.push(`${url}?${params.toString()}`); }, onError: (err) => { toast.error("Could not create workspace", { description: err.message }); @@ -107,7 +227,48 @@ export function HomePromptInput({ shouldFocus }: HomePromptInputProps) { return (
-
+
+ + + {/* Drag overlay */} + {isDragActive && ( +
+
+ +

Drop PDFs here

+
+
+ )} + + {/* Uploaded files display */} + {uploadedFiles.length > 0 && ( +
+ {uploadedFiles.map((file) => ( +
+ + {file.filename} + +
+ ))} +
+ )} + {/* Input container styled to look like one input */}
inputRef.current?.focus()} diff --git a/src/hooks/workspace/use-create-workspace.ts b/src/hooks/workspace/use-create-workspace.ts index 7a7361bf..67c94cd7 100644 --- a/src/hooks/workspace/use-create-workspace.ts +++ b/src/hooks/workspace/use-create-workspace.ts @@ -69,7 +69,7 @@ export function useCreateWorkspace() { // Optimistically add the new workspace to the list queryClient.setQueryData(["workspaces"], (old) => { if (!old) return old; - + const optimisticWorkspace: WorkspaceWithState = { id: `temp-${Date.now()}`, slug: `temp-${Date.now()}`, @@ -140,6 +140,7 @@ export function useCreateWorkspaceFromPrompt() { prompt: string, options?: { template?: WorkspaceTemplate; + initialState?: AgentState; onSuccess?: (workspace: CreateWorkspaceResponse["workspace"]) => void; onError?: (error: Error) => void; } @@ -154,6 +155,7 @@ export function useCreateWorkspaceFromPrompt() { icon: icon || null, color: (color as CardColor) || null, template: options?.template || "getting_started", + initialState: options?.initialState, }); options?.onSuccess?.(result.workspace); diff --git a/src/hooks/workspace/use-pdf-upload.ts b/src/hooks/workspace/use-pdf-upload.ts new file mode 100644 index 00000000..244a8ba8 --- /dev/null +++ b/src/hooks/workspace/use-pdf-upload.ts @@ -0,0 +1,88 @@ +import { useState, useCallback } from "react"; +import type { PdfData } from "@/lib/workspace-state/types"; + +export interface UploadedPdfMetadata { + fileUrl: string; + filename: string; + fileSize: number; + name: string; +} + +export interface PdfUploadState { + isUploading: boolean; + error: Error | null; + uploadedFiles: UploadedPdfMetadata[]; +} + +/** + * Hook for uploading PDF files to Supabase storage + * Consolidates upload logic used across thread.tsx and WorkspaceSection.tsx + */ +export function usePdfUpload() { + const [state, setState] = useState({ + isUploading: false, + error: null, + uploadedFiles: [], + }); + + const uploadFiles = useCallback(async (files: File[]): Promise => { + setState((prev) => ({ ...prev, isUploading: true, error: null })); + + try { + const uploadPromises = files.map(async (file) => { + const formData = new FormData(); + formData.append("file", file); + + const uploadResponse = await fetch("/api/upload-file", { + method: "POST", + body: formData, + }); + + if (!uploadResponse.ok) { + throw new Error(`Failed to upload PDF: ${uploadResponse.statusText}`); + } + + const { url: fileUrl, filename } = await uploadResponse.json(); + + return { + fileUrl, + filename: filename || file.name, + fileSize: file.size, + name: file.name.replace(/\.pdf$/i, ""), + }; + }); + + const uploadResults = await Promise.all(uploadPromises); + + setState((prev) => ({ + ...prev, + isUploading: false, + uploadedFiles: [...prev.uploadedFiles, ...uploadResults], + })); + + return uploadResults; + } catch (error) { + const err = error instanceof Error ? error : new Error("Failed to upload PDFs"); + setState((prev) => ({ ...prev, isUploading: false, error: err })); + throw err; + } + }, []); + + const clearFiles = useCallback(() => { + setState({ isUploading: false, error: null, uploadedFiles: [] }); + }, []); + + const removeFile = useCallback((fileUrl: string) => { + setState((prev) => ({ + ...prev, + uploadedFiles: prev.uploadedFiles.filter((f) => f.fileUrl !== fileUrl), + })); + }, []); + + return { + ...state, + uploadFiles, + clearFiles, + removeFile, + }; +} diff --git a/src/lib/ai/tools/web-search.ts b/src/lib/ai/tools/web-search.ts index 1bcf5411..93866b80 100644 --- a/src/lib/ai/tools/web-search.ts +++ b/src/lib/ai/tools/web-search.ts @@ -17,7 +17,7 @@ export function createWebSearchTool() { execute: async ({ query }) => { // Use a lightweight model for the internal search loop const { text, providerMetadata } = await generateText({ - model: google('gemini-2.5-flash-lite'), + model: google('gemini-flash-lite-latest'), tools: { googleSearch: google.tools.googleSearch({}), }, diff --git a/src/lib/ai/workers/quiz-worker.ts b/src/lib/ai/workers/quiz-worker.ts index 56ae251b..f7e77aeb 100644 --- a/src/lib/ai/workers/quiz-worker.ts +++ b/src/lib/ai/workers/quiz-worker.ts @@ -5,7 +5,7 @@ import { logger } from "@/lib/utils/logger"; import { QuizQuestion, QuestionType } from "@/lib/workspace-state/types"; import { generateItemId } from "@/lib/workspace-state/item-helpers"; -const DEFAULT_CHAT_MODEL_ID = "gemini-2.5-flash-lite"; +const DEFAULT_CHAT_MODEL_ID = "gemini-flash-lite-latest"; export type QuizWorkerParams = { topic?: string; // Used only if no context provided From 0a663b1d8f5f782f568c33236251141af876a58a Mon Sep 17 00:00:00 2001 From: 1shCha Date: Tue, 3 Feb 2026 01:46:00 -0500 Subject: [PATCH 2/5] text-input box change --- src/components/home/HomeContent.tsx | 4 +- src/components/home/HomePromptInput.tsx | 218 +++++++++++++++++------- 2 files changed, 161 insertions(+), 61 deletions(-) diff --git a/src/components/home/HomeContent.tsx b/src/components/home/HomeContent.tsx index fb4a4785..4a0146d7 100644 --- a/src/components/home/HomeContent.tsx +++ b/src/components/home/HomeContent.tsx @@ -26,7 +26,7 @@ export function HomeContent() { const [scrollY, setScrollY] = useState(0); const [searchQuery, setSearchQuery] = useState(""); const [heroVisible, setHeroVisible] = useState(true); - + const createWorkspace = useCreateWorkspace(); const [workspacesVisible, setWorkspacesVisible] = useState(false); const scrollRef = useRef(null); @@ -126,7 +126,7 @@ export function HomeContent() { {/* Hero Section - Reduced height so "Recent workspaces" text peeks at bottom */}
-
+
{/* Hero Glow Effect */} diff --git a/src/components/home/HomePromptInput.tsx b/src/components/home/HomePromptInput.tsx index a84955f5..98f8969a 100644 --- a/src/components/home/HomePromptInput.tsx +++ b/src/components/home/HomePromptInput.tsx @@ -5,11 +5,26 @@ import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { useCreateWorkspaceFromPrompt } from "@/hooks/workspace/use-create-workspace"; import { usePdfUpload } from "@/hooks/workspace/use-pdf-upload"; -import { ArrowUp, Loader2, X, FileText, Upload } from "lucide-react"; +import { ArrowUp, AudioLines, FileText, Loader2, Plus, Upload, X, Link as LinkIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; import TypingText from "@/components/ui/typing-text"; import { useDropzone } from "react-dropzone"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogClose, +} from "@/components/ui/dialog"; import type { PdfData } from "@/lib/workspace-state/types"; const PLACEHOLDER_OPTIONS = [ @@ -49,8 +64,6 @@ const PLACEHOLDER_OPTIONS = [ "physics kinematics problems", ]; -const baseText = "Create a workspace for "; - interface HomePromptInputProps { shouldFocus?: boolean; } @@ -58,6 +71,8 @@ interface HomePromptInputProps { export function HomePromptInput({ shouldFocus }: HomePromptInputProps) { const router = useRouter(); const [value, setValue] = useState(""); + const [isUrlDialogOpen, setIsUrlDialogOpen] = useState(false); + const [urlInput, setUrlInput] = useState(""); const inputRef = useRef(null); const typingKeyRef = useRef(0); @@ -65,7 +80,7 @@ export function HomePromptInput({ shouldFocus }: HomePromptInputProps) { const { uploadFiles, uploadedFiles, isUploading, removeFile, clearFiles } = usePdfUpload(); // Setup drop zone for PDF files - const { getRootProps, getInputProps, isDragActive } = useDropzone({ + const { getRootProps, getInputProps, isDragActive, open } = useDropzone({ accept: { 'application/pdf': ['.pdf'], }, @@ -225,8 +240,23 @@ export function HomePromptInput({ shouldFocus }: HomePromptInputProps) { }); }; + const handleUrlSubmit = () => { + if (!urlInput.trim()) return; + + // Append URL to current value, adding a space if needed + const newValue = value + (value && !value.endsWith(' ') ? ' ' : '') + urlInput.trim() + ' '; + setValue(newValue); + setUrlInput(""); + setIsUrlDialogOpen(false); + + // Focus back on input + setTimeout(() => { + inputRef.current?.focus(); + }, 100); + }; + return ( - +
@@ -273,29 +303,16 @@ export function HomePromptInput({ shouldFocus }: HomePromptInputProps) {
inputRef.current?.focus()} className={cn( - "relative flex items-center gap-0 min-h-[56px] w-full", - "bg-background/80 backdrop-blur-xl", - "border border-white/10 rounded-xl", - "shadow-[0_0_60px_-15px_rgba(255,255,255,0.1)]", - "focus-within:shadow-[0_0_80px_-10px_rgba(255,255,255,0.15)]", - "focus-within:border-white/60", + "relative w-full min-h-[96px]", + "rounded-[32px] border border-white/25", + "bg-gradient-to-r from-[#292929] to-[#242424] backdrop-blur-xl", + "px-4 py-3 md:px-6 md:py-4", + "shadow-[0_24px_90px_-40px_rgba(0,0,0,0.85)]", + "focus-within:border-white/40", "transition-all duration-300", "cursor-text" )} > - {/* Prefix label */} - - {baseText} - - - {/* Input field */}
{ if (e.key === "Enter" && !e.shiftKey) { @@ -335,14 +352,10 @@ export function HomePromptInput({ shouldFocus }: HomePromptInputProps) {
)} +
- {/* Submit arrow */} - +
+ + + + + + open()}> + + Upload PDF + + setIsUrlDialogOpen(true)}> + + Add URL + + + + +
+ + + + + +
+ + + + + Add Link + +
+ setUrlInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + handleUrlSubmit(); + } + }} + autoFocus + /> +
+ + + + +
+
); } From cde54a36849165169112f2123e29ce8cd33b6771 Mon Sep 17 00:00:00 2001 From: 1shCha Date: Tue, 3 Feb 2026 02:55:07 -0500 Subject: [PATCH 3/5] improved text box changes iter1 --- src/components/home/HomePromptInput.tsx | 46 ++++++++++++++++--------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/components/home/HomePromptInput.tsx b/src/components/home/HomePromptInput.tsx index 98f8969a..b7dc3e86 100644 --- a/src/components/home/HomePromptInput.tsx +++ b/src/components/home/HomePromptInput.tsx @@ -71,9 +71,10 @@ interface HomePromptInputProps { export function HomePromptInput({ shouldFocus }: HomePromptInputProps) { const router = useRouter(); const [value, setValue] = useState(""); + const [isExpanded, setIsExpanded] = useState(false); const [isUrlDialogOpen, setIsUrlDialogOpen] = useState(false); const [urlInput, setUrlInput] = useState(""); - const inputRef = useRef(null); + const inputRef = useRef(null); const typingKeyRef = useRef(0); const createFromPrompt = useCreateWorkspaceFromPrompt(); @@ -117,10 +118,18 @@ export function HomePromptInput({ shouldFocus }: HomePromptInputProps) { } }, [shouldFocus]); - // Handle user typing - stop animation - const handleInput = (e: React.ChangeEvent) => { + // Handle user typing - stop animation and auto-resize + const handleInput = (e: React.ChangeEvent) => { const newValue = e.target.value; setValue(newValue); + + // Auto-resize + const target = e.target; + target.style.height = 'auto'; + target.style.height = `${target.scrollHeight}px`; + + // Check expansion for shape shifting (threshold approx 3 lines) + setIsExpanded(target.scrollHeight > 80); }; const handleSubmit = async (e: React.FormEvent) => { @@ -283,7 +292,7 @@ export function HomePromptInput({ shouldFocus }: HomePromptInputProps) { )} > - {file.filename} + {file.name}