add initial complete webui, more ai commands for moderation, add api

This commit is contained in:
Aidan 2025-07-05 14:36:17 -04:00
parent 19e794e34c
commit 173d4e7a52
112 changed files with 8176 additions and 780 deletions

549
webui/app/about/page.tsx Executable file
View file

@ -0,0 +1,549 @@
import { Button } from "@/components/ui/button"
import {
Sparkles,
Users,
Download,
Brain,
Shield,
Zap,
Tv,
Heart,
Code,
Globe,
MessageSquare,
Layers,
Network,
Lock,
UserCheck,
BarChart3,
Languages,
Trash2,
FileText,
Headphones,
CloudSun,
Smartphone,
Dices,
Cat,
Music,
Bot
} from "lucide-react";
import { SiTypescript, SiPostgresql, SiDocker, SiNextdotjs, SiBun, SiForgejo } from "react-icons/si";
import { RiTelegram2Line } from "react-icons/ri";
import { BsInfoLg } from "react-icons/bs";
import { TbRocket, TbSparkles } from "react-icons/tb";
import Link from "next/link";
import { TbPalette } from "react-icons/tb";
import Footer from "@/components/footer";
export default function About() {
return (
<div className="flex flex-col min-h-screen">
<section className="flex flex-col items-center justify-center py-24 px-6 text-center bg-gradient-to-br from-background to-muted">
<div className="max-w-4xl mx-auto space-y-8">
<div className="flex items-center justify-center mb-6">
<div className="flex items-center justify-center w-20 h-20 rounded-full bg-primary/10 p-4">
<BsInfoLg className="w-10 h-10" />
</div>
</div>
<h1 className="text-6xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
About Kowalski
</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
Kowalski is an open-source, feature-rich Telegram bot built with modern web technologies.
From AI-powered conversations to video downloads, user management, and community features
it&apos;s designed to enhance your Telegram experience while respecting your privacy.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center pt-8">
<Button size="lg" className="min-w-32" asChild>
<Link href="https://git.p0ntus.com/ABOCN/TelegramBot" target="_blank">
<SiForgejo />
View Source Code
</Link>
</Button>
<Button variant="outline" size="lg" className="min-w-32" asChild>
<Link href="https://p0ntus.com/services/hosting" target="_blank">
<TbRocket />
Deploy free with p0ntus
</Link>
</Button>
<Button variant="outline" size="lg" className="min-w-32" asChild>
<Link href="https://t.me/KowalskiNodeBot" target="_blank">
<RiTelegram2Line />
Try on Telegram
</Link>
</Button>
</div>
</div>
</section>
<section className="py-24 px-6">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">Architecture</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
We&apos;ve built Kowalski with modern technologies and best practices for reliability and maintainability.
</p>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-blue-500/10 text-blue-500">
<Code className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Tech Stack</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Kowalski is built completely in TypeScript with Node.js and Telegraf.
The web interface uses Next.js with Tailwind CSS, while data persistence is handled by PostgreSQL with Drizzle ORM.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<SiTypescript className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">TypeScript + Node.js</div>
<div className="text-sm text-muted-foreground">Type-safe backend w/ Telegraf</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<SiNextdotjs className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">Next.js WebUI</div>
<div className="text-sm text-muted-foreground">Modern, responsive admin and user panel</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<SiPostgresql className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">PostgreSQL + Drizzle ORM</div>
<div className="text-sm text-muted-foreground">Reliable data persistence</div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-green-500/10 text-green-500">
<SiDocker className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Deployment</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Kowalski is built to be deployed anywhere, and has been tested on multiple platforms.
We prioritize support for Docker and Bun for easy deployment.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<SiDocker className="w-5 h-5 mx-3 text-green-500" />
<div>
<div className="font-medium">Docker Support</div>
<div className="text-sm text-muted-foreground">Easy containerized deployment w/ Docker Compose</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<SiBun className="w-5 h-5 mx-3 text-green-500" />
<div>
<div className="font-medium">Bun</div>
<div className="text-sm text-muted-foreground">A fast JavaScript runtime for best performance</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Layers className="w-5 h-5 mx-3 text-green-500" />
<div> {/* some ppl probably don't know what af means :( */}
<div className="font-medium">Modular AF Backend</div>
<div className="text-sm text-muted-foreground">Command-based structure for easy feature addition</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="py-24 px-6 bg-muted/30">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">AI Integrations</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Powered by Ollama, Kowalski has support for 50+ AI models, with customizable
options for users and admins.
</p>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-purple-500/10 text-purple-500">
<Brain className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Vast Model Support</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Kowalski has support for 50+ models, both thinking and non-thinking. We have
good Markdown parsing, with customizable options for both users and admins.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<TbSparkles className="w-5 h-5 mx-3 text-purple-500" />
<div>
<div className="font-medium">/ask - Quick Responses</div>
<div className="text-sm text-muted-foreground">Fast answers using smaller non-thinking models</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Brain className="w-5 h-5 mx-3 text-purple-500" />
<div>
<div className="font-medium">/think - Deep Reasoning</div>
<div className="text-sm text-muted-foreground">Advanced thinking models with togglable reasoning visibility</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Bot className="w-5 h-5 mx-3 text-purple-500" />
<div>
<div className="font-medium">/ai - Your Custom Model!</div>
<div className="text-sm text-muted-foreground">Use your personally configured AI model</div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-orange-500/10 text-orange-500">
<Zap className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Kowalski&apos;s <span className="italic">Powerful</span></h3>
</div>
<p className="text-muted-foreground leading-relaxed">
We have amazing Markdown V2 parsing, queue management, and usage statistics tracking.
It&apos;s hella private, too. AI is disabled by default for the best user experience.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Network className="w-5 h-5 mx-3 text-orange-500" />
<div>
<div className="font-medium">Streaming</div>
<div className="text-sm text-muted-foreground">Real-time Markdown V2 message updates as the model generates</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<BarChart3 className="w-5 h-5 mx-3 text-orange-500" />
<div>
<div className="font-medium">Usage Stats</div>
<div className="text-sm text-muted-foreground">Track your AI requests and usage with /aistats</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<UserCheck className="w-5 h-5 mx-3 text-orange-500" />
<div>
<div className="font-medium">Queues</div>
<div className="text-sm text-muted-foreground">High usage limits with intelligent request queuing</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="py-24 px-6">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">We&apos;re User-First</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Kowalski has privacy-focused user management with customizable settings,
multilingual support, and transparent data handling.
</p>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-emerald-500/10 text-emerald-500">
<Lock className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Privacy</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
User data is minimized and linked only by Telegram ID. No personal information
is shared with third parties, and users maintain full control over their data
with easy account deletion options.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Shield className="w-5 h-5 mx-3 text-emerald-500" />
<div>
<div className="font-medium">Limited Data Collection</div>
<div className="text-sm text-muted-foreground">Only essential data is stored, linked by Telegram ID</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<FileText className="w-5 h-5 mx-3 text-emerald-500" />
<div>
<div className="font-medium">Transparent Policies</div>
<div className="text-sm text-muted-foreground">Clear privacy policy accessible via /privacy</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Trash2 className="w-5 h-5 mx-3 text-emerald-500" />
<div>
<div className="font-medium">Easy Account Deletion</div>
<div className="text-sm text-muted-foreground">You can delete your data at any time</div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-blue-500/10 text-blue-500">
<TbPalette className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Customization</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Personalize your experience with custom AI preferences,
temperature settings, language selection, and detailed usage statistics.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Bot className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">AI Preferences</div>
<div className="text-sm text-muted-foreground">Choose default models and configure temperature</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Languages className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">Multilingual Support</div>
<div className="text-sm text-muted-foreground">English and Portuguese language options</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<BarChart3 className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">Usage Analytics</div>
<div className="text-sm text-muted-foreground">Personal statistics and usage tracking</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="py-24 px-6 bg-muted/30">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">There&apos;s <span className="text-5xl">WAYYYYY</span> more!</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Beyond AI, Kowalski has a ton of entertainment, utility, fun, configuration, and information
commands.
</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-red-500/10 text-red-500">
<Download className="w-5 h-5" />
</div>
<h3 className="text-xl font-semibold">Media Downloads</h3>
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
Download videos from YouTube and 1000s of other platforms using yt-dlp.
Featuring automatic size checking for Telegram&apos;.
</p>
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<Tv className="w-4 h-4 text-red-500" />
<span>/yt [URL] - Video downloads</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Shield className="w-4 h-4 text-red-500" />
<span>Automatic size limit handling</span>
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-blue-500/10 text-blue-500">
<Globe className="w-5 h-5" />
</div>
<h3 className="text-xl font-semibold">Information & Utilities</h3>
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
Access real-world information like weather reports, device specifications,
HTTP status codes, and a Last.fm music integration.
</p>
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<CloudSun className="w-4 h-4 text-blue-500" />
<span>/weather - Weather reports</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Smartphone className="w-4 h-4 text-blue-500" />
<span>/device - GSMArena specs</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Headphones className="w-4 h-4 text-blue-500" />
<span>/last - Last.fm integration</span>
</div>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-green-500/10 text-green-500">
<Heart className="w-5 h-5" />
</div>
<h3 className="text-xl font-semibold">Entertainment</h3>
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
Interactive emojis, random animal pictures, My Little Pony,
and fun commands to engage you and your community.
</p>
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<Dices className="w-4 h-4 text-green-500" />
<span>/dice, /slot - Interactive games</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Cat className="w-4 h-4 text-green-500" />
<span>/cat, /dog - Random animals</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Music className="w-4 h-4 text-green-500" />
<span>/mlp - My Little Pony DB</span>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="py-24 px-6">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">Our Community</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Kowalski is built by developers, for developers. We use open licenses and
take input from our development communities.
</p>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-purple-500/10 text-purple-500">
<SiForgejo className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Open Development</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Kowalski is licensed under BSD-3-Clause with components under Unlicense. Our
codebase is available on our Forgejo and GitHub, with lots of documentation.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<SiForgejo className="w-5 h-5 mx-3 text-purple-500" />
<div>
<div className="font-medium">Public Code</div>
<div className="text-sm text-muted-foreground">Feel free to contribute or review our code</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<FileText className="w-5 h-5 mx-3 text-purple-500" />
<div>
<div className="font-medium">Documentation</div>
<div className="text-sm text-muted-foreground">We have documentation to help contributors, users, and admins</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Users className="w-5 h-5 mx-3 text-purple-500" />
<div>
<div className="font-medium">Contributor Friendly</div>
<div className="text-sm text-muted-foreground">Our communities are welcoming to new contributors</div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-orange-500/10 text-orange-500">
<Heart className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Community Centric</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Kowalski was created by Lucas Gabriel (lucmsilva). It is now also maintained by ihatenodejs,
givfnz2, and other contributors. Thank you to all of our contributors!
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<MessageSquare className="w-5 h-5 mx-3 text-orange-500" />
<div>
<div className="font-medium">Active Maintenance</div>
<div className="text-sm text-muted-foreground">Regular updates and fixes w/ room for input and feedback</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Code className="w-5 h-5 mx-3 text-orange-500" />
<div>
<div className="font-medium">Quality Code</div>
<div className="text-sm text-muted-foreground">We use TypeScript, linting, and modern standards</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Sparkles className="w-5 h-5 mx-3 text-orange-500" />
<div>
<div className="font-medium">Focus on New Features</div>
<div className="text-sm text-muted-foreground">We are always looking for new features to add</div>
</div>
</div>
</div>
</div>
</div>
<div className="mt-16 text-center">
<div className="inline-flex items-center gap-4 p-6 rounded-lg bg-muted/50 border">
<div className="flex items-center gap-2">
<SiForgejo className="w-5 h-5" />
<span className="font-medium">Ready to contribute?</span>
</div>
<Button asChild>
<Link href="https://git.p0ntus.com/ABOCN/TelegramBot" target="_blank">
<SiForgejo />
View on Forgejo
</Link>
</Button>
</div>
</div>
</div>
</section>
<Footer />
</div>
);
}

