import React, { useEffect, useRef, useState } from 'react'; import Icon from '../Icon.jsx'; import { saveClarification } from '../access.js'; import { useStudio, contextKey, eligibleDocuments, questionsFor, timestamp, uid, bumpVersion, log } from '../store.jsx'; import { Badge, Button, ContextBar, Empty, Field, Heading, Modal, Note, SourceDialog, UploadBox, WorkspaceRequired } from '../ui.jsx'; export default function Chat({ navigate }) { const { state: s, workspace: w, mutate, notify } = useStudio(); const [input, setInput] = useState(''), [selected, setSelected] = useState(''), [source, setSource] = useState(''), [dialog, setDialog] = useState(''), [answer, setAnswer] = useState(''), [reference, setReference] = useState(''), [answerSource, setAnswerSource] = useState(''), [historic, setHistoric] = useState(''); const bottom = useRef(null); const key = w ? contextKey(w) : ''; const activeKey = historic || key; const thread = s.threads[activeKey]; const messages = thread?.messages || []; useEffect(() => { setHistoric(''); setSelected(''); }, [key]); useEffect(() => { bottom.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }, [messages.length]); if (!w) return <WorkspaceRequired navigate={navigate}/>; const questions = questionsFor(s, w), current = questions.find(q => q.id === selected) || questions.find(q => q.status !== 'resolved') || questions[0]; const docs = eligibleDocuments(s, w).filter(d => ['Ready', 'Reviewed locally'].includes(d.status)); function send(e) { e.preventDefault(); if (!input.trim() || historic) return; const words = input.toLowerCase().match(/[a-z]{4,}/g)?.filter(word => !['what', 'does', 'which', 'this', 'that', 'have', 'with', 'from', 'please'].includes(word)) || []; const hits = docs.map(d => ({ doc: d, score: words.filter(word => `${d.name} ${d.excerpt}`.toLowerCase().includes(word)).length })).filter(hit => hit.score > 0 && hit.doc.excerpt).sort((a, b) => b.score - a.score).slice(0, 2); mutate(d => { if (!d.threads[key]) d.threads[key] = { workspaceId: w.id, version: w.version, team: w.team, baselineId: w.baselineId, crIds: w.crIds, label: `${s.baselines.find(b => b.id === w.baselineId)?.ref} + ${w.crIds.map(id => s.crs.find(c => c.id === id)?.ref).join(' + ')} · Context ${w.version}`, messages: [] }; d.threads[key].messages.push({ id: uid('message'), role: 'user', content: input.trim(), at: timestamp(), actor: s.session.name }, { id: uid('message'), role: 'assistant', actor: 'Workspace assistant', at: timestamp(), content: hits.length ? hits.map((hit, i) => `[${i + 1}] ${hit.doc.excerpt.slice(0, 1600)}`).join('\n\n') : 'No matching text was found in this workspace’s locally readable evidence. Attach a source or ask a more specific question. ', sourceIds: hits.map(h => h.doc.id) }); }); setInput(''); } function submitAnswer(event) { event.preventDefault(); try { mutate(d => { saveClarification(d, w, current.id, answer, answerSource, reference, docs); log(d, `Resolved ${current.requirement} with source-backed team evidence`); }); setDialog(''); notify('Answer saved. Clarification resolved without a separate approval.'); } catch (error) { notify(error.message); } } return <><Heading eyebrow={`${w.ref} · ${w.name.toUpperCase()}`} title="Workspace chat" description="Ask with context. Resolve with evidence."><Button icon="clock" onClick={() => setDialog('history')}>Thread history</Button></Heading><ContextBar navigate={navigate}/>{historic && <div className="history-banner"><Icon name="clock" size={16}/>{thread?.label} · Read-only historical thread<Button onClick={() => setHistoric('')}>Return to current context</Button></div>}<div className="chat-grid"><section className="card conversation"><div className="conversation-heading"><div className="assistant-label"><span className="assistant-symbol"><Icon name="layers" size={17}/></span><span>Workspace assistant<small>Selected workspace knowledge</small></span></div></div><div className="chat-messages">{!messages.length && <><div className="chat-welcome"><span className="soft-icon soft-purple"><Icon name="chat" size={24}/></span><h3>Your evidence, in context.</h3><p>Ask about the selected requirements, source documents, and change requests.</p><div className="suggestion-list">{['What does the member identifier requirement say?', 'Find evidence for timeout behavior.', 'What changes are selected?'].map(text => <button key={text} onClick={() => setInput(text)}>{text}<Icon name="arrow" size={13}/></button>)}</div></div>{current && current.status !== 'resolved' && <div className="clarification-message"><div className="clarification-message-heading"><span className="attention-icon soft-amber"><Icon name="chat" size={17}/></span><div><strong>Clarification needs your input</strong><small>{current.requirement}</small></div><Badge tone="amber">Blocking</Badge></div><p>{current.title}</p><div className="clarification-note"><Icon name="info" size={14}/>{current.why}</div><button className="text-button" onClick={() => { setAnswer(current.answer || ''); setReference(current.sourceReference || ''); setAnswerSource(current.answerSourceId || (docs.some(d => d.id === current.sourceId) ? current.sourceId : '')); setDialog('answer'); }}>Provide an answer <Icon name="arrow" size={14}/></button></div>}</>}{messages.map(m => <div className={`message message-${m.role}`} key={m.id}><span className={`message-avatar ${m.role === 'user' ? 'user-avatar' : 'assistant-avatar'}`}><Icon name={m.role === 'user' ? 'users' : 'layers'} size={17}/></span><div><div className="message-author">{m.actor}<small>{new Date(m.at).toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' })}</small></div><div className="message-bubble">{m.content}{m.sourceIds?.length > 0 && <div className="citation-buttons">{m.sourceIds.map((id, i) => <button key={id} onClick={() => setSource(id)}><Icon name="file" size={13}/>{i + 1}. {s.documents.find(d => d.id === id)?.name}<Icon name="external" size={11}/></button>)}</div>}</div></div></div>)}<div ref={bottom}/></div><form className="chat-composer" onSubmit={send}><textarea disabled={!!historic} value={input} onChange={e => setInput(e.target.value)} placeholder={historic ? 'This historic context is read-only.' : 'Ask about the selected rules, requirements, or dependencies…'} aria-label="Workspace message" rows={2} onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(e); } }}/><div className="composer-bottom"><button type="button" className="attach-button" disabled={!!historic} onClick={() => setDialog('attach')}><Icon name="attach" size={16}/>Attach evidence</button><span>Enter to send · Shift + Enter for a new line</span><button type="submit" className="send-button" disabled={!input.trim() || !!historic} aria-label="Send workspace message"><Icon name="arrowUp" size={18}/></button></div></form><div className="chat-footer"><Icon name="lock" size={11}/>Workspace {w.ref} · Context {historic ? thread?.version : w.version}</div></section><aside className="chat-aside"><section className="card clarification-panel"><div className="section-heading"><h2>Clarifications</h2><span className="count-pill count-amber">...
Source:https://dev.to/iamry02/ccil-test-studio-source-19-srcpageschatjsx-3ff8