@atmem/architect 0.2.0-alpha.4.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +21 -21
- package/cli/runtime.mjs +3 -1
- package/dist/assets/{index-qtCM2D7O.js → index-D4CZ_n8f.js} +19166 -19094
- package/dist/assets/index-DuveVTIx.css +1 -0
- package/dist/assets/{index.es-C6C0sneB.js → index.es-Fcqg-orY.js} +1 -1
- package/dist/assets/{visual-export-DjbjgQbA.js → visual-export-4PDDTAP7.js} +3 -3
- package/dist/index.html +2 -2
- package/dist/licenses/atmem-APACHE-2.0.txt +202 -0
- package/dist/licenses/memory-dependency-notices.txt +149 -2241
- package/package.json +3 -3
- package/public/licenses/atmem-APACHE-2.0.txt +202 -0
- package/public/licenses/memory-dependency-notices.txt +149 -2241
- package/server/app.py +1 -1
- package/server/codex_app_server.py +5 -5
- package/server/codex_worker_policy.py +27 -4
- package/server/engineering_artifacts.py +7 -1
- package/server/engineering_handoff.py +9 -0
- package/server/engineering_routes.py +16 -2
- package/server/local_providers.py +7 -3
- package/server/memory-requirements.lock +6 -6
- package/server/memory-requirements.txt +2 -2
- package/server/memory_context.py +58 -0
- package/server/memory_routes.py +67 -8
- package/server/memory_service.py +40 -5
- package/server/memory_worker.py +76 -11
- package/server/orca_handoff.py +90 -0
- package/server/pricing.py +161 -1
- package/server/prompts.py +13 -0
- package/server/request_uploads.py +65 -0
- package/server/terraform_routes.py +47 -1
- package/server/windows_snapshot.py +119 -0
- package/src/ArchitectureWorkPanel.jsx +0 -7
- package/src/ArtifactChecks.jsx +2 -2
- package/src/ChatMessage.jsx +12 -2
- package/src/EngagementViews.jsx +18 -16
- package/src/EngineeringPanel.jsx +35 -28
- package/src/FileTree.jsx +7 -0
- package/src/FinalChecklist.jsx +23 -0
- package/src/HelpNote.jsx +3 -0
- package/src/ImplementationDelivery.jsx +56 -17
- package/src/JourneyViews.jsx +20 -17
- package/src/LocationChoices.jsx +12 -0
- package/src/MemoryPanel.jsx +43 -31
- package/src/OpenAIVoicePanel.jsx +15 -14
- package/src/OutputsView.jsx +28 -16
- package/src/ProjectChecks.jsx +58 -0
- package/src/StarPrompt.jsx +26 -0
- package/src/WorkbenchDialogs.jsx +1 -1
- package/src/conversation.mjs +1 -0
- package/src/delivery.mjs +1 -1
- package/src/engineering.css +10 -0
- package/src/estimate.mjs +5 -3
- package/src/final-package.mjs +34 -0
- package/src/journey-workspace.css +39 -0
- package/src/journey.mjs +12 -2
- package/src/large-request.mjs +25 -0
- package/src/main.jsx +129 -98
- package/src/output-bundle.mjs +18 -4
- package/src/pricing.mjs +59 -3
- package/src/project-file.mjs +1 -1
- package/src/useChatScroll.jsx +5 -4
- package/src/useEngineeringStatus.mjs +17 -0
- package/src/validation-receipt.mjs +1 -1
- package/dist/assets/index-B97MkVMs.css +0 -1
package/src/JourneyViews.jsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Review, Estimate and Deploy steps of the AtMem AI Architect journey.
|
|
2
2
|
import React from "react";
|
|
3
|
+
import {Check} from "lucide-react";
|
|
3
4
|
import { ATTRIBUTES, acceptRisk, blockers } from "./review.mjs";
|
|
4
5
|
import { DISCLAIMER, INPUTS, PROJECT_SIZES, setProjectSize, setPrice, setUsage } from "./estimate.mjs";
|
|
5
6
|
import { deploymentSteps } from "./document.mjs";
|
|
@@ -30,7 +31,8 @@ function Accept({ onAccept }) {
|
|
|
30
31
|
);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
export function ReviewView({ project, findings, onChange, onOpen, onAddRequirement }) {
|
|
34
|
+
export function ReviewView({ project, findings, onChange, onOpen, onAddRequirement,expanded=false }) {
|
|
35
|
+
const ChoicesContainer=expanded?'section':'details',ChoicesHeading=expanded?'h3':'summary';
|
|
34
36
|
const names = Object.fromEntries(project.nodes.map((n) => [n.id, n.data.label]));
|
|
35
37
|
const block = blockers(findings);
|
|
36
38
|
const open = findings.filter((f) => f.status === "open");
|
|
@@ -46,7 +48,7 @@ export function ReviewView({ project, findings, onChange, onOpen, onAddRequireme
|
|
|
46
48
|
: "Nothing open. Rules run on every change."}
|
|
47
49
|
</p>
|
|
48
50
|
</header>
|
|
49
|
-
<
|
|
51
|
+
<ChoicesContainer className="extra"><ChoicesHeading>Agreed choices and consequences</ChoicesHeading>{(project.decisions||[]).filter(d=>d.status==='Accepted').map(d=><article key={d.id||d.subject}><strong>{d.title||d.subject}</strong><p>{d.decision}</p>{d.context&&<p>{d.context}</p>}{d.consequences&&<p>{d.consequences}</p>}</article>)}</ChoicesContainer>
|
|
50
52
|
{ATTRIBUTES.map(([id, label]) => {
|
|
51
53
|
const fs = findings.filter((f) => f.attribute === id);
|
|
52
54
|
if (!fs.length) return null;
|
|
@@ -90,24 +92,24 @@ export function ReviewView({ project, findings, onChange, onOpen, onAddRequireme
|
|
|
90
92
|
);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
export function EstimateView({ project, result, onChange, onEstimate, busy=false }) {
|
|
95
|
+
export function EstimateView({ project, result, onChange, onEstimate, busy=false, pricing, onNext, onChooseRegion }) {
|
|
94
96
|
const e = result;
|
|
95
97
|
return (
|
|
96
98
|
<section className="doc" aria-label="Estimate">
|
|
97
99
|
<header className="doc-head">
|
|
98
|
-
<h2>
|
|
99
|
-
<p className="muted">
|
|
100
|
+
<div className="cost-heading"><h2>1. Cost estimate</h2>{onEstimate&&<button type="button" className="btn" disabled={!!pricing?.busy} onClick={()=>onEstimate(project)}>{pricing?.busy?'Calculating…':'Recalculate'}</button>}</div>
|
|
101
|
+
<p className="muted">Monthly planning estimate. {DISCLAIMER}</p>
|
|
100
102
|
</header>
|
|
101
|
-
<fieldset
|
|
102
|
-
<div className="row">{PROJECT_SIZES.map(size=><button type="button" className="btn" key={size.id} aria-pressed={project.estimate?.projectSize===size.id} onClick={()=>{const next=setProjectSize(project,size.id);onChange(next);onEstimate?.(next);}}>{size.label}</button>)}</div>
|
|
103
|
-
<p className="muted small">Small: 10,000 requests / 10 GB; Medium: 100,000 / 100 GB; Large: 1 million / 1 TB; Extra Large: 10 million / 10 TB per month. Existing usage figures take priority. Model assumptions: 3,000 tokens per request and 730 endpoint hours; training usage needs separate agreement.</p>
|
|
103
|
+
<fieldset className="cost-size-selector"><legend>How large do you expect this project to be?</legend>
|
|
104
|
+
<div className="row">{PROJECT_SIZES.map(size=><button type="button" className={project.estimate?.projectSize===size.id?"btn primary":"btn"} key={size.id} aria-pressed={project.estimate?.projectSize===size.id} onClick={()=>{if(project.estimate?.projectSize===size.id)return;const next=setProjectSize(project,size.id);onChange(next);onEstimate?.(next);}}>{project.estimate?.projectSize===size.id&&<Check size={16} aria-hidden="true"/>}{size.label}</button>)}</div>
|
|
105
|
+
<details className="cost-assumptions"><summary>Planning assumptions</summary><p className="muted small">Small: 10,000 requests / 10 GB; Medium: 100,000 / 100 GB; Large: 1 million / 1 TB; Extra Large: 10 million / 10 TB per month. Existing usage figures take priority. Model assumptions: 3,000 tokens per request and 730 endpoint hours; training usage needs separate agreement.</p></details>
|
|
104
106
|
</fieldset>
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
+
{project.estimate?.projectSize&&<p className="cost-selection-status" role="status">{PROJECT_SIZES.find(size=>size.id===project.estimate.projectSize)?.label} · {pricing?.busy?'Updating prices…':e.expected!==null?'Estimate ready':'Prices unavailable'}</p>}{pricing?.errors?.map((error,i)=><p className={e.expected!==null?"muted small":"warn-text"} key={i}>{error.component&&`${error.component}: `}{error.reason} <button className="link" disabled={busy||pricing?.busy} onClick={()=>error.action==='choose-region'?onChooseRegion?.():onEstimate?.(project)}>{error.action==='choose-region'?'Choose region':'Retry pricing'}</button></p>)}
|
|
108
|
+
|
|
107
109
|
<div className="totals">
|
|
108
110
|
<div>
|
|
109
111
|
<small className="muted">Estimated subtotal / month</small>
|
|
110
|
-
<strong>{e.lines.some((l) => l.cost != null) ? money(e.expected, e.currency) : "—"}</strong>
|
|
112
|
+
<strong>{e.lines.some((l) => l.cost != null) ? e.expected===0&&e.preciseExpected>0?`< ${money(0.01,e.currency)}`:money(e.expected, e.currency) : "—"}</strong>
|
|
111
113
|
</div>
|
|
112
114
|
<div>
|
|
113
115
|
<small className="muted">Range</small>
|
|
@@ -117,8 +119,8 @@ export function EstimateView({ project, result, onChange, onEstimate, busy=false
|
|
|
117
119
|
</div>
|
|
118
120
|
<div>
|
|
119
121
|
<small className="muted">Status</small>
|
|
120
|
-
<span className={e.
|
|
121
|
-
{e.
|
|
122
|
+
<span className={e.mixedCurrencies ? "warn-text" : e.expected!==null ? "" : "muted"}>
|
|
123
|
+
{e.mixedCurrencies ? "Choose one currency" : e.expected!==null ? "Planning estimate" : "Not estimated"}
|
|
122
124
|
</span>
|
|
123
125
|
</div>
|
|
124
126
|
{e.budget && (
|
|
@@ -131,14 +133,15 @@ export function EstimateView({ project, result, onChange, onEstimate, busy=false
|
|
|
131
133
|
</div>
|
|
132
134
|
)}
|
|
133
135
|
</div>
|
|
134
|
-
<p className="muted small">
|
|
136
|
+
<p className="muted small">{e.lines.some(line=>line.price?.planningReference)?'US reference rates · cloud region undecided. Unpriced charges excluded.':'Unpriced charges are excluded.'}</p>
|
|
135
137
|
{e.drivers.length > 0 && (
|
|
136
138
|
<p className="small">
|
|
137
139
|
Biggest drivers: {e.drivers.map((d) => `${d.component} (${money(d.cost, d.price.currency)})`).join(", ")}
|
|
138
140
|
</p>
|
|
139
141
|
)}
|
|
140
|
-
{e.lines.some(l=>l.cost==null)&&<p role="status">{e.lines.filter(l=>l.cost==null).length} charges
|
|
141
|
-
|
|
142
|
+
{e.lines.some(l=>l.cost==null)&&<p role="status">{e.lines.filter(l=>l.cost==null).length} charges unpriced.</p>}
|
|
143
|
+
{onNext&&<button type="button" className="btn" onClick={onNext}>Project files ↓</button>}
|
|
144
|
+
<details className="extra"><summary>Cost breakdown and price sources</summary><div className="resource-schedule" aria-label="Estimate breakdown">{e.lines.map(l=><article key={l.id}><strong>{l.component} · {l.driver}</strong><span>{l.cost==null?'Not yet estimated':money(l.cost,l.price.currency)+'/month'}</span><small>{l.note||l.price?.note}</small><small>{l.quantity??'Usage unresolved'} {l.unit}{l.price&&<> · {l.price.source}{l.price.url&&<> · <a href={l.price.url} target="_blank" rel="noreferrer">price source</a></>}</>}</small></article>)}</div></details>
|
|
142
145
|
<details className="extra"><summary>Adjust usage assumptions and prices</summary>
|
|
143
146
|
{e.lines.some((l) => !l.detailed) && <>
|
|
144
147
|
<div className="doc-sub">
|
|
@@ -394,7 +397,7 @@ export function ProjectsDialog({ list, currentId, onOpen, onNew, onBin, onRestor
|
|
|
394
397
|
</ul>
|
|
395
398
|
</details>
|
|
396
399
|
)}
|
|
397
|
-
<p className="modal-fine">
|
|
400
|
+
<p className="modal-fine">{folder || "~/.atforma"}</p>
|
|
398
401
|
</div>
|
|
399
402
|
);
|
|
400
403
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React,{useState,useEffect}from'react';
|
|
2
|
+
import{newDecision}from'./engagement.mjs';import{reviseDecision}from'./records.mjs';
|
|
3
|
+
const latest=(project,subject)=>[...(project.decisions||[])].reverse().find(d=>d.subject===subject&&d.status==='Accepted');
|
|
4
|
+
export function LocationChoices({project,onChange,onDiscuss}){
|
|
5
|
+
const cloud=latest(project,'platform.cloud')?.decision,country=latest(project,'data.country')?.decision,current=latest(project,'cloud.region');
|
|
6
|
+
const [region,setRegion]=useState(current?.decision||''),[error,setError]=useState('');
|
|
7
|
+
useEffect(()=>setRegion(current?.decision||''),[project.id,current?.decision]);
|
|
8
|
+
function apply(event){event.preventDefault();const value=region.trim();if(!cloud||!value)return;if(!/^[a-z][a-z0-9-]{2,39}$/.test(value)){setError('Enter a region code.');return;}if(cloud==='gcp'&&/^(usa|us|united states)$/i.test(country||'')&&!value.startsWith('us-')){setError('Choose a US region for USA residency.');return;}
|
|
9
|
+
const fields={title:'Cloud region',decision:value,status:'Accepted',owner:'user',reason:'Selected in Choices.'};let next=current?reviseDecision(project,current.id,fields,fields.reason):{...project,decisions:[...(project.decisions||[]),newDecision(project,{...fields,subject:'cloud.region'})]};
|
|
10
|
+
next={...next,nodes:next.nodes.map(node=>{const previous=node.data.config?.region;return node.data.provider===cloud&&node.data.origin!=='existing'&&(!previous||/select|unknown|tbd/i.test(previous)||previous===current?.decision)?{...node,data:{...node.data,config:{...node.data.config,region:value}}}:node;})};onChange(next);setError('');}
|
|
11
|
+
return <section className="doc location-choices" aria-label="Country and region"><header className="doc-head"><h2>Country & region</h2></header><p><strong>Data country:</strong> {country||'Not selected'}</p><form className="location-region-form" onSubmit={apply}><label>Cloud region<input aria-label="Cloud region" value={region} maxLength={40} placeholder={cloud==='gcp'?'e.g. us-central1':'Region code'} onChange={event=>setRegion(event.target.value)}/></label><button className="btn primary" disabled={!cloud||!region.trim()||region.trim()===current?.decision}>Apply</button></form>{current&&<p className="ok-text" role="status">✓ {current.decision}</p>}{error&&<p role="alert">{error}</p>}<button className="link" onClick={onDiscuss}>Discuss location</button></section>;
|
|
12
|
+
}
|
package/src/MemoryPanel.jsx
CHANGED
|
@@ -12,35 +12,47 @@ export function ContextUsed({ value }) {
|
|
|
12
12
|
</details>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function MemoryPanel({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
</
|
|
15
|
+
export function MemoryPanel({session,project,onError,onStatus}){
|
|
16
|
+
const [report,setReport]=useState(null),[tab,setTab]=useState('project'),[error,setError]=useState(''),[working,setWorking]=useState(false),[loading,setLoading]=useState(true);
|
|
17
|
+
const [query,setQuery]=useState(''),[search,setSearch]=useState(''),[status,setStatus]=useState('active'),[cursor,setCursor]=useState(null),[history,setHistory]=useState([]),[page,setPage]=useState(null),[revision,setRevision]=useState(0);
|
|
18
|
+
const [selected,setSelected]=useState(null),[preview,setPreview]=useState(null),[field,setField]=useState('preferences'),[value,setValue]=useState(''),[profileName,setProfileName]=useState('Default profile');
|
|
19
|
+
const scope=tab==='profile'?'user':'project',payload={projectId:project.id},date=value=>value?new Date(value).toLocaleString():'Unknown',short=value=>value?value.slice(0,8):'Unknown';
|
|
20
|
+
async function refresh(){
|
|
21
|
+
setWorking(true);setError('');try{
|
|
22
|
+
await memoryCall(session,'sync',{...payload,file:toProjectFile(project)});
|
|
23
|
+
const result=await memoryCall(session,'overview',payload);setReport(result);setProfileName(result.userProfile?.name||'Default profile');
|
|
24
|
+
onStatus?.({enabled:result.enabled,status:result.status,pending:result.pending,rejected:result.rejected,engine:result.engine});
|
|
25
|
+
setRevision(value=>value+1);
|
|
26
|
+
}catch(error){setError(error.message);}finally{setWorking(false);}
|
|
27
|
+
}
|
|
28
|
+
useEffect(()=>{void refresh();},[project.id,session?.token]);
|
|
29
|
+
useEffect(()=>{const timer=setTimeout(()=>{setSearch(query);setCursor(null);setHistory([]);},250);return()=>clearTimeout(timer);},[query]);
|
|
30
|
+
useEffect(()=>{
|
|
31
|
+
if(!['profile','project'].includes(tab))return;
|
|
32
|
+
const controller=new AbortController();setLoading(true);setError('');
|
|
33
|
+
memoryCall(session,'records',{...payload,scope,status,query:search,cursor,limit:50},controller.signal).then(value=>{if(!controller.signal.aborted)setPage(value);}).catch(error=>{if(!controller.signal.aborted)setError(error.message);}).finally(()=>{if(!controller.signal.aborted)setLoading(false);});
|
|
34
|
+
return()=>controller.abort();
|
|
35
|
+
},[project.id,session?.token,scope,tab,status,search,cursor,revision]);
|
|
36
|
+
async function act(operation){setWorking(true);setError('');try{await operation();setCursor(null);setHistory([]);await refresh();}catch(error){setError(error.message);onError?.(error.message);}finally{setWorking(false);}}
|
|
37
|
+
async function openRecord(record){setSelected({record,audit:[]});try{setSelected(await memoryCall(session,'record',{...payload,scope,recordId:record.id}));}catch(error){setError(error.message);}}
|
|
38
|
+
function chooseTab(value){setTab(value);setSelected(null);setPreview(null);setCursor(null);setHistory([]);setQuery('');setSearch('');}
|
|
39
|
+
return <div className="memory-panel memory-browser">
|
|
40
|
+
<header className="memory-heading"><h2>AtMem memory</h2><button className="btn" disabled={working} onClick={refresh}>Refresh</button></header>
|
|
41
|
+
<div className="memory-profile-bar"><label>Profile <input aria-label="Profile name" value={profileName} maxLength={80} onChange={event=>setProfileName(event.target.value)}/></label><button className="btn" disabled={working||!profileName.trim()||profileName===report?.userProfile?.name} onClick={()=>act(async()=>{await memoryCall(session,'user-profile',{name:profileName});})}>Save</button><code title="Local profile user ID">{report?.userProfile?.id||'Loading profile…'}</code><span className="memory-state">{report?.enabled?'On':'Off'}{report?.status==='unavailable'?' · Unavailable':''}</span><button className="link" disabled={working||!report} onClick={()=>act(()=>memoryCall(session,'settings',{...payload,enabled:!report.enabled}))}>{report?.enabled?'Turn off':'Turn on'}</button></div>
|
|
42
|
+
<div className="seg" role="tablist" aria-label="Memory views">{[['project','This project'],['profile','About you'],['skills','Skills'],['activity','Activity'],['providers','Context providers']].map(([id,label])=><button key={id} role="tab" aria-selected={tab===id} className={tab===id?'on':''} onClick={()=>chooseTab(id)}>{label}</button>)}</div>
|
|
43
|
+
{error&&<p className="error-box" role="alert">{error}</p>}
|
|
44
|
+
{(working||loading&&['project','profile'].includes(tab))&&<p className="memory-feedback" role="status"><BrainActivity/> Updating memory…</p>}
|
|
45
|
+
{report&&(report.pending>0||report.rejected>0)&&<p className="warn-text">{report.pending} pending · {report.rejected} rejected</p>}
|
|
46
|
+
{['profile','project'].includes(tab)&&<>
|
|
47
|
+
<div className="memory-toolbar"><input type="search" aria-label="Search memory" placeholder="Search memory or record ID" value={query} maxLength={200} onChange={event=>setQuery(event.target.value)}/><select aria-label="Memory status" value={status} onChange={event=>{setStatus(event.target.value);setCursor(null);setHistory([]);}}><option value="active">Current</option><option value="superseded">Earlier</option><option value="quarantined">Quarantined</option><option value="all">All</option></select><strong>{page?.total?.toLocaleString()??'—'} {page?.total===1?'record':'records'}</strong></div>
|
|
48
|
+
<div className="memory-table-wrap" aria-busy={loading}><table className="memory-table"><thead><tr><th>Key</th><th>Memory</th><th>Status</th><th>User</th><th>Updated</th><th><span className="sr-only">Actions</span></th></tr></thead><tbody>{page?.records?.map(record=><tr key={record.id}><td><button className="link" onClick={()=>openRecord(record)}>{record.fact_key||record.id}</button></td><td><button className="memory-content" onClick={()=>openRecord(record)}>{record.content}</button></td><td><span className={'memory-badge '+record.status}>{record.status==='active'?'Current':record.status==='superseded'?'Earlier':record.status}</span></td><td title={record.governance?.userId||'Creator was not recorded'}>{short(record.governance?.userId)}</td><td>{date(record.updated_at||record.created_at)}</td><td><button className="link" disabled={working} onClick={()=>act(async()=>{setPreview({...await memoryCall(session,'forget-preview',{...payload,scope,recordId:record.id}),scope,recordId:record.id});})}>Forget</button></td></tr>)}</tbody></table>{!loading&&!page?.records?.length&&<p className="memory-empty">{query?'No matching memories.':'No memories yet.'}</p>}</div>
|
|
49
|
+
<footer className="memory-pagination"><span>Page {history.length+1} · 50 per page</span><button className="btn" disabled={loading||!history.length} onClick={()=>{setCursor(history.at(-1));setHistory(value=>value.slice(0,-1));}}>Previous</button><button className="btn" disabled={loading||!page?.hasMore} onClick={()=>{setHistory(value=>[...value,cursor]);setCursor(page.nextCursor);}}>Next</button></footer>
|
|
50
|
+
</>}
|
|
51
|
+
{tab==='profile'&&<form className="memory-preference-form" onSubmit={event=>{event.preventDefault();act(async()=>{await memoryCall(session,'profile',{...payload,field,value});setValue('');});}}><select aria-label="Preference type" value={field} onChange={event=>setField(event.target.value)}><option value="preferences">Preference</option><option value="explanation">Explanation style</option><option value="experience">Role</option><option value="iac">Infrastructure tool</option></select><input aria-label="Remember about me" placeholder="Add a preference" value={value} maxLength={1000} onChange={event=>setValue(event.target.value)}/><button className="btn primary" disabled={working||!report?.enabled||!value.trim()}>Remember</button></form>}
|
|
52
|
+
{selected&&<section className="memory-record-detail" aria-label="Memory details"><header className="memory-heading"><h3>{selected.record.fact_key||'Memory'}</h3><button className="icon" aria-label="Close memory details" onClick={()=>setSelected(null)}>×</button></header><p>{selected.record.content}</p><dl>{[['Record',selected.record.id],['Creator user ID',selected.record.governance?.userId||'Unknown · legacy record'],['Owner user ID',selected.record.governance?.ownerUserId],['Actor',selected.record.governance?.actor||'Unknown'],['Source',selected.record.governance?.source||'Unknown'],['Created',date(selected.record.created_at)],['Updated',date(selected.record.updated_at)]].map(([label,value])=><React.Fragment key={label}><dt>{label}</dt><dd>{value||'Unknown'}</dd></React.Fragment>)}</dl>{selected.audit?.length>0&&<table className="memory-table"><thead><tr><th>Time</th><th>Actor</th><th>Action</th></tr></thead><tbody>{selected.audit.map((event,index)=><tr key={index}><td>{date(event.created_at)}</td><td>{event.actor}</td><td>{event.event_type}</td></tr>)}</tbody></table>}</section>}
|
|
53
|
+
{preview&&<section className="memory-preview" aria-label="Confirm memory removal"><h3>Forget memory?</h3><p>{preview.total??preview.records.length} records, including history. Project files stay.</p><button className="btn danger" disabled={working} onClick={()=>act(async()=>{await memoryCall(session,'forget',{...payload,scope:preview.scope,recordId:preview.recordId});setPreview(null);setSelected(null);})}>Forget</button><button className="btn" onClick={()=>setPreview(null)}>Cancel</button></section>}
|
|
54
|
+
{tab==='skills'&&<table className="memory-table"><thead><tr><th>Skill</th><th>Version</th><th>Integrity</th><th>Source</th><th>Status</th></tr></thead><tbody>{report?.skills?.map(skill=><tr key={skill.id}><td>{skill.name}</td><td>{skill.version}</td><td>{skill.integrity?'Verified':'Failed'}</td><td>{skill.source.startsWith('https:')?<a href={skill.source} target="_blank" rel="noreferrer">Source ↗</a>:'Local'}</td><td><button className="link" disabled={working||!skill.integrity} onClick={()=>act(()=>memoryCall(session,'skill',{...payload,skillId:skill.id,enabled:!skill.enabled}))}>{skill.enabled?'Disable':'Enable'}</button></td></tr>)}</tbody></table>}
|
|
55
|
+
{tab==='providers'&&<section aria-label="Context providers"><table className="memory-table"><thead><tr><th>Provider</th><th>Type</th><th>Status</th></tr></thead><tbody>{report?.contextProviders?.map(provider=><tr key={provider.id}><td>{provider.label}</td><td>{provider.kind}</td><td>{provider.enabled?'Enabled':'Disabled'}</td></tr>)}</tbody></table>{!report?.contextProviders?.length&&<p>No external providers configured.</p>}<p className="muted small">Connect RAG or memory through the server’s MEMORY_CONTEXT_PROVIDERS setting. Retrieved context is never saved automatically.</p></section>}
|
|
56
|
+
{tab==='activity'&&<table className="memory-table"><thead><tr><th>Time</th><th>Result</th><th>Records</th><th>Skills</th></tr></thead><tbody>{report?.activity?.map(activity=><tr key={activity.id}><td>{date(activity.createdAt)}</td><td>{activity.success?'Completed':'Incomplete'}</td><td>{activity.recordIds.length}</td><td>{activity.skillIds.join(', ')||'—'}</td></tr>)}</tbody></table>}
|
|
57
|
+
</div>;
|
|
46
58
|
}
|
package/src/OpenAIVoicePanel.jsx
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {InlineNote} from './HelpNote.jsx';
|
|
2
|
+
import {X,Volume2} from 'lucide-react';
|
|
3
|
+
import {BrainActivity} from './BrainActivity.jsx';
|
|
1
4
|
import React,{useEffect,useRef,useState} from 'react';
|
|
2
5
|
import {OpenAIVoice} from './openai-voice.mjs';
|
|
3
6
|
|
|
@@ -34,29 +37,27 @@ export function OpenAIVoicePanel({session,connection,transportRef,onTranscript,o
|
|
|
34
37
|
}});
|
|
35
38
|
current.readReplies=readReplies;engine.current=current;transportRef.current=current;void current.start().then(()=>{if(live.current&¤t.channel?.readyState==='open')setOpen(false)});
|
|
36
39
|
}
|
|
37
|
-
const caption={off:'
|
|
40
|
+
const caption={off:'Off',connecting:'Connecting OpenAI audio…',listening:automatic?'Listening · auto-send':'Listening',speaking:'Speaking',error:'Stopped · retry'}[phase];
|
|
38
41
|
return <>
|
|
39
|
-
<div className="voice-status"><button type="button" onClick={()=>setOpen(true)}
|
|
42
|
+
<div className="voice-status"><button type="button" className="icon" aria-label="Voice settings" title="Voice settings" onClick={()=>setOpen(true)}><Volume2 size={18}/></button>{active&&<button type="button" onClick={()=>engine.current?.stop({preservePending:true})}>Stop voice</button>}{active&&error.includes('playback')&&<button type="button" onClick={()=>void engine.current?.resumeAudio()}>Enable voice playback</button>}{(active||error)&&<span role="status">{error||caption}</span>}</div>
|
|
40
43
|
{open&&<div className="backdrop" onMouseDown={event=>{if(event.target===event.currentTarget)setOpen(false)}}><section className="modal" role="dialog" aria-modal="true" aria-label="OpenAI voice" onKeyDown={event=>{if(event.key==='Escape'){event.stopPropagation();setOpen(false)}}}>
|
|
41
|
-
<h2>
|
|
44
|
+
<h2>Voice</h2><button type="button" className="icon modal-close" aria-label="Close voice settings" onClick={()=>setOpen(false)}><X size={18}/></button>
|
|
42
45
|
<section aria-label="OpenAI voice settings">
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{status&&<details open={!status.available}><summary>Voice API setup</summary>
|
|
48
|
-
<p>Configure a dedicated OpenAI API key for voice. Your Architect connection stays unchanged. The server keeps this key in memory for its current session. An existing server OpenAI key can also be used.</p>
|
|
46
|
+
<small>AI audio · OpenAI</small><InlineNote>Audio and spoken replies go to OpenAI. Audio is not saved. Live verification pending.</InlineNote>
|
|
47
|
+
{!connection&&<button type="button" className="btn" onClick={()=>{setOpen(false);onConnections()}}>Choose AI provider</button>}
|
|
48
|
+
{status&&<details open={!status.available}><summary>API key</summary>
|
|
49
|
+
|
|
49
50
|
<label className="dialog-field">OpenAI voice API key<input aria-label="OpenAI voice API key" type="password" autoComplete="off" value={key} onChange={event=>setKey(event.target.value)}/></label>
|
|
50
|
-
<button type="button" className="btn" disabled={saving||active||!key.trim()} onClick={configure}>
|
|
51
|
+
<button type="button" className="btn" disabled={saving||active||!key.trim()} onClick={configure}>Use key</button>
|
|
51
52
|
</details>}
|
|
52
|
-
<button type="button" className="link" disabled={saving} onClick={()=>void refresh().catch(e=>setError(e.message))}>
|
|
53
|
+
<button type="button" className="link" disabled={saving} onClick={()=>void refresh().catch(e=>setError(e.message))}>Refresh</button>
|
|
53
54
|
<div className="record-row">
|
|
54
|
-
<button type="button" className="btn" disabled={!status?.available||!connection} onClick={active?()=>engine.current?.stop({preservePending:true}):start}>{active?'Stop
|
|
55
|
+
<button type="button" className="btn" disabled={!status?.available||!connection} onClick={active?()=>engine.current?.stop({preservePending:true}):start}>{active?'Stop voice':'Start voice'}</button>
|
|
55
56
|
{active&&<button type="button" className="btn" onClick={()=>engine.current?.interrupt()}>Stop speaking</button>}
|
|
56
57
|
{active&&error.includes('playback')&&<button type="button" className="btn" onClick={()=>void engine.current?.resumeAudio()}>Enable voice playback</button>}
|
|
57
58
|
</div>
|
|
58
|
-
<p><label><input type="checkbox" checked={automatic} onChange={event=>setAutomatic(event.target.checked)}/>
|
|
59
|
-
<p><label><input type="checkbox" checked={readReplies} onChange={event=>{setReadReplies(event.target.checked);if(engine.current){engine.current.readReplies=event.target.checked;if(!event.target.checked)engine.current.interrupt();}}}/>
|
|
59
|
+
<p><label><input type="checkbox" checked={automatic} onChange={event=>setAutomatic(event.target.checked)}/> Auto-send</label></p>
|
|
60
|
+
<p><label><input type="checkbox" checked={readReplies} onChange={event=>{setReadReplies(event.target.checked);if(engine.current){engine.current.readReplies=event.target.checked;if(!event.target.checked)engine.current.interrupt();}}}/> Read replies</label></p>
|
|
60
61
|
<p role="status">{caption}</p>
|
|
61
62
|
{error&&<p role="alert">{error}</p>}
|
|
62
63
|
</section>
|
package/src/OutputsView.jsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import {FileTree} from './FileTree.jsx';
|
|
2
|
+
import {strFromU8} from 'fflate';
|
|
1
3
|
import {CodePreview} from './CodePreview.jsx';
|
|
2
4
|
import {BrainActivity} from "./BrainActivity.jsx";
|
|
3
|
-
import React,{useEffect,useState} from 'react';
|
|
5
|
+
import React,{useEffect,useState,useRef,useMemo} from 'react';
|
|
4
6
|
import {EmulationPanel} from './EmulationPanel.jsx';
|
|
5
7
|
import {outputBundle} from './output-bundle.mjs';
|
|
6
8
|
import {ImplementationDelivery} from './ImplementationDelivery.jsx';
|
|
@@ -33,33 +35,43 @@ function Preview({name,content}){
|
|
|
33
35
|
if(name.endsWith('.md'))return <Document text={content}/>;
|
|
34
36
|
return <CodePreview className="output-code" content={content} name={name}/>;
|
|
35
37
|
}
|
|
36
|
-
export function OutputsView({project,run,environment,session,onDownload,onBack,emulation,onDiscuss,discussionBusy,onBuild,onSave}){
|
|
38
|
+
export function OutputsView({project,run,environment,session,onDownload,onBack,emulation,onDiscuss,discussionBusy,onBuild,onSave,onDownloaded,onReview,checkReceipt,onNavigate}){
|
|
39
|
+
const [exploration,setExploration]=useState(null);
|
|
40
|
+
const projectRef=useRef(project);projectRef.current=project;
|
|
41
|
+
const [packing,setPacking]=useState(false);
|
|
37
42
|
const [bundle,setBundle]=useState(null),[error,setError]=useState(''),[loading,setLoading]=useState(true),[retry,setRetry]=useState(0),[selected,setSelected]=useState('spec-kit/implementation-spec.md');
|
|
38
43
|
useEffect(()=>{
|
|
39
44
|
let active=true;setLoading(true);setError('');
|
|
40
|
-
outputBundle(project,run,environment).then(value=>{if(active){setBundle(value);setLoading(false);}}).catch(e=>{if(active){setError(e.message);setLoading(false);}});
|
|
45
|
+
outputBundle(project,run,environment,checkReceipt,{deferVisuals:true}).then(value=>{if(active){setBundle(value);setLoading(false);}}).catch(e=>{if(active){setError(e.message);setLoading(false);}});
|
|
41
46
|
return ()=>{active=false;};
|
|
42
|
-
},[project,run,environment,retry]);
|
|
47
|
+
},[project,run,environment,retry,checkReceipt]);
|
|
43
48
|
const readiness=bundle?.files['spec-kit/readiness.json']?JSON.parse(bundle.files['spec-kit/readiness.json']):null;
|
|
44
|
-
const
|
|
49
|
+
const artifactMode=exploration?.project===project,prefix=artifactMode?'design-documents/':'';
|
|
50
|
+
const fileValues=useMemo(()=>artifactMode?{...exploration.files,...Object.fromEntries(Object.entries(bundle?.files||{}).map(([name,value])=>[prefix+name,value]))}:bundle?.files||{},[artifactMode,exploration,bundle,prefix]);
|
|
51
|
+
const treeFiles=useMemo(()=>({...Object.fromEntries((bundle?.visualPaths||[]).map(path=>[prefix+path,null])),...fileValues}),[bundle,fileValues,prefix]);
|
|
52
|
+
async function selectFile(name){setSelected(name);if(treeFiles[name]===null){try{const full=await bundle.prepare();if(projectRef.current===project)setBundle(full);}catch(error){if(projectRef.current===project)setError(error.message);}}}
|
|
53
|
+
async function downloadDesign(){if(packing)return;setPacking(true);setError('');try{const full=await bundle.prepare();if(projectRef.current!==project)return;setBundle(full);await onDownload(full);}catch(error){if(projectRef.current===project)setError(error.message);}finally{setPacking(false);}}
|
|
54
|
+
const selectedPath=Object.hasOwn(treeFiles,selected)?selected:fileValues['START_HERE.md']?'START_HERE.md':Object.keys(fileValues).find(name=>name.endsWith('implementation-spec.md'))||Object.keys(fileValues)[0];
|
|
55
|
+
const raw=treeFiles[selectedPath],content=typeof raw==='string'?raw:raw&&/\.(?:md|txt|json|py|js|mjs|tf|sh|yaml|yml|toml|html|css)$/.test(selectedPath)?strFromU8(raw):raw;
|
|
56
|
+
|
|
45
57
|
return <section className="doc outputs-view" aria-label="Outputs">
|
|
46
|
-
<header className="doc-head"><h2>Delivery</h2
|
|
47
|
-
<button className="link" onClick={onBack}>Back to
|
|
48
|
-
{session?.engineeringEnabled&&<ImplementationDelivery key={project.id} project={project} session={session} onBuild={onBuild} onSave={onSave} contextBusy={discussionBusy}/>}
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
{!session?.engineeringEnabled&&<><p>
|
|
58
|
+
<header className="doc-head"><h2>Delivery</h2></header>
|
|
59
|
+
<button className="link" onClick={onReview||onBack}>Back to Review</button>
|
|
60
|
+
{session?.engineeringEnabled&&<ImplementationDelivery key={project.id} project={project} session={session} onBuild={onBuild} onSave={onSave} contextBusy={discussionBusy} designBundle={loading?null:bundle} onDesignDownload={onDownload} onDownloaded={onDownloaded} checkReceipt={checkReceipt} onExplore={setExploration} onNavigate={onNavigate}/>}
|
|
61
|
+
<section aria-label="Project file explorer"><h3>Explore files</h3>
|
|
62
|
+
|
|
63
|
+
{!session?.engineeringEnabled&&<><p>Design package · no implementation generated.</p><button className="btn" disabled={!bundle||loading||!!error||packing} onClick={downloadDesign}>{packing?'Preparing ZIP…':'Download project ZIP'}</button></>}
|
|
52
64
|
|
|
53
65
|
{loading&&<p role="status"><BrainActivity/> Preparing the current outputs…</p>}
|
|
54
66
|
{error&&<div role="alert"><p>{error}</p><button className="btn" onClick={()=>setRetry(n=>n+1)}>Retry preparing outputs</button></div>}
|
|
55
67
|
{bundle&&!loading&&!error&&<>
|
|
56
|
-
<p className="muted small">
|
|
57
|
-
{readiness&&<aside aria-label="Handoff readiness"><strong>Engineering handoff — decisions and implementation remain</strong><p>{readiness.counts.priorityDecisions} priority decisions · {readiness.counts.generatedFoundations} generated foundations / {readiness.counts.components} logical components</p><p>Next: {readiness.nextAction}</p><button className="link" onClick={()=>setSelected('spec-kit/readiness.md')}>Review handoff gaps</button></aside>}
|
|
68
|
+
<p className="muted small">r{bundle.manifest.designRevision} · {Object.keys(bundle.files).length} design files</p>
|
|
69
|
+
{readiness&&<details><summary>Remaining work</summary><aside aria-label="Handoff readiness"><strong>Engineering handoff — decisions and implementation remain</strong><p>{readiness.counts.priorityDecisions} priority decisions · {readiness.counts.generatedFoundations} generated foundations / {readiness.counts.components} logical components</p><p>Next: {readiness.nextAction}</p><button className="link" onClick={()=>setSelected('spec-kit/readiness.md')}>Review handoff gaps</button></aside></details>}
|
|
58
70
|
<div className="outputs-browser" data-snapshot-id={bundle.manifest.snapshotId}>
|
|
59
|
-
<
|
|
60
|
-
<div className="output-preview" aria-label="Output preview"><h3>{
|
|
71
|
+
<FileTree files={treeFiles} selected={selectedPath} onSelect={selectFile}/>
|
|
72
|
+
<div className="output-preview" aria-label="Output preview"><h3>{selectedPath}</h3>{raw===null?<p role="status"><BrainActivity/> Preparing preview…</p>:content!==undefined&&(typeof content==='string'||/\.(?:pdf|png|svg)$/.test(selectedPath)?<Preview key={selectedPath} name={selectedPath} content={content}/>:<p>{content.byteLength} bytes</p>)}</div>
|
|
61
73
|
</div>
|
|
62
74
|
</>}
|
|
63
|
-
</
|
|
75
|
+
</section>
|
|
64
76
|
</section>;
|
|
65
77
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React,{useEffect,useMemo,useRef,useState,useId}from'react';
|
|
2
|
+
import{postLargeJson}from'./large-request.mjs';
|
|
3
|
+
import{BrainActivity}from'./BrainActivity.jsx';
|
|
4
|
+
import{engineeringCall}from'./engineering-client.mjs';
|
|
5
|
+
import{terraformSketch}from'./terraform.mjs';
|
|
6
|
+
import{verifiedReceipt}from'./validation-receipt.mjs';
|
|
7
|
+
export function ProjectChecks({project,environment,session,onReceipt,onBusy,receipt,generationPending=false}){
|
|
8
|
+
const [report,setReport]=useState(null),[catalogReady,setCatalogReady]=useState(false),[catalog,setCatalog]=useState([]),[checks,setChecks]=useState([]),[result,setResult]=useState(null),[working,setWorking]=useState(false),[error,setError]=useState('');
|
|
9
|
+
const hintId=useId();
|
|
10
|
+
const [uploadProgress,setUploadProgress]=useState(null);
|
|
11
|
+
const active=useRef(null),locked=useRef(false),files=useMemo(()=>terraformSketch(project,environment),[project,environment]),key=JSON.stringify(files);
|
|
12
|
+
useEffect(()=>{let stopped=false;setResult(null);active.current?.abort();let previous=receipt;try{previous=previous||JSON.parse(localStorage.getItem('atmem-tf-receipt:'+project.id)||'null');}catch{}void verifiedReceipt(files,previous).then(value=>{if(!stopped){setResult(value);onReceipt?.(value);}});return()=>{stopped=true;active.current?.abort();};},[key]);
|
|
13
|
+
useEffect(()=>{
|
|
14
|
+
setReport(null);setCatalog([]);setChecks([]);setCatalogReady(false);
|
|
15
|
+
if(!session?.engineeringEnabled)return;
|
|
16
|
+
let stopped=false,timer;const controller=new AbortController();
|
|
17
|
+
async function poll(){try{
|
|
18
|
+
const value=await engineeringCall(session,'delivery',{projectId:project.id},{signal:controller.signal});if(stopped)return;setReport(value);
|
|
19
|
+
if(value.artifact){const payload={projectId:project.id,sessionId:value.session.id};const [definitions,results]=await Promise.all([engineeringCall(session,'validators',payload,{signal:controller.signal}),engineeringCall(session,'validation/list',{...payload,artifactId:value.artifact.id},{signal:controller.signal})]);if(!stopped){setCatalog(definitions.validators||[]);setChecks(results.checks||[]);setCatalogReady(true);}}else{setCatalog([]);setChecks([]);setCatalogReady(false);}
|
|
20
|
+
}catch(e){if(!stopped&&e.name!=='AbortError'&&e.status!==404)setError(e.message);}finally{if(!stopped)timer=setTimeout(poll,2000);}}
|
|
21
|
+
void poll();return()=>{stopped=true;clearTimeout(timer);controller.abort();};
|
|
22
|
+
},[project.id,session?.token,session?.engineeringEnabled]);
|
|
23
|
+
const running=checks.some(c=>c.state!=='finished'&&c.state!=='recovery_required');
|
|
24
|
+
useEffect(()=>{onBusy?.(!!working||running);return()=>onBusy?.(false);},[working,running,onBusy]);
|
|
25
|
+
const available=catalog.filter(v=>v.available!==false);
|
|
26
|
+
async function run(kind){
|
|
27
|
+
if(generationPending||locked.current||!session?.token||kind==='application'&&(!report?.artifact||!available.length))return;
|
|
28
|
+
locked.current=true;setWorking(kind);setError('');const controller=new AbortController();active.current=controller;
|
|
29
|
+
try{
|
|
30
|
+
if(kind==='application'){
|
|
31
|
+
const payload={projectId:project.id,sessionId:report.session.id,artifactId:report.artifact.id};
|
|
32
|
+
await Promise.all(available.map(validator=>{
|
|
33
|
+
const previous=checks.filter(c=>c.validatorId===validator.id&&c.contractSha256===validator.contractSha256).at(-1);
|
|
34
|
+
if(previous&&previous.state!=='finished')return;
|
|
35
|
+
return engineeringCall(session,'validation/start',{...payload,validatorId:validator.id,retry:!!previous},{signal:controller.signal});
|
|
36
|
+
}));
|
|
37
|
+
const results=await engineeringCall(session,'validation/list',payload,{signal:controller.signal});
|
|
38
|
+
if(!controller.signal.aborted)setChecks(results.checks||[]);
|
|
39
|
+
}else{
|
|
40
|
+
const value=await postLargeJson('/api/terraform/validate',{project,environment},{token:session.token,signal:AbortSignal.any([controller.signal,AbortSignal.timeout(500000)]),onProgress:setUploadProgress});
|
|
41
|
+
const verified=await verifiedReceipt(files,value);if(!controller.signal.aborted){setResult(value);onReceipt?.(verified);try{if(verified)localStorage.setItem('atmem-tf-receipt:'+project.id,JSON.stringify(verified));else localStorage.removeItem('atmem-tf-receipt:'+project.id);}catch{}}
|
|
42
|
+
}
|
|
43
|
+
}catch(e){if(!controller.signal.aborted){setError(e.message);}}
|
|
44
|
+
finally{locked.current=false;setWorking(false);setUploadProgress(null);}
|
|
45
|
+
}
|
|
46
|
+
const label=status=>({passed:'Passed',failed:'Failed',incomplete:'Incomplete',unavailable:'Not run'}[status]||'Not run');
|
|
47
|
+
const terraformHint=generationPending?'Project files are being generated. Wait until generation finishes.':'Validate the Terraform draft from your saved design.';
|
|
48
|
+
const applicationHint=generationPending?'Project files are being generated. Wait until generation finishes.':!report?.artifact?'Generate project files first.':!catalogReady?'Loading available application tests.':!available.length?'No application tests are configured.':'Run the generated application tests.';
|
|
49
|
+
const applicationStatus=generationPending?'Generating files…':!report?.artifact?'Generate files first':working==='application'||running?'Running…':checks.length?checks.every(c=>c.state==='finished'&&c.result?.status==='passed')?'Passed':'Needs attention':!catalogReady?'Loading…':available.length?'Not run':'Not configured';
|
|
50
|
+
return <section className="project-checks" aria-label="Project checks"><div className="section-action"><h3>Checks</h3></div>
|
|
51
|
+
<ul className="check-rows">
|
|
52
|
+
<li><span>Terraform validation</span><strong className={result?.status==='passed'?'check-complete':'check-pending'}>{working==='terraform'?uploadProgress!==null?`Uploading ${uploadProgress}%`:'Running…':label(result?.status)}</strong><span className="check-run-action" title={terraformHint} tabIndex={generationPending?0:undefined} aria-label={terraformHint}><button className="btn" aria-label="Run Terraform validation" title={terraformHint} disabled={generationPending||!session?.token||!!working||running} onClick={()=>run('terraform')}>{working==='terraform'?<><BrainActivity size={18}/> Running…</>:'Run'}</button></span></li>
|
|
53
|
+
<li><span>Application tests</span><strong id={hintId} className={applicationStatus==='Passed'?'check-complete':'check-pending'}>{applicationStatus}</strong><span className="check-run-action" title={applicationHint} tabIndex={generationPending||!report?.artifact||!available.length?0:undefined} aria-label={applicationHint}><button className="btn" aria-label="Run application tests" aria-describedby={hintId} title={applicationHint} disabled={generationPending||!session?.token||!!working||running||!report?.artifact||!available.length} onClick={()=>run('application')}>{working==='application'||running?<><BrainActivity size={18}/> Running…</>:'Run'}</button></span></li>
|
|
54
|
+
</ul>
|
|
55
|
+
{(result||checks.length>0)&&<details><summary>Check details</summary>{result&&<><p>{result.message}</p>{result.checks?.map((c,i)=><details key={i}><summary>{c.name} · {c.passed?'Passed':'Failed'}</summary><pre>{c.output}</pre></details>)}</>}{checks.map(c=><details key={c.id}><summary>{c.label}</summary><pre>{c.result?.output||c.result?.reason||c.state}</pre></details>)}</details>}
|
|
56
|
+
{error&&<p role="alert">{error}</p>}
|
|
57
|
+
</section>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React,{useEffect,useRef}from'react';
|
|
2
|
+
import{Star,X}from'lucide-react';
|
|
3
|
+
export const STAR_URL='https://github.com/aetna000/atmem-ai-architect';
|
|
4
|
+
const KEY='atmem-architect-star-prompt-v1',SNOOZE=30*24*60*60*1000;
|
|
5
|
+
export function shouldOfferStar(value,now=Date.now()){
|
|
6
|
+
if(!value)return true;
|
|
7
|
+
try{const record=JSON.parse(value);return record.choice!=='opened'&&(!Number.isFinite(record.after)||record.after<=now);}catch{return true;}
|
|
8
|
+
}
|
|
9
|
+
export function StarPrompt({offer=0}){
|
|
10
|
+
const dialog=useRef(null),choice=useRef('later');
|
|
11
|
+
function dismiss(value='later'){
|
|
12
|
+
choice.current=value;
|
|
13
|
+
try{localStorage.setItem(KEY,JSON.stringify({choice:value,after:Date.now()+SNOOZE}));}catch{}
|
|
14
|
+
dialog.current?.close();
|
|
15
|
+
}
|
|
16
|
+
useEffect(()=>{
|
|
17
|
+
if(!offer)return;
|
|
18
|
+
let allowed=true;try{allowed=shouldOfferStar(localStorage.getItem(KEY));}catch{}
|
|
19
|
+
if(allowed&&!dialog.current.open){choice.current='later';dialog.current.showModal();}
|
|
20
|
+
},[offer]);
|
|
21
|
+
return <dialog ref={dialog} className="star-prompt" aria-labelledby="star-prompt-title" onCancel={event=>{event.preventDefault();dismiss();}}>
|
|
22
|
+
<header><h2 id="star-prompt-title"><Star size={22} aria-hidden="true"/> Enjoying Architect?</h2><button type="button" className="icon" aria-label="Close star invitation" onClick={()=>dismiss()}><X size={20}/></button></header>
|
|
23
|
+
<p>If Architect helped, a GitHub star helps others find it.</p>
|
|
24
|
+
<div className="star-prompt-actions"><a className="btn primary" href={STAR_URL} target="_blank" rel="noopener noreferrer" onClick={()=>dismiss('opened')}><Star size={18} aria-hidden="true"/> Star on GitHub</a><button type="button" className="btn" onClick={()=>dismiss()}>Later</button></div>
|
|
25
|
+
</dialog>;
|
|
26
|
+
}
|
package/src/WorkbenchDialogs.jsx
CHANGED
|
@@ -154,7 +154,7 @@ export function Connections({ session, connection, onCheck, onSave, onRetry, wel
|
|
|
154
154
|
(kind === "aws" && !capability?.available);
|
|
155
155
|
return (
|
|
156
156
|
<div className="connect">
|
|
157
|
-
<h2>{welcome ? "
|
|
157
|
+
<h2>{welcome ? "AI providers" : "AI providers"}</h2>
|
|
158
158
|
{!session && (
|
|
159
159
|
<p className="error-box">
|
|
160
160
|
Looks like AtMem AI Architect's helper isn't running. Start it with <code>npm run local</code>, then{" "}
|
package/src/conversation.mjs
CHANGED
|
@@ -6,6 +6,7 @@ export function conversationMessages(project, fallback = []) {
|
|
|
6
6
|
function cleanMessages(messages) {
|
|
7
7
|
return messages.filter(m => m && ['user','assistant'].includes(m.role) && typeof m.text === 'string').map(m => ({
|
|
8
8
|
role:m.role, text:m.text,
|
|
9
|
+
...(typeof m.createdAt==='string'&&Number.isFinite(Date.parse(m.createdAt))?{createdAt:m.createdAt}:{}),
|
|
9
10
|
...(typeof m.activityId==='string'&&/^[a-f0-9-]{36}$/.test(m.activityId)?{activityId:m.activityId}:{}),
|
|
10
11
|
...(m.requestId?{requestId:String(m.requestId).slice(0,100)}:{}),
|
|
11
12
|
...(['queued','processing','complete','superseded','failed','cancelled'].includes(m.workState)?{workState:m.workState}:{}),
|
package/src/delivery.mjs
CHANGED
|
@@ -20,7 +20,7 @@ const json = (v) => JSON.stringify(v, null, 2);
|
|
|
20
20
|
const list = (items) => items.length ? items.map((x) => `- ${x}`).join("\n") : "None recorded.";
|
|
21
21
|
export function solutionSpecification(project) {
|
|
22
22
|
const a = project.assessment;
|
|
23
|
-
return `# ${project.name} — solution specification\n\n## Problem and intended outcome\n\n${implementationOutcome(project)}\n\n## Scope\n\n${list((project.useCases || []).map((u) => `${u.title}: ${u.verdict}${u.reason ? ` — ${u.reason}` : ""}`))}\n\n## Requirements and acceptance criteria\n\n${list((project.needs || []).map((r) => `[${requirementNumber(project,r.id)}] ${r.statement} (${r.priority}, ${r.status}); target: ${r.measure ? `${r.measure.metric} ${r.measure.op} ${r.measure.target}${r.measure.unit || ""}` : "acceptance target needed"}; linked blocks: ${r.links?.join(", ") || "not linked"}${`\n\nDesign explanation: ${requirementExplanation(r)}`}${r.basis ? `\n\nBasis: ${r.basis}` : ""}`))}\n\n${systemDescriptionMarkdown(project)}\n\n## Architecture\n\n\`\`\`mermaid\n${mermaid(project)}\n\`\`\`\n\n${resourceMarkdown(project)}\n\n## Options and tradeoffs\n\n${a ? `Scores are entered judgements (1–5), not provider benchmarks or prices. Unscored options remain open.\n\n| Option | ${CRITERIA.map((c) => c.label).join(" | ")} | Weighted score | Rationale |\n|---|${CRITERIA.map(() => "---|").join("")}---|---|\n${OPTIONS.map((o) => `| ${OPTION_NAMES[o]} | ${CRITERIA.map((c) => a.scores?.[o]?.[c.id] ?? "—").join(" | ")} | ${optionScore(a, o) ?? "—"} | ${text(a.notes?.[o])} |`).join("\n")}` : "Use Design → Options to compare cloud choices and record the rationale before choosing."}\n\n##
|
|
23
|
+
return `# ${project.name} — solution specification\n\n## Problem and intended outcome\n\n${implementationOutcome(project)}\n\n## Scope\n\n${list((project.useCases || []).map((u) => `${u.title}: ${u.verdict}${u.reason ? ` — ${u.reason}` : ""}`))}\n\n## Requirements and acceptance criteria\n\n${list((project.needs || []).map((r) => `[${requirementNumber(project,r.id)}] ${r.statement} (${r.priority}, ${r.status}); target: ${r.measure ? `${r.measure.metric} ${r.measure.op} ${r.measure.target}${r.measure.unit || ""}` : "acceptance target needed"}; linked blocks: ${r.links?.join(", ") || "not linked"}${`\n\nDesign explanation: ${requirementExplanation(r)}`}${r.basis ? `\n\nBasis: ${r.basis}` : ""}`))}\n\n${systemDescriptionMarkdown(project)}\n\n## Architecture\n\n\`\`\`mermaid\n${mermaid(project)}\n\`\`\`\n\n${resourceMarkdown(project)}\n\n## Options and tradeoffs\n\n${a ? `Scores are entered judgements (1–5), not provider benchmarks or prices. Unscored options remain open.\n\n| Option | ${CRITERIA.map((c) => c.label).join(" | ")} | Weighted score | Rationale |\n|---|${CRITERIA.map(() => "---|").join("")}---|---|\n${OPTIONS.map((o) => `| ${OPTION_NAMES[o]} | ${CRITERIA.map((c) => a.scores?.[o]?.[c.id] ?? "—").join(" | ")} | ${optionScore(a, o) ?? "—"} | ${text(a.notes?.[o])} |`).join("\n")}` : "Use Design → Options to compare cloud choices and record the rationale before choosing."}\n\n## Design checks and decisions\n\n${adrMarkdown(project.decisions)}\n\n### Design checks\n\n${review(project).map(f=>`- [${f.status}] ${f.statement}\n Rationale: ${f.rationale}\n Next action: ${f.action}\n Source: ${f.source}${project.reviewAccepted?.[f.key]?`\n Accepted by: ${project.reviewAccepted[f.key].owner}; reason: ${project.reviewAccepted[f.key].reason}`:''}`).join('\n\n')}\n\n## Assumptions\n\n${list((project.assumptions || []).map((a) => `[${a.id}] ${a.statement} (${a.madeAt})`))}\n\n## Questions to resolve\n\n${list((project.openQuestions || []).filter((q) => q.status === "open").map((q) => `[${q.id}] ${q.question}${q.owner ? ` — ${q.owner}` : ""}`))}\n`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function deliveryPack(project, run, environment = "staging", now = new Date()) {
|
package/src/engineering.css
CHANGED
|
@@ -29,3 +29,13 @@
|
|
|
29
29
|
.architecture-work > ul { max-height: 24rem; overflow: auto; padding-left: 1.3rem; }
|
|
30
30
|
.architecture-work pre { white-space: pre-wrap; overflow-wrap: anywhere; font-size: .8rem; }
|
|
31
31
|
.architecture-work small { display: block; }
|
|
32
|
+
|
|
33
|
+
.generation-progress { height: 8px; overflow: hidden; border-radius: 999px; background: var(--line, #e1e6ee); margin: 1rem 0 .5rem; }
|
|
34
|
+
.generation-progress > span { display: block; height: 100%; width: 35%; border-radius: inherit; background: var(--action, #f3a2b4); animation: generation-sweep 1.8s ease-in-out infinite; }
|
|
35
|
+
@keyframes generation-sweep { from { transform: translateX(-80%); } to { transform: translateX(266%); } }
|
|
36
|
+
@media (prefers-reduced-motion: reduce) { .generation-progress > span { animation: none; width: 100%; opacity: .65; } }
|
|
37
|
+
|
|
38
|
+
.engineering-progress-count { display: inline-block; font-variant-numeric: tabular-nums; transform-origin: center; }
|
|
39
|
+
.engineering-progress-count.updated { animation: progress-count-pulse 360ms ease-out; }
|
|
40
|
+
@keyframes progress-count-pulse { 0% { opacity: .55; transform: scale(.92); } 45% { opacity: 1; transform: scale(1.1); } 100% { transform: scale(1); } }
|
|
41
|
+
@media (prefers-reduced-motion: reduce) { .engineering-progress-count.updated { animation: none; } }
|
package/src/estimate.mjs
CHANGED
|
@@ -79,8 +79,8 @@ export function estimate(project, now = new Date()) {
|
|
|
79
79
|
const priced = validNumber(l.price) && /^[A-Z]{3}$/.test(l.currency || "") && l.source?.trim() && l.unit?.trim();
|
|
80
80
|
lines.push({ id: l.id, component: n.data.label, componentId: n.id, detailed: true,
|
|
81
81
|
driver: l.description || "Charge", unit: l.unit, inputs: [], quantity: missing.length ? null : l.quantity,
|
|
82
|
-
missing, price: priced ? l : null,
|
|
83
|
-
cost: !missing.length && priced ?
|
|
82
|
+
missing, note:l.note, price: priced ? l : null,
|
|
83
|
+
cost: !missing.length && priced ? l.quantity * l.price : null,
|
|
84
84
|
status: missing.length ? "input needed" : priced ? "priced" : "price needed" });
|
|
85
85
|
}
|
|
86
86
|
continue;
|
|
@@ -103,7 +103,7 @@ export function estimate(project, now = new Date()) {
|
|
|
103
103
|
quantity,
|
|
104
104
|
missing,
|
|
105
105
|
price: priced ? price : null,
|
|
106
|
-
cost: quantity != null && priced ?
|
|
106
|
+
cost: quantity != null && priced ? quantity * price.price : null,
|
|
107
107
|
status: missing.length ? "input needed" : priced ? "priced" : "price needed",
|
|
108
108
|
});
|
|
109
109
|
}
|
|
@@ -115,6 +115,7 @@ export function estimate(project, now = new Date()) {
|
|
|
115
115
|
const gaps = [
|
|
116
116
|
...(est.details || []).filter((l) => !project.nodes.some((n) => n.id === l.componentId && n.data.kind !== "note")).map((l) => `${l.description || l.id}: linked component was removed; reassign this charge (excluded from totals).`),
|
|
117
117
|
...lines.filter((l) => l.status !== "priced").map((l) => `${l.component} / ${l.driver}: ${l.status}${l.missing.length ? ` (${l.missing.join(", ")})` : ""}.`),
|
|
118
|
+
...lines.filter(l=>l.price?.coverageIncomplete).map(l=>`${l.component}: additional charges are excluded from this estimate.`),
|
|
118
119
|
...notCosted.map((name) => `${name}: no cost lines; add charges or an explicit zero-cost line with a reason.`),
|
|
119
120
|
...(currencies.length > 1 ? ["Choose one currency; different currencies cannot be added."] : []),
|
|
120
121
|
...lines.filter((l) => l.price && (!l.price.region?.trim() || !l.price.service?.trim() || !l.price.date || !Number.isFinite(Date.parse(l.price.date)))).map((l) => `${l.component} / ${l.driver}: confirm region, service/SKU and price date.`),
|
|
@@ -124,6 +125,7 @@ export function estimate(project, now = new Date()) {
|
|
|
124
125
|
return {
|
|
125
126
|
lines,
|
|
126
127
|
notCosted,
|
|
128
|
+
preciseExpected:costed.length&¤cies.length===1?total:null,
|
|
127
129
|
usage: u,
|
|
128
130
|
currency: currencies.length === 1 ? currencies[0] : null,
|
|
129
131
|
mixedCurrencies: currencies.length > 1,
|