204
webui/app/account/delete/page.tsx Executable file
View file

@ -0,0 +1,204 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Trash2, ArrowLeft, AlertTriangle } from "lucide-react";
import Link from "next/link";
import { useAuth } from "@/contexts/auth-context";
import { motion } from "framer-motion";
export default function DeleteAccountPage() {
const [isDeleting, setIsDeleting] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const { user, isAuthenticated, loading } = useAuth();
const router = useRouter();
const handleDeleteAccount = async () => {
if (!user) return;
setIsDeleting(true);
try {
const response = await fetch('/api/user/delete', {
method: 'DELETE',
credentials: 'include'
});
if (response.ok) {
alert('Your account has been deleted. You will now be redirected to the home page. Thanks for using Kowalski!');
window.location.href = '/';
} else {
const error = await response.json();
alert(`Failed to delete account: ${error.message || 'Unknown error'}`);
}
} catch (error) {
console.error('Error deleting account:', error);
alert('An error occurred while deleting your account. Please try again.');
} finally {
setIsDeleting(false);
setDialogOpen(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
);
}
if (!isAuthenticated) {
router.push('/login');
return null;
}
return (
<div className="w-full h-full bg-background">
<div className="container mx-auto px-6 py-8 max-w-2xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<div className="flex items-center gap-4 mb-8">
<Button variant="outline" size="sm" asChild>
<Link href="/account">
<ArrowLeft className="w-4 h-4" />
Back to Account
</Link>
</Button>
</div>
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center">
<Trash2 className="w-8 h-8 text-red-600" />
</div>
<div>
<h1 className="text-3xl font-bold">Delete Account</h1>
<p className="text-muted-foreground">Permanently remove your account and data</p>
</div>
</div>
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-6">
<div className="flex items-start gap-3">
<AlertTriangle className="w-6 h-6 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
<div className="space-y-2">
<h3 className="font-semibold text-yellow-800 dark:text-yellow-200">
This action cannot be undone
</h3>
<p className="text-sm text-yellow-700 dark:text-yellow-300">
Deleting your account will permanently remove all your data, including:
</p>
<ul className="text-sm text-yellow-700 dark:text-yellow-300 list-disc list-inside space-y-1 ml-2">
<li>Your user profile and settings</li>
<li>AI usage statistics and request history</li>
<li>Custom AI model preferences</li>
<li>Command configuration and disabled commands</li>
<li>All associated sessions and authentication data</li>
</ul>
</div>
</div>
</div>
<div className="bg-card border rounded-lg p-6">
<h3 className="text-lg font-semibold mb-4">Account Information</h3>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Username:</span>
<span className="font-medium">@{user?.username}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Name:</span>
<span className="font-medium">{user?.firstName} {user?.lastName}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Telegram ID:</span>
<span className="font-medium">{user?.telegramId}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">AI Requests:</span>
<span className="font-medium">{user?.aiRequests.toLocaleString()}</span>
</div>
</div>
</div>
<div className="pt-6 border-t">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Ready to delete your account?</h3>
<p className="text-sm text-muted-foreground">
This will immediately and permanently delete your account.
</p>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button variant="destructive" className="gap-2">
<Trash2 className="w-4 h-4" />
Delete Account
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-red-600" />
Confirm Account Deletion
</DialogTitle>
<DialogDescription className="space-y-2">
<p>
Are you absolutely sure you want to delete your account? This action cannot be undone.
</p>
<p className="font-medium">
Your account <span className="font-bold">@{user?.username}</span> and all associated data will be permanently removed.
</p>
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => setDialogOpen(false)}
disabled={isDeleting}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteAccount}
disabled={isDeleting}
className="gap-2"
>
{isDeleting ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="w-4 h-4" />
Yes, Delete Account
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</div>
</motion.div>
</div>
</div>
);
}

