refactor: rename SolaceScanScout to Solace and update related configurations
- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation. - Changed default base URL for Playwright tests and updated security headers to reflect the new branding. - Enhanced README and API documentation to include new authentication endpoints and product access details. This refactor aligns the project branding and improves clarity in the API documentation.
This commit is contained in:
178
frontend/src/components/common/ExplorerAgentTool.tsx
Normal file
178
frontend/src/components/common/ExplorerAgentTool.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
'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:
|
||||
'Explorer AI Agent Tool 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">Explorer AI Agent Tool</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 px-4 py-3 text-sm font-semibold text-white shadow-lg transition hover:bg-primary-700"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<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>
|
||||
Agent Tool
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user