@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/main.jsx
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import {StarPrompt,STAR_URL} from './StarPrompt.jsx';
|
|
2
|
+
import {prefillAcceptedSettings} from './engineering-defaults.mjs';
|
|
3
|
+
import {LocationChoices} from './LocationChoices.jsx';
|
|
4
|
+
import {InlineNote} from './HelpNote.jsx';
|
|
5
|
+
import {ProjectChecks} from './ProjectChecks.jsx';
|
|
6
|
+
import {useEngineeringStatus} from './useEngineeringStatus.mjs';
|
|
1
7
|
import {ChatResizeHandle} from './ChatResizeHandle.jsx';
|
|
2
8
|
import {copyProject} from './project-copy.mjs';
|
|
3
9
|
import './journey-workspace.css';
|
|
4
10
|
import {synchronizeEngineeringIntent,applyArchitectureChange} from './architecture-work.mjs';
|
|
5
11
|
import {applyArtifactWork} from './artifact-work.mjs';
|
|
6
|
-
import {ArchitectureWorkPanel} from './ArchitectureWorkPanel.jsx';
|
|
7
12
|
import {hasArchitectureDesign} from './design-build-flow.mjs';
|
|
8
13
|
import {ArchitectureTurn,EarlierArchitectureActivity} from './ArchitectureActivity.jsx';
|
|
9
14
|
import {recordArchitectureTurn,finishArchitectureTurn,architectureChatEntries} from './architecture-activity.mjs';
|
|
@@ -33,7 +38,7 @@ import {OpenAIVoicePanel} from './OpenAIVoicePanel.jsx';
|
|
|
33
38
|
import { OutputsView } from "./OutputsView.jsx";
|
|
34
39
|
import { outputBundle } from "./output-bundle.mjs";
|
|
35
40
|
import { TerraformView } from "./TerraformView.jsx";
|
|
36
|
-
import { journey, advanceJourney, DESIGN_SECTIONS,journeySectionId,completedStages, designSection, journeyAction } from "./journey.mjs";
|
|
41
|
+
import { journey, advanceJourney, acknowledgeStage, DESIGN_SECTIONS,journeySectionId,completedStages, designSection, journeyAction } from "./journey.mjs";
|
|
37
42
|
import "@xyflow/react/dist/style.css";
|
|
38
43
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
39
44
|
import { createRoot } from "react-dom/client";
|
|
@@ -59,6 +64,8 @@ import {
|
|
|
59
64
|
ChevronRight,
|
|
60
65
|
GitBranch,
|
|
61
66
|
Maximize2,
|
|
67
|
+
ChevronsLeftRight,
|
|
68
|
+
ChevronsRightLeft,
|
|
62
69
|
MoreHorizontal,
|
|
63
70
|
PanelRightClose,
|
|
64
71
|
PanelRightOpen,
|
|
@@ -99,6 +106,8 @@ import {
|
|
|
99
106
|
Eraser,
|
|
100
107
|
LayoutGrid,
|
|
101
108
|
ArrowLeft,
|
|
109
|
+
ArrowRightToLine,
|
|
110
|
+
ArrowRightFromLine,
|
|
102
111
|
FolderOpen,
|
|
103
112
|
Moon,
|
|
104
113
|
Sun,
|
|
@@ -123,12 +132,12 @@ import { withDerived } from "./derive.mjs";
|
|
|
123
132
|
import { MemoryPanel, ContextUsed } from "./MemoryPanel.jsx";
|
|
124
133
|
import { defaultAssessment } from "./engagement.mjs";
|
|
125
134
|
import { setUsage } from "./estimate.mjs";
|
|
126
|
-
import { applyPriceQuotes } from "./pricing.mjs";
|
|
135
|
+
import { applyPriceQuotes, fetchPlanningPrices, storageNodes, lambdaNodes, googleNodes } from "./pricing.mjs";
|
|
127
136
|
import { applyDecisionUpdates, applyRequirementDrafts, proposeCommentRevisions } from "./consultation.mjs";
|
|
128
137
|
import { terraformSketch } from "./terraform.mjs";
|
|
129
138
|
import { WORKFLOWS, conceptSignature, essentialsKnown, nextQuestion, opening, progress, sketch, understand } from "./discovery.mjs";
|
|
130
139
|
import { review } from "./review.mjs";
|
|
131
|
-
import { estimate } from "./estimate.mjs";
|
|
140
|
+
import { setProjectSize, estimate } from "./estimate.mjs";
|
|
132
141
|
import { DeployView, EstimateView, ProjectsDialog, ReviewView, TestCard } from "./JourneyViews.jsx";
|
|
133
142
|
import { newRequirement } from "./records.mjs";
|
|
134
143
|
import { DiscoverView, OptionsView, DecisionsView, MeasuresEditor, RequirementsView } from "./EngagementViews.jsx";
|
|
@@ -289,7 +298,7 @@ function BlockNode({ data, selected }) {
|
|
|
289
298
|
</span>
|
|
290
299
|
<span className="blk-text">
|
|
291
300
|
<strong>{data.label}</strong>
|
|
292
|
-
{data.
|
|
301
|
+
{data.service&&<small className="blk-service">{data.service}</small>}
|
|
293
302
|
{(data.origin === "existing" || data.provider !== "tools") && (
|
|
294
303
|
<small>
|
|
295
304
|
<i className="pdot" />
|
|
@@ -452,7 +461,7 @@ function Linked({ project, id, hideNeeds = false }) {
|
|
|
452
461
|
);
|
|
453
462
|
}
|
|
454
463
|
|
|
455
|
-
function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewConfirm, onReviewDiscuss, onReviewQuestion, onReviewResolve, onRequirementSave, onWorkModelChange, onChange, onRemove, onOpen, onBack }) {
|
|
464
|
+
function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewConfirm, onReviewDiscuss, onReviewQuestion, onReviewResolve, onRequirementSave, onWorkModelChange, onChange, onRemove, onOpen, onBack,onSpec }) {
|
|
456
465
|
const trace = run?.steps.find((s) => s.id === node.id);
|
|
457
466
|
const inputs = project.edges.filter((e) => e.target === node.id);
|
|
458
467
|
const outputs = project.edges.filter((e) => e.source === node.id);
|
|
@@ -464,11 +473,11 @@ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewCon
|
|
|
464
473
|
<section className="detail" aria-label="Block detail">
|
|
465
474
|
<div className="detail-navigation">
|
|
466
475
|
<button className="back" onClick={onBack} aria-label="Back to the board">
|
|
467
|
-
<ArrowLeft size={15}
|
|
476
|
+
<ArrowLeft size={15} /><kbd>Esc</kbd>
|
|
468
477
|
</button>
|
|
469
478
|
</div>
|
|
470
479
|
<div className="detail-col">
|
|
471
|
-
<h3
|
|
480
|
+
<h3 aria-label="Inputs" title="Inputs"><ArrowRightToLine size={20}/></h3>
|
|
472
481
|
{inputs.length ? (
|
|
473
482
|
inputs.map((e) => (
|
|
474
483
|
<button key={e.id} className="terminal" onClick={() => onOpen(e.source)}>
|
|
@@ -496,9 +505,8 @@ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewCon
|
|
|
496
505
|
onChange={(e) => set({ label: e.target.value })}
|
|
497
506
|
/>
|
|
498
507
|
</div>
|
|
499
|
-
<
|
|
508
|
+
<button className="link" onClick={onSpec}>Specification ↗</button>
|
|
500
509
|
<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
510
|
<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
511
|
<details className="component-settings"><summary>Component settings</summary><div className="fields">
|
|
504
512
|
<label>
|
|
@@ -590,7 +598,7 @@ function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewCon
|
|
|
590
598
|
</button>
|
|
591
599
|
</div>
|
|
592
600
|
<div className={`detail-col ${probe ? "probed" : ""}`}>
|
|
593
|
-
<h3
|
|
601
|
+
<h3 aria-label="Outputs" title="Outputs"><ArrowRightFromLine size={20}/></h3>
|
|
594
602
|
{outputs.length ? (
|
|
595
603
|
outputs.map((e) => (
|
|
596
604
|
<button key={e.id} className="terminal" onClick={() => onOpen(e.target)}>
|
|
@@ -640,7 +648,7 @@ const welcome = (project) => ({
|
|
|
640
648
|
role: "assistant",
|
|
641
649
|
text: project.nodes.length || project.brief?.application?.trim()
|
|
642
650
|
? `Good to see you again — let's keep going.\n\n${opening(project)}`
|
|
643
|
-
: `
|
|
651
|
+
: `What would you like to build?`,
|
|
644
652
|
});
|
|
645
653
|
|
|
646
654
|
const THEME = "atmem-architect-theme";
|
|
@@ -654,7 +662,10 @@ function savedTheme() {
|
|
|
654
662
|
}
|
|
655
663
|
document.documentElement.dataset.theme = savedTheme();
|
|
656
664
|
|
|
665
|
+
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:[]};}}
|
|
666
|
+
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
667
|
function App() {
|
|
668
|
+
const [starOffer,setStarOffer]=useState(0);
|
|
658
669
|
const [project, setProject] = useState(readSaved);
|
|
659
670
|
const projectRef = useRef(project);
|
|
660
671
|
projectRef.current = project;
|
|
@@ -676,9 +687,9 @@ function App() {
|
|
|
676
687
|
[level, setLevel] = useState(initialCanvasView.level),
|
|
677
688
|
[focus, setFocus] = useState(initialCanvasView.focus),
|
|
678
689
|
[selected, setSelected] = useState(initialCanvasView.selected),
|
|
679
|
-
[messages,
|
|
690
|
+
[messages, setMessagesState] = useState(() => conversationMessages(projectRef.current, [welcome(projectRef.current)])),
|
|
680
691
|
[reviewView, setReviewView] = useState("findings"),
|
|
681
|
-
[input, setInput] = useState(
|
|
692
|
+
[input, setInput] = useState(()=>{try{return localStorage.getItem('atmem-chat-draft:'+projectRef.current.id)||'';}catch{return '';}}),
|
|
682
693
|
[pending, setPending] = useState(null),
|
|
683
694
|
[busy, setBusy] = useState(false),
|
|
684
695
|
[scenario, setScenario] = useState("happy"),
|
|
@@ -699,6 +710,19 @@ function App() {
|
|
|
699
710
|
[probe, setProbe] = useState(null),
|
|
700
711
|
[snapshot, setSnapshot] = useState(null),
|
|
701
712
|
[measured, setMeasured] = useState({});
|
|
713
|
+
const setMessages=useCallback(updater=>setMessagesState(previous=>{
|
|
714
|
+
const next=typeof updater==='function'?updater(previous):updater;
|
|
715
|
+
if(typeof updater!=='function')return next;
|
|
716
|
+
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()});
|
|
717
|
+
}),[]);
|
|
718
|
+
const draftOwner=useRef(project.id);
|
|
719
|
+
useEffect(()=>{
|
|
720
|
+
if(draftOwner.current!==project.id){draftOwner.current=project.id;try{setInput(localStorage.getItem('atmem-chat-draft:'+project.id)||'');}catch{setInput('');}return;}
|
|
721
|
+
try{localStorage.setItem('atmem-chat-draft:'+project.id,input);}catch{}
|
|
722
|
+
},[project.id,input]);
|
|
723
|
+
const [chatView,setChatView]=useState(()=>readChatView(project.id));
|
|
724
|
+
useEffect(()=>setChatView(readChatView(project.id)),[project.id]);
|
|
725
|
+
function changeChatView(value){setChatView(value);try{localStorage.setItem('atmem-chat-view:'+project.id,JSON.stringify(value));}catch{}}
|
|
702
726
|
const messagesRef=useRef(messages);messagesRef.current=messages;
|
|
703
727
|
const [session, setSession] = useState(null),
|
|
704
728
|
[connection, setConnection] = useState(readConnection),
|
|
@@ -718,6 +742,37 @@ function App() {
|
|
|
718
742
|
useEffect(()=>{saveCanvasView(project,{level,focus,selected,positions:topologyPositions});},[project.id,level,focus,selected,topologyPositions]);
|
|
719
743
|
const emulation=useEmulation(project,environment,session);
|
|
720
744
|
const emulationWorking=emulation.running||(agentBusy&&!!emulation.result);
|
|
745
|
+
const [pricingState,setPricingState]=useState({busy:false,errors:[]});
|
|
746
|
+
const [generationPending,setGenerationPending]=useState(false);
|
|
747
|
+
const [checking,setChecking]=useState(false),[checkReceipt,setCheckReceipt]=useState(null);
|
|
748
|
+
const engineeringStatus=useEngineeringStatus(session,project.id);
|
|
749
|
+
const [architectAction,setArchitectAction]=useState('Architect is working…');
|
|
750
|
+
const pricingRequest=useRef(0);
|
|
751
|
+
const pricingAttempt=useRef(new Set());
|
|
752
|
+
const pricingKey=pricingSignature(project);
|
|
753
|
+
async function refreshEstimate(selected){
|
|
754
|
+
if(!session?.token)return;
|
|
755
|
+
const resolved=prefillAcceptedSettings(selected);
|
|
756
|
+
const requested=resolved.estimate?.projectSize?resolved:setProjectSize(resolved,'small');
|
|
757
|
+
if(requested!==selected)updateEngagement(requested);
|
|
758
|
+
const request=++pricingRequest.current;pricingAttempt.current.add(pricingSignature(requested));
|
|
759
|
+
const baseline=JSON.stringify([requested.estimate?.prices,requested.estimate?.details]);
|
|
760
|
+
const identity=JSON.stringify([requested.id,requested.estimate?.projectSize,requested.estimate?.usage,requested.facts,requested.nodes]);
|
|
761
|
+
setPricingState({busy:true,errors:[]});
|
|
762
|
+
try{
|
|
763
|
+
const result=await fetchPlanningPrices(session,requested),live=projectRef.current;
|
|
764
|
+
if(request!==pricingRequest.current)return;
|
|
765
|
+
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;}
|
|
766
|
+
// Merge only the fetched pricing fields; preserve concurrent design and conversation edits.
|
|
767
|
+
updateEngagement({...live,estimate:{...live.estimate,prices:result.project.estimate?.prices,details:result.project.estimate?.details}});
|
|
768
|
+
setPricingState({busy:false,errors:result.errors});
|
|
769
|
+
}catch(error){if(request===pricingRequest.current)setPricingState({busy:false,errors:[{reason:error.message}]});}
|
|
770
|
+
}
|
|
771
|
+
useEffect(()=>{
|
|
772
|
+
if(!session?.token||!project.nodes.length)return;
|
|
773
|
+
if(pricingAttempt.current.has(pricingKey))return;
|
|
774
|
+
pricingAttempt.current.add(pricingKey);void refreshEstimate(project);
|
|
775
|
+
},[mode,session?.token,pricingKey]);
|
|
721
776
|
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
777
|
function resizeChat(width){setChatWidth(width);try{localStorage.setItem('atmem-architect-chat-width',String(width));}catch{}}
|
|
723
778
|
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 +1294,7 @@ function App() {
|
|
|
1239
1294
|
setInput("");
|
|
1240
1295
|
}
|
|
1241
1296
|
if(origin==='chat'){setPending(null);if(!intakeOptions.alreadyLogged)setAttachments([]);}
|
|
1242
|
-
|
|
1297
|
+
setChatOpen(true);
|
|
1243
1298
|
if (!connection) {
|
|
1244
1299
|
setRequestState(requestIds,'failed');
|
|
1245
1300
|
setModal("welcome");
|
|
@@ -1261,6 +1316,7 @@ function App() {
|
|
|
1261
1316
|
const controller = new AbortController();
|
|
1262
1317
|
agentAbort.current = controller;
|
|
1263
1318
|
chatScroll.followLatest();
|
|
1319
|
+
setArchitectAction(origin==='block-review'?`Reviewing ${reviewTarget?.label||'the block'}…`:origin==='chat'?'Architect is working…':'Updating the design…');
|
|
1264
1320
|
setAgentBusy(true);
|
|
1265
1321
|
const activityId=session.architectureActivityEnabled?crypto.randomUUID():null;
|
|
1266
1322
|
try {
|
|
@@ -1526,14 +1582,17 @@ function App() {
|
|
|
1526
1582
|
const exportFiles = (files, suffix) => {
|
|
1527
1583
|
download(zipSync(Object.fromEntries(Object.entries(files).map(([name, content]) => [name, typeof content === "string" ? strToU8(content) : content]))), `${slug}-r${project.revision}-${suffix}.zip`, "application/zip");
|
|
1528
1584
|
};
|
|
1529
|
-
const downloadBundle = bundle => {
|
|
1585
|
+
const downloadBundle = async bundle => {
|
|
1586
|
+
bundle=await(bundle.prepare?.()||bundle);
|
|
1587
|
+
if(bundle.project!==projectRef.current)throw Error('Design changed while preparing files. Download again.');
|
|
1530
1588
|
download(zipSync(Object.fromEntries(Object.entries(bundle.files).map(([name,content])=>[name,typeof content === "string"?strToU8(content):content]))),bundle.filename,"application/zip");
|
|
1531
|
-
|
|
1589
|
+
if(bundle.project?.id===projectRef.current.id)updateEngagement(acknowledgeStage(projectRef.current,'delivery'));
|
|
1590
|
+
notify("ZIP downloaded.");setStarOffer(value=>value+1);
|
|
1532
1591
|
};
|
|
1533
1592
|
const exportPack = async () => {
|
|
1534
1593
|
try {
|
|
1535
1594
|
notify("Preparing documents and diagram exports…");
|
|
1536
|
-
downloadBundle(await outputBundle(project,run,environment));
|
|
1595
|
+
await downloadBundle(await outputBundle(project,run,environment));
|
|
1537
1596
|
} catch(e) { notify(e.message); }
|
|
1538
1597
|
};
|
|
1539
1598
|
const exportTerraform = async (validation = null) => {
|
|
@@ -1764,14 +1823,28 @@ function App() {
|
|
|
1764
1823
|
const onBoard = testing || (mode === "design" && designView === "board");
|
|
1765
1824
|
const diff = pending?.next ? proposalDiff(project, pending.next) : null;
|
|
1766
1825
|
|
|
1826
|
+
const specificationChecks=(
|
|
1827
|
+
<ReviewView expanded
|
|
1828
|
+
project={project}
|
|
1829
|
+
findings={findings}
|
|
1830
|
+
onChange={updateEngagement}
|
|
1831
|
+
onOpen={(id) => {
|
|
1832
|
+
setMode("design");
|
|
1833
|
+
setDesignView("board");
|
|
1834
|
+
openBlock(id);
|
|
1835
|
+
}}
|
|
1836
|
+
onAddRequirement={(category) => {
|
|
1837
|
+
setMode("design");setDesignView("requirements");setChatOpen(true);
|
|
1838
|
+
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"});
|
|
1839
|
+
}}
|
|
1840
|
+
/>
|
|
1841
|
+
);
|
|
1767
1842
|
return (
|
|
1768
1843
|
<div style={{"--chat-panel-width":`${chatWidth}px`}} className={`app ${chatOpen&&!buildView ? "" : "chat-closed"} ${buildView?"build-workspace":""}`}>
|
|
1769
1844
|
<header className="bar">
|
|
1770
|
-
<button className="brand" onClick={() => setModal("about")} title="About AtMem AI Architect">
|
|
1845
|
+
<button aria-label="About AtMem AI Architect" className="brand" onClick={() => setModal("about")} title="About AtMem AI Architect">
|
|
1771
1846
|
<Mark />
|
|
1772
|
-
|
|
1773
|
-
AtMem.ai<span className="brand-divider">|</span>AI Architect
|
|
1774
|
-
</span>
|
|
1847
|
+
|
|
1775
1848
|
</button>
|
|
1776
1849
|
<nav className="crumbs" aria-label="Level"><span className="workspace-label">PROJECT</span>
|
|
1777
1850
|
<button className="projects-btn" aria-label="Projects" title="Your projects" onClick={() => (refreshProjects(), setModal("projects"))}>
|
|
@@ -1815,18 +1888,19 @@ function App() {
|
|
|
1815
1888
|
<button role="menuitem" onClick={() => setModal("templates")} disabled={busy}><LayoutTemplate size={16}/>Templates</button>
|
|
1816
1889
|
<hr/><button role="menuitem" onClick={() => setModal("import")} disabled={busy}><GitBranch size={16}/>Import diagram</button>
|
|
1817
1890
|
<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}><
|
|
1891
|
+
<hr/><button role="menuitem" onClick={() => setModal("memory")} disabled={!session}><BrainActivity size={18} active={false}/>AtMem memory</button>
|
|
1819
1892
|
<button role="menuitem" onClick={() => setModal("start-over")} disabled={busy || agentBusy}><RotateCcw size={16}/>Start over</button>
|
|
1820
1893
|
<hr />
|
|
1821
|
-
<button role="menuitem" onClick={() => setModal("connections")}><Waypoints size={16}/>
|
|
1894
|
+
<button role="menuitem" onClick={() => setModal("connections")}><Waypoints size={16}/>AI providers</button>
|
|
1822
1895
|
<button role="menuitem" onClick={() => setModal("licenses")}><ShieldCheck size={16}/>Licences</button>
|
|
1896
|
+
<a role="menuitem" href={STAR_URL} target="_blank" rel="noopener noreferrer">Star on GitHub ↗</a>
|
|
1823
1897
|
</div>
|
|
1824
1898
|
)}
|
|
1825
1899
|
</header>
|
|
1826
1900
|
|
|
1827
1901
|
<section className="journey-wizard unified-journey" aria-label="Architecture journey">
|
|
1828
1902
|
<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"><
|
|
1903
|
+
{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
1904
|
</section>
|
|
1831
1905
|
<main
|
|
1832
1906
|
id="canvas"
|
|
@@ -1846,7 +1920,7 @@ function App() {
|
|
|
1846
1920
|
{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
1921
|
{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
1922
|
{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><
|
|
1923
|
+
{!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
1924
|
<ReactFlow
|
|
1851
1925
|
nodes={canvasNodes}
|
|
1852
1926
|
edges={canvasEdges}
|
|
@@ -2017,6 +2091,7 @@ function App() {
|
|
|
2017
2091
|
onWorkModelChange={changeArchitectureWork}
|
|
2018
2092
|
onRemove={() => removeNode(selectedNode.id)}
|
|
2019
2093
|
onOpen={(id) => openBlock(id)}
|
|
2094
|
+
onSpec={()=>{visitJourney('scope');setTimeout(()=>document.getElementById('component-spec-'+selectedNode.id)?.scrollIntoView({block:'start'}),0);}}
|
|
2020
2095
|
onBack={goUp}
|
|
2021
2096
|
/>
|
|
2022
2097
|
)}
|
|
@@ -2093,31 +2168,15 @@ function App() {
|
|
|
2093
2168
|
|
|
2094
2169
|
</div>
|
|
2095
2170
|
<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>}
|
|
2171
|
+
<header className="review-intro"><h2>Review</h2></header>
|
|
2172
|
+
<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>
|
|
2173
|
+
<section className="review-costs" aria-label="Project costs">
|
|
2174
|
+
{["review","estimate"].includes(mode) && <EstimateView project={project} result={costs} onChange={updateEngagement} busy={busy||agentBusy||!!pending} onEstimate={refreshEstimate} onChooseRegion={()=>{visitJourney('cloud');requestAnimationFrame(()=>{const input=document.querySelector('.location-region-form input');input?.focus();input?.scrollIntoView({block:'center',behavior:'instant'});});}} 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'});}}} />}
|
|
2175
|
+
</section>
|
|
2176
|
+
{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
2177
|
{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'&&<
|
|
2178
|
+
{mode==='review'&&<ProjectChecks generationPending={generationPending} receipt={checkReceipt} project={project} environment={environment} session={session} onReceipt={setCheckReceipt} onBusy={setChecking}/>}
|
|
2179
|
+
{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.expected!==null&&!costs.mixedCurrencies?{...next,extensions:{...next.extensions,journey:{...next.extensions.journey,estimate:journey(next).keys.estimate}}}:next);visitJourney('delivery');}}>Complete →</button></footer>}
|
|
2121
2180
|
</div>
|
|
2122
2181
|
|
|
2123
2182
|
|
|
@@ -2125,16 +2184,16 @@ function App() {
|
|
|
2125
2184
|
|
|
2126
2185
|
{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
2186
|
|
|
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")} />}
|
|
2187
|
+
{mode === "design" && designView === "outputs" && <OutputsView onNavigate={async 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);await new Promise((resolve,reject)=>{const started=performance.now();function focus(){const element=document.querySelector(selector);if(element&&element.getClientRects().length){element.setAttribute('tabindex','-1');element.focus({preventScroll:true});element.scrollIntoView({block:'start',behavior:'instant'});resolve();}else if(performance.now()-started<5000)requestAnimationFrame(focus);else reject(Error('Section is unavailable. Use the stage tabs to continue.'));}requestAnimationFrame(focus);});}} checkReceipt={checkReceipt} onDownloaded={()=>{updateEngagement(acknowledgeStage(projectRef.current,'delivery'));setStarOffer(value=>value+1);}} 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
2188
|
{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
2189
|
<div className="choices-workspace" hidden={!(mode==="design"&&["options","decisions","location"].includes(designView))}>
|
|
2131
2190
|
{mode === "design" && ["options","decisions"].includes(designView) && (
|
|
2132
|
-
<OptionsView project={project} busy={busy||agentBusy||!!pending} onChoose={chooseCloud} onContinue={()=>visitJourney(
|
|
2191
|
+
<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
2192
|
)}
|
|
2134
|
-
{mode === "design" && ["options","decisions","location"].includes(designView) && <
|
|
2193
|
+
{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
2194
|
</div>
|
|
2136
2195
|
{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"); }} />
|
|
2196
|
+
<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
2197
|
)}
|
|
2139
2198
|
</main>
|
|
2140
2199
|
|
|
@@ -2142,9 +2201,9 @@ function App() {
|
|
|
2142
2201
|
{chatOpen&&!buildView && (
|
|
2143
2202
|
<aside className="chat" aria-label="Chat"><ChatResizeHandle width={chatWidth} onChange={resizeChat}/>
|
|
2144
2203
|
<div className="chat-head">
|
|
2145
|
-
<div className="chat-heading-row"><strong className="chat-title">
|
|
2146
|
-
<Mark size={
|
|
2147
|
-
</strong><button className="
|
|
2204
|
+
<div className="chat-heading-row"><strong className="chat-title" aria-label="AI Architect">
|
|
2205
|
+
<Mark size={24} />
|
|
2206
|
+
</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
2207
|
<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
2208
|
<input
|
|
2150
2209
|
type="checkbox"
|
|
@@ -2158,13 +2217,13 @@ function App() {
|
|
|
2158
2217
|
/>
|
|
2159
2218
|
Draw as we talk
|
|
2160
2219
|
</label>
|
|
2161
|
-
<button className="model" onClick={() => setModal("connections")} title="
|
|
2220
|
+
<button className="model" onClick={() => setModal("connections")} title="AI providers">
|
|
2162
2221
|
<span className={`led ${connection && session ? "on" : ""}`} />
|
|
2163
2222
|
{connection ? `${connection.model}${connection.effort ? ` · ${EFFORT_LABEL[connection.effort] || connection.effort}` : ""}` : "Connect a model"}
|
|
2164
2223
|
</button>
|
|
2165
2224
|
</div>
|
|
2166
2225
|
<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")}><
|
|
2226
|
+
<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
2227
|
<button
|
|
2169
2228
|
className={`known-bar ${peek === "known" ? "on" : ""}`}
|
|
2170
2229
|
aria-label="Design context" aria-expanded={peek === "known"} title="Review what is known, assumed or still open"
|
|
@@ -2207,14 +2266,14 @@ function App() {
|
|
|
2207
2266
|
)}
|
|
2208
2267
|
<div className="messages" ref={chatScroll.viewport}>
|
|
2209
2268
|
<div className="message-list" ref={chatScroll.content}>
|
|
2210
|
-
{architectureChatEntries(project,messages).map(entry=>{
|
|
2269
|
+
{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
2270
|
if(entry.kind==='activity-error')return <p key="activity-error" role="alert">{entry.error}</p>;
|
|
2212
2271
|
if(entry.kind==='legacy-activity')return <EarlierArchitectureActivity key={entry.id} entries={entry.entries} projectId={project.id} session={session}/>;
|
|
2213
2272
|
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;
|
|
2273
|
+
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
2274
|
return (
|
|
2216
2275
|
<div key={i} className={`msg ${m.role}`}>
|
|
2217
|
-
<ChatMessage text={m.text}/>
|
|
2276
|
+
<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
2277
|
{(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
2278
|
{!!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
2279
|
{m.fix && (
|
|
@@ -2260,15 +2319,9 @@ function App() {
|
|
|
2260
2319
|
)}
|
|
2261
2320
|
</div>
|
|
2262
2321
|
</div>
|
|
2322
|
+
{chatScroll.away&&<button className="chat-jump-latest" aria-label="Jump to latest message" onClick={chatScroll.followLatest}>↓</button>}
|
|
2263
2323
|
{!!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
2324
|
<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
2325
|
{mode === "design" && project.workflow && !project.nodes.length && !pending && (
|
|
2273
2326
|
<button className="primary-chip" disabled={busy || agentBusy} onClick={sketchIt}>Sketch it →</button>
|
|
2274
2327
|
)}
|
|
@@ -2278,6 +2331,7 @@ function App() {
|
|
|
2278
2331
|
</button>
|
|
2279
2332
|
))}
|
|
2280
2333
|
</div>
|
|
2334
|
+
{(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?'Generating project files…':['starting','running'].includes(engineeringStatus?.state)?'Generating project files…':checking?'Running checks…':'Calculating costs…'}</span></div>}
|
|
2281
2335
|
{readingAttachments&&<p className="attachment-status" role="status"><BrainActivity/> Reading files…</p>}
|
|
2282
2336
|
{attachmentError&&<p className="attachment-status" role="alert">{attachmentError}</p>}
|
|
2283
2337
|
{!!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>}
|
|
@@ -2340,13 +2394,14 @@ function App() {
|
|
|
2340
2394
|
{toast === "Preparing documents and diagram exports…"&&<BrainActivity/>}{toast}
|
|
2341
2395
|
</div>
|
|
2342
2396
|
)}
|
|
2397
|
+
<StarPrompt offer={starOffer}/>
|
|
2343
2398
|
{modal && (
|
|
2344
2399
|
<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}>
|
|
2400
|
+
<section className={`modal ${modal==="memory"?"memory-modal":""}`} role="dialog" aria-modal="true" aria-label={modal}>
|
|
2346
2401
|
{!(modal === "welcome" && !connection) && <button className="icon modal-close" onClick={() => setModal(null)} aria-label="Close dialog">
|
|
2347
2402
|
<X size={16} />
|
|
2348
2403
|
</button>}
|
|
2349
|
-
{modal === "project-file" ? (<><h2>
|
|
2404
|
+
{modal === "project-file" ? (<><h2>Open project</h2><button className="btn primary" onClick={()=>{fileImportAsCopy.current=false;fileRef.current.click();}}>Open project</button></>) : modal === "memory" ? (
|
|
2350
2405
|
<MemoryPanel key={project.id} session={session} project={project} onError={notify} onStatus={status=>setMemorySync({projectId:project.id,...status})} />
|
|
2351
2406
|
) : modal === "templates" ? (
|
|
2352
2407
|
<TemplateDialog onLoad={loadDesign} />
|
|
@@ -2362,7 +2417,7 @@ function App() {
|
|
|
2362
2417
|
environment={environment}
|
|
2363
2418
|
/>
|
|
2364
2419
|
) : modal === "projects" ? (
|
|
2365
|
-
<div><div className="row"><button className="btn" onClick={()=>setModal("project-file")}>Open
|
|
2420
|
+
<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
2421
|
list={saved}
|
|
2367
2422
|
folder={saved.folder}
|
|
2368
2423
|
currentId={project.id}
|
|
@@ -2412,36 +2467,12 @@ function App() {
|
|
|
2412
2467
|
/>
|
|
2413
2468
|
) : modal === "licenses" ? (
|
|
2414
2469
|
<>
|
|
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>
|
|
2470
|
+
<h2>AtMem</h2><a href="/licenses/atmem-APACHE-2.0.txt" target="_blank" rel="noreferrer">Apache 2.0</a>
|
|
2435
2471
|
</>
|
|
2436
2472
|
) : (
|
|
2437
2473
|
<>
|
|
2438
2474
|
<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>
|
|
2475
|
+
<p className="muted">v{appVersion}</p><InlineNote label="Keyboard shortcuts">/ Add block · Esc Board · ⌘↵ Run</InlineNote>
|
|
2445
2476
|
</>
|
|
2446
2477
|
)}
|
|
2447
2478
|
</section>
|
package/src/output-bundle.mjs
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
+
import {verifiedReceipt} from './validation-receipt.mjs';
|
|
1
2
|
import {deliveryPack} from './delivery.mjs';
|
|
2
|
-
export
|
|
3
|
+
export const VISUAL_PATHS=['spec-kit/implementation-spec.pdf','specification.pdf','diagram.svg','diagram.png','diagram.pdf'];
|
|
4
|
+
export async function outputBundle(project,run,environment,checks,{deferVisuals=false}={}){
|
|
3
5
|
const snapshot=structuredClone(project);
|
|
4
6
|
const files=deliveryPack(snapshot,run,environment);
|
|
5
|
-
const
|
|
6
|
-
Object.assign(files,await visualFiles(snapshot,files['specification.md'],files['spec-kit/implementation-spec.md']));
|
|
7
|
+
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
8
|
const manifest=JSON.parse(files['manifest.json']);manifest.files=Object.keys(files);
|
|
8
9
|
files['manifest.json']=JSON.stringify(manifest,null,2);
|
|
9
10
|
const slug=snapshot.name.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'')||'atflow';
|
|
10
|
-
|
|
11
|
+
const base={project,files,manifest,filename:`${slug}-r${snapshot.revision}-delivery.zip`,visualPaths:VISUAL_PATHS};
|
|
12
|
+
let pending;
|
|
13
|
+
async function prepare(){
|
|
14
|
+
if(!pending)pending=(async()=>{
|
|
15
|
+
const {visualFiles}=await import('./visual-export.mjs');
|
|
16
|
+
const fullFiles={...files,...await visualFiles(snapshot,files['specification.md'],files['spec-kit/implementation-spec.md'])};
|
|
17
|
+
const fullManifest={...manifest,files:Object.keys(fullFiles)};
|
|
18
|
+
fullFiles['manifest.json']=JSON.stringify(fullManifest,null,2);
|
|
19
|
+
const full={...base,files:fullFiles,manifest:fullManifest,visualPaths:[]};
|
|
20
|
+
return {...full,prepare:()=>Promise.resolve(full)};
|
|
21
|
+
})().catch(error=>{pending=null;throw error;});
|
|
22
|
+
return pending;
|
|
23
|
+
}
|
|
24
|
+
return deferVisuals?{...base,prepare}:prepare();
|
|
11
25
|
}
|