Skip to content

Commit 146282e

Browse files
committed
connected telegram and added all features of webapp to telegram. user can now link and use it through telegram
1 parent efca68e commit 146282e

9 files changed

Lines changed: 619 additions & 18 deletions

File tree

src/components/QuickActions.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { useState, useRef, useEffect } from "react";
22
import { Button } from "@/components/ui/button";
3-
import { Plus, Mic, Image, MessageSquare } from "lucide-react";
3+
import { Plus, Mic, Image, MessageSquare, Send } from "lucide-react";
44

55
interface QuickActionsProps {
66
onAddEvent: () => void;
77
onVoiceClick: () => void;
88
onImageClick: () => void;
99
onTextClick: () => void;
10+
onTelegramClick: () => void;
1011
}
1112

12-
const QuickActions = ({ onAddEvent, onVoiceClick, onImageClick, onTextClick }: QuickActionsProps) => {
13+
const QuickActions = ({ onAddEvent, onVoiceClick, onImageClick, onTextClick, onTelegramClick }: QuickActionsProps) => {
1314
const [isOpen, setIsOpen] = useState(false);
1415
const menuRef = useRef<HTMLDivElement>(null);
1516

@@ -41,6 +42,14 @@ const QuickActions = ({ onAddEvent, onVoiceClick, onImageClick, onTextClick }: Q
4142
>
4243
<MessageSquare className="h-6 w-6 text-primary" />
4344
</Button>
45+
<Button
46+
variant="outline"
47+
size="icon"
48+
className="rounded-full shadow-md w-14 h-14 bg-background"
49+
onClick={() => { setIsOpen(false); onTelegramClick(); }}
50+
>
51+
<Send className="h-6 w-6 text-primary" />
52+
</Button>
4453
<Button
4554
variant="outline"
4655
size="icon"
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { useState, useEffect } from "react";
2+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
3+
import { Button } from "@/components/ui/button";
4+
import { supabase } from "@/integrations/supabase/client";
5+
import { Send, CheckCircle, RefreshCw, Copy, Plus } from "lucide-react";
6+
import { toast } from "sonner";
7+
8+
interface TelegramLinkingDialogProps {
9+
isOpen: boolean;
10+
onClose: () => void;
11+
}
12+
13+
const TelegramLinkingDialog = ({ isOpen, onClose }: TelegramLinkingDialogProps) => {
14+
const [loading, setLoading] = useState(false);
15+
const [profile, setProfile] = useState<any>(null);
16+
const [generating, setGenerating] = useState(false);
17+
18+
const fetchProfile = async () => {
19+
setLoading(true);
20+
const { data: { user } } = await supabase.auth.getUser();
21+
if (user) {
22+
const { data, error } = await supabase
23+
.from('profiles')
24+
.select('*')
25+
.eq('id', user.id)
26+
.single();
27+
28+
if (data) {
29+
setProfile(data);
30+
} else if (error && error.code === 'PGRST116') { // No rows found
31+
// Create a default profile if it doesn't exist
32+
const defaultUsername = user.email?.split('@')[0] || "user_" + user.id.slice(0, 5);
33+
const { data: newProfile, error: insertError } = await supabase
34+
.from('profiles')
35+
.insert({ id: user.id, username: defaultUsername })
36+
.select()
37+
.single();
38+
39+
if (newProfile) {
40+
setProfile(newProfile);
41+
} else if (insertError) {
42+
console.error("Error creating profile:", insertError);
43+
}
44+
}
45+
}
46+
setLoading(false);
47+
};
48+
49+
useEffect(() => {
50+
if (isOpen) {
51+
fetchProfile();
52+
}
53+
}, [isOpen]);
54+
55+
const generateCode = async () => {
56+
setGenerating(true);
57+
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
58+
const { data: { user } } = await supabase.auth.getUser();
59+
60+
if (user) {
61+
const { error } = await supabase
62+
.from('profiles')
63+
.update({ link_code: code })
64+
.eq('id', user.id);
65+
66+
if (error) {
67+
toast.error("Failed to generate code: " + error.message);
68+
} else {
69+
setProfile({ ...profile, link_code: code });
70+
toast.success("New linking code generated!");
71+
}
72+
}
73+
setGenerating(false);
74+
};
75+
76+
const copyToClipboard = (text: string) => {
77+
navigator.clipboard.writeText(text);
78+
toast.success("Code copied to clipboard!");
79+
};
80+
81+
return (
82+
<Dialog open={isOpen} onOpenChange={onClose}>
83+
<DialogContent className="sm:max-width-[425px]">
84+
<DialogHeader>
85+
<DialogTitle className="flex items-center gap-2">
86+
<Send className="h-5 w-5 text-sky-500" />
87+
Link Telegram Bot
88+
</DialogTitle>
89+
<DialogDescription>
90+
Connect your account to the Maantis Telegram bot to schedule events via chat, voice, or photos.
91+
</DialogDescription>
92+
</DialogHeader>
93+
94+
<div className="py-6 flex flex-col items-center gap-6">
95+
{loading ? (
96+
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" />
97+
) : !profile ? (
98+
<div className="flex flex-col items-center gap-2 text-center">
99+
<Plus className="h-12 w-12 text-muted-foreground" />
100+
<h3 className="font-semibold text-lg">Login Required</h3>
101+
<p className="text-sm text-muted-foreground">
102+
Please sign in to your account to link Telegram.
103+
</p>
104+
</div>
105+
) : profile?.telegram_chat_id ? (
106+
<div className="flex flex-col items-center gap-2 text-center">
107+
<CheckCircle className="h-12 w-12 text-green-500" />
108+
<h3 className="font-semibold text-lg">Connected!</h3>
109+
<p className="text-sm text-muted-foreground">
110+
Your account is successfully linked to Telegram.
111+
</p>
112+
</div>
113+
) : (
114+
<div className="w-full space-y-4">
115+
<div className="bg-red-50 border border-red-100 p-3 rounded text-xs text-red-600 font-medium">
116+
⚠️ IMPORTANT: Do NOT send your email to the bot. Use the generated code below.
117+
</div>
118+
119+
<div className="bg-muted p-4 rounded-lg flex flex-col items-center gap-3">
120+
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Your Linking Code</span>
121+
{profile?.link_code ? (
122+
<div className="flex items-center gap-3 bg-background border rounded-md px-4 py-2 w-full justify-between">
123+
<span className="font-mono text-2xl font-bold tracking-widest">{profile.link_code}</span>
124+
<Button variant="ghost" size="icon" onClick={() => copyToClipboard(`/link ${profile.link_code}`)}>
125+
<Copy className="h-4 w-4" />
126+
</Button>
127+
</div>
128+
) : (
129+
<span className="text-sm italic text-muted-foreground">No code generated yet</span>
130+
)}
131+
<Button
132+
variant="outline"
133+
size="sm"
134+
onClick={generateCode}
135+
disabled={generating}
136+
className="w-full"
137+
>
138+
{generating ? "Generating..." : profile?.link_code ? "Regenerate Code" : "Generate Code"}
139+
</Button>
140+
</div>
141+
142+
<div className="space-y-3">
143+
<h4 className="text-sm font-semibold">How to link:</h4>
144+
<ol className="text-sm space-y-2 list-decimal list-inside text-muted-foreground">
145+
<li>Open the <a href="https://t.me/maantis_bot" target="_blank" className="text-primary hover:underline">Maantis Bot</a> on Telegram.</li>
146+
<li>Click **Start** or type **/start**.</li>
147+
<li>Send your linking code: <code className="bg-muted px-1 rounded">/link {profile?.link_code || "XXXXXX"}</code></li>
148+
</ol>
149+
</div>
150+
</div>
151+
)}
152+
</div>
153+
154+
<DialogFooter>
155+
<Button onClick={onClose} variant="secondary">Close</Button>
156+
</DialogFooter>
157+
</DialogContent>
158+
</Dialog>
159+
);
160+
};
161+
162+
export default TelegramLinkingDialog;

src/integrations/supabase/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,25 @@ export type Database = {
5050
created_at: string
5151
id: string
5252
username: string
53+
telegram_chat_id: number | null
54+
link_code: string | null
55+
last_event_id: string | null
5356
}
5457
Insert: {
5558
created_at?: string
5659
id: string
5760
username: string
61+
telegram_chat_id?: number | null
62+
link_code?: string | null
63+
last_event_id?: string | null
5864
}
5965
Update: {
6066
created_at?: string
6167
id?: string
6268
username?: string
69+
telegram_chat_id?: number | null
70+
link_code?: string | null
71+
last_event_id?: string | null
6372
}
6473
Relationships: []
6574
}

src/pages/Index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import TextInputDialog from "@/components/TextInputDialog";
99
import VoiceInputDialog from "@/components/VoiceInputDialog";
1010
import ImageUploadDialog from "@/components/ImageUploadDialog";
1111
import DeleteRecurringDialog from "@/components/DeleteRecurringDialog";
12+
import TelegramLinkingDialog from "@/components/TelegramLinkingDialog";
1213
import { Event } from "@/types/event";
1314
import { ParsedEvent } from "@/lib/ai";
1415
import { isEventOnDate } from "@/lib/dateUtils";
@@ -26,6 +27,7 @@ const Index = () => {
2627
const [isTextInputOpen, setIsTextInputOpen] = useState(false);
2728
const [isVoiceInputOpen, setIsVoiceInputOpen] = useState(false);
2829
const [isImageUploadOpen, setIsImageUploadOpen] = useState(false);
30+
const [isTelegramLinkingOpen, setIsTelegramLinkingOpen] = useState(false);
2931
const [aiInitialData, setAiInitialData] = useState<Partial<Event> | undefined>(undefined);
3032
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
3133
const [eventToDelete, setEventToDelete] = useState<{ event: Event; targetDateStr: string } | null>(null);
@@ -237,6 +239,12 @@ const Index = () => {
237239
onVoiceClick={() => setIsVoiceInputOpen(true)}
238240
onImageClick={() => setIsImageUploadOpen(true)}
239241
onTextClick={() => setIsTextInputOpen(true)}
242+
onTelegramClick={() => setIsTelegramLinkingOpen(true)}
243+
/>
244+
245+
<TelegramLinkingDialog
246+
isOpen={isTelegramLinkingOpen}
247+
onClose={() => setIsTelegramLinkingOpen(false)}
240248
/>
241249

242250
<AddEventDialog

supabase/functions/analyze-event/index.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,27 +49,41 @@ serve(async (req) => {
4949
throw new Error("No text provided or extracted");
5050
}
5151

52-
// Help the LLM calculate Days by actively identifying the current weekday
5352
const todayDateObj = new Date(todayStr + "T12:00:00Z");
5453
const todayDayOfWeek = todayDateObj.toLocaleDateString('en-US', { weekday: 'long' });
5554
const fullTodayContext = `${todayStr} (${todayDayOfWeek})`;
5655

5756
const systemPrompt = `
58-
You are an expert scheduling assistant. Your ONLY job is to extract event details and output an EXACT, valid JSON object.
59-
Context Information: Today's local date is ${fullTodayContext}.
60-
61-
Use EXACTLY these keys and formats:
62-
- "thought_process": (string) Step 1: Identify the base reference date. Step 2: Apply any math for relative words (e.g., "previous day", "2 months prior"). Step 3: State the exact resulting calendar day (e.g. "13th May", "March 24th").
63-
- "title": (string) A short, clean event title.
64-
- "date_reference": (string) The final absolute calendar date resolved from your thought process. Drop all relational words. (e.g., replace "previous day of 14th May" with strictly "13th May". Replace "next 4 thursdays" with strictly "next thursday"). If no date, return null.
65-
- "description": (string) Clean summary. Remove any junk OCR characters, menus, random symbols. Return "" if none.
66-
- "category": (string) Best match: "work", "personal", "family", "health", "social", or "".
67-
- "recurrence": (string) Base options: "none", "daily", "weekly", "monthly", "yearly". If the user explicitly gives an ending date/duration (like "for 2 months"), try to calculate the end date and append it using ";until=YYYY-MM-DD". Example: "weekly;until=2026-05-18". Default to "none".
68-
69-
Return ONLY JSON. Do not add any text before or after the JSON.
57+
You are an expert scheduling assistant. Today is ${fullTodayContext}.
58+
59+
CRITICAL: Many users want REMINDERS BEFORE an event or deadline. You MUST separate the anchor date from any offset.
60+
61+
Examples:
62+
- "appointment on 21st, remind 3 days before" → event_date_reference: "21st", offset_days: -3
63+
- "project due May 15, remind a week early" → event_date_reference: "May 15", offset_days: -7
64+
- "birthday on April 29, arrange money a month before" → event_date_reference: "April 29", offset_days: -30
65+
- "meeting next Friday" → event_date_reference: "next Friday", offset_days: 0
66+
- "gym tomorrow at 6pm" → event_date_reference: "tomorrow", offset_days: 0
67+
68+
Return ONLY this JSON:
69+
{
70+
"thought_process": "Step-by-step reasoning about the anchor date and any offset",
71+
"title": "Short clean event title",
72+
"event_date_reference": "The anchor/base date as text (e.g., '21st', 'next Friday', 'May 15')",
73+
"offset_days": 0,
74+
"time": "HH:MM or null",
75+
"description": "Clean description or empty string",
76+
"category": "work|personal|family|health|social or empty",
77+
"recurrence": "none|daily|weekly|monthly|yearly or with until like weekly;until=YYYY-MM-DD"
78+
}
79+
80+
Rules:
81+
- offset_days is NEGATIVE for "before/prior/early/earlier" and POSITIVE for "after/later". Default 0.
82+
- event_date_reference is the ANCHOR date — the event/deadline/birthday itself.
83+
- The title should describe the REMINDER/ACTION, not just the anchor (e.g., "Arrange money for birthday" not "Birthday").
7084
`;
7185

72-
const userPrompt = "Extract event details from this text:\\n" + textToParse;
86+
const userPrompt = "Extract event details from this text:\n" + textToParse;
7387

7488
const completion = await groq.chat.completions.create({
7589
messages: [
@@ -84,8 +98,18 @@ Return ONLY JSON. Do not add any text before or after the JSON.
8498
const jsonString = completion.choices[0]?.message?.content || "{}";
8599
const parsed = JSON.parse(jsonString);
86100

87-
// Deep deterministic NLP Date extraction based on the LLM's mathematically translated phrase
88-
if (parsed.date_reference) {
101+
// Deterministic date arithmetic
102+
if (parsed.event_date_reference) {
103+
const anchorDate = chrono.parseDate(parsed.event_date_reference, todayDateObj, { forwardDate: true });
104+
if (anchorDate) {
105+
const offset = parseInt(parsed.offset_days) || 0;
106+
const finalDate = new Date(anchorDate);
107+
finalDate.setDate(finalDate.getDate() + offset);
108+
parsed.date = finalDate.toISOString().split('T')[0];
109+
}
110+
}
111+
// Fallback: legacy date_reference field
112+
if (!parsed.date && parsed.date_reference) {
89113
const resultDate = chrono.parseDate(parsed.date_reference, todayDateObj, { forwardDate: true });
90114
if (resultDate) {
91115
parsed.date = resultDate.toISOString().split('T')[0];

0 commit comments

Comments
 (0)