Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
import LandingPage from "./Pages/LandingPage";
import ChatbotPage from "./Pages/ChatbotPage";
import "./App.css";
import NotFoundPage from "./Pages/NotFoundPage";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/chatbot" element={<ChatbotPage />} />

{/* Catch all route for 404 */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
);
Expand Down
69 changes: 62 additions & 7 deletions src/Pages/ChatbotPage.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,71 @@
import type React from "react"
import Navbar from "../components/Navbar"
import Chatbot from "../services/Chatbot"
import Footer from "../components/Footer"
import { useEffect, useState } from "react"
import TemplateSelector from "@/components/TemplateSelector"
import { ContractValidator } from "@/services/contractValidator"
import useMessages from "@/hooks/useMessages"
import type { Message } from "@/hooks/useMessages"
import Sidebar from "@/components/Sidebar"
import { useSearchParams } from "react-router-dom"

const ChatbotPage: React.FC = () => {
const [isTemplateSelectorOpen, setIsTemplateSelectorOpen] = useState(false)
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false)
const [chatId, setChatId] = useState<string | undefined>(undefined)
const [searchParams, setSearchParams] = useSearchParams()

useEffect(() => {
let idFromQuery = searchParams.get("chatId") || undefined
if (!idFromQuery) {
idFromQuery = crypto?.randomUUID ? crypto.randomUUID() : String(Date.now())
setSearchParams((prev) => {
prev.set("chatId", idFromQuery as string)
return prev
})
}
setChatId(idFromQuery)
}, [searchParams, setSearchParams])

const { messages, setMessages, appendUserMessage, appendAiMessage } = useMessages(chatId)


const validateSolidityCode = (code: string): boolean => {
// Basic validation - check for essential Solidity elements
const hasPragma = code.includes("pragma solidity");
const hasContract = code.includes("contract ");
const hasFunction = code.includes("function ");

return hasPragma && hasContract && hasFunction;
}

const handleTemplateSelect = (code: string) => {
const aiMessage: Message = {
id: Date.now().toString(),
type: "ai",
content: code,
timestamp: new Date(),
isCode: true,
compilationStatus: validateSolidityCode(code) ? "success" : "error",
validation: ContractValidator.validateContract(code),
}
setMessages((prev) => [...prev, aiMessage])
}

return (
<div className="chatbot-page bg-gradient-to-br from-gray-900 via-purple-900 to-violet-900">
<Navbar />
<main className="chatbot-main">
<Chatbot />
<div className="bg-gradient-to-br from-gray-900 via-purple-900 to-violet-900 min-h-screen !max-h-screen">
<main className="min-h-screen flex w-full justify-start items-start">
<Sidebar isSidebarCollapsed={isSidebarCollapsed} setIsSidebarCollapsed={setIsSidebarCollapsed} setIsTemplateSelectorOpen={setIsTemplateSelectorOpen} setChatId={setChatId} />

<Chatbot
validateSolidityCode={validateSolidityCode}
messages={messages}
setMessages={setMessages}
appendUserMessage={appendUserMessage}
appendAiMessage={appendAiMessage}
/>

<TemplateSelector isOpen={isTemplateSelectorOpen} onClose={() => setIsTemplateSelectorOpen(false)} onSelectTemplate={handleTemplateSelect} />
</main>
<Footer />
</div>
)
}
Expand Down
40 changes: 40 additions & 0 deletions src/Pages/NotFoundPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ArrowLeft } from "lucide-react"
import { Link } from "react-router-dom"
import cosmicCompass from "../assets/compass.png"

const NotFoundPage = () => {
return (
<div className="flex md:flex-row flex-col items-center justify-center min-h-screen h-full [background:linear-gradient(135deg,rgba(139,92,246,0),rgba(6,182,212,0))] bg-cover bg-center">
<div className="flex flex-col items-start justify-between w-1/2 gap-4 h-full">
<h1 className="text-5xl font-bold leading-12">Oops! You've Explored <br /> Beyond The Map</h1>

<div className="flex flex-col items-start justify-center gap-2 !pt-20">
<p className="text-gray-400">We couldn't find the page you were looking for but</p>
<p className="font-bold text-center text-2xl italic">"Not all who wander are lost"</p>
<Link to="/" className="template-button !py-3 !px-8 flex items-center justify-center gap-2 !mt-10">
<ArrowLeft size={20} />
<span>Back to Known Paths</span>
</Link>
</div>
</div>

<div className="hero-visual rotate-12">
<div className="code-preview">
<div className="code-header">
<div className="code-dots">
<span></span>
<span></span>
<span></span>
</div>
<span className="code-title">Cosmic Compass</span>
</div>
<div className="code-content !p-0">
<img src={cosmicCompass} alt="Cosmic Compass" className="w-[400px] h-auto object-cover rounded-lg" />
</div>
</div>
</div>
</div>
)
}

