contract-extractor / frontend /src /components /ContractChat.jsx
myke69's picture
Add files using upload-large-folder tool
3752e5d verified
Raw
History Blame Contribute Delete
3.83 kB
import React, { useEffect, useRef, useState } from 'react'
import { api } from '../api.js'
const SUGGESTIONS = [
"What's the termination notice period?",
"Who bears liability for data breaches?",
"When does this contract expire?",
"What are the payment terms?",
"Is there an auto-renewal clause?",
]
export default function ContractChat({ contractId, onSelectClause }) {
const [messages, setMessages] = useState([
{
role: 'bot',
text: "Ask me anything about this contract — I'll search the clauses and cite my sources.",
citations: [],
},
])
const [input, setInput] = useState('')
const [loading, setLoading] = useState(false)
const bottomRef = useRef(null)
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages, loading])
async function send(question) {
const q = (question || input).trim()
if (!q || loading) return
setInput('')
setMessages((m) => [...m, { role: 'user', text: q, citations: [] }])
setLoading(true)
try {
const res = await api(`/api/contracts/${contractId}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question: q }),
})
const data = await res.json()
setMessages((m) => [
...m,
{ role: 'bot', text: data.answer || 'No answer returned.', citations: data.citations || [] },
])
} catch {
setMessages((m) => [
...m,
{ role: 'bot', text: 'Error contacting the server. Please try again.', citations: [] },
])
} finally {
setLoading(false)
}
}
const showSuggestions = messages.length === 1
return (
<div className="chat-wrap">
<div className="chat-messages">
{messages.map((m, i) => (
<div key={i} className={`chat-msg ${m.role}`}>
<div className="chat-bubble">
{m.text.split('\n\n').map((para, j) => (
<p key={j} className="chat-para">{para}</p>
))}
</div>
{m.citations.length > 0 && (
<div className="chat-citations">
<span className="cite-label">Sources:</span>
{m.citations.map((c, j) => (
<button
key={j}
className="citation-chip"
title={c.snippet}
onClick={() => onSelectClause(c.clause_id)}
>
§ {c.heading}
</button>
))}
</div>
)}
</div>
))}
{showSuggestions && (
<div className="chat-suggestions">
{SUGGESTIONS.map((s, i) => (
<button key={i} className="suggestion-chip" onClick={() => send(s)}>
{s}
</button>
))}
</div>
)}
{loading && (
<div className="chat-msg bot">
<div className="chat-bubble chat-thinking">
<span className="dot-pulse" />
</div>
</div>
)}
<div ref={bottomRef} />
</div>
<div className="chat-input-row">
<input
className="chat-input"
placeholder="Ask about termination, liability, payment…"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && send()}
disabled={loading}
/>
<button
className="chat-send"
onClick={() => send()}
disabled={loading || !input.trim()}
>
</button>
</div>
</div>
)
}