From b4233f45efeb69ed3aef864d27efbc3d4d9f8647 Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Fri, 15 Aug 2025 00:44:26 -0400 Subject: [PATCH] ai integration --- src/app/api/ai-event/route.ts | 51 ++++++++++ src/app/api/ai-summary/route.ts | 46 +++++++++ src/app/page.tsx | 169 +++++++++++++++++++------------- 3 files changed, 199 insertions(+), 67 deletions(-) create mode 100644 src/app/api/ai-event/route.ts create mode 100644 src/app/api/ai-summary/route.ts diff --git a/src/app/api/ai-event/route.ts b/src/app/api/ai-event/route.ts new file mode 100644 index 0000000..b91d99c --- /dev/null +++ b/src/app/api/ai-event/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from "next/server"; + +export async function POST(request: Request) { + const { prompt } = await request.json(); + + const systemPrompt = ` +You are an assistant that converts natural language event descriptions into JSON objects +matching this TypeScript type EXACTLY: +{ + title: string, + description?: string, + location?: string, + url?: string, + start: string, // ISO datetime like 2024-06-14T13:00:00Z + end?: string, + allDay?: boolean +} +Today is ${new Date().toISOString().split("T")[0]}. +If no time is given, assume allDay event. +If no end time is given and not allDay, make it 1 hour after start. +Output ONLY valid JSON, nothing else. +`; + + const res = await fetch("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "openai/gpt-3.5-turbo", // Or 'mistral/mistral-tiny' for cheaper + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: prompt }, + ], + }), + }); + + const data = await res.json(); + + try { + const content = data.choices[0].message.content; + const parsed = JSON.parse(content); + return NextResponse.json(parsed); + } catch (e) { + return NextResponse.json( + { error: "Failed to parse AI output", raw: data }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/ai-summary/route.ts b/src/app/api/ai-summary/route.ts new file mode 100644 index 0000000..2ae4ff5 --- /dev/null +++ b/src/app/api/ai-summary/route.ts @@ -0,0 +1,46 @@ +import { NextResponse } from "next/server"; +import type { CalendarEvent } from "@/lib/types"; + +export async function POST(request: Request) { + try { + const { events } = await request.json(); + + if (!events || !Array.isArray(events)) { + return NextResponse.json( + { error: "Invalid events array" }, + { status: 400 }, + ); + } + + const res = await fetch("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`, // Server-side only + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "@preset/i-cal-editor-summarize", // FREE model + messages: [ + { + role: "system", + content: `You summarize a list of events in natural language. Include date, time, and title. Be concise.`, + }, + { role: "user", content: JSON.stringify(events) }, + ], + temperature: 0.4, + // max_tokens: 300, + }), + }); + + const data = await res.json(); + const summary = + data?.choices?.[0]?.message?.content || "No summary generated."; + return NextResponse.json({ summary }); + } catch (error) { + console.error(error); + return NextResponse.json( + { error: "Failed to summarize events" }, + { status: 500 }, + ); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 1b46c4b..f78beb8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -25,6 +25,10 @@ export default function HomePage() { const [end, setEnd] = useState('') const [allDay, setAllDay] = useState(false) + // AI + const [aiPrompt, setAiPrompt] = useState('') + const [aiLoading, setAiLoading] = useState(false) + useEffect(() => { (async () => { const stored = await getAllEvents() @@ -128,6 +132,64 @@ export default function HomePage() { } } + // --- AI CREATE --- + const handleAiCreate = async () => { + if (!aiPrompt.trim()) return + setAiLoading(true) + try { + const res = await fetch('/api/ai-event', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt: aiPrompt }) + }) + const data = await res.json() + if (data.title) { + setTitle(data.title || '') + setDescription(data.description || '') + setLocation(data.location || '') + setUrl(data.url || '') + setStart(data.start || '') + setEnd(data.end || '') + setAllDay(data.allDay || false) + setEditingId(null) + setDialogOpen(true) + } else { + alert('AI could not parse event.') + } + } catch (err) { + console.error(err) + alert('AI request error') + } finally { + setAiLoading(false) + } + } + + const handleAiSummarize = async () => { + if (events.length === 0) { + alert("No events to summarize") + return + } + setAiLoading(true) + try { + const res = await fetch('/api/ai-summary', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ events }) + }) + const data = await res.json() + if (data.summary) { + alert(data.summary) + } else { + alert('No summary generated.') + } + } catch (err) { + console.error(err) + alert('Error summarizing events') + } finally { + setAiLoading(false) + } + } + return (
+
+ setAiPrompt(e.target.value)} + /> + + +
+
{events.length > 0 && ( @@ -163,16 +240,11 @@ export default function HomePage() {
- {events.length === 0 && ( -

No events yet. Add or drop an .ics file here.

- )} + {events.length === 0 &&

No events yet.

} -