725
webui/app/account/page.tsx Executable file
View file

@ -0,0 +1,725 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import {
User,
Bot,
Brain,
Settings,
CloudSun,
Smartphone,
Heart,
Cat,
Dices,
Thermometer,
BarChart3,
LogOut,
Edit3,
Save,
X,
Network,
Cpu,
Languages,
Bug,
Lightbulb,
ExternalLink,
Quote,
Info,
Shuffle,
Rainbow,
Database,
Hash,
Download,
Archive
} from "lucide-react";
import { RiTelegram2Line } from "react-icons/ri";
import { motion } from "framer-motion";
import { ModelPicker } from "@/components/account/model-picker";
import { useAuth } from "@/contexts/auth-context";
import { FaLastfm } from "react-icons/fa";
import { TiInfinity } from "react-icons/ti";
interface CommandCard {
id: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
title: string;
description: string;
commands: string[];
category: "ai" | "entertainment" | "utility" | "media" | "admin" | "animals";
gradient: string;
enabled: boolean;
}
const allCommands: CommandCard[] = [
{
id: "ai-ask-think",
icon: Brain,
title: "AI Chats",
description: "Chat with AI models and use deep thinking",
commands: ["/ask", "/think"],
category: "ai",
gradient: "from-purple-500 to-pink-500",
enabled: true
},
{
id: "ai-custom",
icon: Bot,
title: "Custom AI Model",
description: "Use your personally configured AI model",
commands: ["/ai"],
category: "ai",
gradient: "from-indigo-500 to-purple-500",
enabled: true
},
{
id: "ai-stats",
icon: BarChart3,
title: "AI Statistics",
description: "View your AI usage statistics",
commands: ["/aistats"],
category: "ai",
gradient: "from-purple-600 to-indigo-600",
enabled: true
},
{
id: "games-dice",
icon: Dices,
title: "Interactive Emojis",
description: "Roll dice, play slots, and other interactive emojis",
commands: ["/dice", "/slot", "/ball", "/dart", "/bowling"],
category: "entertainment",
gradient: "from-green-500 to-teal-500",
enabled: true
},
{
id: "fun-random",
icon: Shuffle,
title: "Fun Commands",
description: "Random numbers and fun responses",
commands: ["/random", "/furry", "/gay"],
category: "entertainment",
gradient: "from-pink-500 to-rose-500",
enabled: true
},
{
id: "infinite-dice",
icon: TiInfinity,
title: "Infinite Dice",
description: "Sends an infinite dice sticker",
commands: ["/idice"],
category: "entertainment",
gradient: "from-yellow-500 to-orange-500",
enabled: true
},
{
id: "animals-basic",
icon: Cat,
title: "Animal Pictures",
description: "Get random cute animal pictures",
commands: ["/cat", "/dog", "/duck", "/fox"],
category: "animals",
gradient: "from-orange-500 to-red-500",
enabled: true
},
{
id: "soggy-cat",
icon: Heart,
title: "Soggy Cat",
description: "Wet cats!",
commands: ["/soggy", "/soggycat"],
category: "animals",
gradient: "from-blue-500 to-purple-500",
enabled: true
},
{
id: "weather",
icon: CloudSun,
title: "Weather",
description: "Get current weather for any location",
commands: ["/weather", "/clima"],
category: "utility",
gradient: "from-blue-500 to-cyan-500",
enabled: true
},
{
id: "device-specs",
icon: Smartphone,
title: "Device Specifications",
description: "Look up phone specifications via GSMArena",
commands: ["/device", "/d"],
category: "utility",
gradient: "from-slate-500 to-gray-500",
enabled: true
},
{
id: "http-status",
icon: Network,
title: "HTTP Status Codes",
description: "Look up HTTP status codes and meanings",
commands: ["/http", "/httpcat"],
category: "utility",
gradient: "from-emerald-500 to-green-500",
enabled: true
},
{
id: "codename-lookup",
icon: Hash,
title: "Codename Lookup",
description: "Look up codenames and meanings",
commands: ["/codename", "/whatis"],
category: "utility",
gradient: "from-teal-500 to-cyan-500",
enabled: true
},
{
id: "info-commands",
icon: Info,
title: "Information",
description: "Get chat and user information",
commands: ["/chatinfo", "/userinfo"],
category: "utility",
gradient: "from-indigo-500 to-blue-500",
enabled: true
},
{
id: "quotes",
icon: Quote,
title: "Random Quotes",
description: "Get random quotes",
commands: ["/quote"],
category: "utility",
gradient: "from-amber-500 to-yellow-500",
enabled: true
},
{
id: "youtube-download",
icon: Download,
title: "Video Downloads",
description: "Download videos from YouTube and 1000+ platforms",
commands: ["/yt", "/ytdl", "/video", "/dl"],
category: "media",
gradient: "from-red-500 to-pink-500",
enabled: true
},
{
id: "lastfm",
icon: FaLastfm,
title: "Last.fm Integration",
description: "Connect your music listening history",
commands: ["/last", "/lfm", "/setuser"],
category: "media",
gradient: "from-violet-500 to-purple-500",
enabled: true
},
{
id: "mlp-content",
icon: Database,
title: "MLP Database",
description: "My Little Pony content and information",
commands: ["/mlp", "/mlpchar", "/mlpep", "/mlpcomic"],
category: "media",
gradient: "from-fuchsia-500 to-pink-500",
enabled: true
},
{
id: "modarchive",
icon: Archive,
title: "Mod Archive",
description: "Access classic tracker music files",
commands: ["/modarchive", "/tma"],
category: "media",
gradient: "from-cyan-500 to-blue-500",
enabled: true
},
{
id: "random-pony",
icon: Rainbow,
title: "Random Pony Art",
description: "Get random My Little Pony artwork",
commands: ["/rpony", "/randompony", "/mlpart"],
category: "media",
gradient: "from-pink-500 to-purple-500",
enabled: true
},
];
const categoryColors = {
ai: "bg-purple-500/10 text-purple-600 border-purple-200 dark:border-purple-800",
entertainment: "bg-green-500/10 text-green-600 border-green-200 dark:border-green-800",
utility: "bg-blue-500/10 text-blue-600 border-blue-200 dark:border-blue-800",
media: "bg-red-500/10 text-red-600 border-red-200 dark:border-red-800",
admin: "bg-orange-500/10 text-orange-600 border-orange-200 dark:border-orange-800",
animals: "bg-emerald-500/10 text-emerald-600 border-emerald-200 dark:border-emerald-800"
};
const languageOptions = [
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'pt', name: 'Português', flag: '🇧🇷' },
];
export default function AccountPage() {
const [editingTemp, setEditingTemp] = useState(false);
const [tempValue, setTempValue] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [reportTab, setReportTab] = useState("bug");
const [commands, setCommands] = useState<CommandCard[]>(allCommands);
const { user, loading, logout, refreshUser } = useAuth();
useEffect(() => {
if (user) {
setTempValue(user.aiTemperature.toString());
setCommands(allCommands.map(cmd => ({
...cmd,
enabled: !user.disabledCommands.includes(cmd.id)
})));
}
}, [user]);
const updateSetting = async (setting: string, value: boolean | number | string) => {
try {
const response = await fetch('/api/user/settings', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ [setting]: value }),
credentials: 'include'
});
if (response.ok) {
await refreshUser();
}
} catch (error) {
console.error('Error updating setting:', error);
}
};
const saveTemperature = () => {
const temp = parseFloat(tempValue);
if (temp >= 0.1 && temp <= 2.0) {
updateSetting('aiTemperature', temp);
setEditingTemp(false);
}
};
const toggleCommand = async (commandId: string) => {
if (!user) return;
const commandToToggle = commands.find(cmd => cmd.id === commandId);
if (!commandToToggle) return;
const newEnabledState = !commandToToggle.enabled;
setCommands(prev => prev.map(cmd =>
cmd.id === commandId ? { ...cmd, enabled: newEnabledState } : cmd
));
try {
let newDisabledCommands: string[];
if (newEnabledState) {
newDisabledCommands = user.disabledCommands.filter(id => id !== commandId);
} else {
newDisabledCommands = [...user.disabledCommands, commandId];
}
const response = await fetch('/api/user/settings', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ disabledCommands: newDisabledCommands }),
credentials: 'include'
});
if (response.ok) {
await refreshUser();
} else {
setCommands(prev => prev.map(cmd =>
cmd.id === commandId ? { ...cmd, enabled: !newEnabledState } : cmd
));
console.error('Failed to update command state');
}
} catch (error) {
setCommands(prev => prev.map(cmd =>
cmd.id === commandId ? { ...cmd, enabled: !newEnabledState } : cmd
));
console.error('Error updating command state:', error);
}
};
const filteredCommands = selectedCategory
? commands.filter(cmd => cmd.category === selectedCategory)
: commands;
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
);
}
if (!user) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">Authentication Required</h1>
<Button onClick={() => window.location.href = '/login'}>
Go to Login
</Button>
</div>
</div>
);
}
return (
<div className="w-full min-h-screen bg-background">
<div className="container mx-auto px-6 py-8">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-full bg-primary/10 items-center justify-center hidden md:flex">
<User className="w-8 h-8 text-primary" />
</div>
<div>
<h1 className="text-3xl font-bold">Welcome back, {user.firstName}!</h1>
<p className="text-muted-foreground">@{user.username}</p>
</div>
</div>
<Button variant="outline" onClick={logout} className="gap-2">
<LogOut className="w-4 h-4" />
Logout
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<motion.div
className="p-6 rounded-lg border bg-gradient-to-br from-purple-500/10 to-pink-500/10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
>
<div className="flex items-center gap-3 mb-4">
<BarChart3 className="w-8 h-8 text-purple-600" />
<h3 className="text-xl font-semibold">AI Usage</h3>
</div>
<div className="space-y-2">
<p className="text-2xl font-bold">{user.aiRequests}</p>
<p className="text-sm text-muted-foreground">Total AI Requests</p>
<p className="text-lg">{user.aiCharacters.toLocaleString()}</p>
<p className="text-sm text-muted-foreground">Characters Generated</p>
</div>
</motion.div>
<motion.div
className="p-6 rounded-lg border bg-gradient-to-br from-blue-500/10 to-cyan-500/10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
<div className="flex items-center gap-3 mb-4">
<Settings className="w-8 h-8 text-blue-600" />
<h3 className="text-xl font-semibold">AI Settings</h3>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm">AI Enabled</span>
<Button
size="sm"
variant={user.aiEnabled ? "default" : "outline"}
onClick={() => updateSetting('aiEnabled', !user.aiEnabled)}
className="h-8 px-3"
>
{user.aiEnabled ? "ON" : "OFF"}
</Button>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Show Thinking</span>
<Button
size="sm"
variant={user.showThinking ? "default" : "outline"}
onClick={() => updateSetting('showThinking', !user.showThinking)}
className="h-8 px-3"
>
{user.showThinking ? "ON" : "OFF"}
</Button>
</div>
</div>
</motion.div>
<motion.div
className="p-6 rounded-lg border bg-gradient-to-br from-green-500/10 to-emerald-500/10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.3 }}
>
<div className="flex items-center gap-3 mb-4">
<Thermometer className="w-8 h-8 text-green-600" />
<h3 className="text-xl font-semibold">Temperature</h3>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
{editingTemp ? (
<>
<Input
type="number"
min="0.1"
max="2.0"
step="0.1"
value={tempValue}
onChange={(e) => setTempValue(e.target.value)}
className="h-8 w-20"
/>
<Button size="sm" onClick={saveTemperature} className="h-8 w-8 p-0">
<Save className="w-4 h-4" />
</Button>
<Button size="sm" variant="outline" onClick={() => setEditingTemp(false)} className="h-8 w-8 p-0">
<X className="w-4 h-4" />
</Button>
</>
) : (
<>
<span className="text-2xl font-bold">{user.aiTemperature}</span>
<Button size="sm" variant="outline" onClick={() => setEditingTemp(true)} className="h-8 w-8 p-0">
<Edit3 className="w-4 h-4" />
</Button>
</>
)}
</div>
<p className="text-xs text-muted-foreground">Controls randomness in AI responses. Lower values (0.1-0.5) = more focused, higher values (0.7-2.0) = more creative.</p>
</div>
</motion.div>
<motion.div
className="p-6 rounded-lg border bg-gradient-to-br from-teal-500/10 to-cyan-500/10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.4 }}
>
<div className="flex items-center gap-3 mb-4">
<Languages className="w-8 h-8 text-teal-600" />
<h3 className="text-xl font-semibold">Language Options</h3>
</div>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-2">
{languageOptions.map((lang) => (
<Button
key={lang.code}
variant={user.languageCode === lang.code ? "default" : "outline"}
onClick={() => updateSetting('languageCode', lang.code)}
className="justify-start gap-3 h-10"
>
<span className="text-lg">{lang.flag}</span>
<span>{lang.name}</span>
</Button>
))}
</div>
<p className="text-xs text-muted-foreground">Choose your preferred language for bot responses and interface text.</p>
</div>
</motion.div>
<motion.div
className="p-6 rounded-lg border bg-gradient-to-br from-indigo-500/10 to-violet-500/10 col-span-1 md:col-span-2"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.5 }}
>
<div className="flex items-center gap-3 mb-4">
<Cpu className="w-8 h-8 text-indigo-600" />
<h3 className="text-xl font-semibold">My Model</h3>
</div>
<div className="space-y-3">
<ModelPicker
value={user.customAiModel}
onValueChange={(newModel) => updateSetting('customAiModel', newModel)}
className="w-full"
/>
<p className="text-xs text-muted-foreground">Your selected AI model for custom /ai commands. Different models have varying capabilities, speeds, and response styles.</p>
</div>
</motion.div>
<motion.div
className="p-6 rounded-lg border bg-gradient-to-br from-orange-500/10 to-red-500/10 col-span-1 md:col-span-2"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.6 }}
>
<div className="flex items-center gap-3 mb-4">
<Bug className="w-8 h-8 text-orange-600" />
<h3 className="text-xl font-semibold">Report An Issue</h3>
</div>
<div className="space-y-4">
<Tabs value={reportTab} onValueChange={setReportTab}>
<TabsList className="grid w-full grid-cols-2 gap-2">
<TabsTrigger value="bug" className="gap-2">
<Bug className="w-4 h-4" />
Bug Report
</TabsTrigger>
<TabsTrigger value="feature" className="gap-2">
<Lightbulb className="w-4 h-4" />
Feature Request
</TabsTrigger>
</TabsList>
<div className="mt-4">
<TabsContent value="bug" className="space-y-12">
<p className="text-sm text-muted-foreground">Found a bug or issue? Report it to help us improve Kowalski.</p>
<Button asChild className="w-full gap-2">
<a
href="https://libre-cloud.atlassian.net/jira/software/c/form/4a535b59-dc7e-4b55-b905-a79ff831928e?atlOrigin=eyJpIjoiNzQwYTcxZDdmMjJkNDljNzgzNTY2MjliYjliMjMzMDkiLCJwIjoiaiJ9"
target="_blank"
rel="noopener noreferrer"
>
<Bug className="w-4 h-4" />
Report Bug
<ExternalLink className="w-4 h-4" />
</a>
</Button>
</TabsContent>
<TabsContent value="feature" className="space-y-12">
<p className="text-sm text-muted-foreground">Have an idea for a new feature? Let us know what you&apos;d like to see!</p>
<Button asChild className="w-full gap-2">
<a
href="https://libre-cloud.atlassian.net/jira/software/c/form/5ce1e6e9-9618-4b46-94ee-122e7bde2ba1?atlOrigin=eyJpIjoiZjMwZTc3MDVlY2MwNDBjODliYWNhMTgzN2ZjYzI5MDAiLCJwIjoiaiJ9"
target="_blank"
rel="noopener noreferrer"
>
<Lightbulb className="w-4 h-4" />
Request Feature
<ExternalLink className="w-4 h-4" />
</a>
</Button>
</TabsContent>
</div>
</Tabs>
</div>
</motion.div>
</div>
<motion.div
className="mb-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.7 }}
>
<h2 className="text-2xl font-bold mb-4">Command Management</h2>
<div className="flex flex-wrap gap-2">
<Button
variant={selectedCategory === null ? "default" : "outline"}
onClick={() => setSelectedCategory(null)}
className="mb-2"
>
All Commands
</Button>
{Object.entries(categoryColors).map(([category, colorClass]) => (
<Button
key={category}
variant={selectedCategory === category ? "default" : "outline"}
onClick={() => setSelectedCategory(category)}
className={`mb-2 capitalize ${selectedCategory === category ? '' : colorClass}`}
>
{category === "ai" ? "AI" : category}
</Button>
))}
</div>
</motion.div>
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: 0.8 }}
>
{filteredCommands.map((command) => (
<div
key={command.id}
className={`p-4 rounded-lg border transition-all duration-200 ${
command.enabled
? 'bg-card hover:shadow-md shadow-sm'
: 'bg-muted/30 border-muted-foreground/20'
}`}
>
<div className="flex items-start justify-between mb-3">
<div className={`w-10 h-10 rounded-lg bg-gradient-to-br ${command.gradient} flex items-center justify-center ${
command.enabled ? '' : 'grayscale opacity-50'
}`}>
<command.icon className="w-5 h-5 text-white" />
</div>
<div className="flex items-center space-x-2">
<div
className={`w-11 h-6 rounded-full cursor-pointer transition-colors duration-200 ${
command.enabled
? 'bg-green-500 dark:bg-green-600'
: 'bg-gray-300 dark:bg-gray-600'
}`}
onClick={() => toggleCommand(command.id)}
>
<div className={`w-5 h-5 rounded-full shadow-sm transition-transform duration-200 ${
command.enabled
? 'translate-x-5 bg-white dark:bg-gray-100'
: 'translate-x-0.5 bg-white dark:bg-gray-200'
} mt-0.5`} />
</div>
</div>
</div>
<h3 className={`text-base font-semibold mb-2 ${command.enabled ? '' : 'text-muted-foreground'}`}>
{command.title}
</h3>
<p className={`text-sm mb-3 ${command.enabled ? 'text-muted-foreground' : 'text-muted-foreground/60'}`}>
{command.description}
</p>
<div className="flex items-center justify-between">
<div className="flex flex-wrap gap-1">
{command.commands.slice(0, 2).map((cmd, idx) => (
<code key={idx} className={`px-1.5 py-0.5 rounded text-xs ${
command.enabled
? 'bg-muted text-foreground'
: 'bg-muted-foreground/10 text-muted-foreground/60'
}`}>
{cmd}
</code>
))}
{command.commands.length > 2 && (
<span className={`text-xs ${command.enabled ? 'text-muted-foreground' : 'text-muted-foreground/60'}`}>
+{command.commands.length - 2}
</span>
)}
</div>
<div className={`px-2 py-1 rounded-full text-xs border ${
command.enabled
? categoryColors[command.category]
: 'bg-muted-foreground/10 text-muted-foreground/60 border-muted-foreground/20'
}`}>
{command.category === "ai" ? "AI" : command.category}
</div>
</div>
</div>
))}
</motion.div>
<motion.div
className="mt-12 text-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 1.0 }}
>
<div className="inline-flex items-center gap-16 p-6 px-8 rounded-lg bg-muted/50 border">
<span className="font-medium">Ready to start using Kowalski?</span>
<Button asChild>
<a href="https://t.me/KowalskiNodeBot" target="_blank" rel="noopener noreferrer">
<RiTelegram2Line />
Open on Telegram
</a>
</Button>
</div>
</motion.div>
</div>
</div>
);
}

