diff --git a/src/App.tsx b/src/App.tsx index 76852dd..378ace7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ 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 ( @@ -9,6 +10,9 @@ function App() { } /> } /> + + {/* Catch all route for 404 */} + } /> ); diff --git a/src/Pages/ChatbotPage.tsx b/src/Pages/ChatbotPage.tsx index 087adbb..77a60a0 100644 --- a/src/Pages/ChatbotPage.tsx +++ b/src/Pages/ChatbotPage.tsx @@ -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(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 ( -
- -
- +
+
+ + + + + setIsTemplateSelectorOpen(false)} onSelectTemplate={handleTemplateSelect} />
-
) } diff --git a/src/Pages/NotFoundPage.tsx b/src/Pages/NotFoundPage.tsx new file mode 100644 index 0000000..45360f1 --- /dev/null +++ b/src/Pages/NotFoundPage.tsx @@ -0,0 +1,40 @@ +import { ArrowLeft } from "lucide-react" +import { Link } from "react-router-dom" +import cosmicCompass from "../assets/compass.png" + +const NotFoundPage = () => { + return ( +
+
+

Oops! You've Explored
Beyond The Map

+ +
+

We couldn't find the page you were looking for but

+

"Not all who wander are lost"

+ + + Back to Known Paths + +
+
+ +
+
+
+
+ + + +
+ Cosmic Compass +
+
+ Cosmic Compass +
+
+
+
+) +} + +export default NotFoundPage \ No newline at end of file diff --git a/src/assets/compass.png b/src/assets/compass.png new file mode 100644 index 0000000..a358384 Binary files /dev/null and b/src/assets/compass.png differ diff --git a/src/components/ChatItem.tsx b/src/components/ChatItem.tsx new file mode 100644 index 0000000..5dde75c --- /dev/null +++ b/src/components/ChatItem.tsx @@ -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 ( +
setIsMenuOpen(false)}> + + +
+ + +
+ + +
+
+
+ ) +} + +export default ChatItem \ No newline at end of file diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..8e1cc04 --- /dev/null +++ b/src/components/Sidebar.tsx @@ -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 ( +
+
+
+ + + + +
+ +
+ + + +
+ +
+ + + {favorites.length > 0 && ( +
+

Favorites

+ {favorites.map((id) => { + const chat = chats.find(c => c.id === id) + if (!chat) return null + return ( +
+ + +
+ ) + })} +
+ )} +
+ +
+

Chats

+ +
+ {chats.map((chat) => ( + + ))} +
+
+ +
+ +
+
+
+ +
+
+

Smart Contract Assistant

+

Powered by DeepSeek AI

+
+
+ +
+
+ ) +} + +export default Sidebar \ No newline at end of file diff --git a/src/components/TemplateSelector.tsx b/src/components/TemplateSelector.tsx index 7ce7473..9fb0531 100644 --- a/src/components/TemplateSelector.tsx +++ b/src/components/TemplateSelector.tsx @@ -1,8 +1,8 @@ import type React from "react" import { useState, useEffect, useRef } from "react" import { X, Coins, Image, Vote, Shield, TrendingUp, ChevronRight, Sparkles } from "lucide-react" +import type { LucideIcon } from "lucide-react" import { getAllTemplates, type ContractTemplate } from "../templates" -import { useNavigate } from 'react-router-dom' interface TemplateSelectorProps { isOpen: boolean @@ -10,7 +10,7 @@ interface TemplateSelectorProps { onSelectTemplate: (code: string, templateName: string) => void } -const categoryIcons: Record = { +const categoryIcons: Record = { token: Coins, nft: Image, dao: Vote, @@ -18,19 +18,19 @@ const categoryIcons: Record = { defi: TrendingUp, } -const difficultyColors: Record = { +const difficultyColors: Record = { beginner: "difficulty-beginner", intermediate: "difficulty-intermediate", advanced: "difficulty-advanced", } const TemplateSelector: React.FC = ({ isOpen, onClose, onSelectTemplate }) => { - const [selectedCategory, setSelectedCategory] = useState("all") + type ParameterValue = string | number | boolean + const [selectedCategory, setSelectedCategory] = useState<'all' | ContractTemplate['category']>("all") const [selectedTemplate, setSelectedTemplate] = useState(null) - const [parameters, setParameters] = useState>({}) + const [parameters, setParameters] = useState>({}) const [errors, setErrors] = useState>({}) const dialogRef = useRef(null) - const navigate = useNavigate() // When opened inline, optionally focus the heading for accessibility useEffect(() => { @@ -41,7 +41,7 @@ const TemplateSelector: React.FC = ({ isOpen, onClose, on }, [isOpen]) const templates = getAllTemplates() - const categories = [ + const categories: Array<{ id: 'all' | ContractTemplate['category']; label: string }> = [ { id: "all", label: "All Templates" }, { id: "token", label: "Tokens" }, { id: "nft", label: "NFTs" }, @@ -57,7 +57,7 @@ const TemplateSelector: React.FC = ({ isOpen, onClose, on const handleTemplateSelect = (template: ContractTemplate) => { setSelectedTemplate(template) - const defaultParams: Record = {} + const defaultParams: Record = {} template.parameters.forEach((param) => { if (param.defaultValue !== undefined) { defaultParams[param.name] = param.defaultValue @@ -67,7 +67,7 @@ const TemplateSelector: React.FC = ({ isOpen, onClose, on setErrors({}) } - const handleParameterChange = (paramName: string, value: any) => { + const handleParameterChange = (paramName: string, value: ParameterValue) => { setParameters((prev) => ({ ...prev, [paramName]: value })) // Clear error for this field @@ -80,7 +80,7 @@ const TemplateSelector: React.FC = ({ isOpen, onClose, on } } - const validateField = (param: ContractTemplate['parameters'][number], rawValue: any, acc: Record) => { + const validateField = (param: ContractTemplate['parameters'][number], rawValue: ParameterValue | undefined | null, acc: Record) => { const value = rawValue const empty = value === undefined || value === "" || value === null if (param.required && empty) { @@ -138,11 +138,11 @@ const TemplateSelector: React.FC = ({ isOpen, onClose, on if (!isOpen) return null return ( -
+
- + {/* */}

Contract Templates

@@ -234,18 +234,22 @@ const TemplateSelector: React.FC = ({ isOpen, onClose, on
handleParameterChange(param.name, e.target.checked)} />
) : ( handleParameterChange( param.name, - param.type === "number" ? e.target.value : e.target.value + param.type === "number" ? Number(e.target.value) : e.target.value ) } placeholder={param.placeholder} diff --git a/src/components/ui/chatbot.css b/src/components/ui/chatbot.css index 4d5b44b..ecc7505 100644 --- a/src/components/ui/chatbot.css +++ b/src/components/ui/chatbot.css @@ -2,7 +2,8 @@ display: flex; flex-direction: column; height: 100%; - min-height: 0; + min-height: 100vh; + max-height: 100vh; width: 100%; background: rgba(15, 15, 23, 0.95); backdrop-filter: blur(20px); @@ -10,15 +11,15 @@ border-radius: 0; overflow: hidden; box-shadow: none; - padding-bottom: 2rem; + /* padding-bottom: 2rem; */ } .chatbot-header { background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(6, 182, 212, 0.1)); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - padding: 1rem; + padding: 1rem 0.5rem; flex-shrink: 0; display: flex; + flex-direction: column; align-items: center; justify-content: space-between; gap: 1rem; @@ -613,7 +614,7 @@ background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 16px; - padding: 0.75rem; + padding: 0.5rem 0.75rem; backdrop-filter: blur(10px); transition: all 0.2s ease; } @@ -711,27 +712,28 @@ } /* Template Selector Styles */ - .template-selector-overlay { - position: relative; - background: transparent; - display: block; - z-index: auto; - padding-top: 1rem; - padding-bottom: 2rem; - } + .template-selector-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: block; + z-index: 1000; + padding: 1.5rem; + } - /* Inline mode modifier (current default) */ - .template-selector-overlay.inline-mode { - max-width: 1400px; - margin: 0 auto; - width: 100%; - } + /* Inline mode modifier (current default) */ + .template-selector-overlay.inline-mode { + max-width: 1400px; + width: 100%; + } .template-selector-modal { background: rgba(15, 15, 23, 0.85); border: 1px solid rgba(139, 92, 246, 0.35); border-radius: 20px; width: 100%; + max-width: 1400px; + max-height: calc(100vh - 3rem); margin: 0 auto; display: flex; flex-direction: column; @@ -791,10 +793,11 @@ .chatbot-main { flex: 1; - padding-top: 80px; /* navbar height */ - padding-bottom: 2rem; /* footer breathing room */ + padding-top: 80px; + padding-bottom: 2rem; display: flex; flex-direction: column; + min-height: 100vh; } .template-selector-header .header-icon { @@ -831,8 +834,9 @@ .template-selector-body { flex: 1 1 auto; - overflow-y: visible; + overflow-y: auto; padding: 1.75rem 2rem 2.25rem 2rem; + max-height: calc(100vh - 9rem); } .template-selector-body::-webkit-scrollbar { diff --git a/src/hooks/useFavorites.ts b/src/hooks/useFavorites.ts new file mode 100644 index 0000000..50fcd2c --- /dev/null +++ b/src/hooks/useFavorites.ts @@ -0,0 +1,50 @@ +import { useEffect, useState } from "react" + +const STORAGE_FAVORITES_KEY = "cf_favorite_chat_ids_v1" + +const useFavorites = () => { + const [favorites, setFavorites] = useState(() => { + try { + const raw = localStorage.getItem(STORAGE_FAVORITES_KEY) + if (!raw) return [] + const ids = JSON.parse(raw) as string[] + return Array.isArray(ids) ? ids : [] + } catch { + return [] + } + }) + + useEffect(() => { + try { + const deduped = Array.from(new Set(favorites)) + localStorage.setItem(STORAGE_FAVORITES_KEY, JSON.stringify(deduped)) + } catch { + // no-op + } + }, [favorites]) + + const addFavorite = (chatId: string) => { + setFavorites((prev: string[]) => Array.from(new Set([...prev, chatId]))) + } + + const removeFavorite = (chatId: string) => { + setFavorites((prev: string[]) => prev.filter((id: string) => id !== chatId)) + } + + const isFavorite = (chatId: string | undefined): boolean => { + if (!chatId) return false + return favorites.includes(chatId) + } + + const toggleFavorite = (chatId: string | undefined) => { + if (!chatId) return + setFavorites((prev: string[]) => { + if (prev.includes(chatId)) return prev.filter(id => id !== chatId) + return [...prev, chatId] + }) + } + + return { favorites, addFavorite, removeFavorite, isFavorite, toggleFavorite } +} + +export default useFavorites \ No newline at end of file diff --git a/src/hooks/useHistory.ts b/src/hooks/useHistory.ts new file mode 100644 index 0000000..856c4e2 --- /dev/null +++ b/src/hooks/useHistory.ts @@ -0,0 +1,166 @@ +import { useEffect, useState } from "react" +import type { Message } from "@/hooks/useMessages" + +const STORAGE_KEY = "cf_chat_messages_v1" // legacy single-session storage +const STORAGE_CHATS_KEY = "cf_chats_v2" // multi-session storage by chatId + +export interface StoredMessage { + id: string + type: "user" | "ai" + content: string + isCode?: boolean + timestamp: string +} + +export interface StoredChat { + id: string + title?: string + createdAt: string + updatedAt: string + messages: StoredMessage[] +} + +export interface ChatSummary { + id: string + title?: string + createdAt: string + updatedAt: string + lastMessagePreview?: string +} + +function serializeMessages(messages: Message[]): StoredMessage[] { + return messages.map((m) => ({ + id: m.id, + type: m.type, + content: m.content, + isCode: m.isCode, + timestamp: m.timestamp instanceof Date ? m.timestamp.toISOString() : new Date(m.timestamp).toISOString(), + })) +} + +function deserializeMessages(stored: StoredMessage[] | null | undefined): Message[] { + if (!stored) return [] + return stored.map((m) => ({ + id: m.id, + type: m.type, + content: m.content, + isCode: m.isCode, + // validation and compilationStatus intentionally omitted from storage; recompute if needed at runtime + timestamp: new Date(m.timestamp), + })) +} + +export function loadHistory(): Message[] { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return [] + const parsed = JSON.parse(raw) as StoredMessage[] + return deserializeMessages(parsed) + } catch { + return [] + } +} + +export function saveHistory(messages: Message[]): void { + try { + const serialized = serializeMessages(messages) + localStorage.setItem(STORAGE_KEY, JSON.stringify(serialized)) + } catch { + // no-op + } +} + +export function clearHistory(): void { + try { + localStorage.removeItem(STORAGE_KEY) + } catch { + // no-op + } +} + +// Multi-chat APIs +function readChatsStore(): Record { + try { + const raw = localStorage.getItem(STORAGE_CHATS_KEY) + if (!raw) return {} + const obj = JSON.parse(raw) as Record + return obj || {} + } catch { + return {} + } +} + +function writeChatsStore(chats: Record): void { + try { + localStorage.setItem(STORAGE_CHATS_KEY, JSON.stringify(chats)) + } catch { + // no-op + } +} + +export function loadChat(chatId: string): Message[] { + const store = readChatsStore() + const chat = store[chatId] + if (!chat) return [] + return deserializeMessages(chat.messages) +} + +export function saveChat(chatId: string, messages: Message[], title?: string): void { + const store = readChatsStore() + const nowIso = new Date().toISOString() + const existing = store[chatId] + const storedMessages = serializeMessages(messages) + const computedTitle = title || existing?.title || deriveTitleFromMessages(messages) + store[chatId] = { + id: chatId, + title: computedTitle, + createdAt: existing?.createdAt || nowIso, + updatedAt: nowIso, + messages: storedMessages, + } + writeChatsStore(store) +} + +export function listChats(): ChatSummary[] { + const store = readChatsStore() + const summaries = Object.values(store).map((c) => ({ + id: c.id, + title: c.title, + createdAt: c.createdAt, + updatedAt: c.updatedAt, + lastMessagePreview: c.messages.length ? truncate(c.messages[c.messages.length - 1].content, 80) : undefined, + })) + // sort by updatedAt desc + return summaries.sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1)) +} + +export function deleteChat(chatId: string): void { + const store = readChatsStore() + if (store[chatId]) { + delete store[chatId] + writeChatsStore(store) + } +} + +function deriveTitleFromMessages(messages: Message[]): string | undefined { + const firstUser = messages.find((m) => m.type === "user") + if (!firstUser) return undefined + return truncate(firstUser.content.replace(/<[^>]+>/g, ""), 40) +} + +function truncate(text: string, max: number): string { + if (text.length <= max) return text + return text.slice(0, max - 1) + "…" +} + +const useHistory = () => { + const [history, setHistory] = useState(() => loadHistory()) + + useEffect(() => { + saveHistory(history) + }, [history]) + + return { history, setHistory, clearHistory } +} + +export default useHistory \ No newline at end of file diff --git a/src/hooks/useMessages.ts b/src/hooks/useMessages.ts new file mode 100644 index 0000000..32e424f --- /dev/null +++ b/src/hooks/useMessages.ts @@ -0,0 +1,89 @@ +import type { ValidationResult } from "@/services/contractValidator" +import { useEffect, useRef, useState } from "react" +import { loadHistory, saveHistory, loadChat, saveChat } from "@/hooks/useHistory" + +export interface Message { + id: string + type: "user" | "ai" + content: string + timestamp: Date + isCode?: boolean + compilationStatus?: "success" | "error" | "pending" + validation?: ValidationResult +} + +function getInitialGreeting(): Message[] { + return [ + { + id: "1", + type: "ai", + content: + "Hello! I'm your Smart Contract Assistant for ResilientDB. I can generate Solidity smart contracts from natural language descriptions.

💡 Try these examples:
• \"Create a simple token contract with transfer and balance functions\"
• \"Build a voting system where users can create and vote on proposals\"
• \"Make a multi-signature wallet that requires 2 out of 3 signatures\"
• \"Create a crowdfunding contract where people can contribute and claim rewards\"
• Click the \"Templates\" button to browse pre-built contract templates

What type of smart contract would you like to create?", + timestamp: new Date(), + }, + ] +} + +const useMessages = (chatId?: string) => { + const [messages, setMessages] = useState(() => { + if (chatId) { + const chatMessages = loadChat(chatId) + // If the chatId exists in history (has any messages), load it; else start fresh with greeting + return chatMessages && chatMessages.length > 0 ? chatMessages : getInitialGreeting() + } + const history = loadHistory() + return history && history.length > 0 ? history : getInitialGreeting() + }) + const persistEnabledRef = useRef(false) + + // Reset message list when chatId changes (new chat or switching chats) + useEffect(() => { + if (chatId) { + const chatMessages = loadChat(chatId) + // Only load from history if there is prior content; otherwise show greeting and wait for first user message + setMessages(chatMessages && chatMessages.length > 0 ? chatMessages : getInitialGreeting()) + persistEnabledRef.current = (chatMessages || []).some(m => m.type === "user") + return + } + const history = loadHistory() + setMessages(history && history.length > 0 ? history : getInitialGreeting()) + persistEnabledRef.current = (history || []).some(m => m.type === "user") + }, [chatId]) + + const appendUserMessage = (content: string) => { + const newMessage: Message = { + id: Date.now().toString(), + type: "user", + content: content.trim(), + timestamp: new Date(), + } + setMessages(prev => { + const next = [...prev, newMessage] + if (chatId) { + persistEnabledRef.current = true + saveChat(chatId, next) + } else { + saveHistory(next) + } + return next + }) + } + + const appendAiMessage = (message: Message) => { + setMessages(prev => { + const next = [...prev, message] + if (chatId) { + if (persistEnabledRef.current) { + saveChat(chatId, next) + } + } else { + saveHistory(next) + } + return next + }) + } + + return { messages, setMessages, appendUserMessage, appendAiMessage } +} + +export default useMessages \ No newline at end of file diff --git a/src/index.css b/src/index.css index 1389c48..1a5a946 100644 --- a/src/index.css +++ b/src/index.css @@ -2,9 +2,8 @@ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap'); @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap'); -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; +@source "./index.html" "./src/**/*.{js,ts,jsx,tsx}"; @layer base { :root { @@ -28,6 +27,9 @@ --input: 217.2 32.6% 17.5%; --ring: 224.3 76.3% 94.1%; --radius: 0.5rem; + + --sidebar-width: 260px; + --sidebar-width-collapsed: 80px; } } diff --git a/src/services/Chatbot.tsx b/src/services/Chatbot.tsx index 3a267ce..355de8d 100644 --- a/src/services/Chatbot.tsx +++ b/src/services/Chatbot.tsx @@ -1,15 +1,14 @@ "use client" -import type React from "react" import { useState, useRef, useEffect } from "react" -import { Send, Copy, Code, Sparkles, Bot, User, CheckCircle, AlertCircle, Info, AlertTriangle, Lightbulb, FileJson, Download, Layout } from "lucide-react" +import { Send, Copy, Code, Bot, User, CheckCircle, AlertCircle, Info, AlertTriangle, Lightbulb, FileJson, Download } from "lucide-react" import { generateSmartContract, generateJSONFromSolidity } from "./deepseekService" import { ContractValidator } from "./contractValidator" -import type { ValidationResult } from "./contractValidator" import JSONModal from "../components/JSONModal" -import TemplateSelector from "../components/TemplateSelector" import Modal from "react-modal" import "../components/ui/chatbot.css" +import type { Message } from "@/hooks/useMessages" +// Chatbot is UI-only; chat loading/persistence happens in hooks at the page level // Initialize react-modal safely if (typeof document !== 'undefined') { @@ -19,36 +18,25 @@ if (typeof document !== 'undefined') { } } -interface Message { - id: string - type: "user" | "ai" - content: string - timestamp: Date - isCode?: boolean - compilationStatus?: "success" | "error" | "pending" - validation?: ValidationResult +interface ChatbotProps { + validateSolidityCode: (code: string) => boolean + messages: Message[] + setMessages: React.Dispatch> + appendUserMessage?: (content: string) => void + appendAiMessage?: (message: Message) => void } -const Chatbot: React.FC = () => { - const [messages, setMessages] = useState([ - { - id: "1", - type: "ai", - content: - "Hello! I'm your Smart Contract Assistant for ResilientDB. I can generate Solidity smart contracts from natural language descriptions.

💡 Try these examples:
• \"Create a simple token contract with transfer and balance functions\"
• \"Build a voting system where users can create and vote on proposals\"
• \"Make a multi-signature wallet that requires 2 out of 3 signatures\"
• \"Create a crowdfunding contract where people can contribute and claim rewards\"
• Click the \"Templates\" button to browse pre-built contract templates

What type of smart contract would you like to create?", - timestamp: new Date(), - }, - ]) +const Chatbot = ({ validateSolidityCode, messages, setMessages, appendUserMessage, appendAiMessage }: ChatbotProps) => { const [input, setInput] = useState("") const [isLoading, setIsLoading] = useState(false) const [isJSONModalOpen, setIsJSONModalOpen] = useState(false) - const [isTemplateSelectorOpen, setIsTemplateSelectorOpen] = useState(false) const [jsonConfig, setJsonConfig] = useState("") const [contractName, setContractName] = useState("") const [exampleConfig, setExampleConfig] = useState("") const [isGeneratingJSON, setIsGeneratingJSON] = useState(false) const messagesEndRef = useRef(null) const textareaRef = useRef(null) + const scrollToBottom = () => { const container = document.querySelector('.messages-container') as HTMLElement; @@ -64,27 +52,26 @@ const Chatbot: React.FC = () => { } }, [messages]) - 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 handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!input.trim() || isLoading) return - const userMessage: Message = { - id: Date.now().toString(), - type: "user", - content: input.trim(), - timestamp: new Date(), - } + // chatId always exists from page mount; first user message will cause saving to begin in useMessages - setMessages((prev) => [...prev, userMessage]) + const userContent = input.trim() + if (appendUserMessage) { + appendUserMessage(userContent) + } else { + const userMessage: Message = { + id: Date.now().toString(), + type: "user", + content: userContent, + timestamp: new Date(), + } + setMessages((prev) => [...prev, userMessage]) + } setInput("") setIsLoading(true) @@ -101,15 +88,23 @@ const Chatbot: React.FC = () => { compilationStatus: isSolidityCode ? (validateSolidityCode(response) ? "success" : "error") : undefined, validation: isSolidityCode ? ContractValidator.validateContract(response) : undefined, } - setMessages((prev) => [...prev, aiMessage]) - } catch (error) { + if (appendAiMessage) { + appendAiMessage(aiMessage) + } else { + setMessages((prev) => [...prev, aiMessage]) + } + } catch { const errorMessage: Message = { id: (Date.now() + 1).toString(), type: "ai", content: "Sorry, I encountered an error while generating your smart contract. Please try again.", timestamp: new Date(), } - setMessages((prev) => [...prev, errorMessage]) + if (appendAiMessage) { + appendAiMessage(errorMessage) + } else { + setMessages((prev) => [...prev, errorMessage]) + } } finally { setIsLoading(false) } @@ -186,40 +181,8 @@ const Chatbot: React.FC = () => { } } - const handleTemplateSelect = (code: string, _templateName: 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 (
-
-
-
- -
-
-

Smart Contract Assistant

-

Powered by DeepSeek AI

-
-
- -
{messages.map((message) => ( @@ -405,12 +368,6 @@ const Chatbot: React.FC = () => {
- - setIsTemplateSelectorOpen(false)} - onSelectTemplate={handleTemplateSelect} - />
) } diff --git a/src/services/deepseekService.ts b/src/services/deepseekService.ts index 382d48d..a0f01a0 100644 --- a/src/services/deepseekService.ts +++ b/src/services/deepseekService.ts @@ -16,20 +16,23 @@ export async function generateSmartContract(prompt: string): Promise { try { // Check if API key is configured const apiKey = import.meta.env.VITE_DEEPSEEK_API_KEY; + const MODEL = import.meta.env.VITE_DEEPSEEK_MODEL; + const BASE_URL = import.meta.env.VITE_DEEPSEEK_BASE_URL; + console.log('MODEL', MODEL); if (!apiKey || apiKey === 'sk-1234567890abcdef') { throw new Error('DeepSeek API key not configured. Please add VITE_DEEPSEEK_API_KEY to your .env file.'); } - const response = await fetch('https://api.deepseek.com/v1/chat/completions', { + const response = await fetch(`${BASE_URL}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ - model: 'deepseek-chat', - temperature: 0.3, // Lower temperature for more consistent code generation - max_tokens: 4000, // Increased for longer contracts + model: MODEL, + // temperature: 0.3, // Lower temperature for more consistent code generation + // max_tokens: 4000, // Increased for longer contracts messages: [ { role: 'system', @@ -123,6 +126,8 @@ export async function generateJSONFromSolidity(solidityCode: string): Promise