importReact,{useEffect,useRef,useState}from'react';importIconfrom'../Icon.jsx';importTemplateUploadfrom'../TemplateUpload.jsx';import{ChatSources,ChatTestDraft}from'../ChatResults.jsx';import{assertChatContext,buildChatRequest,requestWorkspaceAssistant,saveChatDraft,sourceCitation,testKinds}from'../chatAssistant.js';import{suiteExport}from'../suiteExport.js';import{saveClarification}from'../access.js';import{useStudio,contextKey,eligibleDocuments,questionsFor,timestamp,uid,bumpVersion,download,log}from'../store.jsx';import{Badge,Button,ContextBar,Empty,Field,Heading,Modal,Note,SourceDialog,UploadBox,WorkspaceRequired}from'../ui.jsx';import'../chat.css';conststarters=[{icon:'chat',title:'Understand your knowledge',text:'Ask a question about your documents.',prompt:'What are the member validation requirements?'},{icon:'spark',title:'Create test cases',text:'Turn requirements into a testable draft.',prompt:'Generate technical test cases for the selected requirements'},{icon:'file',title:'Find the source',text:'See the evidence behind an answer.',prompt:'Show sources for the selected knowledge'},];exportdefaultfunctionChat({navigate}){const{state:s,workspace:w,mutate,notify}=useStudio();const[input,setInput]=useState(''),[kind,setKind]=useState('Auto'),[templateId,setTemplateId]=useState(w?.templateId||'');const[selected,setSelected]=useState(''),[source,setSource]=useState(null),[dialog,setDialog]=useState('');const[answer,setAnswer]=useState(''),[reference,setReference]=useState(''),[answerSource,setAnswerSource]=useState('');const[historic,setHistoric]=useState(''),[busy,setBusy]=useState(false),[error,setError]=useState('');constbottom=useRef(null),composer=useRef(null),controller=useRef(null),latest=useRef(s);latest.current=s;constkey=w?contextKey(w):'',activeKey=historic||key;constthread=s.threads[activeKey],messages=thread?.messages||[];consttemplates=s.templates.filter(t=>t.team===s.session.team&&(t.visibility!=='workspace'||t.workspaceId===w?.id));constselectedTemplate=templates.find(t=>t.id===templateId)||templates.find(t=>t.id===w?.templateId)||templates[0];useEffect(()=>{controller.current?.abort();controller.current=null;setBusy(false);setError('');setHistoric('');setSelected('');setKind('Auto');setInput('');},[key]);useEffect(()=>()=>controller.current?.abort(),[]);useEffect(()=>{setTemplateId(w?.templateId||'');},[w?.id,w?.templateId]);useEffect(()=>{bottom.current?.scrollIntoView({behavior:'auto',block:'nearest'});},[messages.length,busy]);if (!w)return<WorkspaceRequirednavigate={navigate}/>;constdocs=eligibleDocuments(s,w).filter(d=>['Ready','Reviewed locally'].includes(d.status));constbaseline=s.baselines.find(b=>b.id===w.baselineId);constquestions=questionsFor(s,w),current=questions.find(q=>q.id===selected)||questions.find(q=>q.status!=='resolved')||questions[0];constsuggestions=[...messages].reverse().find(m=>m.role==='assistant')?.suggestions||[];constopenSource=value=>setSource(typeofvalue==='string'?sourceCitation(s.documents.find(d=>d.id===value)||{id:value}):value);functionprompt(text){if (historic||busy)return;setInput(text);setError('');composer.current?.focus();}functionchooseTemplate(id){setTemplateId(id);mutate(d=>{constowner=d.workspaces.find(item=>item.id===w.id);if (owner)owner.templateId=id;});}functionstop(){controller.current?.abort();controller.current=null;setBusy(false);setError('Request stopped. You can send another message.');}asyncfunctionsend(event){event.preventDefault();if (!input.trim()||historic||controller.current)return;letrequest;try{request=buildChatRequest(s,w,{prompt:input,kind,templateId:selectedTemplate?.id,messages});}catch (failure){setError(failure.message);return;}constpending=newAbortController(),snapshot=structuredClone(s);controller.current=pending;setBusy(true);setError('');setInput('');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:`${baseline?.ref||w.ref} · Context ${w.version}`,messages:[]};d.threads[key].messages.push({id:uid('message'),role:'user',content:request.prompt,at:timestamp(),actor:s.session.name});});try{constresponse=awaitrequestWorkspaceAssistant(request,{state:snapshot,signal:pending.signal});if (pending.signal.aborted||controller.current!==pending)return;assertChatContext(latest.current,request);mutate(d=>{try{assertChatContext(d,request);}catch{return;}d.threads[key].messages.push({...response,id:uid('message'),role:'assistant',actor:'Workspace assistant',at:timestamp(),sourceIds:response.citations.map(c=>c.id)});});}catch (failure){if (failure.name!=='AbortError'&&controller.current===pending){setError(failure.message);setInput(request.prompt);}}finally{if (controller.current===pending){controller.current=null;setBusy(false);}}}functionsave(message){try{saveChatDraft(structuredClone(s),w,message.id,activeKey);mutate(d=>{saveChatDraft(d,w,message.id,activeKey);log(d,`Saved ${message.draft.kind.toLowerCase()} tests from workspace chat`);});notify('Test cases saved to Test suites.');}catch (failure){setError(failure.message);}}functionexportDraft(message){try{assertChatContext(s,{context:message.requestContext});constsuite=s.suites.find(item=>item.id===message.savedSuiteId)||message.draft;constfile=suiteExport(s,w,suite);download(file.filename,file.content,file.mimeType);notify('Test cases exported using your selected template columns.');}catch (failure){setError(failure.message);}}functionsubmitAnswer(event){event.preventDefault();try{saveClarification(structuredClone(s),w,current.id,answer,answerSource,reference,docs);mutate(d=>{saveClarification(d,w,current.id,answer,answerSource,reference,docs);log(d,`Resolved ${current.requirement} with source-backed evidence`);});setDialog('');notify('Answer saved. You can refine the test cases in chat.');}catch (failure){notify(failure.message);}}return<><Headingeyebrow={`${w.ref} · ${w.name.toUpperCase()}`}title="Workspace chat"description="Ask questions. Create test cases. Follow the evidence."><divclassName="chat-heading-actions"><Buttonicon="clock"onClick={()=>setDialog('history')}>Thread history</Button><Buttonicon="checkSquare"onClick={()=>navigate('suites')}>Test suites</Button></div></Heading><ContextBarnavigate={navigate}/>{historic&&<divclassName="history-banner"><Iconname="clock"size={16}/>{thread?.label} · Read-only historical thread<ButtononClick={()=>setHistoric('')}>Return to current context</Button></div>}<divclassName="chat-grid chat-studio"><sectionclassName="card conversation"><divclassName="conversation-heading"><divclassName="assistant-label"><spanclassName="assistant-symbol"><Iconname="spark"size={19}/></span><span>Workspace assistant<small>Answers and test cases, with sources</small></span></div><Buttonicon="book"onClick={()=>setDialog('sources')}>View sources</Button></div><divclassName="chat-messages"role="log"aria-label="Workspace conversation"aria-live="polite"aria-relevant="additions">{!messages.length&&<divclassName="chat-welcome"><spanclassName="chat-welcome-symbol"><Iconname="spark"size={29}/></span><divclassName="chat-kicker">YOUR WORKSPACE, ONE CONVERSATION</div><h2>What would you like to test?</h2><p>Explore your documents, turn requirements into test cases, and see where every answer comes from.</p><divclassName="chat-starters">{starters.map(item=><buttonkey={item.title}onClick={()=>prompt(item.prompt)}><Iconname={item.icon}size={20}/><strong>{item.title}</strong><span>{item.text}</span><Iconname="arrow"size={14}/></button>)}</div></div>}{messages.map(message=>{constcitations=message.citations||(message.sourceIds||[]).map(id=>s.documents.find(d=>d.id===id)).filter(Boolean).map(d=>sourceCitation(d));constsavedSuite=s.suites.find(item=>item.id===message.savedSuiteId);return<articleclassName={`message message-${message.role}`}key={message.id}><spanclassName={`message-avatar ${message.role==='user'?'user-avatar':'assistant-avatar'}`}><Iconname={message.role==='user'?'users':'spark'}size={16}/></span><divclassName="chat-message-content"><divclassName="message-author">{message.actor}<small>{newDate(message.at).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})}</small></div><divclassName="message-bubble"><divclassName="chat-response-text">{message.content}</div>{message.draft&&<ChatTestDraftmessage={message}savedSuite={savedSuite}historical={!!historic}busy={busy}onSource={openSource}onSave={save}onOpen={id=>navigate(`suites?suite=${encodeURIComponent(id)}`)}onExport={exportDraft}onPrompt={prompt}/>}{!!citations.length&&<detailsclassName="chat-message-sources"open={message.intent==='sources'}><summary>{message.draft?'Evidence behind these cases':'Supporting sources'}</summary><ChatSourcescitations={citations}onSource={openSource}/></details>}</div></div></article>;})}{busy&&<divclassName="chat-working"role="status"><Iconname="clock"size={16}/>Working with your workspace evidence<ButtononClick={stop}>Stop</Button></div>}<divref={bottom}/></div>{!!suggestions.length&&!historic&&<divclassName="chat-followups"aria-label="Suggested follow-ups">{suggestions.map(text=><buttonkey={text}disabled={busy}onClick={()=>prompt(text)}>{text}<Iconname="arrow"size={11}/></button>)}</div>}{error&&<divclassName="chat-error"role="alert"><Iconname="info"size={16}/><span>{error}</span><buttonaria-label="Dismiss message"onClick={()=>setError('')}><Iconname="close"size={14}/></button></div>}<formclassName="chat-composer"onSubmit={send}><divclassName="chat-composer-top"><labelhtmlFor="workspace-question">Message your workspace</label><labelclassName="chat-kind-select"><span>Test type</span><selectaria-label="Test case type"value={kind}disabled={!!historic||busy}onChange={e=>setKind(e.target.value)}>{testKinds.map(type=><optionkey={type}>{type}</option>)}</select></label></div><textarearef={composer}id="workspace-question"disabled={!!historic||busy}value={input}onChange={e=>setInput(e.target.value)}placeholder={historic?'This historical conversation is read-only.':'Ask about your documents, or describe the test cases you need…'}aria-label="Message your workspace"rows={3}maxLength={12000}onKeyDown={e=>{if (e.key==='Enter'&&!e.shiftKey&&!e.nativeEvent.isComposing){e.preventDefault();send(e);}}}/><divclassName="composer-bottom"><buttontype="button"className="attach-button"disabled={!!historic||busy}onClick={()=>setDialog('attach')}><Iconname="attach"size={16}/>Attach evidence</button><span>Enter to send · Shift + Enter for a new line</span>{busy?<buttontype="button"className="chat-stop"onClick={stop}aria-label="Stop workspace request"><span/>Stop</button>:<buttontype="submit"className="send-button"disabled={!input.trim()||!!historic}aria-label="Send workspace message"><Iconname="arrowUp"size={18}/></button>}</div></form><divclassName="chat-footer"><Iconname="shield"size={11}/>Workspace {w.ref} · Context {historic?thread?.version:w.version}<span>Review generated cases against their sources.</span></div></section><asideclassName="chat-aside"><sectionclassName="card chat-template-card"><divclassName="chat-kicker">TEST-CASE FORMAT</div><h2>Your format, every time.</h2><p>Choose a template before generating, or upload the Excel format you already use.</p><labelclassName="chat-template-label"htmlFor="chat-template">Selected template</label><selectid="chat-template"aria-label="Chat test-case template"value={selectedTemplate?.id||''}disabled={!!historic||busy}onChange={e=>chooseTemplate(e.target.value)}>{templates.map(template=><optionkey={template.id}value={template.id}>{template.name} · v{template.version}</option>)}</select>{selectedTemplate&&<divclassName="chat-template-fields">{selectedTemplate.fields.slice(0,4).map(field=><spankey={field}>{field}</span>)}{selectedTemplate.fields.length>4&&<span>+{selectedTemplate.fields.length-4} columns</span>}</div>}<TemplateUploaddisabled={!!historic||busy}onSelected={setTemplateId}/></section><sectionclassName="card chat-context-card"><divclassName="section-heading"><h2>Selected knowledge</h2><Iconname="layers"size={16}/></div><div><span>System baseline</span><strong>{baseline?.ref||'Not selected'}{baseline&&` · v${baseline.version}`}</strong></div><div><span>Workspace</span><strong>{w.ref}</strong></div><div><span>Change requests</span><strong>{w.crIds.map(id=>s.crs.find(cr=>cr.id===id)?.ref).filter(Boolean).join(', ')||'None selected'}</strong></div><buttonclassName="text-button"onClick={()=>setDialog('sources')}>Browse source documents <Iconname="arrow"size={13}/></button><pclassName="chat-evidence-note"><Iconname="shield"size={15}/>Answers and drafts use the evidence selected for this workspace.</p></section><detailsclassName="card clarification-panel optional-clarifications"><summary>Optional clarifications</summary><pclassName="panel-description">Generate now. Add answers whenever you want to refine the results.</p>{questions.map(q=><buttonclassName={`clarification-option ${current?.id===q.id?'selected':''}`}onClick={()=>setSelected(q.id)}key={q.id}><span><strong>{q.title}</strong><small>{q.requirement} · {q.status}</small></span><Iconname="chevron"size={14}/></button>)}{current?<divclassName="question-detail"><Badgetone={current.status==='resolved'?'green':'amber'}>{current.status==='resolved'?'Resolved':'Optional answer'}</Badge><h3>{current.title}</h3><p>{current.why}</p><pclassName="margin-top">{current.requested}</p><buttonclassName="text-button margin-top"onClick={()=>openSource(current.sourceId)}>Open cited source <Iconname="external"size={12}/></button>{current.answer&&<divclassName="answer-preview"><small>USER-PROVIDED EVIDENCE</small><p>{current.answer}</p><span>{current.sourceReference}</span></div>}<divclassName="stack-actions"><Buttondisabled={!!historic||busy}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>:<pclassName="panel-description">No open questions for this context.</p>}</details></aside></div>{dialog==='sources'&&<Modaltitle="Workspace sources"close={()=>setDialog('')}wide><pclassName="modal-intro">Open a document to inspect its selected revision and evidence.</p>{docs.length?<ChatSourcescitations={docs.map(doc=>sourceCitation(doc))}onSource={value=>{setDialog('');openSource(value);}}/>:<Emptytitle="Add your first source"description="Choose a baseline and reviewed source documents for this workspace."/>}</Modal>}{dialog==='answer'&¤t&&<Modaltitle="Answer a clarification"close={()=>setDialog('')}><pclassName="modal-intro">{current.title}</p><formonSubmit={submitAnswer}><Fieldlabel="Behavior / answer"><textarearequiredrows={5}value={answer}onChange={e=>setAnswer(e.target.value)}maxLength={5000}/></Field><Fieldlabel="Supporting evidence source"><selectrequiredvalue={answerSource}onChange={e=>setAnswerSource(e.target.value)}><optionvalue="">Choose reviewed evidence</option>{docs.map(d=><optionkey={d.id}value={d.id}>{d.name}</option>)}</select></Field><Fieldlabel="Source revision and location"><inputrequiredvalue={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 its revision and location.</Note><divclassName="modal-actions"><ButtononClick={()=>setDialog('')}>Cancel</Button><Buttontype="submit"variant="primary">Save answer and resolve</Button></div></form></Modal>}{dialog==='history'&&<Modaltitle="Conversation history"close={()=>setDialog('')}><pclassName="modal-intro">Conversations keep the knowledge context they were created with.</p>{Object.entries(s.threads).filter(([,t])=>t.workspaceId===w.id&&t.team===s.session.team).map(([id,t])=><buttonclassName="history-row"key={id}onClick={()=>{controller.current?.abort();controller.current=null;setBusy(false);setError('');setHistoric(id===key?'':id);setDialog('');}}><Iconname="chat"size={18}/><span><strong>{t.label}</strong><small>{id===key?'Current context':'Read-only history'}</small></span><Iconname="chevron"size={14}/></button>)}{!Object.values(s.threads).some(t=>t.workspaceId===w.id&&t.team===s.session.team)&&<Emptytitle="No saved conversations"description="Send a message to start a conversation."/>}</Modal>}{dialog==='attach'&&<Modaltitle="Attach supporting evidence"close={()=>setDialog('')}><Note>Add a document, then review and link its requirements in CR evidence before generating test cases from it.</Note><divclassName="margin-top"><UploadBoxscope={{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 chat evidence to ${w.ref}`);});setDialog('');notify('Evidence added. Review its routing in CR evidence.');}}/></div></Modal>}{source&&<SourceDialogsourceId={source.id}citation={source}close={()=>setSource(null)}/>}</>;}
Top comments (0)
Subscribe
We're a place where coders share, stay up-to-date and grow their careers.
Top comments (0)