View file

@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from "next/server";
import { invalidateSession } from "@/lib/auth";
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
export async function POST(request: NextRequest) {
try {
const cookieToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
const authHeader = request.headers.get('authorization');
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
const sessionToken = bearerToken || cookieToken;
if (sessionToken) {
await invalidateSession(sessionToken);
}
const response = NextResponse.json({ success: true });
response.cookies.set(SESSION_COOKIE_NAME, '', {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
expires: new Date(0),
path: "/",
});
return response;
} catch (error) {
console.error("Error in logout API:", error);
return NextResponse.json({
error: "Internal server error"
}, { status: 500 });
}
}

View file

@ -0,0 +1,91 @@
import { NextRequest, NextResponse } from "next/server";
import { eq } from "drizzle-orm";
import * as schema from "@/lib/schema";
import { db } from "@/lib/db";
export async function POST(request: NextRequest) {
try {
const requestContentType = request.headers.get('content-type');
if (!requestContentType || !requestContentType.includes('application/json')) {
return NextResponse.json({ success: false, error: "Invalid content type" }, { status: 400 });
}
const body = await request.json();
const { username } = body;
if (!username) {
return NextResponse.json({ success: false, error: "Username is required" }, { status: 400 });
}
if (typeof username !== 'string' || username.length < 3 || username.length > 32) {
return NextResponse.json({ success: false, error: "Invalid username format" }, { status: 400 });
}
const cleanUsername = username.replace('@', '');
const user = await db.query.usersTable.findFirst({
where: eq(schema.usersTable.username, cleanUsername),
columns: {
telegramId: true,
username: true,
},
});
if (!user) {
const botUsername = process.env.botUsername || "KowalskiNodeBot";
return NextResponse.json({ success: false, error: `Please DM @${botUsername} before signing in.` }, { status: 404 });
}
const botApiUrl = process.env.botApiUrl || "http://kowalski:3030";
const fullUrl = `${botApiUrl}/2fa/get`;
const botApiResponse = await fetch(fullUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userId: user.telegramId }),
});
if (!botApiResponse.ok) {
const errorText = await botApiResponse.text();
console.error("Bot API error response:", errorText);
return NextResponse.json({
success: false,
error: `Bot API error: ${botApiResponse.status} - ${errorText.slice(0, 200)}`
}, { status: 500 });
}
const contentType = botApiResponse.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
const errorText = await botApiResponse.text();
console.error("Bot API returned non-JSON:", errorText.slice(0, 200));
return NextResponse.json({
success: false,
error: "Bot API returned invalid response format"
}, { status: 500 });
}
const botApiResult = await botApiResponse.json();
if (!botApiResult.generated) {
return NextResponse.json({
success: false,
error: botApiResult.error || "Failed to send 2FA code"
}, { status: 500 });
}
return NextResponse.json({
success: true,
message: "2FA code sent successfully",
userId: user.telegramId
});
} catch (error) {
console.error("Error in username API:", error);
return NextResponse.json({
success: false,
error: "Internal server error"
}, { status: 500 });
}
}

