From 1dbabc33b27907e97f1fa87e63ac31f6d6d45598 Mon Sep 17 00:00:00 2001 From: PoulavBhowmick03 Date: Fri, 10 Jan 2025 12:10:45 +0530 Subject: [PATCH] frontend --- app/api/transactions/route.ts | 6 +- components/chat-panel.tsx | 234 +++++++++++++++++----------------- process-env.d.ts | 1 + 3 files changed, 123 insertions(+), 118 deletions(-) diff --git a/app/api/transactions/route.ts b/app/api/transactions/route.ts index 60d8676..d2e6649 100644 --- a/app/api/transactions/route.ts +++ b/app/api/transactions/route.ts @@ -1,9 +1,7 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { NextRequest, NextResponse } from 'next/server'; import { transactionProcessor } from '@/lib/transactions/processor'; -import type { - BrianResponse, - BrianTransactionData, -} from '@/lib/transactions/types'; +import type { BrianResponse } from '@/lib/transactions/types'; const BRIAN_API_URL = 'https://api.brianknows.org/api/v0/agent'; diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index dfcacb5..5d8c798 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -1,9 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + 'use client'; -import { useEffect, useState, useRef, useCallback } from 'react'; -import { useRouter } from 'next/navigation'; +import { useState, useRef, ReactNode } from 'react'; import type { AI, UIState } from '@/app/actions'; -import { useUIState, useActions, useAIState } from 'ai/rsc'; +import { useUIState } from 'ai/rsc'; import { cn } from '@/lib/utils'; import { UserMessage } from './user-message'; import { Button } from './ui/button'; @@ -11,37 +13,59 @@ import { ArrowRight, Plus } from 'lucide-react'; import { EmptyScreen } from './empty-screen'; import Textarea from 'react-textarea-autosize'; import { generateId } from 'ai'; -import { useAppState } from '@/lib/utils/app-state'; import { ModelSelector } from './model-selector'; import { models } from '@/lib/types/models'; import { useLocalStorage } from '@/lib/hooks/use-local-storage'; import { getDefaultModelId } from '@/lib/utils'; -import { toast } from 'sonner'; import BsvButton from './bsv/BsvButton'; + interface ChatPanelProps { messages: UIState; query?: string; onModelChange?: (id: string) => void; } +type Message = { + component: ReactNode; + id: string; + role: string; + content: string; + timestamp: string; + user: string; + transaction?: { + data: { + transactions: Array<{ + contractAddress: string; + entrypoint: string; + calldata: string[]; + }>; + fromToken?: any; + toToken?: any; + fromAmount?: string; + toAmount?: string; + receiver?: string; + gasCostUSD?: string; + solver?: string; + }; + type: string; + }; +}; + export function ChatPanel({ messages, query, onModelChange }: ChatPanelProps) { const [input, setInput] = useState(''); const [showEmptyScreen, setShowEmptyScreen] = useState(false); const [, setMessages] = useUIState(); - const [aiMessage, setAIMessage] = useAIState(); - const { isGenerating, setIsGenerating } = useAppState(); - const { submit } = useActions(); - const router = useRouter(); const inputRef = useRef(null); - const isFirstRender = useRef(true); // For development environment + const [textMessages, setTextMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); const [selectedModelId, setSelectedModelId] = useLocalStorage( 'selectedModel', getDefaultModelId(models) ); - const [isComposing, setIsComposing] = useState(false); // Composition state - const [enterDisabled, setEnterDisabled] = useState(false); // Disable Enter after composition ends + const [isComposing, setIsComposing] = useState(false); + const [enterDisabled, setEnterDisabled] = useState(false); const handleCompositionStart = () => setIsComposing(true); @@ -53,104 +77,96 @@ export function ChatPanel({ messages, query, onModelChange }: ChatPanelProps) { }, 300); }; - const handleQuerySubmit = useCallback( - async (query: string, formData?: FormData) => { - setInput(query); - setIsGenerating(true); + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim()) return; + + const userMessage: Message = { + component: , + id: generateId(), + role: 'user', + content: input, + timestamp: new Date().toLocaleTimeString(), + user: 'User', + }; + + setTextMessages((prev) => [...prev, userMessage]); + setInput(''); + setIsLoading(true); - // Add user message to UI state - setMessages((currentMessages) => [ - ...currentMessages, - { - id: generateId(), - component: , + try { + const response = await fetch('/api/transactions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, - ]); - - // Use existing formData or create new one - const data = formData || new FormData(); - - // Add or update the model information - const modelString = selectedModelId; - data.set('model', modelString); - - // Add or update the input query if not already present - if (!formData) { - data.set('input', query); + body: JSON.stringify({ + prompt: input, + // address: address, + chainId: '4012', + messages: textMessages.concat(userMessage).map((msg) => ({ + sender: msg.role === 'user' ? 'user' : 'brian', + content: msg.content, + })), + }), + }); + + const data = await response.json(); + let agentMessage: Message; + + if ( + data.error && + typeof data.error === 'string' && + !data.error.includes('not recognized') + ) { + agentMessage = { + component: 'error', + id: generateId(), + role: 'agent', + content: data.error, + timestamp: new Date().toLocaleTimeString(), + user: 'Agent', + }; + } else if (response.ok && data.result?.[0]?.data) { + const { description, transaction } = data.result[0].data; + agentMessage = { + component: 'transaction', + id: generateId(), + role: 'agent', + content: description, + timestamp: new Date().toLocaleTimeString(), + user: 'Agent', + transaction: transaction, + }; + } else { + agentMessage = { + component: 'empty', + id: generateId(), + role: 'agent', + content: + "I'm sorry, I couldn't understand that. Could you try rephrasing your request? For example, you can say 'swap', 'transfer', 'deposit', or 'bridge'.", + timestamp: new Date().toLocaleTimeString(), + user: 'Agent', + }; } - const responseMessage = await submit(data); - setMessages((currentMessages) => [...currentMessages, responseMessage]); - }, - [selectedModelId, setIsGenerating, setMessages, submit] - ); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - try { - await handleQuerySubmit(input, formData); + setMessages((prev) => [...prev, agentMessage]); } catch (error) { - console.error('Error submitting form:', error); - toast.error(`${error}`); - - handleClear(); + console.error('Error:', error); + const errorMessage: Message = { + component: 'error', + id: generateId(), + role: 'agent', + content: 'Sorry, something went wrong. Please try again.', + timestamp: new Date().toLocaleTimeString(), + user: 'Agent', + }; + setMessages((prev) => [...prev, errorMessage]); + } finally { + setIsLoading(false); } }; - // if query is not empty, submit the query - useEffect(() => { - if (isFirstRender.current && query && query.trim().length > 0) { - handleQuerySubmit(query); - isFirstRender.current = false; - } - }, [handleQuerySubmit, query]); - - useEffect(() => { - const lastMessage = aiMessage.messages.slice(-1)[0]; - if (lastMessage?.type === 'followup' || lastMessage?.type === 'inquiry') { - setIsGenerating(false); - } - }, [aiMessage, setIsGenerating]); - - // Clear messages - const handleClear = () => { - setIsGenerating(false); - setMessages([]); - setAIMessage({ messages: [], chatId: '' }); - setInput(''); - router.push('/'); - }; - - useEffect(() => { - // focus on input when the page loads - inputRef.current?.focus(); - }, []); - - // If there are messages and the new button has not been pressed, display the new Button - if (messages.length > 0) { - return ( -
- -
- ); - } - - if (query && query.trim().length > 0) { - return null; - } - return (
{ - // Enter should submit the form, but disable it right after IME input confirmation if ( e.key === 'Enter' && !e.shiftKey && - !isComposing && // Not in composition - !enterDisabled // Not within the delay after confirmation + !isComposing && + !enterDisabled ) { - // Prevent the default action to avoid adding a new line if (input.trim().length === 0) { e.preventDefault(); return; @@ -201,19 +215,11 @@ export function ChatPanel({ messages, query, onModelChange }: ChatPanelProps) { } }} onHeightChange={(height) => { - // Ensure inputRef.current is defined if (!inputRef.current) return; - - // The initial height and left padding is 70px and 2rem const initialHeight = 70; - // The initial border radius is 32px const initialBorder = 32; - // The height is incremented by multiples of 20px const multiple = (height - initialHeight) / 20; - - // Decrease the border radius by 4px for each 20px height increase const newBorder = initialBorder - 4 * multiple; - // The lowest border radius will be 8px inputRef.current.style.borderRadius = `${Math.max( 8, newBorder diff --git a/process-env.d.ts b/process-env.d.ts index 3038f9d..7b0413e 100644 --- a/process-env.d.ts +++ b/process-env.d.ts @@ -14,6 +14,7 @@ declare global { readonly POSTGRES_PASSWORD: string; readonly POSTGRES_DATABASE: string; readonly CLERK_SECRET_KEY: string; + readonly TAAL_API_KEY: string; // public readonly NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: string; readonly NEXT_PUBLIC_CLERK_SIGN_IN_URL: string;