export default NotFoundPage
Binary file added src/assets/compass.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions src/components/ChatItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { EllipsisIcon, StarIcon, Trash2Icon } from "lucide-react"
import { useState } from "react"

interface ChatItemProps {
chat: {
id: string
title: string
}
handleChatSelect: (id: string) => void
chatId: string,
handleDeleteChat: (id: string) => void,
handleFavoriteChat: (id: string) => void,
isFavorite: (id: string) => boolean,
}

const ChatItem = ({ chat, handleChatSelect, chatId, handleDeleteChat, handleFavoriteChat, isFavorite }: ChatItemProps) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);

const handleDelete = () => {
handleDeleteChat(chat.id)
setIsMenuOpen(false)
}

const handleFavorite = () => {
handleFavoriteChat(chat.id)
setIsMenuOpen(false)
}

return (
<div className="w-full group relative flex items-center gap-2 group" onMouseLeave={() => setIsMenuOpen(false)}>
<button title={chat?.title} onClick={() => handleChatSelect(chat?.id)} className={`hover:bg-gray-700 rounded-md !p-1.5 duration-200 cursor-pointer !w-full flex items-center justify-start ${chat?.id === chatId ? 'bg-gray-800' : ''} relative `}>
<p className="!text-sm !text-gray-300 text-start w-full !pr-6 truncate">{chat.title}</p>
</button>

<div className="absolute right-2 top-1/2 -translate-y-1/2 z-10 hidden group-hover:flex items-center gap-1">
<button className="hover:bg-gray-700 rounded-md !p-1.5 duration-200 cursor-pointer" onClick={() => setIsMenuOpen(!isMenuOpen)}>
<EllipsisIcon size={16} />
</button>

<div className={`absolute left-0 top-full bg-gray-800 rounded-md z-10 ${isMenuOpen ? 'block' : 'hidden'} flex flex-col items-start justify-start !p-2`}>
<button onClick={handleFavorite} title={isFavorite(chat.id) ? 'Unfavorite' : 'Favorite'} className="p-1 cursor-pointer rounded-md hover:bg-gray-600 flex items-center justify-start gap-2 !px-2.5 !py-2 w-full">
<StarIcon size={20} className={isFavorite(chat.id) ? 'text-yellow-400' : 'text-gray-500'} />
<span className="!text-sm !text-gray-300 text-start w-full truncate">{isFavorite(chat.id) ? 'Unfavorite' : 'Favorite'}</span>
</button>
<button onClick={handleDelete} title="Delete Chat" className="p-1 cursor-pointer rounded-md hover:bg-gray-600 flex items-center justify-start gap-2 !px-2.5 !py-2 w-full">
<Trash2Icon size={20} className="text-red-500" />
<span className="!text-sm !text-gray-300 text-start w-full truncate">Delete Chat</span>
</button>
</div>
</div>
</div>
)
}

export default ChatItem
144 changes: 144 additions & 0 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { Sparkles, Layout, HomeIcon, SidebarIcon, SquarePenIcon, StarIcon, MessageCircleIcon } from "lucide-react"
import { Link } from "react-router-dom"
import { listChats, deleteChat } from "@/hooks/useHistory"
import { useSearchParams } from "react-router-dom"
import useFavorites from "@/hooks/useFavorites"
import ChatItem from "./ChatItem"

interface SidebarProps {
isSidebarCollapsed: boolean
setIsSidebarCollapsed: (isCollapsed: boolean) => void
setIsTemplateSelectorOpen: (isOpen: boolean) => void
setChatId: (id: string) => void
}

const Sidebar = ({ isSidebarCollapsed, setIsSidebarCollapsed, setIsTemplateSelectorOpen, setChatId }: SidebarProps) => {

const chats = listChats()
const [searchParams, setSearchParams] = useSearchParams()
const chatId = searchParams.get("chatId")
const { favorites, toggleFavorite, isFavorite } = useFavorites()

const handleChatSelect = (chatId: string) => {
setChatId(chatId)
setSearchParams((prev: URLSearchParams) => {
prev.set("chatId", chatId)
return prev
})
}

const handleNewChat = () => {
const newChatId = crypto?.randomUUID ? crypto.randomUUID() : String(Date.now())
setChatId(newChatId)
setSearchParams((prev: URLSearchParams) => {
prev.set("chatId", newChatId)
return prev
})
}

const handleDeleteChat = (id: string) => {
deleteChat(id)
// If deleting currently open chat, start a new one
if (id === chatId) {
handleNewChat()
} else {
// Force URL refresh to trigger rerender of chat list if needed
setSearchParams((prev: URLSearchParams) => {
return prev
})
}
}

return (
<div className={`hidden sm:flex flex-col items-center justify-between shrink-0 transition-all duration-100 ease-in min-h-screen h-full border border-r border-gray-800 w-[var(--sidebar-width)] ${isSidebarCollapsed ? 'w-[var(--sidebar-width-collapsed)]' : 'w-[var(--sidebar-width)]'} bg-[rgba(15,15,23)]`}>
<div className="px-3 py-2 w-full h-full flex-1 flex flex-col justify-start items-center gap-2">
<div className={`w-full flex items-center justify-between !px-2 !py-2 flex-wrap gap-2 ${isSidebarCollapsed ? 'justify-center' : 'justify-between'}`}>
<Link title="Home" to="/" className="hover:bg-gray-800 rounded-md !p-2 duration-100 cursor-pointer">
<HomeIcon size={24} />
</Link>
<button title="Collapse Sidebar" onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)} className="hover:bg-gray-800 rounded-md !p-2 duration-100 cursor-pointer">
<SidebarIcon size={24} />
</button>
</div>