View file

@ -0,0 +1,107 @@
import { NextRequest, NextResponse } from "next/server";
import { eq, and, gt } from "drizzle-orm";
import * as schema from "@/lib/schema";
import { db } from "@/lib/db";
import { createSession, getSessionCookieOptions } from "@/lib/auth";
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
export async function POST(request: NextRequest) {
try {
const contentType = request.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return NextResponse.json({
success: false,
error: "Invalid content type"
}, { status: 400 });
}
const body = await request.json();
const { userId, code } = body;
if (!userId || !code) {
return NextResponse.json({
success: false,
error: "User ID and code are required"
}, { status: 400 });
}
if (typeof userId !== 'string' || typeof code !== 'string') {
return NextResponse.json({
success: false,
error: "Invalid input format"
}, { status: 400 });
}
if (!/^\d{6}$/.test(code)) {
return NextResponse.json({
success: false,
error: "Invalid code format"
}, { status: 400 });
}
const twoFactorRecord = await db.query.twoFactorTable.findFirst({
where: and(
eq(schema.twoFactorTable.userId, userId),
gt(schema.twoFactorTable.codeExpiresAt, new Date())
),
});
if (!twoFactorRecord) {
return NextResponse.json({
success: false,
error: "No valid 2FA code found or code has expired"
}, { status: 404 });
}
if (twoFactorRecord.codeAttempts >= 5) {
await db.delete(schema.twoFactorTable)
.where(eq(schema.twoFactorTable.userId, userId));
return NextResponse.json({
success: false,
error: "Too many failed attempts. Please request a new code."
}, { status: 429 });
}
if (twoFactorRecord.currentCode !== code) {
await db.update(schema.twoFactorTable)
.set({
codeAttempts: twoFactorRecord.codeAttempts + 1,
updatedAt: new Date()
})
.where(eq(schema.twoFactorTable.userId, userId));
console.log(`2FA verification failed for user: ${userId}, attempts: ${twoFactorRecord.codeAttempts + 1}`);
return NextResponse.json({
success: false,
error: "Invalid 2FA code"
}, { status: 401 });
}
const session = await createSession(userId);
await db.delete(schema.twoFactorTable)
.where(eq(schema.twoFactorTable.userId, userId));
console.log("2FA verification successful for user:", userId);
const response = NextResponse.json({
success: true,
message: "2FA verification successful",
redirectTo: "/account",
sessionToken: session.sessionToken
});
const cookieOptions = getSessionCookieOptions();
response.cookies.set(SESSION_COOKIE_NAME, session.sessionToken, cookieOptions);
return response;
} catch (error) {
console.error("Error in verify API:", error);
return NextResponse.json({
success: false,
error: "Internal server error"
}, { status: 500 });
}
}

