All checks were successful
phoenix-deploy Deployed to explorer-live
Deploy Explorer Live / deploy (push) Successful in 2m18s
180 lines
6.9 KiB
TypeScript
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>
|
|
)
|
|
}
|