<div className={`${isSidebarCollapsed ? 'flex' : 'hidden'} w-full flex-col items-center justify-center gap-2 !px-4 text-sm font-light`}>
<button title="Favorites" onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)} className="hover:bg-gray-800 rounded-md !p-2 duration-100 cursor-pointer">
<StarIcon size={24} />
</button>

<button title="Chats" onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)} className="hover:bg-gray-800 rounded-md !p-2 duration-100 cursor-pointer">
<MessageCircleIcon size={24} />
</button>
</div>

<div className={`${isSidebarCollapsed ? 'hidden' : 'block'} w-full flex flex-col gap-1 items-center justify-center !px-2`}>
<button title="New Chat" onClick={handleNewChat} className="hover:bg-gray-800 rounded-md !p-2 duration-200 cursor-pointer !w-full flex items-center justify-start gap-2">
<SquarePenIcon size={20} className="text-gray-300" />
<span className={`${isSidebarCollapsed ? 'hidden' : 'block'} !text-sm text-gray-300`}>New Chat</span>
</button>

{favorites.length > 0 && (
<div className="w-full flex flex-col items-center justify-start gap-0 !px-2 !mt-2 text-sm font-light">
<p className="!text-sm !text-gray-300 text-start w-full font-semibold">Favorites</p>
{favorites.map((id) => {
const chat = chats.find(c => c.id === id)
if (!chat) return null
return (
<div key={id} className="w-full flex items-center justify-between">
<button onClick={() => toggleFavorite(id)} title={isFavorite(id) ? 'Unfavorite' : 'Favorite'} className="p-1 cursor-pointer rounded-md">
<StarIcon size={16} className={`${isFavorite(id) ? 'text-yellow-400' : 'text-gray-500'}`} />
</button>
<button title={chat.title} onClick={() => handleChatSelect(id)} className="hover:bg-gray-800 rounded-md !p-1.5 duration-200 cursor-pointer !w-full flex items-center justify-start gap-2">
<span className="!text-sm !text-gray-300 text-start w-full truncate">{chat.title || id}</span>
</button>
</div>
)
})}
</div>
)}
</div>

<div className={`${isSidebarCollapsed ? 'hidden' : 'flex'} w-full flex-col items-center justify-start gap-2 !px-4 !mt-2 text-sm font-light`}>
<p className="!text-sm !text-gray-300 text-start w-full font-semibold">Chats</p>

<div className="w-full flex flex-col items-center justify-start">
{chats.map((chat) => (
<ChatItem
key={chat.id}
chat={{ id: chat.id, title: chat.title || '' }}
handleChatSelect={handleChatSelect}
chatId={chatId || ''}
handleDeleteChat={handleDeleteChat}
handleFavoriteChat={toggleFavorite}
isFavorite={isFavorite}
/>
))}
</div>
</div>

</div>

<div className="chatbot-header !mt-auto">
<div className={`items-center justify-center gap-2 ${isSidebarCollapsed ? 'hidden' : 'flex'}`}>
<div className="header-icon">
<Sparkles className="sparkles-icon" />
</div>
<div className="header-text flex flex-col gap-0">
<h2 className="!text-base">Smart Contract Assistant</h2>
<p className="!text-xs !text-gray-500">Powered by DeepSeek AI</p>
</div>
</div>
<button
className={`template-button w-full ${isSidebarCollapsed ? '!w-auto' : 'w-full'}`}
onClick={() => setIsTemplateSelectorOpen(true)}
title="Browse Templates"
>
<Layout size={20} />
<span className={`${isSidebarCollapsed ? 'hidden' : 'block'} !text-sm`}>Templates</span>
</button>
</div>
</div>
)
}

export default Sidebar
Loading