View file

@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from "next/server";
import { validateSession } from "@/lib/auth";
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
import { db } from "@/lib/db";
import { usersTable, sessionsTable, twoFactorTable } from "@/lib/schema";
import { eq } from "drizzle-orm";
export async function DELETE(request: NextRequest) {
try {
const cookieToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
const authHeader = request.headers.get('authorization');
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
const sessionToken = bearerToken || cookieToken;
if (!sessionToken) {
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
}
const sessionData = await validateSession(sessionToken);
if (!sessionData || !sessionData.user) {
return NextResponse.json({ error: "Invalid or expired session" }, { status: 401 });
}
const userId = sessionData.user.telegramId;
await db.transaction(async (tx) => {
await tx.delete(sessionsTable)
.where(eq(sessionsTable.userId, userId));
await tx.delete(twoFactorTable)
.where(eq(twoFactorTable.userId, userId));
await tx.delete(usersTable)
.where(eq(usersTable.telegramId, userId));
});
const response = NextResponse.json({
success: true,
message: "Account deleted successfully"
});
response.cookies.set(SESSION_COOKIE_NAME, '', {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
expires: new Date(0),
path: "/",
});
return response;
} catch (error) {
console.error("Error deleting account:", error);
return NextResponse.json({
error: "Failed to delete account"
}, { status: 500 });
}
}

View file

@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from "next/server";
import { validateSession } from "@/lib/auth";
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
export async function GET(request: NextRequest) {
try {
const cookieToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
const authHeader = request.headers.get('authorization');
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
const sessionToken = bearerToken || cookieToken;
if (!sessionToken) {
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
}
const sessionData = await validateSession(sessionToken);
if (!sessionData || !sessionData.user) {
return NextResponse.json({ error: "Invalid or expired session" }, { status: 401 });
}
const { user } = sessionData;
const sanitizedUser = {
telegramId: user.telegramId,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
aiEnabled: user.aiEnabled,
showThinking: user.showThinking,
customAiModel: user.customAiModel,
aiTemperature: user.aiTemperature,
aiRequests: user.aiRequests,
aiCharacters: user.aiCharacters,
disabledCommands: user.disabledCommands,
languageCode: user.languageCode,
};
return NextResponse.json(sanitizedUser);
} catch (error) {
console.error("Error in profile API:", error);
return NextResponse.json({
error: "Internal server error"
}, { status: 500 });
}
}

View file

