@atmem/architect 0.2.2 → 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/README.md +4 -0
- package/dist/assets/{index-CWUupGC1.js → index-Bn0-zM3N.js} +382 -382
- package/dist/assets/index-DABXOHnQ.css +1 -0
- package/dist/assets/{index.es-H4csXVqE.js → index.es-d0Mtn3LG.js} +1 -1
- package/dist/assets/{visual-export-D0f1PbRp.js → visual-export-DPn_mbuF.js} +3 -3
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/atbots_engineering_backend.py +214 -0
- package/server/codex_engineering_backend.py +3 -0
- package/server/engineering_handoff.py +16 -1
- package/server/engineering_routes.py +5 -3
- package/server/engineering_sessions.py +2 -0
- package/server/local_validation.py +5 -2
- package/server/terraform_routes.py +2 -1
- package/src/EngineeringPanel.jsx +2 -2
- package/src/ImplementationDelivery.jsx +7 -3
- package/src/OutputsView.jsx +2 -2
- package/src/ReadyExample.jsx +1 -1
- package/src/chat-composer.css +62 -0
- package/src/engineering-client.mjs +7 -0
- package/src/hosted-workspace.mjs +52 -0
- package/src/journey-workspace.css +5 -2
- package/src/local-agent.mjs +1 -1
- package/src/main.jsx +67 -25
- package/src/onboarding.css +14 -0
- package/src/visual-export.mjs +1 -1
- package/dist/assets/index-DBMP6Pxk.css +0 -1
|
@@ -64,6 +64,8 @@ class RecoveredStart:
|
|
|
64
64
|
|
|
65
65
|
class EngineeringBackend(Protocol):
|
|
66
66
|
name: str
|
|
67
|
+
@property
|
|
68
|
+
def session_count(self) -> int: ...
|
|
67
69
|
async def create(self,scope: ProjectScope,workspace: str,selection: ModelSelection) -> str: ...
|
|
68
70
|
async def resume(self,scope: ProjectScope,handle: str,workspace: str,selection: ModelSelection) -> None: ...
|
|
69
71
|
async def observe(self,scope: ProjectScope,handle: str,turn: str) -> BackendEvent | None: ...
|
|
@@ -133,10 +133,13 @@ class DockerValidationWorker:
|
|
|
133
133
|
env=env,capture_output=True,text=True,timeout=15)
|
|
134
134
|
if inspect.returncode!=0 or inspect.stdout.strip()!=spec.image:
|
|
135
135
|
raise RuntimeError('The reviewed local image/platform is unavailable; no image was pulled')
|
|
136
|
-
|
|
136
|
+
# A registered config image ID identifies one architecture, not a
|
|
137
|
+
# multi-platform tag/index. Inspect that exact cached config directly;
|
|
138
|
+
# --platform inspection requires a newer daemon API unnecessarily.
|
|
139
|
+
platform=subprocess.run([*prefix,'image','inspect',spec.image,'--format','{{.Os}}/{{.Architecture}} {{.Id}}'],
|
|
137
140
|
env=env,capture_output=True,text=True,timeout=15)
|
|
138
141
|
selection=platform.stdout.strip().split()
|
|
139
|
-
if platform.returncode!=0 or len(selection)!=2 or selection[0]!=spec.platform or
|
|
142
|
+
if platform.returncode!=0 or len(selection)!=2 or selection[0]!=spec.platform or selection[1]!=spec.image:
|
|
140
143
|
raise RuntimeError('The reviewed cached platform is unavailable; no fallback or image pull was used')
|
|
141
144
|
browser=spec.isolation_profile=='chromium-userns-v1'
|
|
142
145
|
postgres=spec.isolation_profile=='postgresql-local-v1'
|
|
@@ -71,7 +71,8 @@ def validate(payload):
|
|
|
71
71
|
env = {'PATH': os.getenv('PATH', ''), 'HOME': folder, 'TMPDIR': folder, 'TF_IN_AUTOMATION': '1', 'CHECKPOINT_DISABLE': '1', 'AWS_EC2_METADATA_DISABLED': 'true'}
|
|
72
72
|
cache=Path(setting('PROVIDER_CACHE') or ROOT/'.local/provider-cache')
|
|
73
73
|
if cache.is_dir():
|
|
74
|
-
|
|
74
|
+
writable_cache=Path(folder)/'.provider-cache';writable_cache.mkdir()
|
|
75
|
+
env['TF_PLUGIN_CACHE_DIR']=str(writable_cache)
|
|
75
76
|
mirror=Path(folder)/'.atmem-provider-mirror.tfrc'
|
|
76
77
|
mirror.write_text('provider_installation { filesystem_mirror { path = '+json.dumps(str(cache.resolve()))+' } }\n')
|
|
77
78
|
env['TF_CLI_CONFIG_FILE']=str(mirror)
|
package/src/EngineeringPanel.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {BrainActivity} from './BrainActivity.jsx';
|
|
2
2
|
import React,{useEffect,useRef,useState} from 'react';
|
|
3
|
-
import {engineeringCall,engineeringConversation,mergeEngineeringEvents} from './engineering-client.mjs';
|
|
3
|
+
import {engineeringCall,engineeringConversation,mergeEngineeringEvents,engineeringSelectionSupported} from './engineering-client.mjs';
|
|
4
4
|
import './engineering.css';
|
|
5
5
|
import {ArtifactChecks} from './ArtifactChecks.jsx';
|
|
6
6
|
import {verifyArtifactWork} from './artifact-work.mjs';
|
|
@@ -60,7 +60,7 @@ export function EngineeringPanel({projectId,session,connection,saveProject,onCon
|
|
|
60
60
|
try{await operation();}catch(e){if(live.current)setError(e.message);}finally{actionLock.current=false;if(live.current)setBusy(false);}
|
|
61
61
|
}
|
|
62
62
|
useEffect(()=>{onBusyChange?.(busy||loading||['running','starting'].includes(job?.state));return()=>onBusyChange?.(false);},[busy,loading,job?.state,onBusyChange]);
|
|
63
|
-
const exactSelection=connection
|
|
63
|
+
const exactSelection=engineeringSelectionSupported(session,connection);
|
|
64
64
|
const running=job?.state==='running',uncertain=job?.state==='starting';
|
|
65
65
|
const lastProgress=events.filter(event=>event.runId===job?.run&&['progress','message'].includes(event.kind)).at(-1);
|
|
66
66
|
const [lastReceived,setLastReceived]=useState(null),[now,setNow]=useState(Date.now());
|
|
@@ -3,10 +3,10 @@ import {FinalChecklist} from './FinalChecklist.jsx';
|
|
|
3
3
|
import {BrainActivity} from './BrainActivity.jsx';
|
|
4
4
|
import {completeProjectZip} from './final-package.mjs';
|
|
5
5
|
import React,{useEffect,useState,useRef} from 'react';
|
|
6
|
-
import {engineeringCall,verifiedHandoffDownload} from './engineering-client.mjs';
|
|
6
|
+
import {engineeringCall,verifiedHandoffDownload,engineeringDeliveryNeedsRefresh} from './engineering-client.mjs';
|
|
7
7
|
import {ArtifactChecks} from './ArtifactChecks.jsx';
|
|
8
8
|
|
|
9
|
-
export function ImplementationDelivery({project,session,onBuild,onSave,contextBusy=false,designBundle,onDesignDownload,onDownloaded,checkReceipt,onExplore,onNavigate}){
|
|
9
|
+
export function ImplementationDelivery({project,session,workStatus,onBuild,onSave,contextBusy=false,designBundle,onDesignDownload,onDownloaded,checkReceipt,onExplore,onNavigate}){
|
|
10
10
|
const [orcaResult,setOrcaResult]=useState(null);
|
|
11
11
|
const projectRef=useRef(project);projectRef.current=project;
|
|
12
12
|
const [prepared,setPrepared]=useState(null),[preparing,setPreparing]=useState(false);
|
|
@@ -18,7 +18,11 @@ export function ImplementationDelivery({project,session,onBuild,onSave,contextBu
|
|
|
18
18
|
if(!controller.signal.aborted)setLoaded({project,report:value});
|
|
19
19
|
}).catch(e=>{if(!controller.signal.aborted){if(e.status===404&&e.message.startsWith('Save or restore this project'))setLoaded({project,report:{artifact:null}});else setError(e.message);}});
|
|
20
20
|
return()=>controller.abort();
|
|
21
|
-
},[project,session.token,retry]);
|
|
21
|
+
},[project,session.token,retry,workStatus?.run,workStatus?.state,workStatus?.last_status]);
|
|
22
|
+
useEffect(()=>{
|
|
23
|
+
if(!engineeringDeliveryNeedsRefresh(report,workStatus))return;
|
|
24
|
+
const timer=setTimeout(()=>setRetry(value=>value+1),2500);return()=>clearTimeout(timer);
|
|
25
|
+
},[report,workStatus?.run,workStatus?.state]);
|
|
22
26
|
useEffect(()=>{
|
|
23
27
|
if(!report?.canPackage||designBundle?.project!==project){setPrepared(null);return;}
|
|
24
28
|
let stopped=false;const controller=new AbortController();setPreparing(true);setError('');
|
package/src/OutputsView.jsx
CHANGED
|
@@ -37,7 +37,7 @@ function Preview({name,content}){
|
|
|
37
37
|
if(name.endsWith('.md'))return <Document text={content}/>;
|
|
38
38
|
return <CodePreview className="output-code" content={content} name={name}/>;
|
|
39
39
|
}
|
|
40
|
-
export function OutputsView({project,run,environment,session,onDownload,onBack,emulation,onDiscuss,discussionBusy,onBuild,onSave,onDownloaded,onReview,checkReceipt,onNavigate}){
|
|
40
|
+
export function OutputsView({project,run,environment,session,workStatus,onDownload,onBack,emulation,onDiscuss,discussionBusy,onBuild,onSave,onDownloaded,onReview,checkReceipt,onNavigate}){
|
|
41
41
|
const [exploration,setExploration]=useState(null);
|
|
42
42
|
const projectRef=useRef(project);projectRef.current=project;
|
|
43
43
|
const [packing,setPacking]=useState(false);
|
|
@@ -60,7 +60,7 @@ export function OutputsView({project,run,environment,session,onDownload,onBack,e
|
|
|
60
60
|
<header className="doc-head"><h2>Delivery</h2></header>
|
|
61
61
|
<button className="link" onClick={onReview||onBack}>Back to Review</button>
|
|
62
62
|
{isReadyExample(project)&&<ReadyExampleChecklist/>}
|
|
63
|
-
{session?.engineeringEnabled&&(isReadyExample(project)?<details><summary>Checks for your edited project</summary><ImplementationDelivery key={project.id} project={project} session={session} onBuild={onBuild} onSave={onSave} contextBusy={discussionBusy} designBundle={loading?null:bundle} onDesignDownload={onDownload} onDownloaded={onDownloaded} checkReceipt={checkReceipt} onExplore={setExploration} onNavigate={onNavigate}/></details>:<ImplementationDelivery key={project.id} project={project} session={session} onBuild={onBuild} onSave={onSave} contextBusy={discussionBusy} designBundle={loading?null:bundle} onDesignDownload={onDownload} onDownloaded={onDownloaded} checkReceipt={checkReceipt} onExplore={setExploration} onNavigate={onNavigate}/>)}
|
|
63
|
+
{session?.engineeringEnabled&&(isReadyExample(project)?<details><summary>Checks for your edited project</summary><ImplementationDelivery key={project.id} project={project} session={session} workStatus={workStatus} onBuild={onBuild} onSave={onSave} contextBusy={discussionBusy} designBundle={loading?null:bundle} onDesignDownload={onDownload} onDownloaded={onDownloaded} checkReceipt={checkReceipt} onExplore={setExploration} onNavigate={onNavigate}/></details>:<ImplementationDelivery key={project.id} project={project} session={session} workStatus={workStatus} onBuild={onBuild} onSave={onSave} contextBusy={discussionBusy} designBundle={loading?null:bundle} onDesignDownload={onDownload} onDownloaded={onDownloaded} checkReceipt={checkReceipt} onExplore={setExploration} onNavigate={onNavigate}/>)}
|
|
64
64
|
<section aria-label="Project file explorer"><h3>Explore files</h3>
|
|
65
65
|
|
|
66
66
|
{!session?.engineeringEnabled&&<><p>Design package · no implementation generated.</p><button className="btn" disabled={!bundle||loading||!!error||packing} onClick={downloadDesign}>{packing?'Preparing ZIP…':'Download project ZIP'}</button></>}
|
package/src/ReadyExample.jsx
CHANGED
|
@@ -3,7 +3,7 @@ import receipt from './reference-example/receipt.json';
|
|
|
3
3
|
export function ReadyExampleDownload({compact=false}){
|
|
4
4
|
const [busy,setBusy]=useState(false),[error,setError]=useState('');
|
|
5
5
|
async function download(){setBusy(true);setError('');try{
|
|
6
|
-
const response=await fetch(receipt.url);if(!response.ok)throw Error('Example download unavailable. Retry.');
|
|
6
|
+
const response=await fetch(import.meta.env.BASE_URL+receipt.url.replace(/^\//,''));if(!response.ok)throw Error('Example download unavailable. Retry.');
|
|
7
7
|
const bytes=await response.arrayBuffer();const hash=[...new Uint8Array(await crypto.subtle.digest('SHA-256',bytes))].map(n=>n.toString(16).padStart(2,'0')).join('');
|
|
8
8
|
if(hash!==receipt.sha256)throw Error('Example integrity check failed. Retry.');
|
|
9
9
|
const url=URL.createObjectURL(new Blob([bytes],{type:'application/zip'})),link=document.createElement('a');link.href=url;link.download='atmem-ai-complete-example.zip';link.click();setTimeout(()=>URL.revokeObjectURL(url),60000);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* Keep the conversation and the place to reply visibly distinct. */
|
|
2
|
+
.chat { container-type:inline-size; }
|
|
3
|
+
.chat > .chat-body { display:flex;flex:1;flex-direction:column;min-height:0;min-width:0; }
|
|
4
|
+
.chat-history { position:relative;display:flex;flex:1;flex-direction:column;min-height:0;min-width:0;overflow:hidden; }
|
|
5
|
+
.chat-history-label { margin:8px 16px 0;font-size:12px;font-weight:600;color:var(--ink-2); }
|
|
6
|
+
.chat-history .messages { min-height:0;padding-top:10px; }
|
|
7
|
+
.chat-compose-pane { overflow-anchor:none;display:flex;flex-direction:column;flex-shrink:0;min-width:0; }
|
|
8
|
+
.chat-history .chat-jump-latest { bottom:12px;right:16px; }
|
|
9
|
+
.chat .composer { border:2px solid var(--color-brand-text);background:var(--paper);padding:10px;gap:6px; }
|
|
10
|
+
.composer-label { display:block;font-size:13px;font-weight:700;color:var(--ink);line-height:1.4; }
|
|
11
|
+
.chat .composer textarea { box-sizing:border-box;min-height:76px;padding:8px;background:var(--board);border-radius:8px; }
|
|
12
|
+
.chat .composer textarea:focus { outline:2px solid var(--color-brand-text);outline-offset:-2px; }
|
|
13
|
+
.chat .composer-actions .send { display:inline-flex;align-items:center;justify-content:center;gap:5px;width:auto;min-width:70px;padding:0 10px;border-radius:10px;background:var(--color-brand);color:var(--color-brand-ink);font-weight:700; }
|
|
14
|
+
.chat .composer-actions .send:disabled { background:var(--line);color:var(--ink-2); }
|
|
15
|
+
/* A wide chat drawer can use the same two-column conversation/reply layout. */
|
|
16
|
+
@container (min-width:600px) {
|
|
17
|
+
.chat-body { display:grid!important;grid-template-columns:minmax(0,1.2fr) minmax(220px,1fr); }
|
|
18
|
+
.chat-compose-pane { min-height:0;overflow:auto;padding-top:8px;border-left:1px solid var(--line); }
|
|
19
|
+
.chat-compose-pane .composer { order:-1;position:sticky;top:8px;flex-shrink:0;z-index:1; }
|
|
20
|
+
}
|
|
21
|
+
@media (max-width:900px) {
|
|
22
|
+
.app:not(.chat-closed) { height:100dvh;min-height:0;grid-template-rows:auto auto minmax(0,2fr) minmax(300px,3fr);overflow:hidden; }
|
|
23
|
+
.app:not(.chat-closed) .chat { height:auto;min-height:0;overflow:hidden; }
|
|
24
|
+
.chat > .chat-body { display:grid;grid-template-columns:minmax(0,1fr) minmax(148px,.9fr); }
|
|
25
|
+
.chat-compose-pane { min-height:0;overflow:auto;border-left:1px solid var(--line);padding:8px 0 0; }
|
|
26
|
+
.chat-compose-pane .composer { order:-1;position:sticky;top:8px;flex-shrink:0;z-index:1;margin:0 8px 8px;padding:8px; }
|
|
27
|
+
.chat-head { display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;padding:8px 12px; }
|
|
28
|
+
.chat-heading-row { grid-column:1/-1; }
|
|
29
|
+
.chat-head .model { grid-column:1;grid-row:2;justify-self:start;max-width:100%; }
|
|
30
|
+
.chat-head .live-toggle { grid-column:2;grid-row:2;order:0;flex-basis:auto;white-space:nowrap; }
|
|
31
|
+
.chat .composer textarea { min-height:64px;max-height:180px;font-size:16px; }
|
|
32
|
+
.chat .composer-actions { gap:4px; }
|
|
33
|
+
.chat .composer-actions .attach-button,.chat .composer-actions .mic { width:28px;height:32px;padding:3px;flex-shrink:0; }
|
|
34
|
+
.chat .composer-actions .send { min-width:54px;padding:0 7px;font-size:12px; }
|
|
35
|
+
.chat-history-label { margin-left:12px; }
|
|
36
|
+
.board-start { top:50%;max-height:calc(100% - 24px);overflow:auto; }
|
|
37
|
+
.board-start h2 { margin-top:0;font-size:18px; }
|
|
38
|
+
.board-surface:has(.board-start) > .board-components:not([data-expanded=true]) { top:auto;bottom:8px; }
|
|
39
|
+
.chat-history .messages { padding:10px 12px; }
|
|
40
|
+
.chat .skills { flex-wrap:nowrap;overflow-x:auto;flex-shrink:0; }
|
|
41
|
+
.chat .skills > * { flex-shrink:0;white-space:nowrap; }
|
|
42
|
+
.chat-compose-pane .prompts { padding:0 8px 8px; }
|
|
43
|
+
}
|
|
44
|
+
@media (max-width:900px) and (max-height:600px) {
|
|
45
|
+
.app:not(.chat-closed) { grid-template-rows:auto auto minmax(0,1fr) minmax(238px,3fr); }
|
|
46
|
+
.chat .composer textarea { min-height:54px; }
|
|
47
|
+
.chat-head { padding-top:6px;padding-bottom:4px; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@media (max-width:900px) and (min-height:760px) { .chat .composer textarea { min-height:88px; } }
|
|
51
|
+
/* Keep navigation usable when a short window or on-screen keyboard reduces height. */
|
|
52
|
+
@media (max-width:900px) and (max-height:600px) {
|
|
53
|
+
.app:not(.chat-closed) { grid-template-rows:auto auto minmax(0,1fr) minmax(280px,3fr); }
|
|
54
|
+
.unified-journey { flex-direction:row;align-items:center;padding:4px;gap:4px; }
|
|
55
|
+
.unified-journey .stage-tabs { flex:1;min-width:0;width:0;padding:4px; }
|
|
56
|
+
.unified-journey .journey-current { flex:0 1 110px;min-width:0;max-width:110px;padding:0; }
|
|
57
|
+
.unified-journey .journey-current .btn { padding:6px 8px;font-size:11px;min-height:36px; }
|
|
58
|
+
.chat-compose-pane .composer { padding:6px;gap:4px; }
|
|
59
|
+
}
|
|
60
|
+
/* Keep the next action on the same row as the scrolling stage tabs. */
|
|
61
|
+
.unified-journey .journey-current { flex:0 0 auto;max-width:none;margin:0;padding:0; }
|
|
62
|
+
.unified-journey .journey-current .btn { white-space:nowrap;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.2; }
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// Backend-owned jobs: no native thread IDs, host paths or model credentials.
|
|
2
2
|
import {unzipSync} from 'fflate';
|
|
3
|
+
export function engineeringSelectionSupported(session,connection){
|
|
4
|
+
if(!connection?.model||connection.model==='default'||!connection.effort)return false;
|
|
5
|
+
return connection.provider==='codex'||!!(session?.hosted&&connection.provider==='openrouter'&&connection.effort==='medium');
|
|
6
|
+
}
|
|
7
|
+
export function engineeringDeliveryNeedsRefresh(report,job){
|
|
8
|
+
return ['starting','running'].includes((job||report?.session)?.state)||!!report?.checks?.some(check=>['queued','running'].includes(check.state));
|
|
9
|
+
}
|
|
3
10
|
export async function engineeringCall(session, action, payload, {signal,fetcher=fetch}={}) {
|
|
4
11
|
if(!session?.token)throw new Error('Connect to the session service first.');
|
|
5
12
|
const response=await fetcher(`/api/engineering/${action}`,{method:'POST',signal,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Same-origin parent owns Firebase identity; the workspace never receives a token or provider key.
|
|
2
|
+
export const HOSTED=import.meta.env?.VITE_ARCHITECT_HOSTED==='1';
|
|
3
|
+
let initial=null,port=null,sequence=0;const waiting=new Map(),versions=new Map();let changeSequence=0;
|
|
4
|
+
export function hostedRevision(id){return versions.get(id)||0;}
|
|
5
|
+
export function noteHostedChange(projectId){const version=++changeSequence;versions.set(projectId,version);return hostedCall('changed',{projectId,version});}
|
|
6
|
+
export function hostedInitial(){if(!initial)throw Error('Hosted workspace is not connected.');return initial;}
|
|
7
|
+
export function hostedCall(action,payload={},signal){
|
|
8
|
+
if(!port)return Promise.reject(Error('Return to Architect and reopen your project.'));
|
|
9
|
+
if(signal?.aborted)return Promise.reject(new DOMException('Aborted','AbortError'));
|
|
10
|
+
const id=++sequence;
|
|
11
|
+
return new Promise((resolve,reject)=>{
|
|
12
|
+
const timeout=setTimeout(()=>finish(Error('The workspace request timed out. Your draft remains open.')),10*60*1000);
|
|
13
|
+
const abort=()=>{port.postMessage({type:'abort',id});finish(new DOMException('Aborted','AbortError'));};
|
|
14
|
+
function finish(error,value){clearTimeout(timeout);signal?.removeEventListener('abort',abort);waiting.delete(id);error?reject(error):resolve(value);}
|
|
15
|
+
waiting.set(id,finish);signal?.addEventListener('abort',abort,{once:true});port.postMessage({type:'request',id,action,payload});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export async function connectHostedWorkspace(){
|
|
19
|
+
if(!HOSTED)return;
|
|
20
|
+
if(window.parent===window)throw Error('Open your project from atmem.ai/architect.');
|
|
21
|
+
initial=await new Promise((resolve,reject)=>{
|
|
22
|
+
const timeout=setTimeout(()=>{window.removeEventListener('message',receive);reject(Error('Return to Architect and reopen your project.'));},15000);
|
|
23
|
+
function receive(event){
|
|
24
|
+
if(event.origin!==location.origin||event.source!==parent||event.data?.type!=='atmem.workspace.initialize'||event.ports.length!==1)return;
|
|
25
|
+
clearTimeout(timeout);window.removeEventListener('message',receive);port=event.ports[0];
|
|
26
|
+
port.onmessage=({data})=>{if(data?.type==='response')waiting.get(data.id)?.(data.error?Error(data.error):null,data.value);};port.start();resolve(event.data.initial);
|
|
27
|
+
}
|
|
28
|
+
window.addEventListener('message',receive);window.parent.postMessage({type:'atmem.workspace.ready'},location.origin);
|
|
29
|
+
});
|
|
30
|
+
// Do not leave project drafts, conversations or profile state in a shared browser's local storage.
|
|
31
|
+
const values=new Map([['atmem-architect-theme',initial.theme||'light']]);
|
|
32
|
+
const transient={getItem:key=>values.get(String(key))??null,setItem:(key,value)=>values.set(String(key),String(value)),removeItem:key=>values.delete(String(key)),clear:()=>values.clear(),key:index=>[...values.keys()][index]??null,get length(){return values.size;}};
|
|
33
|
+
Object.defineProperty(window,'localStorage',{value:transient,configurable:true});
|
|
34
|
+
const network=window.fetch.bind(window);
|
|
35
|
+
window.fetch=async(input,options={})=>{
|
|
36
|
+
const url=new URL(typeof input==='string'?input:input.url,location.href);
|
|
37
|
+
if(url.origin!==location.origin||!url.pathname.startsWith('/api/'))return network(input,options);
|
|
38
|
+
let body=null;
|
|
39
|
+
if(options.body){
|
|
40
|
+
if(typeof options.body==='string')body=JSON.parse(options.body);
|
|
41
|
+
else{
|
|
42
|
+
const bytes=new Uint8Array(options.body instanceof Blob?await options.body.arrayBuffer():options.body);
|
|
43
|
+
let binary='';for(let start=0;start<bytes.length;start+=8192)binary+=String.fromCharCode(...bytes.subarray(start,start+8192));
|
|
44
|
+
body={__workspaceBinary:btoa(binary)};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if(url.pathname==='/api/projects/save'&&body)body.bridgeVersion??=hostedRevision(body.id);
|
|
48
|
+
const result=await hostedCall('fetch',{path:url.pathname,method:options.method||'GET',body},options.signal);
|
|
49
|
+
const content=result.base64?Uint8Array.from(atob(result.base64),character=>character.charCodeAt(0)):result.bytes?new Uint8Array(result.bytes):JSON.stringify(result.body);
|
|
50
|
+
return new Response(content,{status:result.status,headers:result.headers||{'Content-Type':'application/json'}});
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
.stage-tabs>button[aria-selected=true]{outline:2px solid var(--accent,#be5679);outline-offset:-2px;font-weight:600}
|
|
5
5
|
.stage-tabs>button.stage-complete{background:#e7f4eb;color:#215d3b;border-color:#a9cdb4}
|
|
6
6
|
.stage-tabs>button>span{font-size:.85em;border:1px solid currentColor;border-radius:50%;width:22px;height:22px;display:grid;place-items:center}
|
|
7
|
-
.unified-journey{display:flex;flex-direction:
|
|
8
|
-
.unified-journey .
|
|
7
|
+
.unified-journey{display:flex;flex-direction:row;align-items:center}
|
|
8
|
+
.unified-journey .stage-tabs{width:auto;flex:0 1 auto;min-width:0;padding:2px}
|
|
9
|
+
.unified-journey .journey-current{margin:0;padding:0;display:flex;flex:0 0 auto;gap:0;align-items:center}
|
|
9
10
|
.unified-journey .journey-current p{margin:4px 0;flex:1}
|
|
10
11
|
.project-rename{display:flex;align-items:center;min-width:0}.project-rename>svg{opacity:0}.project-rename:hover>svg,.project-rename:focus-within>svg{opacity:1}
|
|
11
12
|
.bar .projects-btn{display:inline-flex;align-items:center;gap:7px;width:auto;min-width:100px;padding:7px 10px}
|
|
@@ -109,3 +110,5 @@
|
|
|
109
110
|
.star-prompt{width:min(460px,calc(100vw - 32px));max-width:460px;border:1px solid var(--line);border-radius:16px;padding:22px;background:var(--paper);color:var(--ink);box-shadow:0 18px 60px #0003}.star-prompt::backdrop{background:#0005}.star-prompt header,.star-prompt h2,.star-prompt-actions{display:flex;align-items:center;gap:10px}.star-prompt header{justify-content:space-between}.star-prompt h2{font-size:1.2rem}.star-prompt h2>svg{color:var(--accent);fill:var(--accent-soft)}.star-prompt p{margin:18px 0;line-height:1.5}.star-prompt-actions>a{flex:1;text-decoration:none}.star-prompt-actions>button{min-width:90px}
|
|
110
111
|
|
|
111
112
|
.check-run-action{display:inline-flex;margin-left:12px;flex-shrink:0}.check-run-action>button:disabled{pointer-events:none}.check-run-action:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
|
113
|
+
|
|
114
|
+
@media(max-width:650px){.unified-journey .stage-tabs{flex:1;min-width:0;width:0}.unified-journey .journey-current{max-width:120px}.unified-journey .journey-current>.btn{white-space:normal;padding:7px 9px;font-size:12px;min-height:36px}}
|
package/src/local-agent.mjs
CHANGED
|
@@ -99,7 +99,7 @@ export async function discoverLocalAgent() {
|
|
|
99
99
|
});
|
|
100
100
|
if (!response.ok) throw Error("Local sidecar unavailable");
|
|
101
101
|
const session = await response.json();
|
|
102
|
-
if (!session.token ||
|
|
102
|
+
if (!session.token || !["local-sidecar","hosted-workspace"].includes(session.status))
|
|
103
103
|
throw Error("Invalid local sidecar response");
|
|
104
104
|
return session;
|
|
105
105
|
}
|
package/src/main.jsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {HOSTED,connectHostedWorkspace,hostedInitial,hostedCall,hostedRevision,noteHostedChange} from './hosted-workspace.mjs';
|
|
1
2
|
import {readyExample,isReadyExample} from './ready-example.mjs';
|
|
2
3
|
import {ReadyExampleDownload} from './ReadyExample.jsx';
|
|
3
4
|
import {StarPrompt,STAR_URL} from './StarPrompt.jsx';
|
|
@@ -171,6 +172,7 @@ import { zipSync, strToU8 } from "fflate";
|
|
|
171
172
|
const STORAGE = "atforma-workspace-v3";
|
|
172
173
|
// A new project starts empty: the user describes what they want first.
|
|
173
174
|
import './onboarding.css';
|
|
175
|
+
import './chat-composer.css';
|
|
174
176
|
const emptyProject = () => ({ id: crypto.randomUUID(), version: 1, revision: 1, name: "New project", provider: "tools", requirements: "", nodes: [], edges: [] });
|
|
175
177
|
const ALL_CATALOG = [...CATALOG, ...UPSTREAM_CATALOG];
|
|
176
178
|
const KIND_ICON = {
|
|
@@ -257,6 +259,7 @@ function Mark({ size = 20 }) {
|
|
|
257
259
|
// Which architect the user chose; absent on first visit, so we ask before anything else.
|
|
258
260
|
const CONNECTION = "atforma-connection-v1";
|
|
259
261
|
function readConnection() {
|
|
262
|
+
if(HOSTED)return hostedInitial().connection;
|
|
260
263
|
try {
|
|
261
264
|
const c = JSON.parse(localStorage.getItem(CONNECTION) || "null");
|
|
262
265
|
// Older builds stored "offline"; the architect now always needs a real model.
|
|
@@ -273,6 +276,7 @@ function saveConnection(c) {
|
|
|
273
276
|
} catch {}
|
|
274
277
|
}
|
|
275
278
|
function readSaved() {
|
|
279
|
+
if(HOSTED)return withDerived(fromProjectFile(hostedInitial().file).project);
|
|
276
280
|
try {
|
|
277
281
|
const raw = localStorage.getItem(STORAGE);
|
|
278
282
|
const p = validateImport(raw ? JSON.parse(raw) : emptyProject());
|
|
@@ -665,14 +669,14 @@ function savedTheme() {
|
|
|
665
669
|
return "light";
|
|
666
670
|
}
|
|
667
671
|
}
|
|
668
|
-
|
|
672
|
+
|
|
669
673
|
|
|
670
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:[]};}}
|
|
671
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])]);
|
|
672
676
|
function App() {
|
|
673
677
|
const [starOffer,setStarOffer]=useState(0);
|
|
674
678
|
const [introOpen,setIntroOpen]=useState(false),[voiceOpenRequest,setVoiceOpenRequest]=useState(0);
|
|
675
|
-
const [project, setProject] = useState(()
|
|
679
|
+
const [project, setProject] = useState(()=>!HOSTED&&new URL(location.href).searchParams.get('example')==='1'&&!projectIdFromUrl(location.href)?readyExample():readSaved());
|
|
676
680
|
const projectRef = useRef(project);
|
|
677
681
|
projectRef.current = project;
|
|
678
682
|
const [initialCanvasView] = useState(()=>readCanvasView(project));
|
|
@@ -917,16 +921,19 @@ function App() {
|
|
|
917
921
|
return () => { clearTimeout(zoom); clearTimeout(clear); };
|
|
918
922
|
}, [attention, fitView]);
|
|
919
923
|
|
|
924
|
+
function showConnections(){if(HOSTED){void hostedCall('account').catch(error=>notify(error.message));return;}setModal('connections');}
|
|
920
925
|
const refreshSession = useCallback(async () => {
|
|
921
926
|
try {
|
|
922
927
|
const next = await discoverLocalAgent();
|
|
923
928
|
setSession(next);
|
|
929
|
+
if(HOSTED&&next.defaultConnection)setConnection(current=>current?.model===next.defaultConnection.model&¤t?.effort===next.defaultConnection.effort?current:next.defaultConnection);
|
|
924
930
|
return next;
|
|
925
931
|
} catch {
|
|
926
932
|
setSession(null);
|
|
927
933
|
return null;
|
|
928
934
|
}
|
|
929
935
|
}, []);
|
|
936
|
+
useEffect(()=>{if(HOSTED&&session?.token)void hostedCall('ready').catch(()=>{});},[session?.token]);
|
|
930
937
|
useEffect(() => {
|
|
931
938
|
refreshSession();
|
|
932
939
|
const timer=setInterval(refreshSession,30000);
|
|
@@ -1216,7 +1223,11 @@ function App() {
|
|
|
1216
1223
|
const raw = JSON.parse(content);
|
|
1217
1224
|
if (raw.format || raw.version === 1) {
|
|
1218
1225
|
const { project: opened, migrated, from } = parseProjectFile(content);
|
|
1219
|
-
|
|
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 });
|
|
1220
1231
|
notify(fileImportAsCopy.current?"New project created from the saved design. The original is preserved.":migrated ? `Project opened and upgraded from ${from}.` : "Project opened.");
|
|
1221
1232
|
} else {
|
|
1222
1233
|
loadDesign(importUpstreamProject(raw));
|
|
@@ -1649,13 +1660,15 @@ function App() {
|
|
|
1649
1660
|
updateEngagement({ ...skipped, lastAsked: next });
|
|
1650
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." }]);
|
|
1651
1662
|
}
|
|
1652
|
-
|
|
1663
|
+
useEffect(()=>{if(HOSTED)void noteHostedChange(project.id).catch(()=>{});},[project,messages]);
|
|
1664
|
+
// Autosave through the selected local or hosted session service.
|
|
1653
1665
|
const binned = useRef(new Set());
|
|
1654
1666
|
useEffect(() => {
|
|
1655
|
-
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;
|
|
1656
1669
|
const t = setTimeout(async () => {
|
|
1657
1670
|
try {
|
|
1658
|
-
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}:{}) });
|
|
1659
1672
|
if(saved.memory)setMemorySync({projectId:project.id,...saved.memory});
|
|
1660
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}`); }
|
|
1661
1674
|
}, 800);
|
|
@@ -1711,6 +1724,7 @@ function App() {
|
|
|
1711
1724
|
}
|
|
1712
1725
|
async function openSaved(id) {
|
|
1713
1726
|
try {
|
|
1727
|
+
if(HOSTED)await saveEngineeringProject(projectRef.current.id);
|
|
1714
1728
|
const { project: opened } = fromProjectFile(await projectStore(session, "get", { id }));
|
|
1715
1729
|
switchTo(opened);
|
|
1716
1730
|
setModal(null);
|
|
@@ -1739,20 +1753,30 @@ function App() {
|
|
|
1739
1753
|
})();
|
|
1740
1754
|
return () => { active = false; };
|
|
1741
1755
|
}, [session?.token]);
|
|
1742
|
-
function newProject() {
|
|
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
|
+
}
|
|
1743
1764
|
function createOwnProject(){
|
|
1765
|
+
if(HOSTED){void newProject();return;}
|
|
1744
1766
|
switchTo(validateImport(emptyProject()));setModal(connection?null:'connections');
|
|
1745
1767
|
requestAnimationFrame(()=>document.querySelector('input[aria-label="Project name"]')?.focus());
|
|
1746
1768
|
}
|
|
1747
|
-
function openReadyExample(){
|
|
1769
|
+
async function openReadyExample(){
|
|
1748
1770
|
if(busy||agentBusy||intake.length)return;
|
|
1749
|
-
const example=readyExample();example.id=crypto.randomUUID();
|
|
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);
|
|
1750
1774
|
}
|
|
1751
1775
|
async function binProject(id) {
|
|
1752
1776
|
try {
|
|
1753
1777
|
binned.current.add(id);
|
|
1754
1778
|
await projectStore(session, "trash", { id });
|
|
1755
|
-
if
|
|
1779
|
+
if(id===projectRef.current.id){if(HOSTED){const {file}=await hostedCall('new-project');switchTo(fromProjectFile(file).project);}else switchTo(validateImport(emptyProject()));}
|
|
1756
1780
|
await refreshProjects();
|
|
1757
1781
|
notify("Moved to the bin. Restore it from Projects any time.");
|
|
1758
1782
|
} catch (e) {
|
|
@@ -1868,7 +1892,7 @@ function App() {
|
|
|
1868
1892
|
</button>
|
|
1869
1893
|
<nav className="crumbs" aria-label="Level"><span className="workspace-label">PROJECT</span>
|
|
1870
1894
|
<button className="projects-btn" aria-label="Projects" title="Your projects" onClick={() => (refreshProjects(), setModal("projects"))}>
|
|
1871
|
-
<FolderOpen size={22}
|
|
1895
|
+
<FolderOpen size={22} /><span>Projects</span>
|
|
1872
1896
|
</button>
|
|
1873
1897
|
<label className="project-rename"><input
|
|
1874
1898
|
className="project-name"
|
|
@@ -1879,7 +1903,8 @@ function App() {
|
|
|
1879
1903
|
/><PencilRuler size={14} aria-hidden="true"/></label>
|
|
1880
1904
|
</nav>
|
|
1881
1905
|
<div className="bar-actions">
|
|
1882
|
-
{
|
|
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>}</>}
|
|
1883
1908
|
{storageError && <span className="warn-text">Not saved</span>}
|
|
1884
1909
|
<button
|
|
1885
1910
|
className="icon"
|
|
@@ -1915,7 +1940,7 @@ function App() {
|
|
|
1915
1940
|
<hr/><button role="menuitem" onClick={() => setModal("memory")} disabled={!session}><BrainActivity size={18} active={false}/>AtMem memory</button>
|
|
1916
1941
|
<button role="menuitem" onClick={() => setModal("start-over")} disabled={busy || agentBusy}><RotateCcw size={16}/>Start over</button>
|
|
1917
1942
|
<hr />
|
|
1918
|
-
|
|
1943
|
+
{!HOSTED&&<button role="menuitem" onClick={() => showConnections()}><Waypoints size={16}/>AI providers</button>}
|
|
1919
1944
|
<button role="menuitem" onClick={() => setModal("licenses")}><ShieldCheck size={16}/>Licences</button>
|
|
1920
1945
|
<a role="menuitem" href={STAR_URL} target="_blank" rel="noopener noreferrer">Star on GitHub ↗</a>
|
|
1921
1946
|
</div>
|
|
@@ -2197,7 +2222,7 @@ function App() {
|
|
|
2197
2222
|
<section className="review-costs" aria-label="Project costs">
|
|
2198
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'});}}} />}
|
|
2199
2224
|
</section>
|
|
2200
|
-
{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={()=>
|
|
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>}
|
|
2201
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>}
|
|
2202
2227
|
{mode==='review'&&<ProjectChecks generationPending={generationPending} receipt={checkReceipt} project={project} environment={environment} session={session} onReceipt={setCheckReceipt} onBusy={setChecking}/>}
|
|
2203
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>}
|
|
@@ -2208,7 +2233,7 @@ function App() {
|
|
|
2208
2233
|
|
|
2209
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>}
|
|
2210
2235
|
|
|
2211
|
-
{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")} />}
|
|
2212
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")} />}
|
|
2213
2238
|
<div className="choices-workspace" hidden={!(mode==="design"&&["options","decisions","location"].includes(designView))}>
|
|
2214
2239
|
{mode === "design" && ["options","decisions"].includes(designView) && (
|
|
@@ -2241,10 +2266,11 @@ function App() {
|
|
|
2241
2266
|
/>
|
|
2242
2267
|
Draw as we talk
|
|
2243
2268
|
</label>
|
|
2244
|
-
|
|
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">
|
|
2245
2271
|
<span className={`led ${connection && session ? "on" : ""}`} />
|
|
2246
2272
|
{connection ? `${connection.model}${connection.effort ? ` · ${EFFORT_LABEL[connection.effort] || connection.effort}` : ""}` : "Connect a model"}
|
|
2247
|
-
</button
|
|
2273
|
+
</button></>}
|
|
2248
2274
|
<span className="chat-top-utilities">
|
|
2249
2275
|
{hasIntroduction&&<button type="button" aria-expanded={introOpen} onClick={()=>setIntroOpen(value=>!value)}>{introOpen?'▾':'▸'} Introduction</button>}
|
|
2250
2276
|
{session&&<button type="button" className="icon" aria-label="Voice settings" title="Voice settings" onClick={()=>setVoiceOpenRequest(value=>value+1)}><Volume2 size={15}/></button>}
|
|
@@ -2292,6 +2318,8 @@ function App() {
|
|
|
2292
2318
|
))}
|
|
2293
2319
|
</ul>
|
|
2294
2320
|
)}
|
|
2321
|
+
<div className="chat-body">
|
|
2322
|
+
<section className="chat-history" aria-label="Chat history"><h2 className="chat-history-label">Chat history</h2>
|
|
2295
2323
|
<div className="messages" ref={chatScroll.viewport}>
|
|
2296
2324
|
<div className="message-list" ref={chatScroll.content}>
|
|
2297
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=>{
|
|
@@ -2306,7 +2334,7 @@ function App() {
|
|
|
2306
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>}
|
|
2307
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>}
|
|
2308
2336
|
{m.fix && (
|
|
2309
|
-
<button className="btn fix" onClick={() =>
|
|
2337
|
+
<button className="btn fix" onClick={() => showConnections()}>
|
|
2310
2338
|
Fix connection
|
|
2311
2339
|
</button>
|
|
2312
2340
|
)}
|
|
@@ -2349,6 +2377,8 @@ function App() {
|
|
|
2349
2377
|
</div>
|
|
2350
2378
|
</div>
|
|
2351
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">
|
|
2352
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>}
|
|
2353
2383
|
<div className="prompts">
|
|
2354
2384
|
{mode === "design" && project.workflow && !project.nodes.length && !pending && (
|
|
@@ -2371,11 +2401,12 @@ function App() {
|
|
|
2371
2401
|
void postMessage();
|
|
2372
2402
|
}}
|
|
2373
2403
|
>
|
|
2374
|
-
<
|
|
2404
|
+
<label className="composer-label" htmlFor="architect-message">Your message</label>
|
|
2405
|
+
<textarea id="architect-message"
|
|
2375
2406
|
onPaste={e=>{const files=[...e.clipboardData.files];if(files.length){e.preventDefault();void attachFiles(files);}}}
|
|
2376
2407
|
ref={messageInput}
|
|
2377
2408
|
aria-label="Your message"
|
|
2378
|
-
placeholder=
|
|
2409
|
+
placeholder="Type here…"
|
|
2379
2410
|
rows={2}
|
|
2380
2411
|
value={input}
|
|
2381
2412
|
onChange={(e) => { stopVoice(); setInput(e.target.value); }}
|
|
@@ -2401,11 +2432,11 @@ function App() {
|
|
|
2401
2432
|
{voiceActive ? <Square size={13} /> : <Mic size={15} />}
|
|
2402
2433
|
</button>
|
|
2403
2434
|
<button type="submit" className="send" aria-label="Send message" disabled={(!input.trim() && !voiceActive && !attachments.length) || busy || readingAttachments || voiceState === "transcribing"}>
|
|
2404
|
-
<ArrowUp size={15}
|
|
2435
|
+
<ArrowUp size={15} /><span>Send</span>
|
|
2405
2436
|
</button>
|
|
2406
2437
|
</div>
|
|
2407
2438
|
</form>
|
|
2408
|
-
{session&&<OpenAIVoicePanel hideTrigger openRequest={voiceOpenRequest} key={project.id} session={session} connection={connection} transportRef={openaiVoice} onConnections={()=>
|
|
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)=>{
|
|
2409
2440
|
if(automatic&&!input.trim()&&!attachments.length){setInput(text);void send(text);}
|
|
2410
2441
|
else setInput(current=>current+(current.trim()?' ':'')+text);
|
|
2411
2442
|
}}/>}
|
|
@@ -2413,10 +2444,12 @@ function App() {
|
|
|
2413
2444
|
{!voiceError&&["starting","transcribing"].includes(voiceState)&&<BrainActivity size={22}/>}
|
|
2414
2445
|
{voiceError || ({ starting: "Opening microphone…", listening: "Dictating locally — review your words, then press Send.", transcribing: "Finishing your transcript…" }[voiceState] || "")}
|
|
2415
2446
|
</div>
|
|
2447
|
+
</div>
|
|
2448
|
+
</div>
|
|
2416
2449
|
</aside>
|
|
2417
2450
|
)}
|
|
2418
2451
|
|
|
2419
|
-
<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{
|
|
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);}}}/>
|
|
2420
2453
|
<input id="project-file-input" type="file" ref={fileRef} accept="application/json,.json" hidden onChange={importProject} />
|
|
2421
2454
|
{toast && (
|
|
2422
2455
|
<div className="toast" role="status">
|
|
@@ -2494,7 +2527,7 @@ function App() {
|
|
|
2494
2527
|
/>
|
|
2495
2528
|
) : modal === "licenses" ? (
|
|
2496
2529
|
<>
|
|
2497
|
-
<h2>AtMem</h2><a href=
|
|
2530
|
+
<h2>AtMem</h2><a href={(HOSTED?'/architect/workspace/':'/')+'licenses/atmem-APACHE-2.0.txt'} target="_blank" rel="noreferrer">Apache 2.0</a>
|
|
2498
2531
|
</>
|
|
2499
2532
|
) : (
|
|
2500
2533
|
<>
|
|
@@ -2509,8 +2542,17 @@ function App() {
|
|
|
2509
2542
|
);
|
|
2510
2543
|
}
|
|
2511
2544
|
|
|
2512
|
-
|
|
2545
|
+
async function mountArchitect(){
|
|
2546
|
+
await connectHostedWorkspace();
|
|
2547
|
+
document.documentElement.dataset.theme=savedTheme();
|
|
2548
|
+
createRoot(document.getElementById("root")).render(
|
|
2513
2549
|
<ReactFlowProvider>
|
|
2514
2550
|
<App />
|
|
2515
2551
|
</ReactFlowProvider>,
|
|
2516
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
|
+
});
|
package/src/onboarding.css
CHANGED
|
@@ -8,3 +8,17 @@
|
|
|
8
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
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
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}}
|