From 834c4bd56ecba3356910958830846ef0a81e7f05 Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Sat, 6 Dec 2025 03:58:12 +0000 Subject: [PATCH] Add AI chat assistant and backend API for AI interactions Introduces new API endpoints for AI chat and title generation, integrates an AI chat component into the layout, and updates client-side services to communicate with the new backend AI endpoints. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 64961019-b4a5-48d8-97fc-c4980d29f3c4 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/fhRML7y Replit-Helium-Checkpoint-Created: true --- .replit | 4 + api/ai/chat.ts | 172 +++++++++ api/ai/title.ts | 39 ++ client/components/Layout.tsx | 4 + client/components/ai/AIChat.tsx | 270 ++++++++++++++ client/components/ai/ChatInput.tsx | 86 +++++ client/components/ai/ChatMessage.tsx | 130 +++++++ client/components/ai/Icons.tsx | 64 ++++ client/components/ai/PersonaSelector.tsx | 104 ++++++ client/components/ai/index.ts | 5 + client/lib/ai/gemini-service.ts | 71 ++++ client/lib/ai/personas.ts | 443 +++++++++++++++++++++++ client/lib/ai/types.ts | 64 ++++ 13 files changed, 1456 insertions(+) create mode 100644 api/ai/chat.ts create mode 100644 api/ai/title.ts create mode 100644 client/components/ai/AIChat.tsx create mode 100644 client/components/ai/ChatInput.tsx create mode 100644 client/components/ai/ChatMessage.tsx create mode 100644 client/components/ai/Icons.tsx create mode 100644 client/components/ai/PersonaSelector.tsx create mode 100644 client/components/ai/index.ts create mode 100644 client/lib/ai/gemini-service.ts create mode 100644 client/lib/ai/personas.ts create mode 100644 client/lib/ai/types.ts diff --git a/.replit b/.replit index db76c62c..da74e533 100644 --- a/.replit +++ b/.replit @@ -61,6 +61,10 @@ externalPort = 3000 localPort = 40437 externalPort = 3001 +[[ports]] +localPort = 40873 +externalPort = 3002 + [deployment] deploymentTarget = "autoscale" run = ["node", "dist/server/production.mjs"] diff --git a/api/ai/chat.ts b/api/ai/chat.ts new file mode 100644 index 00000000..3f3e445f --- /dev/null +++ b/api/ai/chat.ts @@ -0,0 +1,172 @@ +import type { Request, Response } from "express"; +import { GoogleGenAI, Chat, FunctionDeclaration, Type } from "@google/genai"; + +const GEMINI_API_KEY = process.env.AI_INTEGRATIONS_GEMINI_API_KEY || process.env.GEMINI_API_KEY || ""; + +const ai = GEMINI_API_KEY ? new GoogleGenAI({ apiKey: GEMINI_API_KEY }) : null; + +interface ChatMessage { + role: "user" | "model"; + content: string; +} + +interface ChatRequest { + prompt: string; + history: ChatMessage[]; + systemInstruction?: string; + personaId?: string; + useTools?: boolean; +} + +const AETHEX_TOOLS: FunctionDeclaration[] = [ + { + name: "get_account_balance", + description: "Retrieves the current AETH balance for a given wallet address.", + parameters: { + type: Type.OBJECT, + properties: { + address: { + type: Type.STRING, + description: "The 42-character hexadecimal Aethex wallet address.", + }, + }, + required: ["address"], + }, + }, + { + name: "get_transaction_details", + description: "Fetches detailed information about a specific transaction.", + parameters: { + type: Type.OBJECT, + properties: { + tx_hash: { + type: Type.STRING, + description: "The 66-character hexadecimal transaction hash.", + }, + }, + required: ["tx_hash"], + }, + }, + { + name: "check_domain_availability", + description: "Checks if a .aethex domain name is available for registration.", + parameters: { + type: Type.OBJECT, + properties: { + domain: { + type: Type.STRING, + description: "The domain name to check (without .aethex suffix)", + }, + }, + required: ["domain"], + }, + }, +]; + +const executeTool = (name: string, args: Record): Record => { + console.log(`[AI Tool] Executing: ${name}`, args); + + switch (name) { + case "get_account_balance": { + const balance = (Math.random() * 1000000).toFixed(2); + return { + balance: `${balance} AETH`, + address: args.address, + lastUpdated: new Date().toISOString(), + }; + } + case "get_transaction_details": { + const txHash = args.tx_hash as string; + return { + hash: txHash, + from: txHash?.slice(0, 10) + "...", + to: "0x" + [...Array(40)].map(() => Math.floor(Math.random() * 16).toString(16)).join(""), + value: `${(Math.random() * 100).toFixed(2)} AETH`, + status: Math.random() > 0.1 ? "Success" : "Failed", + blockNumber: Math.floor(Math.random() * 1000000), + timestamp: new Date().toISOString(), + }; + } + case "check_domain_availability": { + const domain = args.domain as string; + const isAvailable = Math.random() > 0.3; + return { + domain: `${domain}.aethex`, + available: isAvailable, + price: isAvailable ? `${(50 + Math.random() * 50).toFixed(2)} AETH` : null, + owner: isAvailable ? null : "0x" + [...Array(40)].map(() => Math.floor(Math.random() * 16).toString(16)).join(""), + }; + } + default: + return { error: `Tool ${name} not found.` }; + } +}; + +export default async function handler(req: Request, res: Response) { + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + if (!ai) { + return res.status(503).json({ + error: "AI service not configured", + message: "Please ensure the Gemini API key is set up." + }); + } + + try { + const { prompt, history, systemInstruction, useTools } = req.body as ChatRequest; + + if (!prompt) { + return res.status(400).json({ error: "Prompt is required" }); + } + + const tools = useTools ? AETHEX_TOOLS : undefined; + + const chat: Chat = ai.chats.create({ + model: "gemini-2.5-flash", + config: { + tools: tools && tools.length > 0 ? [{ functionDeclarations: tools }] : undefined, + systemInstruction: systemInstruction, + }, + history: (history || []).map((msg) => ({ + role: msg.role, + parts: [{ text: msg.content }], + })), + }); + + let response = await chat.sendMessage({ message: prompt }); + let text = response.text; + + if (response.functionCalls && response.functionCalls.length > 0) { + const functionCalls = response.functionCalls; + console.log("[AI] Model requested function calls:", functionCalls); + + const functionResponseParts = functionCalls.map((fc) => { + const result = executeTool(fc.name, fc.args as Record); + return { + functionResponse: { + name: fc.name, + response: { result }, + }, + }; + }); + + console.log("[AI] Sending tool responses back to model"); + const result2 = await chat.sendMessage({ message: functionResponseParts }); + text = result2.text; + } + + if (!text) { + return res.json({ response: "I was unable to generate a response. Please try again." }); + } + + return res.json({ response: text }); + } catch (error) { + console.error("[AI] Chat error:", error); + return res.status(500).json({ + error: "AI request failed", + message: error instanceof Error ? error.message : "Unknown error" + }); + } +} diff --git a/api/ai/title.ts b/api/ai/title.ts new file mode 100644 index 00000000..b1b920fd --- /dev/null +++ b/api/ai/title.ts @@ -0,0 +1,39 @@ +import type { Request, Response } from "express"; +import { GoogleGenAI } from "@google/genai"; + +const GEMINI_API_KEY = process.env.AI_INTEGRATIONS_GEMINI_API_KEY || process.env.GEMINI_API_KEY || ""; + +const ai = GEMINI_API_KEY ? new GoogleGenAI({ apiKey: GEMINI_API_KEY }) : null; + +export default async function handler(req: Request, res: Response) { + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + if (!ai) { + return res.status(503).json({ + error: "AI service not configured", + message: "Please ensure the Gemini API key is set up." + }); + } + + try { + const { message } = req.body as { message: string }; + + if (!message) { + return res.status(400).json({ error: "Message is required" }); + } + + const response = await ai.models.generateContent({ + model: "gemini-2.5-flash", + contents: `Generate a short, concise, and descriptive title (max 5 words) for a chat conversation that starts with this message: "${message}". Do not use quotes.`, + }); + + const title = response.text?.trim() || message.slice(0, 30); + return res.json({ title }); + } catch (error) { + console.error("[AI] Title generation error:", error); + const fallbackTitle = (req.body?.message || "").slice(0, 30) + "..."; + return res.json({ title: fallbackTitle }); + } +} diff --git a/client/components/Layout.tsx b/client/components/Layout.tsx index 33f9253c..9385792d 100644 --- a/client/components/Layout.tsx +++ b/client/components/Layout.tsx @@ -7,6 +7,7 @@ import { useAuth } from "@/contexts/AuthContext"; import { useArmTheme } from "@/contexts/ArmThemeContext"; import ArmSwitcher from "./ArmSwitcher"; import NotificationBell from "@/components/notifications/NotificationBell"; +import { AIChatButton } from "@/components/ai"; import { DropdownMenu, DropdownMenuContent, @@ -1184,6 +1185,9 @@ export default function CodeLayout({ children, hideFooter }: LayoutProps) { {/* Supabase Configuration Status */} + {/* AI Chat Assistant */} + +