@ -0,0 +1,103 @@
import { NextRequest, NextResponse } from "next/server";
import { eq } from "drizzle-orm";
import { validateSession } from "@/lib/auth";
import { SESSION_COOKIE_NAME } from "@/lib/auth-constants";
import { db } from "@/lib/db";
import * as schema from "@/lib/schema";
interface UserUpdates {
aiEnabled?: boolean;
showThinking?: boolean;
customAiModel?: string;
aiTemperature?: number;
disabledCommands?: string[];
languageCode?: string;
updatedAt?: Date;
}
export async function PATCH(request: NextRequest) {
try {
const cookieToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
const authHeader = request.headers.get('authorization');
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
const sessionToken = bearerToken || cookieToken;
if (!sessionToken) {
return NextResponse.json({ error: "Authentication required" }, { status: 401 });
}
const sessionData = await validateSession(sessionToken);
if (!sessionData || !sessionData.user) {
return NextResponse.json({ error: "Invalid or expired session" }, { status: 401 });
}
const contentType = request.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return NextResponse.json({ error: "Invalid content type" }, { status: 400 });
}
const updates = await request.json();
const userId = sessionData.user.telegramId;
if (!updates || typeof updates !== 'object') {
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
}
const allowedFields = [
'aiEnabled',
'showThinking',
'customAiModel',
'aiTemperature',
'disabledCommands',
'languageCode'
];
const filteredUpdates: UserUpdates = {};
for (const [key, value] of Object.entries(updates)) {
if (allowedFields.includes(key)) {
if (key === 'aiEnabled' || key === 'showThinking') {
filteredUpdates[key] = Boolean(value);
} else if (key === 'aiTemperature') {
const temp = Number(value);
if (temp >= 0.1 && temp <= 2.0) {
filteredUpdates[key] = temp;
} else {
return NextResponse.json({ error: "Temperature must be between 0.1 and 2.0" }, { status: 400 });
}
} else if (key === 'customAiModel' || key === 'languageCode') {
if (typeof value === 'string' && value.length > 0 && value.length < 100) {
filteredUpdates[key] = value;
} else {
return NextResponse.json({ error: `Invalid ${key}` }, { status: 400 });
}
} else if (key === 'disabledCommands') {
if (Array.isArray(value) && value.every(item => typeof item === 'string' && item.length < 50) && value.length < 100) {
filteredUpdates[key] = value;
} else {
return NextResponse.json({ error: "Invalid disabled commands" }, { status: 400 });
}
}
}
}
if (Object.keys(filteredUpdates).length === 0) {
return NextResponse.json({ error: "No valid updates provided" }, { status: 400 });
}
filteredUpdates.updatedAt = new Date();
await db.update(schema.usersTable)
.set(filteredUpdates)
.where(eq(schema.usersTable.telegramId, userId));
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error in settings API:", error);
return NextResponse.json({
error: "Internal server error"
}, { status: 500 });
}
}

126
webui/app/globals.css Executable file
View file

@ -0,0 +1,126 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
body {
font-family: var(--font-sora);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

54
webui/app/layout.tsx Executable file
View file

@ -0,0 +1,54 @@
import type { Metadata } from "next";
import { Sora } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/providers";
import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/app-sidebar";
import { AuthProvider } from "@/contexts/auth-context";
import { HeaderAuth } from "@/components/header-auth";
const sora = Sora({
variable: "--font-sora",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Kowalski",
description: "A powerful, multi-function Telegram bot",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning className="scroll-smooth">
<body className={`${sora.variable} antialiased`}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<AuthProvider>
<SidebarProvider>
<AppSidebar />
<SidebarInset className="h-[calc(100vh-16px)] overflow-hidden rounded-lg border bg-background flex flex-col">
<header className="flex h-16 shrink-0 items-center gap-2 px-4 border-b bg-background">
<SidebarTrigger className="-ml-1" />
<div className="ml-auto">
<HeaderAuth />
</div>
</header>
<main className="flex-1 overflow-auto scroll-smooth">
{children}
</main>
</SidebarInset>
</SidebarProvider>
</AuthProvider>
</ThemeProvider>
</body>
</html>
);
}

311
webui/app/login/page.tsx Executable file
View file

@ -0,0 +1,311 @@
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { RiTelegram2Line } from "react-icons/ri";
import { TbLoader } from "react-icons/tb";
import { useState, Suspense } from "react";
import { useSearchParams } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
export const dynamic = 'force-dynamic'
type FormStep = "username" | "twofa";
type VerifyResponse = {
success: boolean;
message?: string;
redirectTo?: string;
sessionToken?: string;
error?: string;
};
const buttonVariants = {
initial: { scale: 1 },
tap: { scale: 0.98 },
};
function LoginForm() {
const [step, setStep] = useState<FormStep>("username");
const [username, setUsername] = useState("");
const [twoFaCode, setTwoFaCode] = useState("");
const [userId, setUserId] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const searchParams = useSearchParams();
const returnTo = searchParams.get('returnTo') || '/account';
const handleUsernameSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!username.trim()) return;
setIsLoading(true);
setError("");
try {
const response = await fetch("/api/auth/username", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username: username.trim() }),
});
const result = await response.json();
if (result.success) {
setUserId(result.userId);
setStep("twofa");
} else {
setError(result.error || "Failed to find user");
}
} catch (err) {
console.error("Username submission error:", err);
setError("Network error. Please try again.");
} finally {
setIsLoading(false);
}
};
const handleTwoFaSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!twoFaCode.trim() || twoFaCode.length !== 6) return;
setIsLoading(true);
setError("");
try {
const response = await fetch("/api/auth/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userId, code: twoFaCode }),
});
const result: VerifyResponse = await response.json();
if (result.success) {
const redirectTo = result.redirectTo || returnTo;
if (result.sessionToken) {
try {
localStorage.setItem('kowalski-session', result.sessionToken);
} catch (storageError) {
console.error('localStorage error:', storageError);
}
}
window.location.href = redirectTo;
} else {
setError(result.error || "Invalid 2FA code");
}
} catch (err) {
console.error("2FA verification error:", err);
console.log("Error details:", {
message: err instanceof Error ? err.message : 'Unknown error',
stack: err instanceof Error ? err.stack : 'No stack trace',
name: err instanceof Error ? err.name : 'Unknown error type'
});
const errorMessage = err instanceof Error ?
`Error: ${err.message}` :
"Network error. Please try again.";
setError(errorMessage);
} finally {
setIsLoading(false);
}
};
const resetForm = () => {
setStep("username");
setUsername("");
setTwoFaCode("");
setUserId("");
setError("");
};
const LoadingSpinner = ({ text }: { text: string }) => (
<div className="flex items-center gap-3">
<TbLoader className="w-4 h-4 animate-spin" />
<span className="text-muted-foreground">{text}</span>
</div>
);
return (
<div className="flex flex-col h-full">
<section className="flex flex-col items-center justify-center py-24 px-6 text-center bg-gradient-to-br from-background to-muted flex-1">
<div className="max-w-4xl mx-auto space-y-8">
<div className="flex items-center justify-center mb-6">
<div className="flex items-center justify-center w-20 h-20 rounded-full bg-primary/10 p-4">
<RiTelegram2Line className="w-10 h-10" />
</div>
</div>
<AnimatePresence mode="wait">
{step === "username" && (
<motion.div
key="username-form"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.2 }}
className="max-w-md mx-auto"
>
<h1 className="text-6xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent mb-4">
Login to Kowalski
</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed mb-8">
Please enter your Telegram username to continue.
</p>
<form onSubmit={handleUsernameSubmit} className="max-w-md mx-auto space-y-4">
<div className="space-y-2">
<Input
type="text"
placeholder="Enter your Telegram username"
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={isLoading}
className="text-center text-lg py-6"
autoFocus
/>
</div>
<AnimatePresence>
{error && (
<motion.p
className="text-red-500 text-sm"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
{error}
</motion.p>
)}
</AnimatePresence>
<motion.div
variants={buttonVariants}
initial="initial"
whileTap={!isLoading && username.trim() ? "tap" : undefined}
>
<Button
type="submit"
disabled={!username.trim() || isLoading}
className="w-full py-6 text-lg"
>
{isLoading ? (
<LoadingSpinner text="Finding your account..." />
) : (
"Continue"
)}
</Button>
</motion.div>
</form>
</motion.div>
)}
{step === "twofa" && (
<motion.div
key="twofa-form"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.2 }}
className="max-w-md mx-auto"
>
<h1 className="text-6xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent mb-4">
Enter 2FA Code
</h1>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed mb-8">
We&apos;ve sent a 6-digit code to your Telegram. Please enter it below.
</p>
<form onSubmit={handleTwoFaSubmit} className="max-w-md mx-auto space-y-4">
<div className="space-y-2">
<Input
type="text"
placeholder="000000"
value={twoFaCode}
onChange={(e) => setTwoFaCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
disabled={isLoading}
className="text-center text-2xl font-mono tracking-widest py-6"
maxLength={6}
autoFocus
/>
</div>
<AnimatePresence>
{error && (
<motion.p
className="text-red-500 text-sm"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
{error}
</motion.p>
)}
</AnimatePresence>
<div className="flex gap-2">
<motion.div
variants={buttonVariants}
initial="initial"
whileTap="tap"
className="flex-1"
>
<Button
type="button"
variant="outline"
onClick={resetForm}
disabled={isLoading}
className="w-full py-6"
>
Back
</Button>
</motion.div>
<motion.div
variants={buttonVariants}
initial="initial"
whileTap={!isLoading && twoFaCode.length === 6 ? "tap" : undefined}
className="flex-1"
>
<Button
type="submit"
disabled={twoFaCode.length !== 6 || isLoading}
className="w-full py-6 text-lg"
>
{isLoading ? (
<LoadingSpinner text="Verifying..." />
) : (
"Verify"
)}
</Button>
</motion.div>
</div>
</form>
</motion.div>
)}
</AnimatePresence>
</div>
</section>
</div>
);
}
export default function LoginPage() {
return (
<Suspense fallback={
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
}>
<LoginForm />
</Suspense>
);
}

