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">{questions.filter(q => q.status !== 'resolved').length}</span></div><p className="panel-description">Save an answer with its supporting source to resolve a question. No separate approval is needed.</p>{questions.map((q, i) => <button className={`clarification-option ${current?.id === q.id ? 'selected' : ''}`} onClick={() => setSelected(q.id)} key={q.id}><span className="question-number">{String(i + 1).padStart(2, '0')}</span><span><strong>{q.title}</strong><small>{q.requirement} · {q.status}</small></span><Icon name="chevron" size={14}/></button>)}{current ? <div className="question-detail"><Badge tone={current.status === 'resolved' ? 'green' : current.status === 'submitted' ? 'blue' : 'amber'}>{current.status === 'resolved' ? 'Resolved' : 'Missing evidence'}</Badge><h3>{current.title}</h3><p>{current.why}</p><p className="margin-top">{current.requested}</p><button className="text-button margin-top" onClick={() => setSource(current.sourceId)}>Open cited source <Icon name="external" size={12}/></button>{current.answer && <div className="answer-preview"><small>USER-PROVIDED EVIDENCE</small><p>{current.answer}</p><span>{current.sourceReference}</span></div>}<div className="stack-actions"><Button onClick={() => { setAnswer(current.answer || ''); setReference(current.sourceReference || ''); setAnswerSource(current.answerSourceId || (docs.some(d => d.id === current.sourceId) ? current.sourceId : '')); setDialog('answer'); }}>{current.answer ? 'Edit answer' : 'Answer clarification'}</Button></div></div> : <Empty title="No open questions" description="Casual Q&A does not block generation."/>}<div className="panel-note"><Icon name="shield" size={16}/><p>Answers stay in this workspace. Updating a System Baseline requires a separate approval.</p></div></section><section className="card chat-context-card"><div className="section-heading"><h2>In this context</h2><Icon name="layers" size={16}/></div><div><span>Selected Baseline</span><strong>{s.baselines.find(b => b.id === w.baselineId)?.ref}</strong></div><div><span>Accessible sources</span><strong>{docs.length}</strong></div><div><span>Knowledge context</span><strong>{w.version}</strong></div><button className="text-button" onClick={() => navigate('readiness')}>Review generation readiness <Icon name="arrow" size={13}/></button></section></aside></div>{dialog === 'answer' && current && <Modal title="Answer a clarification" close={() => setDialog('')}><p className="modal-intro">{current.title}</p><form onSubmit={submitAnswer}><Field label="Behavior / answer"><textarea required rows={5} value={answer} onChange={e => setAnswer(e.target.value)} maxLength={5000}/></Field><Field label="Supporting evidence source"><select required value={answerSource} onChange={e => setAnswerSource(e.target.value)}><option value="">Choose reviewed evidence</option>{docs.map(d => <option key={d.id} value={d.id}>{d.name}</option>)}</select></Field><Field label="Source revision and location"><input required value={reference} onChange={e => setReference(e.target.value)} placeholder="Document, revision, page / section / procedure" maxLength={300}/></Field><Note>Save the answer with an accessible source and a revision / location. This resolves the question immediately for your team.</Note><div className="modal-actions"><Button onClick={() => setDialog('')}>Cancel</Button><Button type="submit" variant="primary">Save answer and resolve</Button></div></form></Modal>}{dialog === 'history' && <Modal title="Version-specific threads" close={() => setDialog('')}><p className="modal-intro">Historic threads retain their selected baseline and CR context.</p>{Object.entries(s.threads).filter(([, t]) => t.workspaceId === w.id && t.team === s.session.team).map(([id, t]) => <button className="history-row" key={id} onClick={() => { setHistoric(id === key ? '' : id); setDialog(''); }}><Icon name="chat" size={18}/><span><strong>{t.label}</strong><small>{t.messages.length} messages · {id === key ? 'Current' : 'Read-only history'}</small></span><Icon name="chevron" size={14}/></button>)}{!Object.values(s.threads).some(t => t.workspaceId === w.id) && <Empty title="No saved conversations" description="Send a message to start this context’s persistent local thread."/>}</Modal>}{dialog === 'attach' && <Modal title="Attach supporting evidence" close={() => setDialog('')}><Note>Attachments go through local file checks and review. Adding evidence creates a new knowledge context and resets clarification readiness.</Note><div className="margin-top"><UploadBox scope={{ team: w.team, workspaceId: w.id, category: 0 }} onStaged={files => { mutate(d => { d.documents.push(...files); bumpVersion(d, d.workspaces.find(item => item.id === w.id)); log(d, `Attached clarification evidence to ${w.ref}`); }); setDialog(''); notify('Evidence staged locally. Review its routing in CR evidence.'); }}/></div></Modal>}{source && <SourceDialog sourceId={source} close={() => setSource('')}/>}</>;
}
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)