2026-04-10 12:52:17 -07:00
'use client'
import { FormEvent , useMemo , useState } from 'react'
import { usePathname } from 'next/navigation'
import { getExplorerApiBase } from '@/services/api/blockscout'
interface AgentMessage {
role : 'assistant' | 'user'
content : string
}
const QUICK_PROMPTS = [
'Explain this page' ,
'Summarize the chain status' ,
'Help me inspect a contract' ,
'Find likely navigation issues' ,
] as const
export default function ExplorerAgentTool() {
const pathname = usePathname ( ) ? ? '/'
const [ open , setOpen ] = useState ( false )
const [ input , setInput ] = useState ( '' )
const [ submitting , setSubmitting ] = useState ( false )
const [ messages , setMessages ] = useState < AgentMessage [ ] > ( [
{
role : 'assistant' ,
content :
2026-04-30 03:06:49 -07:00
'DBIS Explorer AI Assist is ready. I can explain this page, summarize what you are looking at, and help investigate transactions, contracts, routes, and system surfaces.' ,
2026-04-10 12:52:17 -07:00
} ,
] )
const pageContext = useMemo (
( ) = > ( {
path : pathname ,
view : 'explorer' ,
} ) ,
[ pathname ] ,
)
const sendMessage = async ( content : string ) = > {
const trimmed = content . trim ( )
if ( ! trimmed || submitting ) return
const nextMessages : AgentMessage [ ] = [ . . . messages , { role : 'user' , content : trimmed } ]
setMessages ( nextMessages )
setInput ( '' )
setSubmitting ( true )
try {
const response = await fetch ( ` ${ getExplorerApiBase ( ) } /api/v1/ai/chat ` , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
} ,
body : JSON.stringify ( {
messages : nextMessages ,
pageContext ,
} ) ,
} )
const payload = await response . json ( ) . catch ( ( ) = > null )
if ( ! response . ok ) {
throw new Error ( payload ? . error ? . message || ` HTTP ${ response . status } ` )
}
const reply =
payload ? . message ? . content ||
payload ? . reply ||
'The agent did not return a readable reply.'
setMessages ( ( current ) = > [ . . . current , { role : 'assistant' , content : String ( reply ) } ] )
} catch ( error ) {
setMessages ( ( current ) = > [
. . . current ,
{
role : 'assistant' ,
content :
error instanceof Error
? ` Agent tool is temporarily unavailable: ${ error . message } `
: 'Agent tool is temporarily unavailable.' ,
} ,
] )
} finally {
setSubmitting ( false )
}
}
const handleSubmit = async ( event : FormEvent ) = > {
event . preventDefault ( )
await sendMessage ( input )
}
return (
< div className = "fixed bottom-5 right-5 z-40 flex max-w-[calc(100vw-1.5rem)] flex-col items-end gap-3" >
{ open ? (
< section className = "w-[min(24rem,calc(100vw-1.5rem))] overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-900" >
< div className = "flex items-start justify-between gap-3 border-b border-gray-200 px-4 py-3 dark:border-gray-700" >
< div >
2026-04-30 03:06:49 -07:00
< h2 className = "text-sm font-semibold text-gray-900 dark:text-white" > DBIS Explorer AI Assist < / h2 >
2026-04-10 12:52:17 -07:00
< p className = "mt-1 text-xs text-gray-500 dark:text-gray-400" >
Page - aware guidance for the explorer . Helpful , read - only , and designed for quick investigation .
< / p >
< / div >
< button
type = "button"
onClick = { ( ) = > setOpen ( false ) }
className = "rounded-lg px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
>
Close
< / button >
< / div >
< div className = "flex flex-wrap gap-2 border-b border-gray-200 px-4 py-3 dark:border-gray-700" >
{ QUICK_PROMPTS . map ( ( prompt ) = > (
< button
key = { prompt }
type = "button"
onClick = { ( ) = > void sendMessage ( prompt ) }
className = "rounded-full border border-primary-200 bg-primary-50 px-3 py-1 text-xs font-medium text-primary-700 hover:bg-primary-100 dark:border-primary-500/30 dark:bg-primary-500/10 dark:text-primary-300 dark:hover:bg-primary-500/20"
>
{ prompt }
< / button >
) ) }
< / div >
< div className = "max-h-[22rem] space-y-3 overflow-y-auto px-4 py-3" >
{ messages . map ( ( message , index ) = > (
< div
key = { ` ${ message . role } - ${ index } ` }
className = { ` rounded-2xl px-3 py-2 text-sm ${
message . role === 'assistant'
? 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-100'
: 'ml-6 bg-primary-600 text-white'
} ` }
>
{ message . content }
< / div >
) ) }
< / div >
< form onSubmit = { handleSubmit } className = "border-t border-gray-200 px-4 py-3 dark:border-gray-700" >
< label className = "block" >
< span className = "sr-only" > Ask the explorer agent < / span >
< textarea
value = { input }
onChange = { ( event ) = > setInput ( event . target . value ) }
rows = { 3 }
placeholder = "Ask about this page, a transaction, a token, or an access-control flow."
className = "w-full rounded-xl border border-gray-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-gray-700 dark:bg-gray-950 dark:text-white"
/ >
< / label >
< div className = "mt-3 flex items-center justify-between gap-3" >
< p className = "text-xs text-gray-500 dark:text-gray-400" > Current view : { pathname } < / p >
< button
type = "submit"
disabled = { submitting || ! input . trim ( ) }
className = "rounded-xl bg-primary-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-primary-700 disabled:cursor-not-allowed disabled:opacity-60"
>
{ submitting ? 'Thinking…' : 'Send' }
< / button >
< / div >
< / form >
< / section >
) : null }
< button
type = "button"
onClick = { ( ) = > setOpen ( ( value ) = > ! value ) }
2026-04-30 03:06:49 -07:00
className = "inline-flex items-center gap-2 rounded-full bg-primary-600 p-3 text-sm font-semibold text-white shadow-lg transition hover:bg-primary-700 lg:px-4 lg:py-3"
2026-04-10 12:52:17 -07:00
aria - expanded = { open }
2026-04-30 03:06:49 -07:00
aria - label = "Open DBIS Explorer AI Assist"
2026-04-10 12:52:17 -07:00
>
< span className = "inline-flex h-7 w-7 items-center justify-center rounded-full bg-white/15" >
< svg className = "h-4 w-4" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" aria - hidden >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-4l-4 4v-4Z" / >
< / svg >
< / span >
2026-04-30 03:06:49 -07:00
< span className = "hidden lg:inline" > AI Assist < / span >
2026-04-10 12:52:17 -07:00
< / button >
< / div >
)
}