251
webui/app/page.tsx Executable file
View file

@ -0,0 +1,251 @@
import { Button } from "@/components/ui/button"
import {
Bot,
Sparkles,
Users,
Settings,
Download,
Brain,
Shield,
Zap,
Tv,
Trash,
Lock,
} from "lucide-react";
import { SiYoutube, SiForgejo } from "react-icons/si";
import { RiTelegram2Line } from "react-icons/ri";
import { TbEyeSpark } from "react-icons/tb";
import Image from "next/image";
import Footer from "@/components/footer";
import Link from "next/link";
export default function Home() {
return (
<div className="flex flex-col">
<section className="flex flex-col items-center justify-center py-24 px-6 text-center bg-gradient-to-br from-background to-muted">
<div className="max-w-4xl mx-auto space-y-8">
<div className="flex items-center justify-center mb-6">
<div className="flex items-center justify-center w-20 h-20 rounded-full bg-primary/10 p-4">
<Image
src="/kowalski.svg"
alt="Kowalski Logo"
width={48}
height={48}
className="dark:invert -mt-3"
/>
</div>
</div>
<h1 className="text-6xl font-bold tracking-tight bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
Kowalski
</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
A powerful, multi-function Telegram bot with AI capabilities, media downloading,
user management, and much more. Built for communities and power users.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center pt-8">
<Button size="lg" className="min-w-32" asChild>
<Link href="https://t.me/KowalskiNodeBot">
<RiTelegram2Line />
Try on Telegram
</Link>
</Button>
<Button variant="outline" size="lg" className="min-w-32">
<Settings />
Documentation
</Button>
<Button variant="outline" size="lg" className="min-w-32" asChild>
<Link href="https://git.p0ntus.com/ABOCN/TelegramBot">
<SiForgejo />
View on Forgejo
</Link>
</Button>
</div>
</div>
</section>
<section className="py-24 px-6">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16" id="ai-features">
<h2 className="text-4xl font-bold mb-4">Features You&apos;ll Love</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Powered by TypeScript, Telegraf, Next.js, and AI.
</p>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-primary/10 text-primary">
<Sparkles className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">AI Commands</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Interact with over 50 AI models through simple commands. Get intelligent responses,
assistance, or problem-solving help right in Telegram.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Bot className="w-5 h-5 mx-3 text-primary" />
<div>
<div className="font-medium">/ai</div>
<div className="text-sm text-muted-foreground">Ask questions to a custom AI model of your choice</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Zap className="w-5 h-5 mx-3 text-primary" />
<div>
<div className="font-medium">/ask</div>
<div className="text-sm text-muted-foreground">Quick AI responses for everyday questions</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Brain className="w-5 h-5 mx-3 text-primary" />
<div>
<div className="font-medium">/think</div>
<div className="text-sm text-muted-foreground">Deep reasoning with optional visible thinking</div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3" id="youtube-features">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-red-500/10 text-red-500">
<SiYoutube className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">YouTube/Video Downloads</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Download videos directly from YouTube and other platforms and watch them in Telegram.
Supports thousands of sites with integrated yt-dlp.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Download className="w-5 h-5 mx-3 text-red-500" />
<div>
<div className="font-medium">/yt [URL]</div>
<div className="text-sm text-muted-foreground">Quickly download videos up to 50MB</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Shield className="w-5 h-5 mx-3 text-red-500" />
<div>
<div className="font-medium">Automatic Ratelimit Detection</div>
<div className="text-sm text-muted-foreground">We&apos;ll notify you if something goes wrong</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Tv className="w-5 h-5 mx-3 text-red-500" />
<div>
<div className="font-medium">High Quality Downloads</div>
<div className="text-sm text-muted-foreground">Kowalski automatically chooses the best quality for you</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="user-features" className="py-24 px-6 bg-muted/30">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">
Control <span className="italic mr-1.5">and</span> Fun
</h2>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Your user data is always minimized and under your control. That certainly
doesn&apos;t mean the experience is lacking!
</p>
</div>
<div className="grid md:grid-cols-2 gap-12 items-center">
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-blue-500/10 text-blue-500">
<Users className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">User Accounts</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Your user data is linked only by your Telegram ID. No data is ever sent to third parties
or used for anything other than providing you with the best experience.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Settings className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">Personal Settings</div>
<div className="text-sm text-muted-foreground">Custom AI models, temperature, and language preferences</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Brain className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">Account Statistics</div>
<div className="text-sm text-muted-foreground">Track AI requests, characters processed, and more</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Trash className="w-5 h-5 mx-3 text-blue-500" />
<div>
<div className="font-medium">Leave at Any Time</div>
<div className="text-sm text-muted-foreground">We make it easy to delete your account at any time</div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-lg bg-green-500/10 text-green-500">
<Bot className="w-6 h-6" />
</div>
<h3 className="text-2xl font-semibold">Web Interface</h3>
</div>
<p className="text-muted-foreground leading-relaxed">
Kowalski includes a web interface, made with Next.js, to make it easier to manage your
bot, user account, and more. It&apos;s tailored to both users and admins.
</p>
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<TbEyeSpark className="w-5 h-5 mx-3 text-green-500" />
<div>
<div className="font-medium">Everything&apos;s Clean</div>
<div className="text-sm text-muted-foreground">We don&apos;t clutter your view with ads or distractions.</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Sparkles className="w-5 h-5 mx-3 text-green-500" />
<div>
<div className="font-medium">Do Everything!</div>
<div className="text-sm text-muted-foreground">We aim to integrate every feature into the web interface.</div>
</div>
</div>
<div className="flex items-center gap-2 p-3 rounded-lg bg-background border">
<Lock className="w-5 h-5 mx-3 text-green-500" />
<div>
<div className="font-medium">Private</div>
<div className="text-sm text-muted-foreground">We don&apos;t use any analytics, tracking, or third-party scripts.</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<Footer />
</div>
);
}