@atmem/architect 0.2.1 → 0.2.3

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/src/main.jsx CHANGED
@@ -1,3 +1,6 @@
1
+ import {HOSTED,connectHostedWorkspace,hostedInitial,hostedCall,hostedRevision,noteHostedChange} from './hosted-workspace.mjs';
2
+ import {readyExample,isReadyExample} from './ready-example.mjs';
3
+ import {ReadyExampleDownload} from './ReadyExample.jsx';
1
4
  import {StarPrompt,STAR_URL} from './StarPrompt.jsx';
2
5
  import {prefillAcceptedSettings} from './engineering-defaults.mjs';
3
6
  import {LocationChoices} from './LocationChoices.jsx';
@@ -101,6 +104,7 @@ import {
101
104
  Rocket,
102
105
  BrainCircuit,
103
106
  Waypoints,
107
+ Volume2,
104
108
  Mic,
105
109
  Square,
106
110
  Eraser,
@@ -167,6 +171,8 @@ import { zipSync, strToU8 } from "fflate";
167
171
  // Each browser starts fresh; earlier projects are opened deliberately (⋯ → Open project file).
168
172
  const STORAGE = "atforma-workspace-v3";
169
173
  // A new project starts empty: the user describes what they want first.
174
+ import './onboarding.css';
175
+ import './chat-composer.css';
170
176
  const emptyProject = () => ({ id: crypto.randomUUID(), version: 1, revision: 1, name: "New project", provider: "tools", requirements: "", nodes: [], edges: [] });
171
177
  const ALL_CATALOG = [...CATALOG, ...UPSTREAM_CATALOG];
172
178
  const KIND_ICON = {
@@ -253,6 +259,7 @@ function Mark({ size = 20 }) {
253
259
  // Which architect the user chose; absent on first visit, so we ask before anything else.
254
260
  const CONNECTION = "atforma-connection-v1";
255
261
  function readConnection() {
262
+ if(HOSTED)return hostedInitial().connection;
256
263
  try {
257
264
  const c = JSON.parse(localStorage.getItem(CONNECTION) || "null");
258
265
  // Older builds stored "offline"; the architect now always needs a real model.
@@ -269,6 +276,7 @@ function saveConnection(c) {
269
276
  } catch {}
270
277
  }
271
278
  function readSaved() {
279
+ if(HOSTED)return withDerived(fromProjectFile(hostedInitial().file).project);
272
280
  try {
273
281
  const raw = localStorage.getItem(STORAGE);
274
282
  const p = validateImport(raw ? JSON.parse(raw) : emptyProject());
@@ -646,6 +654,7 @@ function harnessSignals(was, now) {
646
654
  }
647
655
  const welcome = (project) => ({
648
656
  role: "assistant",
657
+ introduction: true,
649
658
  text: project.nodes.length || project.brief?.application?.trim()
650
659
  ? `Good to see you again — let's keep going.\n\n${opening(project)}`
651
660
  : `What would you like to build?`,
@@ -660,13 +669,14 @@ function savedTheme() {
660
669
  return "light";
661
670
  }
662
671
  }
663
- document.documentElement.dataset.theme = savedTheme();
672
+
664
673
 
665
674
  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
675
  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])]);
667
676
  function App() {
668
677
  const [starOffer,setStarOffer]=useState(0);
669
- const [project, setProject] = useState(readSaved);
678
+ const [introOpen,setIntroOpen]=useState(false),[voiceOpenRequest,setVoiceOpenRequest]=useState(0);
679
+ const [project, setProject] = useState(()=>!HOSTED&&new URL(location.href).searchParams.get('example')==='1'&&!projectIdFromUrl(location.href)?readyExample():readSaved());
670
680
  const projectRef = useRef(project);
671
681
  projectRef.current = project;
672
682
  const [initialCanvasView] = useState(()=>readCanvasView(project));
@@ -698,7 +708,7 @@ function App() {
698
708
  [history, setHistory] = useState([]),
699
709
  [future, setFuture] = useState([]),
700
710
  [toast, setToast] = useState(""),
701
- [modal, setModal] = useState(() => (readConnection() ? null : "welcome")),
711
+ [modal, setModal] = useState(null),
702
712
  [menu, setMenu] = useState(false),
703
713
  [palette, setPalette] = useState(false),
704
714
  [issuesOpen, setIssuesOpen] = useState(false),
@@ -782,6 +792,12 @@ function App() {
782
792
  const consultationAttempts = useRef(new Set());
783
793
  const agentAbort = useRef(null);
784
794
  const messageInput = useRef(null);
795
+ const hasIntroduction=messages[0]?.role==='assistant'&&(messages[0].introduction===true||messages[0].text==='What would you like to build?'||messages[0].text.startsWith('Good to see you again'));
796
+ const idleChat=hasIntroduction&&!introOpen&&architectureChatEntries(project,messages).length===1;
797
+ useEffect(()=>{
798
+ const resize=()=>{const field=messageInput.current;if(field){field.style.height='32px';field.style.height=Math.min(120,Math.max(32,field.scrollHeight))+'px';}};
799
+ resize();window.addEventListener('resize',resize);return()=>window.removeEventListener('resize',resize);
800
+ },[input,chatWidth,chatOpen]);
785
801
  const intakeRef=useRef(restoreQueuedMessages(messages,project.id));
786
802
  const [intake,setIntake]=useState(intakeRef.current),[intakePaused,setIntakePausedState]=useState(()=>restoreIntakePaused(messages)||intakeRef.current.length>0);
787
803
  const [intakeIssue,setIntakeIssue]=useState(null);
@@ -905,16 +921,19 @@ function App() {
905
921
  return () => { clearTimeout(zoom); clearTimeout(clear); };
906
922
  }, [attention, fitView]);
907
923
 
924
+ function showConnections(){if(HOSTED){void hostedCall('account').catch(error=>notify(error.message));return;}setModal('connections');}
908
925
  const refreshSession = useCallback(async () => {
909
926
  try {
910
927
  const next = await discoverLocalAgent();
911
928
  setSession(next);
929
+ if(HOSTED&&next.defaultConnection)setConnection(current=>current?.model===next.defaultConnection.model&&current?.effort===next.defaultConnection.effort?current:next.defaultConnection);
912
930
  return next;
913
931
  } catch {
914
932
  setSession(null);
915
933
  return null;
916
934
  }
917
935
  }, []);
936
+ useEffect(()=>{if(HOSTED&&session?.token)void hostedCall('ready').catch(()=>{});},[session?.token]);
918
937
  useEffect(() => {
919
938
  refreshSession();
920
939
  const timer=setInterval(refreshSession,30000);
@@ -1204,7 +1223,11 @@ function App() {
1204
1223
  const raw = JSON.parse(content);
1205
1224
  if (raw.format || raw.version === 1) {
1206
1225
  const { project: opened, migrated, from } = parseProjectFile(content);
1207
- loadDesign(fileImportAsCopy.current?copyProject(opened,crypto.randomUUID()):opened, { own: true });
1226
+ if(HOSTED){
1227
+ await saveEngineeringProject(projectRef.current.id);
1228
+ const imported=await hostedCall('import-project',{file:toProjectFile(opened)});
1229
+ loadDesign(fromProjectFile(imported.file).project,{own:true});
1230
+ }else loadDesign(fileImportAsCopy.current?copyProject(opened,crypto.randomUUID()):opened, { own: true });
1208
1231
  notify(fileImportAsCopy.current?"New project created from the saved design. The original is preserved.":migrated ? `Project opened and upgraded from ${from}.` : "Project opened.");
1209
1232
  } else {
1210
1233
  loadDesign(importUpstreamProject(raw));
@@ -1637,13 +1660,15 @@ function App() {
1637
1660
  updateEngagement({ ...skipped, lastAsked: next });
1638
1661
  setMessages((m) => [...m, { role: "assistant", text: next ? `No problem, we'll come back to it.\n\n${next.question}\n${next.beginner}` : "That's everything I need for now." }]);
1639
1662
  }
1640
- // Autosave to the helper (your account on this computer), a moment after each change.
1663
+ useEffect(()=>{if(HOSTED)void noteHostedChange(project.id).catch(()=>{});},[project,messages]);
1664
+ // Autosave through the selected local or hosted session service.
1641
1665
  const binned = useRef(new Set());
1642
1666
  useEffect(() => {
1643
- if (!session || binned.current.has(project.id) || (!project.nodes.length && !project.brief?.application?.trim() && messages.length < 2)) return;
1667
+ if (!session || binned.current.has(project.id) || (!HOSTED && !project.nodes.length && !project.brief?.application?.trim() && messages.length < 2)) return;
1668
+ const bridgeVersion=HOSTED?hostedRevision(project.id):undefined;
1644
1669
  const t = setTimeout(async () => {
1645
1670
  try {
1646
- const saved=await projectStore(session, "save", { id: project.id, file: toProjectFile(withConversation(project, messages)) });
1671
+ const saved=await projectStore(session, "save", { id: project.id, file: toProjectFile(withConversation(project, messages)),...(HOSTED?{bridgeVersion}:{}) });
1647
1672
  if(saved.memory)setMemorySync({projectId:project.id,...saved.memory});
1648
1673
  } catch (error) { setMemorySync(current=>({...((current?.projectId===project.id)?current:{}),projectId:project.id,status:'sync_failed'}));notify(`Project conversation could not be saved: ${error.message}`); }
1649
1674
  }, 800);
@@ -1667,6 +1692,8 @@ function App() {
1667
1692
  }
1668
1693
  }
1669
1694
  function switchTo(next) {
1695
+ setIntroOpen(false);setVoiceOpenRequest(0);
1696
+ const address=new URL(window.location.href);if(address.searchParams.get('project')!==next.id){address.searchParams.delete('project');address.searchParams.delete('example');window.history.replaceState(null,'',address);}
1670
1697
  chooseWorkspace('canvas');setBuildRequest(null);
1671
1698
  next = withDerived(next);
1672
1699
  const leaving = projectRef.current;
@@ -1697,6 +1724,7 @@ function App() {
1697
1724
  }
1698
1725
  async function openSaved(id) {
1699
1726
  try {
1727
+ if(HOSTED)await saveEngineeringProject(projectRef.current.id);
1700
1728
  const { project: opened } = fromProjectFile(await projectStore(session, "get", { id }));
1701
1729
  switchTo(opened);
1702
1730
  setModal(null);
@@ -1725,14 +1753,30 @@ function App() {
1725
1753
  })();
1726
1754
  return () => { active = false; };
1727
1755
  }, [session?.token]);
1728
- function newProject() {
1729
- setModal("start-over");
1756
+ async function newProject() {
1757
+ if(HOSTED){
1758
+ if(agentBusy||busy||intake.length)return;
1759
+ try{await saveEngineeringProject(projectRef.current.id);const {file}=await hostedCall('new-project');switchTo(fromProjectFile(file).project);setModal(null);}catch(error){notify(error.message);}
1760
+ return;
1761
+ }
1762
+ if(!busy&&!agentBusy&&!intake.length)setModal("start-over");
1763
+ }
1764
+ function createOwnProject(){
1765
+ if(HOSTED){void newProject();return;}
1766
+ switchTo(validateImport(emptyProject()));setModal(connection?null:'connections');
1767
+ requestAnimationFrame(()=>document.querySelector('input[aria-label="Project name"]')?.focus());
1768
+ }
1769
+ async function openReadyExample(){
1770
+ if(busy||agentBusy||intake.length)return;
1771
+ const example=readyExample();example.id=crypto.randomUUID();
1772
+ if(HOSTED){try{await saveEngineeringProject(projectRef.current.id);const {file}=await hostedCall('import-project',{file:toProjectFile(example)});switchTo(fromProjectFile(file).project);setModal(null);}catch(error){notify(error.message);}return;}
1773
+ switchTo(example);setModal(null);
1730
1774
  }
1731
1775
  async function binProject(id) {
1732
1776
  try {
1733
1777
  binned.current.add(id);
1734
1778
  await projectStore(session, "trash", { id });
1735
- if (id === projectRef.current.id) switchTo(validateImport(emptyProject()));
1779
+ if(id===projectRef.current.id){if(HOSTED){const {file}=await hostedCall('new-project');switchTo(fromProjectFile(file).project);}else switchTo(validateImport(emptyProject()));}
1736
1780
  await refreshProjects();
1737
1781
  notify("Moved to the bin. Restore it from Projects any time.");
1738
1782
  } catch (e) {
@@ -1840,7 +1884,7 @@ function App() {
1840
1884
  />
1841
1885
  );
1842
1886
  return (
1843
- <div style={{"--chat-panel-width":`${chatWidth}px`}} className={`app ${chatOpen&&!buildView ? "" : "chat-closed"} ${buildView?"build-workspace":""}`}>
1887
+ <div style={{"--chat-panel-width":`${chatWidth}px`}} className={`app ${idleChat?"idle-chat":""} ${chatOpen&&!buildView ? "" : "chat-closed"} ${buildView?"build-workspace":""}`}>
1844
1888
  <header className="bar">
1845
1889
  <button aria-label="About AtMem AI Architect" className="brand" onClick={() => setModal("about")} title="About AtMem AI Architect">
1846
1890
  <Mark />
@@ -1848,7 +1892,7 @@ function App() {
1848
1892
  </button>
1849
1893
  <nav className="crumbs" aria-label="Level"><span className="workspace-label">PROJECT</span>
1850
1894
  <button className="projects-btn" aria-label="Projects" title="Your projects" onClick={() => (refreshProjects(), setModal("projects"))}>
1851
- <FolderOpen size={22} /> Projects
1895
+ <FolderOpen size={22} /><span>Projects</span>
1852
1896
  </button>
1853
1897
  <label className="project-rename"><input
1854
1898
  className="project-name"
@@ -1859,6 +1903,8 @@ function App() {
1859
1903
  /><PencilRuler size={14} aria-hidden="true"/></label>
1860
1904
  </nav>
1861
1905
  <div className="bar-actions">
1906
+ {HOSTED&&<><button className="btn" onClick={newProject} disabled={busy||agentBusy}><Plus size={16}/>New project</button><button className="btn" onClick={()=>hostedCall('account')}>Account</button></>}
1907
+ {isReadyExample(project)&&<><ReadyExampleDownload compact/>{!HOSTED&&<button className="btn" onClick={newProject} disabled={busy||agentBusy}>Create new project</button>}</>}
1862
1908
  {storageError && <span className="warn-text">Not saved</span>}
1863
1909
  <button
1864
1910
  className="icon"
@@ -1883,6 +1929,9 @@ function App() {
1883
1929
  {menu && (
1884
1930
  <div className="menu" role="menu" onClick={() => setMenu(false)}>
1885
1931
  <button role="menuitem" onClick={() => (refreshProjects(), setModal("projects"))} disabled={busy}><FolderOpen size={16}/>Projects</button>
1932
+ <a role="menuitem" href="https://atmem.ai" target="_blank" rel="noopener noreferrer">atmem.ai ↗</a>
1933
+ <a role="menuitem" href="mailto:hello@atmem.ai">Contact us: hello@atmem.ai</a>
1934
+ <button role="menuitem" onClick={openReadyExample} disabled={busy||agentBusy}><LayoutGrid size={16}/>Open complete AI example</button>
1886
1935
  <button role="menuitem" onClick={newProject} disabled={busy}><Plus size={16}/>New project</button>
1887
1936
  <hr/><button role="menuitem" onClick={() => setPalette(true)} disabled={busy}><Plus size={16}/>Add a block</button>
1888
1937
  <button role="menuitem" onClick={() => setModal("templates")} disabled={busy}><LayoutTemplate size={16}/>Templates</button>
@@ -1891,7 +1940,7 @@ function App() {
1891
1940
  <hr/><button role="menuitem" onClick={() => setModal("memory")} disabled={!session}><BrainActivity size={18} active={false}/>AtMem memory</button>
1892
1941
  <button role="menuitem" onClick={() => setModal("start-over")} disabled={busy || agentBusy}><RotateCcw size={16}/>Start over</button>
1893
1942
  <hr />
1894
- <button role="menuitem" onClick={() => setModal("connections")}><Waypoints size={16}/>AI providers</button>
1943
+ {!HOSTED&&<button role="menuitem" onClick={() => showConnections()}><Waypoints size={16}/>AI providers</button>}
1895
1944
  <button role="menuitem" onClick={() => setModal("licenses")}><ShieldCheck size={16}/>Licences</button>
1896
1945
  <a role="menuitem" href={STAR_URL} target="_blank" rel="noopener noreferrer">Star on GitHub ↗</a>
1897
1946
  </div>
@@ -1920,7 +1969,7 @@ function App() {
1920
1969
  {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 &amp; layers</button>
1921
1970
  {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>}
1922
1971
  {treeOpen&&<button className="btn" disabled={busy} onClick={()=>setPalette(true)}><Plus size={16}/> Add component</button>}</aside>}
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>}
1972
+ {!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={openReadyExample}>Explore complete AI example</button><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>}
1924
1973
  <ReactFlow
1925
1974
  nodes={canvasNodes}
1926
1975
  edges={canvasEdges}
@@ -1986,8 +2035,8 @@ function App() {
1986
2035
  >
1987
2036
  <LayoutGrid size={17} />
1988
2037
  </button>
1989
- <button className="icon" title="Clear board" aria-label="Clear board" disabled={!project.nodes.length || busy} onClick={clearBoard}>
1990
- <Eraser size={17} />
2038
+ <button className="btn reset-board" title="Reset board and context" aria-label="Reset board and context" disabled={busy||agentBusy||intake.length>0} onClick={newProject}>
2039
+ <Trash2 size={17} /> Reset board
1991
2040
  </button>
1992
2041
  <button className="icon" title="Undo board change" aria-label="Undo" disabled={!history.length || busy || agentBusy} onClick={undo}>
1993
2042
  <Undo2 size={17} />
@@ -2033,8 +2082,8 @@ function App() {
2033
2082
  <Scan size={16}/> Select all
2034
2083
  </button>
2035
2084
  <div className="ctx-divider" role="separator"/>
2036
- <button role="menuitem" className="danger" disabled={!project.nodes.length} onClick={() => (clearBoard(), setCtx(null))}>
2037
- <Eraser size={16}/> Clear board
2085
+ <button role="menuitem" className="danger" disabled={!project.nodes.length} onClick={() => (newProject(), setCtx(null))}>
2086
+ <Trash2 size={16}/> Reset board and context
2038
2087
  </button>
2039
2088
  </>
2040
2089
  )}
@@ -2173,7 +2222,7 @@ function App() {
2173
2222
  <section className="review-costs" aria-label="Project costs">
2174
2223
  {["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
2224
  </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>}
2225
+ {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={()=>showConnections()}/></section>}
2177
2226
  {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>}
2178
2227
  {mode==='review'&&<ProjectChecks generationPending={generationPending} receipt={checkReceipt} project={project} environment={environment} session={session} onReceipt={setCheckReceipt} onBusy={setChecking}/>}
2179
2228
  {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>}
@@ -2184,7 +2233,7 @@ function App() {
2184
2233
 
2185
2234
  {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>}
2186
2235
 
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")} />}
2236
+ {mode === "design" && designView === "outputs" && <OutputsView workStatus={engineeringStatus} 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")} />}
2188
2237
  {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")} />}
2189
2238
  <div className="choices-workspace" hidden={!(mode==="design"&&["options","decisions","location"].includes(designView))}>
2190
2239
  {mode === "design" && ["options","decisions"].includes(designView) && (
@@ -2199,7 +2248,7 @@ function App() {
2199
2248
 
2200
2249
  {!chatOpen&&!buildView && <button className="icon chat-reopen" title="Show chat" aria-label="Show chat" onClick={()=>setChatOpen(true)}><PanelRightOpen size={18}/></button>}
2201
2250
  {chatOpen&&!buildView && (
2202
- <aside className="chat" aria-label="Chat"><ChatResizeHandle width={chatWidth} onChange={resizeChat}/>
2251
+ <aside className={`chat ${idleChat?"idle":""}`} aria-label="Chat"><ChatResizeHandle width={chatWidth} onChange={resizeChat}/>
2203
2252
  <div className="chat-head">
2204
2253
  <div className="chat-heading-row"><strong className="chat-title" aria-label="AI Architect">
2205
2254
  <Mark size={24} />
@@ -2217,10 +2266,15 @@ function App() {
2217
2266
  />
2218
2267
  Draw as we talk
2219
2268
  </label>
2220
- <button className="model" onClick={() => setModal("connections")} title="AI providers">
2269
+ {HOSTED?<span className="model"><span className={`led ${connection&&session?'on':''}`}/>{connection?.model?.split('/').at(-1)} · Medium</span>:<>
2270
+ <button className="model" onClick={() => showConnections()} title="AI providers">
2221
2271
  <span className={`led ${connection && session ? "on" : ""}`} />
2222
2272
  {connection ? `${connection.model}${connection.effort ? ` · ${EFFORT_LABEL[connection.effort] || connection.effort}` : ""}` : "Connect a model"}
2223
- </button>
2273
+ </button></>}
2274
+ <span className="chat-top-utilities">
2275
+ {hasIntroduction&&<button type="button" aria-expanded={introOpen} onClick={()=>setIntroOpen(value=>!value)}>{introOpen?'▾':'▸'} Introduction</button>}
2276
+ {session&&<button type="button" className="icon" aria-label="Voice settings" title="Voice settings" onClick={()=>setVoiceOpenRequest(value=>value+1)}><Volume2 size={15}/></button>}
2277
+ </span>
2224
2278
  </div>
2225
2279
  <div className="skills" aria-label="Active skills">
2226
2280
  <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>
@@ -2264,12 +2318,15 @@ function App() {
2264
2318
  ))}
2265
2319
  </ul>
2266
2320
  )}
2321
+ <div className="chat-body">
2322
+ <section className="chat-history" aria-label="Chat history"><h2 className="chat-history-label">Chat history</h2>
2267
2323
  <div className="messages" ref={chatScroll.viewport}>
2268
2324
  <div className="message-list" ref={chatScroll.content}>
2269
2325
  {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=>{
2270
2326
  if(entry.kind==='activity-error')return <p key="activity-error" role="alert">{entry.error}</p>;
2271
2327
  if(entry.kind==='legacy-activity')return <EarlierArchitectureActivity key={entry.id} entries={entry.entries} projectId={project.id} session={session}/>;
2272
2328
  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()}/>;
2329
+ if(entry.index===0&&hasIntroduction&&!introOpen)return null;
2273
2330
  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;
2274
2331
  return (
2275
2332
  <div key={i} className={`msg ${m.role}`}>
@@ -2277,7 +2334,7 @@ function App() {
2277
2334
  {(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>}
2278
2335
  {!!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>}
2279
2336
  {m.fix && (
2280
- <button className="btn fix" onClick={() => setModal("connections")}>
2337
+ <button className="btn fix" onClick={() => showConnections()}>
2281
2338
  Fix connection
2282
2339
  </button>
2283
2340
  )}
@@ -2320,6 +2377,8 @@ function App() {
2320
2377
  </div>
2321
2378
  </div>
2322
2379
  {chatScroll.away&&<button className="chat-jump-latest" aria-label="Jump to latest message" onClick={chatScroll.followLatest}>↓</button>}
2380
+ </section>
2381
+ <div className="chat-compose-pane">
2323
2382
  {!!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>}
2324
2383
  <div className="prompts">
2325
2384
  {mode === "design" && project.workflow && !project.nodes.length && !pending && (
@@ -2342,11 +2401,12 @@ function App() {
2342
2401
  void postMessage();
2343
2402
  }}
2344
2403
  >
2345
- <textarea
2404
+ <label className="composer-label" htmlFor="architect-message">Your message</label>
2405
+ <textarea id="architect-message"
2346
2406
  onPaste={e=>{const files=[...e.clipboardData.files];if(files.length){e.preventDefault();void attachFiles(files);}}}
2347
2407
  ref={messageInput}
2348
2408
  aria-label="Your message"
2349
- placeholder={brief(project) ? "Reply in your own words…" : "Describe what you want to build…"}
2409
+ placeholder="Type here…"
2350
2410
  rows={2}
2351
2411
  value={input}
2352
2412
  onChange={(e) => { stopVoice(); setInput(e.target.value); }}
@@ -2372,11 +2432,11 @@ function App() {
2372
2432
  {voiceActive ? <Square size={13} /> : <Mic size={15} />}
2373
2433
  </button>
2374
2434
  <button type="submit" className="send" aria-label="Send message" disabled={(!input.trim() && !voiceActive && !attachments.length) || busy || readingAttachments || voiceState === "transcribing"}>
2375
- <ArrowUp size={15} />
2435
+ <ArrowUp size={15} /><span>Send</span>
2376
2436
  </button>
2377
2437
  </div>
2378
2438
  </form>
2379
- {session&&<OpenAIVoicePanel key={project.id} session={session} connection={connection} transportRef={openaiVoice} onConnections={()=>setModal('connections')} onStart={()=>{voice.current?.cancel();setVoiceError('');}} onTranscript={(text,automatic)=>{
2439
+ {session&&<OpenAIVoicePanel hideTrigger openRequest={voiceOpenRequest} key={project.id} session={session} connection={connection} transportRef={openaiVoice} onConnections={()=>showConnections()} onStart={()=>{voice.current?.cancel();setVoiceError('');}} onTranscript={(text,automatic)=>{
2380
2440
  if(automatic&&!input.trim()&&!attachments.length){setInput(text);void send(text);}
2381
2441
  else setInput(current=>current+(current.trim()?' ':'')+text);
2382
2442
  }}/>}
@@ -2384,10 +2444,12 @@ function App() {
2384
2444
  {!voiceError&&["starting","transcribing"].includes(voiceState)&&<BrainActivity size={22}/>}
2385
2445
  {voiceError || ({ starting: "Opening microphone…", listening: "Dictating locally — review your words, then press Send.", transcribing: "Finishing your transcript…" }[voiceState] || "")}
2386
2446
  </div>
2447
+ </div>
2448
+ </div>
2387
2449
  </aside>
2388
2450
  )}
2389
2451
 
2390
- <input type="file" id="requirements-file-input" ref={requirementsInput} accept={ATTACHMENT_ACCEPT} multiple hidden onChange={async e=>{const files=[...e.target.files];e.target.value='';if(!files.length||readingAttachments)return;const uploadOwner=projectRef.current.id;setReadingAttachments(true);setAttachmentError('');try{const added=await readAttachments(files,attachments);if(projectRef.current.id!==uploadOwner)return;const all=[...attachments,...added];setAttachments(all);setChatOpen(true);setInput('Read these requirements and diagrams. Summarise what you understand, propose requirements and a connected design, and ask one focused question about anything important that is unclear. Preserve existing systems and distinguish source facts from assumptions.');if(connection&&session){setReadingAttachments(false);void send('Read these requirements and diagrams. Summarise what you understand, propose requirements and a connected design, and ask one focused question about anything important that is unclear. Preserve existing systems and distinguish source facts from assumptions.','chat',null,null,{attachments:all});}else{setModal('connections');}}catch(error){setAttachmentError(error.message);}finally{setReadingAttachments(false);}}}/>
2452
+ <input type="file" id="requirements-file-input" ref={requirementsInput} accept={ATTACHMENT_ACCEPT} multiple hidden onChange={async e=>{const files=[...e.target.files];e.target.value='';if(!files.length||readingAttachments)return;const uploadOwner=projectRef.current.id;setReadingAttachments(true);setAttachmentError('');try{const added=await readAttachments(files,attachments);if(projectRef.current.id!==uploadOwner)return;const all=[...attachments,...added];setAttachments(all);setChatOpen(true);setInput('Read these requirements and diagrams. Summarise what you understand, propose requirements and a connected design, and ask one focused question about anything important that is unclear. Preserve existing systems and distinguish source facts from assumptions.');if(connection&&session){setReadingAttachments(false);void send('Read these requirements and diagrams. Summarise what you understand, propose requirements and a connected design, and ask one focused question about anything important that is unclear. Preserve existing systems and distinguish source facts from assumptions.','chat',null,null,{attachments:all});}else{showConnections();}}catch(error){setAttachmentError(error.message);}finally{setReadingAttachments(false);}}}/>
2391
2453
  <input id="project-file-input" type="file" ref={fileRef} accept="application/json,.json" hidden onChange={importProject} />
2392
2454
  {toast && (
2393
2455
  <div className="toast" role="status">
@@ -2429,22 +2491,20 @@ function App() {
2429
2491
  /></div>
2430
2492
  ) : modal === "start-over" ? (
2431
2493
  <>
2432
- <h2>Start over?</h2>
2433
- <p>Start a new idea with an empty board and chat. Requirements, decisions and estimates start fresh too. Your model connection stays.</p>
2494
+ <h2>Reset board and context?</h2>
2495
+ <p>Start fresh with an empty board, chat, requirements and decisions. Your saved projects and model connection stay.</p>
2434
2496
  <div className="reset-actions">
2435
2497
  <button className="btn primary" title="Download your project, then start a new one" aria-label="Save project and start over" onClick={() => {
2436
2498
  if (!saveProjectFile()) return;
2437
- switchTo(validateImport(emptyProject()));
2438
- setModal(null);
2499
+ createOwnProject();
2439
2500
  }}><Package size={18}/> Save & start new</button>
2440
2501
  <button
2441
- className="btn" title="Start a new project without downloading this one" aria-label="Start over without download"
2502
+ className="btn" title="Start a new project without downloading this one" aria-label="Confirm reset board and context"
2442
2503
  onClick={() => {
2443
- switchTo(validateImport(emptyProject()));
2444
- setModal(null);
2504
+ createOwnProject();
2445
2505
  }}
2446
2506
  >
2447
- <Plus size={18}/> Start without saving
2507
+ <Trash2 size={18}/> Reset board and context
2448
2508
  </button>
2449
2509
  <button className="btn ghost" onClick={() => setModal(null)}>
2450
2510
  <ArrowLeft size={18}/> Keep working
@@ -2467,7 +2527,7 @@ function App() {
2467
2527
  />
2468
2528
  ) : modal === "licenses" ? (
2469
2529
  <>
2470
- <h2>AtMem</h2><a href="/licenses/atmem-APACHE-2.0.txt" target="_blank" rel="noreferrer">Apache 2.0</a>
2530
+ <h2>AtMem</h2><a href={(HOSTED?'/architect/workspace/':'/')+'licenses/atmem-APACHE-2.0.txt'} target="_blank" rel="noreferrer">Apache 2.0</a>
2471
2531
  </>
2472
2532
  ) : (
2473
2533
  <>
@@ -2482,8 +2542,17 @@ function App() {
2482
2542
  );
2483
2543
  }
2484
2544
 
2485
- createRoot(document.getElementById("root")).render(
2545
+ async function mountArchitect(){
2546
+ await connectHostedWorkspace();
2547
+ document.documentElement.dataset.theme=savedTheme();
2548
+ createRoot(document.getElementById("root")).render(
2486
2549
  <ReactFlowProvider>
2487
2550
  <App />
2488
2551
  </ReactFlowProvider>,
2489
2552
  );
2553
+ }
2554
+ void mountArchitect().catch(error=>{
2555
+ const root=document.getElementById('root');root.replaceChildren();
2556
+ const message=document.createElement('p');message.textContent=error.message||'The workspace could not open.';
2557
+ const link=document.createElement('a');link.href='/architect';link.textContent='Return to Architect';root.append(message,link);
2558
+ });
@@ -0,0 +1,24 @@
1
+ @media(max-width:900px){html,body,#root{height:100%;overflow:hidden}.app{height:100dvh;min-height:0;grid-template-columns:minmax(0,1fr);grid-template-rows:auto auto minmax(100px,1fr) minmax(300px,50dvh);overflow:hidden}.app.chat-closed,.app.build-workspace{grid-template-rows:auto auto minmax(0,1fr)}.app>.canvas{min-height:0;height:auto;overflow:hidden}.app>.chat{height:auto;min-height:0;overflow:hidden}.chat>.messages{min-height:0;flex:1;overflow:auto}.chat>.composer{position:relative;flex-shrink:0;background:var(--paper)}}
2
+ .chat>.composer{padding:6px 8px;border-radius:14px}.chat>.composer textarea{min-height:32px;height:32px;max-height:120px;padding:4px 6px;font-size:14px;line-height:20px;resize:none;overflow-y:auto}.chat>.composer .composer-actions{padding-top:2px}.chat>.composer .composer-actions button{width:32px;height:32px;min-height:32px;padding:6px}
3
+ .chat>.chat-head{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;flex-wrap:wrap;gap:4px 6px;padding:4px 8px;min-height:0}.chat-head>.chat-heading-row{display:contents}.chat-head .chat-title{display:none}.chat-head>.model{order:0;max-width:100%;font-size:12px;line-height:18px;min-height:26px;padding:3px 7px;margin:0}.chat-head>.live-toggle{order:1;flex:0 0 auto;width:auto;display:inline-flex;align-items:center;gap:4px;white-space:nowrap;font-size:11px;line-height:18px;padding:0;margin:0}.chat-head .live-toggle input{width:13px;height:13px;margin:0}.chat-head .icon{order:2;width:26px;height:26px;min-height:26px;padding:4px;margin:0}.chat-head .icon svg{width:15px;height:15px}.chat>.skills{display:flex;align-items:center;flex-wrap:nowrap;overflow-x:auto;gap:3px;padding:2px 8px 4px;margin:0;min-height:0;background:transparent;border-radius:0}.chat>.skills .known-bar,.chat>.skills .skill{flex:0 0 auto;width:auto;font-size:11px;line-height:16px;min-height:24px;padding:3px 6px;margin:0;gap:4px;border-radius:7px}.chat>.skills svg{width:13px;height:13px}
4
+ .chat-head .chat-top-utilities{display:inline-flex;align-items:center;gap:4px;order:3;margin-left:auto}
5
+ .chat-head .chat-top-utilities button{font-size:11px;line-height:18px;min-height:26px;padding:3px 6px;border:0;background:transparent;color:var(--ink-2)}
6
+ .chat>.voice-status:empty{display:none}
7
+ .chat.idle>.messages{display:none}
8
+ @media(max-width:900px){.app.idle-chat{grid-template-rows:auto auto minmax(100px,1fr) auto}.app>.chat.idle{height:auto;min-height:0}}
9
+ .ready-reference{margin:20px 0}.ready-reference>p{margin:8px 0}.ready-reference li{display:flex;align-items:center;gap:10px;padding:8px 0}.ready-reference li>span:nth-child(2){flex:1}.ready-reference details{margin-top:10px}.ready-reference details p{margin:8px 0}
10
+ .bar-actions .ready-download{font-size:12px;padding:6px 10px}.toolbar .reset-board{font-size:12px;padding:6px 9px;gap:5px}
11
+
12
+ /* Keep project tools in one slim row; the diagram owns the remaining height. */
13
+ .app>.bar{flex-wrap:nowrap;gap:6px;padding:4px 8px;min-height:40px}
14
+ .app>.bar>.brand{padding:2px;flex-shrink:0}
15
+ .app>.bar>.crumbs{order:0;flex:1 1 auto;min-width:0;flex-wrap:nowrap;gap:4px!important}
16
+ .app>.bar .workspace-label{display:none}
17
+ .app>.bar .project-rename{flex:1;min-width:0}
18
+ .app>.bar .project-name{font-size:13px;padding:4px 6px;height:30px;min-width:0}
19
+ .app>.bar .projects-btn{padding:4px 6px;min-width:0;min-height:30px;font-size:12px}
20
+ .app>.bar>.bar-actions{margin-left:auto;padding:0;border:0;gap:4px;flex-shrink:0}
21
+ .app>.bar .bar-actions>.btn{padding:5px 8px;font-size:12px;min-height:30px;gap:4px}
22
+ .app>.bar .bar-actions>.icon{width:30px;height:30px;min-height:30px;padding:5px}
23
+ @media(max-width:900px){.app>.bar .bar-actions>.ready-download{display:none}}
24
+ @media(max-width:650px){.app>.bar .projects-btn>span{display:none}.app>.bar>.bar-actions{gap:2px}.app>.bar .bar-actions>.btn{font-size:11px;padding:5px 6px}.app>.bar .bar-actions>.icon{width:26px}.app>.bar .project-name{font-size:12px;padding:3px;}.app>.bar .project-rename>svg{display:none}}
@@ -0,0 +1,5 @@
1
+ import file from './reference-example/project.json' with {type:'json'};
2
+ import {fromProjectFile} from './project-file.mjs';
3
+ export const READY_EXAMPLE_ID='atmem-ai-ready-example-v1';
4
+ export function readyExample(){const {project}=fromProjectFile(structuredClone(file));return project;}
5
+ export function isReadyExample(project){return project?.extensions?.preparedExample?.version===1&&project?.extensions?.preparedExample?.owner==='AtMem example author';}