Files
explorer-monorepo/frontend/src/components/common/ExplorerAgentTool.tsx
defiQUG 8cd8bfa195
All checks were successful
phoenix-deploy Deployed to explorer-live
Deploy Explorer Live / deploy (push) Successful in 2m18s
Unify explorer DBIS taxonomy and branding
2026-04-30 03:06:49 -07:00

180 lines
6.9 KiB
TypeScript

'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:
'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.',
},
])
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>
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">DBIS Explorer AI Assist</h2>
<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)}
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"
aria-expanded={open}
aria-label="Open DBIS Explorer AI Assist"
>
<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>
<span className="hidden lg:inline">AI Assist</span>
</button>
</div>
)
}