feat: multimodal AI event creation with image support #1

Merged
old4ever merged 20 commits from image-parse into main 2026-04-07 15:21:28 -04:00
Showing only changes of commit 95de6ae46a - Show all commits

View File

@@ -21,6 +21,14 @@ import { EventsList } from "@/components/events-list";
import { EventDialog } from "@/components/event-dialog";
import { DragDropContainer } from "@/components/drag-drop-container";
const fileToBase64 = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(new Error("Failed to read file"));
reader.readAsDataURL(file);
});
export default function HomePage() {
const [events, setEvents] = useState<CalendarEvent[]>([]);
const [dialogOpen, setDialogOpen] = useState(false);
@@ -45,6 +53,10 @@ export default function HomePage() {
const [summary, setSummary] = useState<string | null>(null);
const [summaryUpdated, setSummaryUpdated] = useState<string | null>(null);
// Image
const [imageBase64, setImageBase64] = useState<string | null>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null);
useEffect(() => {
(async () => {
const stored = await getAllEvents();
@@ -66,6 +78,20 @@ export default function HomePage() {
setRecurrenceRule(undefined);
};
const handleImageSelect = async (file: File) => {
const base64 = await fileToBase64(file);
setImageBase64(base64);
setImagePreview(URL.createObjectURL(file));
};
const handleImageClear = () => {
if (imagePreview) {
URL.revokeObjectURL(imagePreview);
}
setImageBase64(null);
setImagePreview(null);
};
const handleSave = async () => {
const eventData: CalendarEvent = {
id: editingId || nanoid(),
@@ -130,7 +156,7 @@ export default function HomePage() {
// AI Create Event
const handleAiCreate = async () => {
if (!aiPrompt.trim()) return;
if (!aiPrompt.trim() && !imageBase64) return;
setAiLoading(true);
const promise = (): Promise<{ message: string }> =>
@@ -139,7 +165,10 @@ export default function HomePage() {
const res = await fetch("/api/ai-event", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: aiPrompt }),
body: JSON.stringify({
prompt: aiPrompt,
imageBase64: imageBase64 || undefined,
}),
});
if (res.status === 401) {
@@ -154,7 +183,6 @@ export default function HomePage() {
if (Array.isArray(data) && data.length > 0) {
if (data.length === 1) {
// Prefill dialog directly (same as before)
const ev = data[0];
setTitle(ev.title || "");
setDescription(ev.description || "");
@@ -167,11 +195,11 @@ export default function HomePage() {
setAiPrompt("");
setDialogOpen(true);
setRecurrenceRule(ev.recurrenceRule || undefined);
handleImageClear();
resolve({
message: "Event has been created!",
});
} else {
// Save them all directly to DB
for (const ev of data) {
const newEvent = {
id: nanoid(),
@@ -186,8 +214,9 @@ export default function HomePage() {
setAiPrompt("");
setSummary(`Added ${data.length} AI-generated events.`);
setSummaryUpdated(new Date().toLocaleString());
handleImageClear();
resolve({
message: "Event has been created!",
message: "Events have been created!",
});
}
} else {
@@ -264,6 +293,7 @@ export default function HomePage() {
isDragOver={isDragOver}
setIsDragOver={setIsDragOver}
onImport={handleImport}
onImageDrop={handleImageSelect}
>
<AIToolbar
isAuthenticated={!!session?.user}
@@ -271,6 +301,9 @@ export default function HomePage() {
aiPrompt={aiPrompt}
setAiPrompt={setAiPrompt}
aiLoading={aiLoading}
imagePreview={imagePreview}
onImageSelect={handleImageSelect}
onImageClear={handleImageClear}
onAiCreate={handleAiCreate}
onAiSummarize={handleAiSummarize}
summary={summary}