add initial complete webui, more ai commands for moderation, add api
This commit is contained in:
parent
19e794e34c
commit
173d4e7a52
112 changed files with 8176 additions and 780 deletions
549
webui/app/about/page.tsx
Executable file
549
webui/app/about/page.tsx
Executable 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'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'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'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'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'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'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'.
|
||||
</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
204
webui/app/account/delete/page.tsx
Executable 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
725
webui/app/account/page.tsx
Executable 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'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>
|
||||
);
|
||||
}
|
34
webui/app/api/auth/logout/route.ts
Executable file
34
webui/app/api/auth/logout/route.ts
Executable 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 });
|
||||
}
|
||||
}
|
91
webui/app/api/auth/username/route.ts
Executable file
91
webui/app/api/auth/username/route.ts
Executable 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 });
|
||||
}
|
||||
}
|
107
webui/app/api/auth/verify/route.ts
Executable file
107
webui/app/api/auth/verify/route.ts
Executable 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 });
|
||||
}
|
||||
}
|
59
webui/app/api/user/delete/route.ts
Executable file
59
webui/app/api/user/delete/route.ts
Executable 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 });
|
||||
}
|
||||
}
|
46
webui/app/api/user/profile/route.ts
Executable file
46
webui/app/api/user/profile/route.ts
Executable 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 });
|
||||
}
|
||||
}
|
103
webui/app/api/user/settings/route.ts
Executable file
103
webui/app/api/user/settings/route.ts
Executable 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
126
webui/app/globals.css
Executable 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
54
webui/app/layout.tsx
Executable 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
311
webui/app/login/page.tsx
Executable 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'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
251
webui/app/page.tsx
Executable 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'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'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'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'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's Clean</div>
|
||||
<div className="text-sm text-muted-foreground">We don'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't use any analytics, tracking, or third-party scripts.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue