@atmem/architect 0.2.0-alpha.4.1 → 0.2.0
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 +19 -21
- package/dist/assets/index-BTLHaa3J.css +1 -0
- package/dist/assets/{index-qtCM2D7O.js → index-CQ2C1gKF.js} +19206 -19139
- package/dist/assets/{index.es-C6C0sneB.js → index.es-DTyqIdaO.js} +1 -1
- package/dist/assets/{visual-export-DjbjgQbA.js → visual-export-Ox0j5xj5.js} +3 -3
- package/dist/index.html +2 -2
- package/dist/licenses/atmem-APACHE-2.0.txt +202 -0
- package/package.json +2 -2
- package/public/licenses/atmem-APACHE-2.0.txt +202 -0
- 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_context.py +58 -0
- package/server/memory_routes.py +67 -8
- package/server/memory_service.py +37 -4
- package/server/memory_worker.py +72 -9
- package/server/orca_handoff.py +90 -0
- package/server/pricing.py +106 -0
- package/server/prompts.py +13 -0
- 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 +27 -27
- package/src/FileTree.jsx +7 -0
- package/src/FinalChecklist.jsx +15 -0
- package/src/HelpNote.jsx +3 -0
- package/src/ImplementationDelivery.jsx +52 -16
- package/src/JourneyViews.jsx +19 -16
- package/src/LocationChoices.jsx +12 -0
- package/src/MemoryPanel.jsx +43 -31
- package/src/OpenAIVoicePanel.jsx +15 -14
- package/src/OutputsView.jsx +21 -15
- package/src/ProjectChecks.jsx +44 -0
- package/src/WorkbenchDialogs.jsx +1 -1
- package/src/conversation.mjs +1 -0
- package/src/delivery.mjs +1 -1
- package/src/estimate.mjs +5 -3
- package/src/final-package.mjs +34 -0
- package/src/journey-workspace.css +34 -0
- package/src/journey.mjs +12 -2
- package/src/main.jsx +119 -96
- package/src/output-bundle.mjs +4 -2
- package/src/pricing.mjs +56 -2
- 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/journey.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export function completedStages(p){
|
|
|
20
20
|
return {board:p.nodes.some(n=>n.data.kind!=='note'),scope:saved.scope===keys.scope,
|
|
21
21
|
cloud:cloudMapping(p).ready&&saved.architecture===keys.architecture,
|
|
22
22
|
review:saved.review===keys.architecture&&saved.estimate===keys.estimate,
|
|
23
|
-
delivery:
|
|
23
|
+
delivery:saved.delivery===keys.delivery}; // Delivery means the exact package was downloaded, not deployed.
|
|
24
24
|
}
|
|
25
25
|
export function journeyAction(guidance,section='board'){
|
|
26
26
|
if(section===journeySectionId(guidance.id)&&guidance.checkpoint&§ion!=='board')return {kind:'continue',label:guidance.continueLabel};
|
|
@@ -42,7 +42,7 @@ const snapshot=value=>{const text=JSON.stringify(value);let a=2166136261,b=5381;
|
|
|
42
42
|
export function journeyKeys(p){
|
|
43
43
|
const scope=snapshot([p.brief?.application||p.requirements||'',(p.needs||[]).filter(r=>r.source==='user').map(r=>[r.id,r.statement,r.measure]).sort()]);
|
|
44
44
|
const architecture=snapshot([scope,p.nodes.filter(n=>n.data.kind!=='note').map(n=>[n.id,n.data.label,n.data.service,n.data.provider,n.data.config,n.data.description]).sort(),p.edges.map(e=>[e.source,e.target,e.label]).sort(),choice(p,'platform.cloud'),choice(p,'cloud.region'),choice(p,'data.country')]);
|
|
45
|
-
return {scope,architecture,estimate:snapshot([architecture,p.
|
|
45
|
+
const estimate=snapshot([architecture,p.estimate||{}]);return {scope,architecture,estimate,delivery:snapshot([architecture,estimate,p.revision])};
|
|
46
46
|
}
|
|
47
47
|
export function cloudMapping(p){
|
|
48
48
|
const provider=choice(p,'platform.cloud');
|
|
@@ -85,3 +85,13 @@ export function advanceJourney(p,checkpoint){
|
|
|
85
85
|
const key=checkpoint==='review'?'architecture':checkpoint;
|
|
86
86
|
return {...p,extensions:{...p.extensions,journey:{...p.extensions?.journey,[checkpoint]:state.keys[key]}}};
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
// Explicit completion actions acknowledge a working draft; navigation never does.
|
|
90
|
+
export function acknowledgeStage(p,stage){
|
|
91
|
+
const keys=journeyKeys(p),previous=p.extensions?.journey||{},saved={...previous};
|
|
92
|
+
if(stage==='scope'&&(p.needs?.length||p.brief?.application?.trim()))saved.scope=keys.scope;
|
|
93
|
+
if(stage==='cloud'&&cloudMapping(p).ready)saved.architecture=keys.architecture;
|
|
94
|
+
if(stage==='review')saved.review=keys.architecture;
|
|
95
|
+
if(stage==='delivery')saved.delivery=keys.delivery;
|
|
96
|
+
return {...p,extensions:{...p.extensions,journey:saved}};
|
|
97
|
+
}
|
package/src/main.jsx
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import {LocationChoices} from './LocationChoices.jsx';
|
|
2
|
+
import {InlineNote} from './HelpNote.jsx';
|
|
3
|
+
import {ProjectChecks} from './ProjectChecks.jsx';
|
|
4
|
+
import {useEngineeringStatus} from './useEngineeringStatus.mjs';
|
|
1
5
|
import {ChatResizeHandle} from './ChatResizeHandle.jsx';
|
|
2
6
|
import {copyProject} from './project-copy.mjs';
|
|
3
7
|
import './journey-workspace.css';
|
|
4
8
|
import {synchronizeEngineeringIntent,applyArchitectureChange} from './architecture-work.mjs';
|
|
5
9
|
import {applyArtifactWork} from './artifact-work.mjs';
|
|
6
|
-
import {ArchitectureWorkPanel} from './ArchitectureWorkPanel.jsx';
|
|
7
10
|
import {hasArchitectureDesign} from './design-build-flow.mjs';
|
|
8
11
|
import {ArchitectureTurn,EarlierArchitectureActivity} from './ArchitectureActivity.jsx';
|
|
9
12
|
import {recordArchitectureTurn,finishArchitectureTurn,architectureChatEntries} from './architecture-activity.mjs';
|
|
@@ -33,7 +36,7 @@ import {OpenAIVoicePanel} from './OpenAIVoicePanel.jsx';
|
|
|
33
36
|
import { OutputsView } from "./OutputsView.jsx";
|
|
34
37
|
import { outputBundle } from "./output-bundle.mjs";
|
|
35
38
|
import { TerraformView } from "./TerraformView.jsx";
|
|
36
|
-
import { journey, advanceJourney, DESIGN_SECTIONS,journeySectionId,completedStages, designSection, journeyAction } from "./journey.mjs";
|
|
39
|
+
import { journey, advanceJourney, acknowledgeStage, DESIGN_SECTIONS,journeySectionId,completedStages, designSection, journeyAction } from "./journey.mjs";
|
|
37
40
|
import "@xyflow/react/dist/style.css";
|
|
38
41
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
39
42
|
import { createRoot } from "react-dom/client";
|
|
@@ -59,6 +62,8 @@ import {
|
|
|
59
62
|
ChevronRight,
|
|
60
63
|
GitBranch,
|
|
61
64
|
Maximize2,
|
|
65
|
+
ChevronsLeftRight,
|
|
66
|
+
ChevronsRightLeft,
|
|
62
67
|
MoreHorizontal,
|
|
63
68
|
PanelRightClose,
|
|
64
69
|
PanelRightOpen,
|
|
@@ -99,6 +104,8 @@ import {
|
|
|
99
104
|
Eraser,
|
|
100
105
|
LayoutGrid,
|
|
101
106
|
ArrowLeft,
|
|
107
|
+
ArrowRightToLine,
|
|
108
|
+
ArrowRightFromLine,
|
|
102
109
|
FolderOpen,
|
|
103
110
|
Moon,
|
|
104
111
|
Sun,
|
|
@@ -123,12 +130,12 @@ import { withDerived } from "./derive.mjs";
|
|
|
123
130
|
import { MemoryPanel, ContextUsed } from "./MemoryPanel.jsx";
|
|
124
131
|
import { defaultAssessment } from "./engagement.mjs";
|
|
125
132
|
import { setUsage } from "./estimate.mjs";
|
|
126
|
-
import { applyPriceQuotes } from "./pricing.mjs";
|
|
133
|
+
import { applyPriceQuotes, fetchPlanningPrices, storageNodes, lambdaNodes, googleNodes } from "./pricing.mjs";
|
|
127
134
|
import { applyDecisionUpdates, applyRequirementDrafts, proposeCommentRevisions } from "./consultation.mjs";
|
|
128
135
|
import { terraformSketch } from "./terraform.mjs";
|
|
129
136
|
import { WORKFLOWS, conceptSignature, essentialsKnown, nextQuestion, opening, progress, sketch, understand } from "./discovery.mjs";
|
|
130
137
|
import { review } from "./review.mjs";
|
|
131
|
-
import { estimate } from "./estimate.mjs";
|
|
138
|
+
import { setProjectSize, estimate } from "./estimate.mjs";
|
|
132
139
|
import { DeployView, EstimateView, ProjectsDialog, ReviewView, TestCard } from "./JourneyViews.jsx";
|
|
133
140
|
import { newRequirement } from "./records.mjs";
|
|
134
141
|
import { DiscoverView, OptionsView, DecisionsView, MeasuresEditor, RequirementsView } from "./EngagementViews.jsx";
|
|
@@ -289,7 +296,7 @@ function BlockNode({ data, selected }) {
|
|
|
289
296
|
</span>
|
|
290
297
|
<span className="blk-text">
|
|
291
298
|
<strong>{data.label}</strong>
|
|
292
|
-
{data.
|
|
299
|
+
{data.service&&<small className="blk-service">{data.service}</small>}
|
|
293
300
|
{(data.origin === "existing" || data.provider !== "tools") && (
|
|
294
301
|
<small>
|
|
295
302
|
<i className="pdot" />
|
|
@@ -452,7 +459,7 @@ function Linked({ project, id, hideNeeds = false }) {
|
|
|
452
459
|
);
|
|
453
460
|
}
|
|
454
461
|
|
|
455
|
-
function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewConfirm, onReviewDiscuss, onReviewQuestion, onReviewResolve, onRequirementSave, onWorkModelChange, onChange, onRemove, onOpen, onBack }) {
|
|
462
|
+
function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewConfirm, onReviewDiscuss, onReviewQuestion, onReviewResolve, onRequirementSave, onWorkModelChange, onChange, onRemove, onOpen, onBack,onSpec }) {
|
|
456
463
|
const trace = run?.steps.find((s) => s.id === node.id);
|
|
457
464
|
const inputs = project.edges.filter((e) => e.target === node.id);
|
|
458
465
|
const outputs = project.edges.filter((e) => e.source === node.id);
|
|
@@ -464,11 +471,11 @@ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewCon
|
|
|
464
471
|
<section className="detail" aria-label="Block detail">
|
|
465
472
|
<div className="detail-navigation">
|
|
466
473
|
<button className="back" onClick={onBack} aria-label="Back to the board">
|
|
467
|
-
<ArrowLeft size={15}
|
|
474
|
+
<ArrowLeft size={15} /><kbd>Esc</kbd>
|
|
468
475
|
</button>
|
|
469
476
|
</div>
|
|
470
477
|
<div className="detail-col">
|
|
471
|
-
<h3
|
|
478
|
+
<h3 aria-label="Inputs" title="Inputs"><ArrowRightToLine size={20}/></h3>
|
|
472
479
|
{inputs.length ? (
|
|
473
480
|
inputs.map((e) => (
|
|
474
481
|
<button key={e.id} className="terminal" onClick={() => onOpen(e.source)}>
|
|
@@ -496,9 +503,8 @@ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewCon
|
|
|
496
503
|
onChange={(e) => set({ label: e.target.value })}
|
|
497
504
|
/>
|
|
498
505
|
</div>
|
|
499
|
-
<
|
|
506
|
+
<button className="link" onClick={onSpec}>Specification ↗</button>
|
|
500
507
|
<BlockRequirements project={project} nodeId={node.id} busy={busy} onSave={onRequirementSave}/>
|
|
501
|
-
{project.extensions?.architectureWork?.artifactMappings?.length>0&&<ArchitectureWorkPanel project={project} onChange={onWorkModelChange} busy={busy}/>}
|
|
502
508
|
<ComponentReview key={node.id} item={reviewItems.find(r=>r.id===node.id)} items={reviewItems} busy={busy} onOpen={onOpen} onConfirm={onReviewConfirm} onDiscuss={onReviewDiscuss} onQuestion={onReviewQuestion} onResolve={onReviewResolve} onDetailAnswer={patch=>onChange({...node,data:{...node.data,...patch,...(patch.config?{config:{...node.data.config,...patch.config}}:{})}})}/>
|
|
503
509
|
<details className="component-settings"><summary>Component settings</summary><div className="fields">
|
|
504
510
|
<label>
|
|
@@ -590,7 +596,7 @@ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewCon
|
|
|
590
596
|
</button>
|
|
591
597
|
</div>
|
|
592
598
|
<div className={`detail-col ${probe ? "probed" : ""}`}>
|
|
593
|
-
<h3
|
|
599
|
+
<h3 aria-label="Outputs" title="Outputs"><ArrowRightFromLine size={20}/></h3>
|
|
594
600
|
{outputs.length ? (
|
|
595
601
|
outputs.map((e) => (
|
|
596
602
|
<button key={e.id} className="terminal" onClick={() => onOpen(e.target)}>
|
|
@@ -640,7 +646,7 @@ const welcome = (project) => ({
|
|
|
640
646
|
role: "assistant",
|
|
641
647
|
text: project.nodes.length || project.brief?.application?.trim()
|
|
642
648
|
? `Good to see you again — let's keep going.\n\n${opening(project)}`
|
|
643
|
-
: `
|
|
649
|
+
: `What would you like to build?`,
|
|
644
650
|
});
|
|
645
651
|
|
|
646
652
|
const THEME = "atmem-architect-theme";
|
|
@@ -654,6 +660,8 @@ function savedTheme() {
|
|
|
654
660
|
}
|
|
655
661
|
document.documentElement.dataset.theme = savedTheme();
|
|
656
662
|
|
|
663
|
+
function readChatView(id){try{const value=JSON.parse(localStorage.getItem('atmem-chat-view:'+id)||'null');return value&&Number.isSafeInteger(value.start)&&value.start>=0&&Array.isArray(value.turnIds)?value:{start:0,turnIds:[]};}catch{return {start:0,turnIds:[]};}}
|
|
664
|
+
const pricingSignature=p=>JSON.stringify([p.id,p.estimate?.projectSize||'small',p.estimate?.usage,p.facts,p.decisions?.filter(d=>d.status==='Accepted'&&['cloud.region','data.country'].includes(d.subject)),[...storageNodes(p),...lambdaNodes(p),...googleNodes(p)].map(n=>[n.id,n.data.provider,n.data.service,n.data.config])]);
|
|
657
665
|
function App() {
|
|
658
666
|
const [project, setProject] = useState(readSaved);
|
|
659
667
|
const projectRef = useRef(project);
|
|
@@ -676,9 +684,9 @@ function App() {
|
|
|
676
684
|
[level, setLevel] = useState(initialCanvasView.level),
|
|
677
685
|
[focus, setFocus] = useState(initialCanvasView.focus),
|
|
678
686
|
[selected, setSelected] = useState(initialCanvasView.selected),
|
|
679
|
-
[messages,
|
|
687
|
+
[messages, setMessagesState] = useState(() => conversationMessages(projectRef.current, [welcome(projectRef.current)])),
|
|
680
688
|
[reviewView, setReviewView] = useState("findings"),
|
|
681
|
-
[input, setInput] = useState(
|
|
689
|
+
[input, setInput] = useState(()=>{try{return localStorage.getItem('atmem-chat-draft:'+projectRef.current.id)||'';}catch{return '';}}),
|
|
682
690
|
[pending, setPending] = useState(null),
|
|
683
691
|
[busy, setBusy] = useState(false),
|
|
684
692
|
[scenario, setScenario] = useState("happy"),
|
|
@@ -699,6 +707,19 @@ function App() {
|
|
|
699
707
|
[probe, setProbe] = useState(null),
|
|
700
708
|
[snapshot, setSnapshot] = useState(null),
|
|
701
709
|
[measured, setMeasured] = useState({});
|
|
710
|
+
const setMessages=useCallback(updater=>setMessagesState(previous=>{
|
|
711
|
+
const next=typeof updater==='function'?updater(previous):updater;
|
|
712
|
+
if(typeof updater!=='function')return next;
|
|
713
|
+
return next.map((m,i)=>m.createdAt||previous.includes(m)||(i<previous.length&&previous[i]?.role===m.role&&previous[i]?.text===m.text)?m:{...m,createdAt:new Date().toISOString()});
|
|
714
|
+
}),[]);
|
|
715
|
+
const draftOwner=useRef(project.id);
|
|
716
|
+
useEffect(()=>{
|
|
717
|
+
if(draftOwner.current!==project.id){draftOwner.current=project.id;try{setInput(localStorage.getItem('atmem-chat-draft:'+project.id)||'');}catch{setInput('');}return;}
|
|
718
|
+
try{localStorage.setItem('atmem-chat-draft:'+project.id,input);}catch{}
|
|
719
|
+
},[project.id,input]);
|
|
720
|
+
const [chatView,setChatView]=useState(()=>readChatView(project.id));
|
|
721
|
+
useEffect(()=>setChatView(readChatView(project.id)),[project.id]);
|
|
722
|
+
function changeChatView(value){setChatView(value);try{localStorage.setItem('atmem-chat-view:'+project.id,JSON.stringify(value));}catch{}}
|
|
702
723
|
const messagesRef=useRef(messages);messagesRef.current=messages;
|
|
703
724
|
const [session, setSession] = useState(null),
|
|
704
725
|
[connection, setConnection] = useState(readConnection),
|
|
@@ -718,6 +739,36 @@ function App() {
|
|
|
718
739
|
useEffect(()=>{saveCanvasView(project,{level,focus,selected,positions:topologyPositions});},[project.id,level,focus,selected,topologyPositions]);
|
|
719
740
|
const emulation=useEmulation(project,environment,session);
|
|
720
741
|
const emulationWorking=emulation.running||(agentBusy&&!!emulation.result);
|
|
742
|
+
const [pricingState,setPricingState]=useState({busy:false,errors:[]});
|
|
743
|
+
const [generationPending,setGenerationPending]=useState(false);
|
|
744
|
+
const [checking,setChecking]=useState(false),[checkReceipt,setCheckReceipt]=useState(null);
|
|
745
|
+
const engineeringStatus=useEngineeringStatus(session,project.id);
|
|
746
|
+
const [architectAction,setArchitectAction]=useState('Architect is working…');
|
|
747
|
+
const pricingRequest=useRef(0);
|
|
748
|
+
const pricingAttempt=useRef(new Set());
|
|
749
|
+
const pricingKey=pricingSignature(project);
|
|
750
|
+
async function refreshEstimate(selected){
|
|
751
|
+
if(!session?.token)return;
|
|
752
|
+
const requested=selected.estimate?.projectSize?selected:setProjectSize(selected,'small');
|
|
753
|
+
if(requested!==selected)updateEngagement(requested);
|
|
754
|
+
const request=++pricingRequest.current;pricingAttempt.current.add(pricingSignature(requested));
|
|
755
|
+
const baseline=JSON.stringify([requested.estimate?.prices,requested.estimate?.details]);
|
|
756
|
+
const identity=JSON.stringify([requested.id,requested.estimate?.projectSize,requested.estimate?.usage,requested.facts,requested.nodes]);
|
|
757
|
+
setPricingState({busy:true,errors:[]});
|
|
758
|
+
try{
|
|
759
|
+
const result=await fetchPlanningPrices(session,requested),live=projectRef.current;
|
|
760
|
+
if(request!==pricingRequest.current)return;
|
|
761
|
+
if(JSON.stringify([live.id,live.estimate?.projectSize,live.estimate?.usage,live.facts,live.nodes])!==identity||JSON.stringify([live.estimate?.prices,live.estimate?.details])!==baseline){setPricingState({busy:false,errors:[]});return;}
|
|
762
|
+
// Merge only the fetched pricing fields; preserve concurrent design and conversation edits.
|
|
763
|
+
updateEngagement({...live,estimate:{...live.estimate,prices:result.project.estimate?.prices,details:result.project.estimate?.details}});
|
|
764
|
+
setPricingState({busy:false,errors:result.errors});
|
|
765
|
+
}catch(error){if(request===pricingRequest.current)setPricingState({busy:false,errors:[{reason:error.message}]});}
|
|
766
|
+
}
|
|
767
|
+
useEffect(()=>{
|
|
768
|
+
if(!session?.token||!project.nodes.length)return;
|
|
769
|
+
if(pricingAttempt.current.has(pricingKey))return;
|
|
770
|
+
pricingAttempt.current.add(pricingKey);void refreshEstimate(project);
|
|
771
|
+
},[mode,session?.token,pricingKey]);
|
|
721
772
|
const [chatWidth,setChatWidth]=useState(()=>{try{const n=Number(localStorage.getItem('atmem-architect-chat-width'));return Number.isFinite(n)&&n>=320&&n<=900?n:360;}catch{return 360;}});
|
|
722
773
|
function resizeChat(width){setChatWidth(width);try{localStorage.setItem('atmem-architect-chat-width',String(width));}catch{}}
|
|
723
774
|
const [dismissedStarts,setDismissedStarts]=useState(()=>{try{const ids=JSON.parse(localStorage.getItem('atforma-dismissed-starts-v1')||'[]');return Array.isArray(ids)?ids.filter(id=>typeof id==='string').slice(-64):[];}catch{return [];}});
|
|
@@ -1239,7 +1290,7 @@ function App() {
|
|
|
1239
1290
|
setInput("");
|
|
1240
1291
|
}
|
|
1241
1292
|
if(origin==='chat'){setPending(null);if(!intakeOptions.alreadyLogged)setAttachments([]);}
|
|
1242
|
-
|
|
1293
|
+
setChatOpen(true);
|
|
1243
1294
|
if (!connection) {
|
|
1244
1295
|
setRequestState(requestIds,'failed');
|
|
1245
1296
|
setModal("welcome");
|
|
@@ -1261,6 +1312,7 @@ function App() {
|
|
|
1261
1312
|
const controller = new AbortController();
|
|
1262
1313
|
agentAbort.current = controller;
|
|
1263
1314
|
chatScroll.followLatest();
|
|
1315
|
+
setArchitectAction(origin==='block-review'?`Reviewing ${reviewTarget?.label||'the block'}…`:origin==='chat'?'Architect is working…':'Updating the design…');
|
|
1264
1316
|
setAgentBusy(true);
|
|
1265
1317
|
const activityId=session.architectureActivityEnabled?crypto.randomUUID():null;
|
|
1266
1318
|
try {
|
|
@@ -1528,7 +1580,8 @@ function App() {
|
|
|
1528
1580
|
};
|
|
1529
1581
|
const downloadBundle = bundle => {
|
|
1530
1582
|
download(zipSync(Object.fromEntries(Object.entries(bundle.files).map(([name,content])=>[name,typeof content === "string"?strToU8(content):content]))),bundle.filename,"application/zip");
|
|
1531
|
-
|
|
1583
|
+
if(bundle.project?.id===projectRef.current.id)updateEngagement(acknowledgeStage(projectRef.current,'delivery'));
|
|
1584
|
+
notify("ZIP downloaded.");
|
|
1532
1585
|
};
|
|
1533
1586
|
const exportPack = async () => {
|
|
1534
1587
|
try {
|
|
@@ -1764,14 +1817,28 @@ function App() {
|
|
|
1764
1817
|
const onBoard = testing || (mode === "design" && designView === "board");
|
|
1765
1818
|
const diff = pending?.next ? proposalDiff(project, pending.next) : null;
|
|
1766
1819
|
|
|
1820
|
+
const specificationChecks=(
|
|
1821
|
+
<ReviewView expanded
|
|
1822
|
+
project={project}
|
|
1823
|
+
findings={findings}
|
|
1824
|
+
onChange={updateEngagement}
|
|
1825
|
+
onOpen={(id) => {
|
|
1826
|
+
setMode("design");
|
|
1827
|
+
setDesignView("board");
|
|
1828
|
+
openBlock(id);
|
|
1829
|
+
}}
|
|
1830
|
+
onAddRequirement={(category) => {
|
|
1831
|
+
setMode("design");setDesignView("requirements");setChatOpen(true);
|
|
1832
|
+
send(`Help us establish the ${category} requirement for this architecture. Explain why it matters and ask for the target we actually need; do not insert a default SLA.`,"workflow",{mode:"design",view:"requirements"});
|
|
1833
|
+
}}
|
|
1834
|
+
/>
|
|
1835
|
+
);
|
|
1767
1836
|
return (
|
|
1768
1837
|
<div style={{"--chat-panel-width":`${chatWidth}px`}} className={`app ${chatOpen&&!buildView ? "" : "chat-closed"} ${buildView?"build-workspace":""}`}>
|
|
1769
1838
|
<header className="bar">
|
|
1770
|
-
<button className="brand" onClick={() => setModal("about")} title="About AtMem AI Architect">
|
|
1839
|
+
<button aria-label="About AtMem AI Architect" className="brand" onClick={() => setModal("about")} title="About AtMem AI Architect">
|
|
1771
1840
|
<Mark />
|
|
1772
|
-
|
|
1773
|
-
AtMem.ai<span className="brand-divider">|</span>AI Architect
|
|
1774
|
-
</span>
|
|
1841
|
+
|
|
1775
1842
|
</button>
|
|
1776
1843
|
<nav className="crumbs" aria-label="Level"><span className="workspace-label">PROJECT</span>
|
|
1777
1844
|
<button className="projects-btn" aria-label="Projects" title="Your projects" onClick={() => (refreshProjects(), setModal("projects"))}>
|
|
@@ -1815,10 +1882,10 @@ function App() {
|
|
|
1815
1882
|
<button role="menuitem" onClick={() => setModal("templates")} disabled={busy}><LayoutTemplate size={16}/>Templates</button>
|
|
1816
1883
|
<hr/><button role="menuitem" onClick={() => setModal("import")} disabled={busy}><GitBranch size={16}/>Import diagram</button>
|
|
1817
1884
|
<button role="menuitem" onClick={() => setModal("project-file")} disabled={busy}><FolderOpen size={16}/>Open project file</button>
|
|
1818
|
-
<hr/><button role="menuitem" onClick={() => setModal("memory")} disabled={!session}><
|
|
1885
|
+
<hr/><button role="menuitem" onClick={() => setModal("memory")} disabled={!session}><BrainActivity size={18} active={false}/>AtMem memory</button>
|
|
1819
1886
|
<button role="menuitem" onClick={() => setModal("start-over")} disabled={busy || agentBusy}><RotateCcw size={16}/>Start over</button>
|
|
1820
1887
|
<hr />
|
|
1821
|
-
<button role="menuitem" onClick={() => setModal("connections")}><Waypoints size={16}/>
|
|
1888
|
+
<button role="menuitem" onClick={() => setModal("connections")}><Waypoints size={16}/>AI providers</button>
|
|
1822
1889
|
<button role="menuitem" onClick={() => setModal("licenses")}><ShieldCheck size={16}/>Licences</button>
|
|
1823
1890
|
</div>
|
|
1824
1891
|
)}
|
|
@@ -1826,7 +1893,7 @@ function App() {
|
|
|
1826
1893
|
|
|
1827
1894
|
<section className="journey-wizard unified-journey" aria-label="Architecture journey">
|
|
1828
1895
|
<nav className="stage-tabs" role="tablist" aria-label="Project stages">{DESIGN_SECTIONS.map((section,i)=><button key={section.id} role="tab" aria-label={section.label} aria-selected={activeSection.id===section.id} className={completedStages(project)[section.id]?'stage-complete':''} onClick={()=>visitJourney(section.id)} title={completedStages(project)[section.id]?'Working draft acknowledged':'Not yet complete'}><span>{completedStages(project)[section.id]?'✓':i+1}</span>{section.label}</button>)}</nav>
|
|
1829
|
-
{nextAction&&<div className="journey-current"><
|
|
1896
|
+
{nextAction&&<div className="journey-current"><button className="btn primary" disabled={busy||agentBusy||!!pending} onClick={()=>nextAction.kind==='navigate'?visitJourney(nextAction.section):nextAction.kind==='continue'?continueJourney():followJourney()}>{nextAction.label}</button></div>}
|
|
1830
1897
|
</section>
|
|
1831
1898
|
<main
|
|
1832
1899
|
id="canvas"
|
|
@@ -1846,7 +1913,7 @@ function App() {
|
|
|
1846
1913
|
{level!=="block"&&<aside className="board-components component-navigator" data-expanded={treeOpen} aria-label="Board components"><button className="btn" aria-label="Toggle components and layers" aria-expanded={treeOpen} onClick={()=>setTreeOpen(!treeOpen)}><LayoutGrid size={16}/> Components & layers</button>
|
|
1847
1914
|
{treeOpen&&<nav className="board-component-tree" aria-label="Component tree"><strong>{project.name}</strong><div className="tree-views"><button disabled={!blockReviews.length} onClick={()=>{reviewBlock((blockReviews.find(r=>r.status!=='reviewed')||blockReviews[0]).id);}}><CheckCheck size={16}/> Review blocks · {blockReviews.filter(r=>r.status==='reviewed').length}/{blockReviews.length}</button></div>{[...new Map(project.nodes.filter(n=>n.data.kind!=='note').map(n=>[layerOf(n).id,layerOf(n)])).values()].map(layer=><details open key={layer.id}><summary>{layer.label}</summary>{project.nodes.filter(n=>n.data.kind!=='note'&&layerOf(n).id===layer.id).map(n=><button key={n.id} aria-current={selected===n.id?'true':undefined} onClick={()=>{setMode('design');setDesignView('board');setFocus(null);setLevel('flow');setSelected(n.id);setTimeout(()=>fitView({nodes:[{id:n.id}],padding:1,maxZoom:1.2,duration:350}),100);}}><Database size={14}/><span>{n.data.label}<small>{n.data.service||n.data.provider}</small><small className={`tree-review-state ${blockReviews.find(r=>r.id===n.id)?.status}`}>{blockReviews.find(r=>r.id===n.id)?.statusLabel}</small></span></button>)}</details>)}</nav>}
|
|
1848
1915
|
{treeOpen&&<button className="btn" disabled={busy} onClick={()=>setPalette(true)}><Plus size={16}/> Add component</button>}</aside>}
|
|
1849
|
-
{!hasDesign&&!palette&&!dismissedStarts.includes(project.id)&&<section className="board-start" aria-label="Start your project"><button type="button" className="icon board-start-close" aria-label="Dismiss welcome card" title="Dismiss welcome card" onClick={dismissStart}><X size={16}/></button><h2>What would you like to create?</h2><
|
|
1916
|
+
{!hasDesign&&!palette&&!dismissedStarts.includes(project.id)&&<section className="board-start" aria-label="Start your project"><button type="button" className="icon board-start-close" aria-label="Dismiss welcome card" title="Dismiss welcome card" onClick={dismissStart}><X size={16}/></button><h2>What would you like to create?</h2><div className="row"><button className="btn primary" onClick={()=>{dismissStart();setChatOpen(true);document.querySelector('.composer textarea, .chat textarea')?.focus();}}>Start chatting</button><button className="btn" onClick={()=>{setChatOpen(true);requirementsInput.current.click();}}>Upload requirements or diagram</button><button className="btn" onClick={()=>setModal("project-file")}>Open project</button></div><InlineNote label="Supported starting files">Word, PDF, text, Markdown, draw.io, images and Architect project JSON.</InlineNote></section>}
|
|
1850
1917
|
<ReactFlow
|
|
1851
1918
|
nodes={canvasNodes}
|
|
1852
1919
|
edges={canvasEdges}
|
|
@@ -2017,6 +2084,7 @@ function App() {
|
|
|
2017
2084
|
onWorkModelChange={changeArchitectureWork}
|
|
2018
2085
|
onRemove={() => removeNode(selectedNode.id)}
|
|
2019
2086
|
onOpen={(id) => openBlock(id)}
|
|
2087
|
+
onSpec={()=>{visitJourney('scope');setTimeout(()=>document.getElementById('component-spec-'+selectedNode.id)?.scrollIntoView({block:'start'}),0);}}
|
|
2020
2088
|
onBack={goUp}
|
|
2021
2089
|
/>
|
|
2022
2090
|
)}
|
|
@@ -2093,31 +2161,15 @@ function App() {
|
|
|
2093
2161
|
|
|
2094
2162
|
</div>
|
|
2095
2163
|
<div className="review-workspace" hidden={mode!=="review"&&mode!=="estimate"}>
|
|
2096
|
-
<header className="review-intro"><h2>Review
|
|
2097
|
-
<
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
onChange={updateEngagement}
|
|
2103
|
-
onOpen={(id) => {
|
|
2104
|
-
setMode("design");
|
|
2105
|
-
setDesignView("board");
|
|
2106
|
-
openBlock(id);
|
|
2107
|
-
}}
|
|
2108
|
-
onAddRequirement={(category) => {
|
|
2109
|
-
setMode("design");setDesignView("requirements");setChatOpen(true);
|
|
2110
|
-
send(`Help us establish the ${category} requirement for this architecture. Explain why it matters and ask for the target we actually need; do not insert a default SLA.`,"workflow",{mode:"design",view:"requirements"});
|
|
2111
|
-
}}
|
|
2112
|
-
/>
|
|
2113
|
-
)}
|
|
2114
|
-
</details>
|
|
2115
|
-
<details className="review-group"><summary>Costs and usage assumptions</summary>
|
|
2116
|
-
{["review","estimate"].includes(mode) && <EstimateView project={project} result={costs} onChange={updateEngagement} busy={busy||agentBusy||!!pending} onEstimate={(sized)=>{setChatOpen(true);send(`Prepare a rough monthly estimate for the saved design using the ${sized.estimate?.projectSize||'selected'} project-size planning assumptions. Preserve explicit business usage, requirements, provider and region. Use available sourced price tools automatically; never ask the client to research unit prices. Explain the subtotal and excluded charges, and ask at most one material unresolved question. Do not invent rates or treat logical security controls as separately billable services without evidence. Planning usage: ${JSON.stringify(estimate(sized).usage)}. Keep the design unchanged.`,"workflow",null,null,{estimateOnly:true});}} />}
|
|
2117
|
-
</details>
|
|
2118
|
-
{mode==='review'&&session?.engineeringEnabled&&<section className="review-generation" aria-label="Generation and verification"><EngineeringPanel key={project.id} project={project} onWorkModelChange={changeArchitectureWork} onIntentContext={syncEngineeringIntent} projectId={project.id} session={session} connection={connection} contextBusy={busy||agentBusy||!!pending} saveProject={()=>saveEngineeringProject(project.id)} buildRequest={buildRequest} onBuildRequestHandled={id=>setBuildRequest(current=>current?.id===id?null:current)} onConnections={()=>setModal('connections')}/></section>}
|
|
2164
|
+
<header className="review-intro"><h2>Review</h2></header>
|
|
2165
|
+
<div className="review-design-status"><ShieldCheck size={18}/><span>{findings.filter(f=>f.status==='open').length} design items</span><button className="icon" aria-label="Open design checks in specification" title="Specification" onClick={()=>visitJourney('scope')}><Code2 size={16}/></button></div>
|
|
2166
|
+
<section className="review-costs" aria-label="Project costs">
|
|
2167
|
+
{["review","estimate"].includes(mode) && <EstimateView project={project} result={costs} onChange={updateEngagement} busy={busy||agentBusy||!!pending} onEstimate={refreshEstimate} pricing={pricingState} onNext={()=>{const section=document.querySelector('.review-generation');const target=section?.querySelector('button')||section;if(target){target.setAttribute('tabindex','-1');target.focus({preventScroll:true});target.scrollIntoView({block:'center'});}}} />}
|
|
2168
|
+
</section>
|
|
2169
|
+
{mode==='review'&&session?.engineeringEnabled&&<section className="review-generation" aria-label="Generation and verification"><EngineeringPanel key={project.id} project={project} onBusyChange={setGenerationPending} onWorkModelChange={changeArchitectureWork} onIntentContext={syncEngineeringIntent} projectId={project.id} session={session} connection={connection} contextBusy={busy||agentBusy||!!pending} saveProject={()=>saveEngineeringProject(project.id)} buildRequest={buildRequest} onBuildRequestHandled={id=>setBuildRequest(current=>current?.id===id?null:current)} onConnections={()=>setModal('connections')}/></section>}
|
|
2119
2170
|
{mode==='review'&&!session?.engineeringEnabled&&<p>Generation and local checks require the engineering service. Your design is retained; Delivery can package the design and remaining tasks.</p>}
|
|
2120
|
-
{mode==='review'&&<
|
|
2171
|
+
{mode==='review'&&<ProjectChecks receipt={checkReceipt} project={project} environment={environment} session={session} onReceipt={setCheckReceipt} onBusy={setChecking}/>}
|
|
2172
|
+
{mode==='review'&&<footer className="review-finish"><div><strong>3. Package and download</strong><p>Explore all files and download one project ZIP in Delivery.</p></div><button className="btn primary" onClick={()=>{const next=acknowledgeStage(projectRef.current,'review');updateEngagement(costs.complete?{...next,extensions:{...next.extensions,journey:{...next.extensions.journey,estimate:journey(next).keys.estimate}}}:next);visitJourney('delivery');}}>Complete →</button></footer>}
|
|
2121
2173
|
</div>
|
|
2122
2174
|
|
|
2123
2175
|
|
|
@@ -2125,16 +2177,16 @@ function App() {
|
|
|
2125
2177
|
|
|
2126
2178
|
{onBoard&&emulationOpen&&<div className="emulation-dock"><EmulationPanel project={project} session={session} emulation={emulation} onDiscuss={discussEmulation} onOpenComponent={id=>{reviewBlock(id);setEmulationOpen(false);}} discussionBusy={busy||agentBusy||!!pending} onClose={()=>setEmulationOpen(false)} onBuild={session?.engineeringEnabled?()=>{setEmulationOpen(false);buildCurrentDesign('test');}:undefined}/></div>}
|
|
2127
2179
|
|
|
2128
|
-
{mode === "design" && designView === "outputs" && <OutputsView onSave={expected=>saveEngineeringProject(project.id,expected)} project={project} run={run} environment={environment} session={session} emulation={emulation} onDiscuss={discussEmulation} discussionBusy={busy||agentBusy||!!pending} onDownload={downloadBundle} onBuild={session?.engineeringEnabled?()=>visitJourney('review'):undefined} onBack={()=>setDesignView("board")} />}
|
|
2180
|
+
{mode === "design" && designView === "outputs" && <OutputsView onNavigate={target=>{const [stage,selector]={requirements:['scope','.requirements-document'],location:['cloud','section[aria-label="Country and region"]'],cost:['review','.review-costs'],implementation:['review','.review-generation'],checks:['review','.project-checks']}[target];visitJourney(stage);requestAnimationFrame(()=>requestAnimationFrame(()=>{const element=document.querySelector(selector);if(element){element.setAttribute('tabindex','-1');element.focus({preventScroll:true});element.scrollIntoView({block:'start'});}}));}} checkReceipt={checkReceipt} onDownloaded={()=>updateEngagement(acknowledgeStage(projectRef.current,'delivery'))} onReview={()=>visitJourney('review')} onSave={expected=>saveEngineeringProject(project.id,expected)} project={project} run={run} environment={environment} session={session} emulation={emulation} onDiscuss={discussEmulation} discussionBusy={busy||agentBusy||!!pending} onDownload={downloadBundle} onBuild={session?.engineeringEnabled?()=>visitJourney('review'):undefined} onBack={()=>setDesignView("board")} />}
|
|
2129
2181
|
{mode === "design" && designView === "terraform" && <TerraformView project={project} environment={environment} session={session} emulation={emulation} onDiscuss={discussEmulation} discussionBusy={busy||agentBusy||!!pending} onDelivery={()=>visitJourney("delivery")} onBuild={session?.engineeringEnabled?()=>visitJourney('review'):undefined} onBack={()=>setDesignView("board")} />}
|
|
2130
2182
|
<div className="choices-workspace" hidden={!(mode==="design"&&["options","decisions","location"].includes(designView))}>
|
|
2131
2183
|
{mode === "design" && ["options","decisions"].includes(designView) && (
|
|
2132
|
-
<OptionsView project={project} busy={busy||agentBusy||!!pending} onChoose={chooseCloud} onContinue={()=>visitJourney(
|
|
2184
|
+
<OptionsView project={project} busy={busy||agentBusy||!!pending} onChoose={chooseCloud} onContinue={()=>{updateEngagement(acknowledgeStage(projectRef.current,'cloud'));visitJourney('review');}} onPreview={(provider)=>{setDesignView("board");setLevel("flow");setChatOpen(true);send(`Preview a concrete ${OPTION_NAMES[provider]} implementation of our working scope, using actual named services in data.service and explicit connections. Keep existing systems and comments. Use the known country/residency constraints, or leave region undecided and ask the country question. This is an alternative preview, not a cloud choice or deployment. Return a graph proposal and explain key integration assumptions.`,"preview",{mode:"design",view:"board"});}} onDiscuss={(question) => { setChatOpen(true); send(question, "workflow"); }} />
|
|
2133
2185
|
)}
|
|
2134
|
-
{mode === "design" && ["options","decisions","location"].includes(designView) && <
|
|
2186
|
+
{mode === "design" && ["options","decisions","location"].includes(designView) && <LocationChoices project={project} onChange={commit} onDiscuss={()=>{setChatOpen(true);send('Review the accepted cloud, data country and cloud region. Ask one material unresolved location question. Preserve selected regions and residency.','workflow');}}/>}
|
|
2135
2187
|
</div>
|
|
2136
2188
|
{mode === "design" && designView === "requirements" && (
|
|
2137
|
-
<RequirementsView onUpload={()=>{setChatOpen(true);requirementsInput.current.click();}} onOpenProject={()=>setModal("project-file")} project={project} onChange={updateEngagement} reviewing={agentBusy} onReview={(comments)=>{setChatOpen(true);send("Propose revisions for these user comments on the requirements document. The document will remain unchanged until the user applies each suggestion. Explain the proposed change; never say you have already changed the document. Return requirementUpdates for the existing IDs where the feedback is clear, quoting the user's comment exactly in sourceText. Preserve other requirements and the diagram. Ask one question if a comment is ambiguous.\n" + comments.map(c=>`Requirement ${c.requirementId}${c.selectedText ? ` (selected text: ${c.selectedText.slice(0,300)})` : ""} — comment: ${c.text}`).join("\n"), "document");}} onDiscuss={(question) => { setChatOpen(true); send(question, "workflow"); }} />
|
|
2189
|
+
<RequirementsView checks={specificationChecks} onContinue={()=>{updateEngagement(acknowledgeStage(projectRef.current,'scope'));visitJourney('cloud');}} onUpload={()=>{setChatOpen(true);requirementsInput.current.click();}} onOpenProject={()=>setModal("project-file")} project={project} onChange={updateEngagement} reviewing={agentBusy} onReview={(comments)=>{setChatOpen(true);send("Propose revisions for these user comments on the requirements document. The document will remain unchanged until the user applies each suggestion. Explain the proposed change; never say you have already changed the document. Return requirementUpdates for the existing IDs where the feedback is clear, quoting the user's comment exactly in sourceText. Preserve other requirements and the diagram. Ask one question if a comment is ambiguous.\n" + comments.map(c=>`Requirement ${c.requirementId}${c.selectedText ? ` (selected text: ${c.selectedText.slice(0,300)})` : ""} — comment: ${c.text}`).join("\n"), "document");}} onDiscuss={(question) => { setChatOpen(true); send(question, "workflow"); }} />
|
|
2138
2190
|
)}
|
|
2139
2191
|
</main>
|
|
2140
2192
|
|
|
@@ -2142,9 +2194,9 @@ function App() {
|
|
|
2142
2194
|
{chatOpen&&!buildView && (
|
|
2143
2195
|
<aside className="chat" aria-label="Chat"><ChatResizeHandle width={chatWidth} onChange={resizeChat}/>
|
|
2144
2196
|
<div className="chat-head">
|
|
2145
|
-
<div className="chat-heading-row"><strong className="chat-title">
|
|
2146
|
-
<Mark size={
|
|
2147
|
-
</strong><button className="
|
|
2197
|
+
<div className="chat-heading-row"><strong className="chat-title" aria-label="AI Architect">
|
|
2198
|
+
<Mark size={24} />
|
|
2199
|
+
</strong><button className="icon" title="Clear chat view" aria-label="Clear chat view" disabled={busy||agentBusy||intake.length>0||messages.length<=chatView.start} onClick={()=>changeChatView({start:messages.length,turnIds:(project.extensions?.architectureActivity?.turns||[]).map(turn=>turn.id)})}><Trash2 size={16}/></button>{(chatView.start>0||chatView.turnIds.length>0)&&<button className="icon" title="Restore chat history" aria-label="Restore chat history" onClick={()=>changeChatView({start:0,turnIds:[]})}><Undo2 size={16}/></button>}<button className="icon chat-width-toggle" onClick={()=>resizeChat(chatWidth>360?360:Math.max(320,Math.min(720,window.innerWidth-360)))} aria-label={chatWidth>360?"Narrow chat":"Widen chat"} title={chatWidth>360?"Narrow chat":"Widen chat"}>{chatWidth>360?<ChevronsRightLeft size={17}/>:<ChevronsLeftRight size={17}/>}</button><button className="icon" title="Hide chat" aria-label="Hide chat" onClick={()=>setChatOpen(false)}><PanelRightClose size={16}/></button></div>
|
|
2148
2200
|
<label className="live-toggle" title="On: changes are drawn as we talk, and you can undo any of them. Off: they wait as previews for you to apply.">
|
|
2149
2201
|
<input
|
|
2150
2202
|
type="checkbox"
|
|
@@ -2158,13 +2210,13 @@ function App() {
|
|
|
2158
2210
|
/>
|
|
2159
2211
|
Draw as we talk
|
|
2160
2212
|
</label>
|
|
2161
|
-
<button className="model" onClick={() => setModal("connections")} title="
|
|
2213
|
+
<button className="model" onClick={() => setModal("connections")} title="AI providers">
|
|
2162
2214
|
<span className={`led ${connection && session ? "on" : ""}`} />
|
|
2163
2215
|
{connection ? `${connection.model}${connection.effort ? ` · ${EFFORT_LABEL[connection.effort] || connection.effort}` : ""}` : "Connect a model"}
|
|
2164
2216
|
</button>
|
|
2165
2217
|
</div>
|
|
2166
2218
|
<div className="skills" aria-label="Active skills">
|
|
2167
|
-
<button className="known-bar" disabled={!session} aria-label="AtMem memory" title="Open AtMem memory, confirmed decisions and sync activity" onClick={()=>setModal("memory")}><
|
|
2219
|
+
<button className="known-bar" disabled={!session} aria-label="AtMem memory" title="Open AtMem memory, confirmed decisions and sync activity" onClick={()=>setModal("memory")}><BrainActivity size={18} active={false}/>{memoryLabel}<ChevronRight size={14}/></button>
|
|
2168
2220
|
<button
|
|
2169
2221
|
className={`known-bar ${peek === "known" ? "on" : ""}`}
|
|
2170
2222
|
aria-label="Design context" aria-expanded={peek === "known"} title="Review what is known, assumed or still open"
|
|
@@ -2207,14 +2259,14 @@ function App() {
|
|
|
2207
2259
|
)}
|
|
2208
2260
|
<div className="messages" ref={chatScroll.viewport}>
|
|
2209
2261
|
<div className="message-list" ref={chatScroll.content}>
|
|
2210
|
-
{architectureChatEntries(project,messages).map(entry=>{
|
|
2262
|
+
{architectureChatEntries(project,messages).filter(entry=>entry.kind==='message'?entry.index>=chatView.start:entry.kind==='activity'?!chatView.turnIds.includes(entry.turn.id):entry.kind==='legacy-activity'?entry.entries.some(item=>!chatView.turnIds.includes(item.turn.id)):true).map(entry=>{
|
|
2211
2263
|
if(entry.kind==='activity-error')return <p key="activity-error" role="alert">{entry.error}</p>;
|
|
2212
2264
|
if(entry.kind==='legacy-activity')return <EarlierArchitectureActivity key={entry.id} entries={entry.entries} projectId={project.id} session={session}/>;
|
|
2213
2265
|
if(entry.kind==='activity')return <ArchitectureTurn key={project.id+':'+entry.turn.id} turn={entry.turn} number={entry.number} projectId={project.id} session={session} active={agentBusy&&entry.turn.clientStatus==='submitted'&&entry.turn.id===latestArchitectureTurn?.id} working={agentBusy&&entry.turn.clientStatus==='submitted'} onStop={()=>agentAbort.current?.abort()}/>;
|
|
2214
|
-
const m=entry.message,i=entry.index;
|
|
2266
|
+
const m=entry.message,i=entry.index,turn=project.extensions?.architectureActivity?.turns?.find(t=>t.id===m.activityId),messageTime=m.createdAt||(m.role==='assistant'?turn?.completedAt:null)||turn?.startedAt;
|
|
2215
2267
|
return (
|
|
2216
2268
|
<div key={i} className={`msg ${m.role}`}>
|
|
2217
|
-
<ChatMessage text={m.text}/>
|
|
2269
|
+
<header className="message-meta"><span>{m.role==='user'?'You':'Architect'}</span><time dateTime={messageTime||undefined} title={messageTime?new Date(messageTime).toLocaleString():'No timestamp recorded'}>{messageTime?new Date(messageTime).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}):'—'}</time></header><ChatMessage text={m.text} concise={m.role==='assistant'}/>
|
|
2218
2270
|
{(m.workState==='queued'||m.workState==='processing'&&intake.some(q=>q.id===m.requestId))&&<small className="message-work-state">Queued for the next update <button className="link" onClick={()=>{updateIntake(intakeRef.current.filter(q=>q.id!==m.requestId));setRequestState([m.requestId],'cancelled');}}>Remove from queue</button><button className="link" onClick={()=>{if(input||attachments.length){notify('Your current draft is preserved. Send or clear it before editing a queued update.');return;}const queued=intakeRef.current.find(q=>q.id===m.requestId);if(!queued)return;setInput(queued.text);setAttachments(queued.attachments);updateIntake(intakeRef.current.filter(q=>q.id!==m.requestId));setRequestState([m.requestId],'cancelled');}}>Edit update</button>{m.attachments?.some(a=>!a.data)&&<button className="link" onClick={()=>{updateIntake(intakeRef.current.map(q=>q.id===m.requestId?{...q,attachments:[]}:q));setIntakePaused(false);}}>Continue with text only</button>}</small>}
|
|
2219
2271
|
{!!m.attachments?.length&&<div className="sent-attachments">{m.attachments.map(a=><figure key={a.id}>{a.data&&a.mime.startsWith('image/')&&<img src={`data:${a.mime};base64,${a.data}`} alt={a.name}/>}<figcaption><Paperclip size={12}/>{a.name}{a.originalNotSaved && <small>Original file not saved; attach it again to review its contents.</small>}</figcaption></figure>)}</div>}
|
|
2220
2272
|
{m.fix && (
|
|
@@ -2260,15 +2312,9 @@ function App() {
|
|
|
2260
2312
|
)}
|
|
2261
2313
|
</div>
|
|
2262
2314
|
</div>
|
|
2315
|
+
{chatScroll.away&&<button className="chat-jump-latest" aria-label="Jump to latest message" onClick={chatScroll.followLatest}>↓</button>}
|
|
2263
2316
|
{!!intake.filter(q=>q.projectId===project.id).length&&<div className="intake-status" role="status">{intake.filter(q=>q.projectId===project.id).length} update(s) {intakePaused?'saved · paused':'queued · combining next'}{intakeIssue&&<span> {intakeIssue}</span>}{intakePaused&&<button className="link" onClick={()=>setIntakePaused(false)}>Resume</button>}</div>}
|
|
2264
2317
|
<div className="prompts">
|
|
2265
|
-
{mode === "design" && brief(project) && ask && (
|
|
2266
|
-
<>
|
|
2267
|
-
<button disabled={busy || agentBusy} onClick={() => send("Not sure")}>Not sure</button>
|
|
2268
|
-
<button onClick={() => setMessages((m) => [...m, { role: "assistant", text: `Why I ask: ${ask.why || "it shapes the design."}` }])}>Why?</button>
|
|
2269
|
-
<button onClick={skipQuestion}>Skip</button>
|
|
2270
|
-
</>
|
|
2271
|
-
)}
|
|
2272
2318
|
{mode === "design" && project.workflow && !project.nodes.length && !pending && (
|
|
2273
2319
|
<button className="primary-chip" disabled={busy || agentBusy} onClick={sketchIt}>Sketch it →</button>
|
|
2274
2320
|
)}
|
|
@@ -2278,6 +2324,7 @@ function App() {
|
|
|
2278
2324
|
</button>
|
|
2279
2325
|
))}
|
|
2280
2326
|
</div>
|
|
2327
|
+
{(agentBusy||generationPending||['starting','running'].includes(engineeringStatus?.state)||pricingState.busy||checking)&&<div className="chat-working-status" role="status" aria-live="polite"><BrainActivity size={26}/><span>{agentBusy?architectAction:generationPending?'Starting project generation…':['starting','running'].includes(engineeringStatus?.state)?'Generating project files…':checking?'Running checks…':'Calculating costs…'}</span></div>}
|
|
2281
2328
|
{readingAttachments&&<p className="attachment-status" role="status"><BrainActivity/> Reading files…</p>}
|
|
2282
2329
|
{attachmentError&&<p className="attachment-status" role="alert">{attachmentError}</p>}
|
|
2283
2330
|
{!!attachments.length&&<div className="attachment-tray" aria-label="Attachments ready to send">{attachments.map(a=><div className="attachment-chip" key={a.id}>{a.data&&a.mime.startsWith('image/')?<img src={`data:${a.mime};base64,${a.data}`} alt=""/>:<Paperclip size={16}/>}<span>{a.name}<small>{!a.data?'Original missing · attach again':a.mime.startsWith('image/')?'Image':a.mime==='application/pdf'||a.mime.includes('wordprocessingml')?'Text will be extracted; embedded images omitted':'Document text'}</small></span><button className="icon" aria-label={`Remove ${a.name}`} onClick={()=>setAttachments(list=>list.filter(item=>item.id!==a.id))}><X size={14}/></button></div>)}<p>Sent to your selected architect with this message. Original files are not stored in project files.</p></div>}
|
|
@@ -2342,11 +2389,11 @@ function App() {
|
|
|
2342
2389
|
)}
|
|
2343
2390
|
{modal && (
|
|
2344
2391
|
<div className="backdrop" onMouseDown={(e) => e.target === e.currentTarget && !(modal === "welcome" && !connection) && setModal(null)}>
|
|
2345
|
-
<section className="modal" role="dialog" aria-modal="true" aria-label={modal}>
|
|
2392
|
+
<section className={`modal ${modal==="memory"?"memory-modal":""}`} role="dialog" aria-modal="true" aria-label={modal}>
|
|
2346
2393
|
{!(modal === "welcome" && !connection) && <button className="icon modal-close" onClick={() => setModal(null)} aria-label="Close dialog">
|
|
2347
2394
|
<X size={16} />
|
|
2348
2395
|
</button>}
|
|
2349
|
-
{modal === "project-file" ? (<><h2>
|
|
2396
|
+
{modal === "project-file" ? (<><h2>Open project</h2><button className="btn primary" onClick={()=>{fileImportAsCopy.current=false;fileRef.current.click();}}>Open project</button></>) : modal === "memory" ? (
|
|
2350
2397
|
<MemoryPanel key={project.id} session={session} project={project} onError={notify} onStatus={status=>setMemorySync({projectId:project.id,...status})} />
|
|
2351
2398
|
) : modal === "templates" ? (
|
|
2352
2399
|
<TemplateDialog onLoad={loadDesign} />
|
|
@@ -2362,7 +2409,7 @@ function App() {
|
|
|
2362
2409
|
environment={environment}
|
|
2363
2410
|
/>
|
|
2364
2411
|
) : modal === "projects" ? (
|
|
2365
|
-
<div><div className="row"><button className="btn" onClick={()=>setModal("project-file")}>Open
|
|
2412
|
+
<div><div className="row"><button className="btn" onClick={()=>setModal("project-file")}>Open project</button><button className="btn" onClick={saveProjectFile}>Save project</button></div><ProjectsDialog
|
|
2366
2413
|
list={saved}
|
|
2367
2414
|
folder={saved.folder}
|
|
2368
2415
|
currentId={project.id}
|
|
@@ -2412,36 +2459,12 @@ function App() {
|
|
|
2412
2459
|
/>
|
|
2413
2460
|
) : modal === "licenses" ? (
|
|
2414
2461
|
<>
|
|
2415
|
-
<h2>
|
|
2416
|
-
<dl className="licences">
|
|
2417
|
-
<dt>OpenArchFlow</dt>
|
|
2418
|
-
<dd>
|
|
2419
|
-
MIT · © 2026 Rafael Sales. Catalogues, templates, import parsers, layout, payload formatting.{" "}
|
|
2420
|
-
<a href="/licenses/openarchflow-LICENSE.txt" target="_blank" rel="noreferrer">Licence</a>
|
|
2421
|
-
</dd>
|
|
2422
|
-
<dt>AWS GenAI Solutions Architect Agent</dt>
|
|
2423
|
-
<dd>
|
|
2424
|
-
MIT-0. Agent composition, retry and rate limiting in the sidecar; prompt replaced with a cloud-neutral one.{" "}
|
|
2425
|
-
<a href="/licenses/aws-architect-LICENSE.txt" target="_blank" rel="noreferrer">Licence</a>
|
|
2426
|
-
</dd>
|
|
2427
|
-
<dt>AWS Labs MCP servers</dt>
|
|
2428
|
-
<dd>
|
|
2429
|
-
Apache-2.0. Manifest only; not bundled or executed.{" "}
|
|
2430
|
-
<a href="/licenses/aws-mcp-LICENSE.txt" target="_blank" rel="noreferrer">Licence</a>
|
|
2431
|
-
</dd>
|
|
2432
|
-
</dl>
|
|
2433
|
-
<a href="/licenses/dependency-notices.txt" target="_blank" rel="noreferrer">Dependency notices</a>
|
|
2434
|
-
<p className="modal-fine">Cloud names identify design concepts. No vendor endorsement or icon packs.</p>
|
|
2462
|
+
<h2>AtMem</h2><a href="/licenses/atmem-APACHE-2.0.txt" target="_blank" rel="noreferrer">Apache 2.0</a>
|
|
2435
2463
|
</>
|
|
2436
2464
|
) : (
|
|
2437
2465
|
<>
|
|
2438
2466
|
<h2>AtMem AI Architect</h2>
|
|
2439
|
-
<p className="muted">
|
|
2440
|
-
<p>
|
|
2441
|
-
Start in Design to describe your project and develop its architecture. Follow Architecture activity while it works, then choose Build this design to create and check its implementation package.
|
|
2442
|
-
Part of atmem.ai. Runs on your computer; nothing is deployed.
|
|
2443
|
-
</p>
|
|
2444
|
-
<p className="muted">Keys: / add block · Esc up a level · ⌘↵ run</p>
|
|
2467
|
+
<p className="muted">v{appVersion}</p><InlineNote label="Keyboard shortcuts">/ Add block · Esc Board · ⌘↵ Run</InlineNote>
|
|
2445
2468
|
</>
|
|
2446
2469
|
)}
|
|
2447
2470
|
</section>
|
package/src/output-bundle.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import {verifiedReceipt} from './validation-receipt.mjs';
|
|
1
2
|
import {deliveryPack} from './delivery.mjs';
|
|
2
|
-
export async function outputBundle(project,run,environment){
|
|
3
|
+
export async function outputBundle(project,run,environment,checks){
|
|
3
4
|
const snapshot=structuredClone(project);
|
|
4
5
|
const files=deliveryPack(snapshot,run,environment);
|
|
5
6
|
const {visualFiles}=await import('./visual-export.mjs');
|
|
6
7
|
Object.assign(files,await visualFiles(snapshot,files['specification.md'],files['spec-kit/implementation-spec.md']));
|
|
8
|
+
if(checks){const terraform=Object.fromEntries(Object.entries(files).filter(([name])=>name.startsWith('terraform/')).map(([name,value])=>[name.slice(10),value]));const receipt=await verifiedReceipt(terraform,checks);if(receipt)files['terraform/validation-receipt.json']=JSON.stringify(receipt,null,2);}
|
|
7
9
|
const manifest=JSON.parse(files['manifest.json']);manifest.files=Object.keys(files);
|
|
8
10
|
files['manifest.json']=JSON.stringify(manifest,null,2);
|
|
9
11
|
const slug=snapshot.name.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'')||'atflow';
|
|
10
|
-
return {files,manifest,filename:`${slug}-r${snapshot.revision}-delivery.zip`};
|
|
12
|
+
return {project,files,manifest,filename:`${slug}-r${snapshot.revision}-delivery.zip`};
|
|
11
13
|
}
|