feat: chat history, inline file editor, split terminal, OCI resource actions, and layout fixes
- Add ChatGPT-style chat history for Chat Agent and Terraform Agent (sessions, rename, delete) - Add inline .tf file editor with Tab support and auto-sync to workspace code - Add split-panel terminal showing Plan/Apply/Destroy output alongside files/plan/resources - Add OCI resource actions: start/stop Compute instances and Autonomous DBs from Resources panel - Add multi-file Terraform generation via // filename: markers - Fix GenAI empty response handling (GPT-5.2 fallback message) - Fix compartment select visibility, Terraform code block light theme - Fix page layout overflow (no more scroll beyond viewport) - Add DB migration for terraform_workspaces.compartment_id column - Update README with new features, API endpoints, and version notes
This commit is contained in:
@@ -27,11 +27,11 @@
|
||||
--trans:cubic-bezier(.22,1,.36,1)
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:var(--fs);background:var(--bg);color:var(--t1);min-height:100vh;-webkit-font-smoothing:antialiased}
|
||||
body{font-family:var(--fs);background:var(--bg);color:var(--t1);height:100vh;overflow:hidden;-webkit-font-smoothing:antialiased}
|
||||
::selection{background:rgba(199,70,52,.15);color:var(--t1)}
|
||||
|
||||
/* ── Sidebar ── */
|
||||
.app{display:flex;min-height:100vh}
|
||||
.app{display:flex;height:100vh;overflow:hidden}
|
||||
.sb{width:260px;background:var(--bg1);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:100;
|
||||
border-right:1px solid var(--bd);transition:transform .4s var(--trans)}
|
||||
.sb-h{padding:1.25rem 1.15rem;background:linear-gradient(145deg,#c74634 0%,#a83428 50%,#8b2a20 100%);position:relative;overflow:hidden}
|
||||
@@ -57,12 +57,12 @@ body{font-family:var(--fs);background:var(--bg);color:var(--t1);min-height:100vh
|
||||
.lo-btn:hover{border-color:var(--rd);color:var(--rd);background:var(--rdl)}
|
||||
|
||||
/* ── Main Content ── */
|
||||
.mc{flex:1;margin-left:260px;min-height:100vh}
|
||||
.mc{flex:1;margin-left:260px;height:100vh;overflow:hidden;display:flex;flex-direction:column}
|
||||
.tb{height:56px;background:rgba(255,255,255,.8);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
|
||||
border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 1.5rem;position:sticky;top:0;z-index:50}
|
||||
.tb-t{font-weight:700;font-size:.92rem;color:var(--t1);letter-spacing:-.01em}
|
||||
.tb-a{display:flex;align-items:center;gap:.6rem}
|
||||
.pc{padding:1.5rem;max-width:1400px}
|
||||
.pc{padding:1.5rem;max-width:1400px;flex:1;overflow-y:auto;min-height:0}
|
||||
|
||||
/* ── Cards ── */
|
||||
.cd{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1.35rem;margin-bottom:.85rem;
|
||||
@@ -114,6 +114,23 @@ tbody tr:last-child td{border-bottom:none}
|
||||
.g2{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}
|
||||
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.75rem}
|
||||
|
||||
/* ── Chat History Panel (inside chat/terraform) ── */
|
||||
.ch-hp{width:0;overflow:hidden;transition:width .25s ease;background:var(--bg1);border-right:0 solid var(--bd);flex-shrink:0;order:0}
|
||||
.ch-hp.open{width:260px;border-right-width:1px}
|
||||
.ch-hp-inner{width:260px;display:flex;flex-direction:column;height:100%;box-sizing:border-box}
|
||||
.ch-hp-hdr{display:flex;align-items:center;justify-content:space-between;padding:.55rem .7rem;border-bottom:1px solid var(--bd);font-size:.74rem;font-weight:700;color:var(--t2)}
|
||||
.ch-hp-new{font-size:.72rem;font-weight:600;color:var(--ac);cursor:pointer;padding:.35rem .7rem;border:1px dashed var(--ac);border-radius:6px;transition:background .15s}
|
||||
.ch-hp-new:hover{background:var(--acl)}
|
||||
.ch-hp-list{overflow-y:auto;padding:.3rem .45rem;flex:1}
|
||||
.ch-hp-grp{font-size:.58rem;font-weight:700;color:var(--t4);text-transform:uppercase;letter-spacing:.06em;padding:.5rem .25rem .15rem}
|
||||
.ch-hp-item{display:flex;align-items:center;gap:.3rem;padding:.4rem .5rem;border-radius:8px;cursor:pointer;font-size:.72rem;color:var(--t2);transition:background .15s;margin-bottom:1px}
|
||||
.ch-hp-item:hover{background:var(--bg3)}
|
||||
.ch-hp-item.active{background:var(--acl2);color:var(--ac);font-weight:600}
|
||||
.ch-hp-ttl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.ch-hp-del{opacity:0;transition:opacity .15s;cursor:pointer;color:var(--t4);flex-shrink:0;display:flex;align-items:center;padding:2px}
|
||||
.ch-hp-del:hover{color:var(--rd)}
|
||||
.ch-hp-item:hover .ch-hp-del{opacity:1}
|
||||
|
||||
/* ── Chat ── */
|
||||
.ch-c{display:flex;height:calc(100vh - 56px - 3rem);overflow:hidden;border:1px solid var(--bd);border-radius:var(--rl);box-shadow:var(--sh2)}
|
||||
.ch-side{width:0;overflow:hidden;transition:width .3s cubic-bezier(.4,0,.2,1);background:var(--bg1);border-left:0 solid var(--bd);flex-shrink:0;order:2}
|
||||
@@ -156,7 +173,7 @@ tbody tr:last-child td{border-bottom:none}
|
||||
.cb code{font-family:var(--fm);font-size:.73rem;background:var(--bg3);padding:.12rem .3rem;border-radius:5px}
|
||||
.cb strong{color:var(--ac)}
|
||||
.ch-i{display:flex;gap:.45rem;padding:.7rem .85rem;background:var(--bg1);border-top:1px solid var(--bd);box-shadow:0 -2px 8px rgba(0,0,0,.02)}
|
||||
.ch-i input{flex:1;background:var(--bg2);border-radius:10px}
|
||||
.ch-i textarea,.ch-i input{flex:1;background:var(--bg2);border-radius:10px;font-family:inherit;font-size:inherit;line-height:1.4;padding:.45rem .65rem;border:1.5px solid var(--bd);min-height:36px;max-height:200px}
|
||||
.ch-i .btn{border-radius:10px}
|
||||
|
||||
/* ── Report ── */
|
||||
@@ -244,26 +261,37 @@ tbody tr:last-child td{border-bottom:none}
|
||||
.exp-cat{padding:.3rem .5rem;font-size:.6rem}
|
||||
}
|
||||
/* ── Terraform Agent ── */
|
||||
.tf-wrap{display:flex;flex-direction:column;height:calc(100vh - 56px)}
|
||||
.tf-chat{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:200px}
|
||||
.tf-wrap{display:flex;flex-direction:column;height:calc(100vh - 56px - 3rem);overflow:hidden}
|
||||
.tf-chat{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:0}
|
||||
.tf-toolbar{display:flex;align-items:center;gap:.5rem;padding:.45rem .7rem;border-bottom:1px solid var(--bd);flex-wrap:wrap;background:var(--bg)}
|
||||
.tf-msgs{flex:1;overflow-y:auto;padding:.7rem}
|
||||
.tf-input{display:flex;gap:.5rem;padding:.5rem .7rem;border-top:1px solid var(--bd)}
|
||||
.tf-input input{flex:1}
|
||||
.tf-input textarea,.tf-input input{flex:1;font-family:inherit;font-size:inherit;line-height:1.4;padding:.45rem .65rem;border:1.5px solid var(--bd);border-radius:10px;background:var(--bg2);min-height:36px;max-height:200px}
|
||||
.tf-resize{height:5px;cursor:row-resize;background:var(--bd);transition:background .15s;position:relative;flex-shrink:0}
|
||||
.tf-resize:hover,.tf-resize.dragging{background:#7b42bc;opacity:.7}
|
||||
.tf-resize::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:28px;height:3px;border-top:1px solid var(--t4);border-bottom:1px solid var(--t4);opacity:.4}
|
||||
.tf-resize:hover::after{border-color:#7b42bc;opacity:.8}
|
||||
.tf-bottom{height:280px;display:flex;flex-direction:column;overflow:hidden;border-top:1px solid var(--bd);background:var(--bg1)}
|
||||
.tf-bottom{flex:1;display:flex;flex-direction:column;overflow:hidden;border-top:1px solid var(--bd);background:var(--bg1);min-height:0}
|
||||
.tf-bottom-tabs{display:flex;align-items:center;gap:0;border-bottom:1px solid var(--bd);background:var(--bg);padding:0 .5rem}
|
||||
.tf-btab{padding:.4rem .75rem;font-size:.72rem;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;color:var(--t3);display:flex;align-items:center;gap:.3rem;transition:all .15s}
|
||||
.tf-btab:hover{color:var(--t1)}
|
||||
.tf-btab.active{color:#7b42bc;border-bottom-color:#7b42bc}
|
||||
.tf-btab svg{width:14px;height:14px}
|
||||
.tf-bottom-actions{margin-left:auto;display:flex;align-items:center;gap:.4rem;padding-right:.3rem}
|
||||
.tf-bottom-content{flex:1;overflow:auto;padding:.5rem .7rem}
|
||||
.tf-file{display:flex;align-items:center;gap:.5rem;padding:.45rem .6rem;border-radius:8px;border:1px solid var(--bd);margin-bottom:.4rem;background:var(--bg);transition:border-color .15s}
|
||||
.tf-bottom-content{flex:1;overflow:hidden;display:flex;flex-direction:row}
|
||||
.tf-bc-left{flex:1;overflow:auto;padding:.5rem .7rem;min-width:0}
|
||||
.tf-bc-right{width:50%;overflow:auto;border-left:1px solid var(--bd);flex-shrink:0;display:flex;flex-direction:column}
|
||||
.tf-bc-right .tf-term-hdr{padding:.3rem .6rem;font-size:.68rem;font-weight:700;color:#cdd6f4;background:#12122a;border-bottom:1px solid #2a2a4a;display:flex;align-items:center;gap:.4rem}
|
||||
.tf-bc-right .tf-term-hdr svg{width:12px;height:12px;fill:#cdd6f4}
|
||||
.tf-bc-right .tf-term-body{flex:1;overflow:auto;background:#1a1a2e;padding:.5rem;font-family:var(--fm);font-size:.68rem;line-height:1.5;color:#cdd6f4;white-space:pre-wrap;word-break:break-word}
|
||||
.tf-file{display:flex;align-items:center;gap:.5rem;padding:.45rem .6rem;border-radius:8px;border:1px solid var(--bd);margin-bottom:.4rem;background:var(--bg);transition:border-color .15s;cursor:pointer}
|
||||
.tf-file:hover{border-color:#7b42bc}
|
||||
.tf-file.editing{border-color:#7b42bc;background:#7b42bc08}
|
||||
.tf-editor{border:1px solid #e2ddf5;border-radius:var(--r);overflow:hidden;margin-bottom:.5rem;background:#f8f6ff}
|
||||
.tf-editor-hdr{display:flex;align-items:center;gap:.4rem;padding:.35rem .6rem;background:#efe9fc;border-bottom:1px solid #e2ddf5;font-size:.7rem;font-weight:600;color:#4a3875}
|
||||
.tf-editor-hdr .tf-ed-name{flex:1;font-family:var(--fm)}
|
||||
.tf-editor-hdr .btn{font-size:.62rem;padding:2px 8px}
|
||||
.tf-editor-ta{width:100%;min-height:120px;max-height:400px;border:none;outline:none;resize:vertical;font-family:var(--fm);font-size:.72rem;line-height:1.6;color:#3b2e58;background:#f8f6ff;padding:.6rem .7rem;box-sizing:border-box;tab-size:2}
|
||||
.tf-file-icon{width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,#7b42bc22,#5c4ee522);display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.tf-file-icon svg{width:16px;height:16px;fill:#7b42bc}
|
||||
.tf-file-info{flex:1;min-width:0}
|
||||
@@ -272,12 +300,12 @@ tbody tr:last-child td{border-bottom:none}
|
||||
.tf-file-actions{display:flex;gap:.3rem}
|
||||
.tf-file-btn{width:26px;height:26px;border-radius:6px;border:1px solid var(--bd);background:var(--bg);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:.7rem;transition:all .15s}
|
||||
.tf-file-btn:hover{border-color:#7b42bc;background:#7b42bc11}
|
||||
.tf-code-block{background:#1e1e2e;border-radius:var(--r);overflow:hidden;margin:.6rem 0}
|
||||
.tf-code-header{display:flex;align-items:center;gap:.4rem;padding:.4rem .7rem;background:#2a2a3e;font-size:.72rem;color:#cdd6f4;border-bottom:1px solid #45475a}
|
||||
.tf-code-block{background:#f8f6ff;border:1px solid #e2ddf5;border-radius:var(--r);overflow:hidden;margin:.6rem 0}
|
||||
.tf-code-header{display:flex;align-items:center;gap:.4rem;padding:.4rem .7rem;background:#efe9fc;font-size:.72rem;color:#4a3875;border-bottom:1px solid #e2ddf5}
|
||||
.tf-code-header span{flex:1;font-weight:600}
|
||||
.tf-code-header .btn{color:#cdd6f4;border-color:#45475a;background:transparent;font-size:.66rem;padding:2px 8px}
|
||||
.tf-code-header .btn:hover{background:#45475a}
|
||||
.tf-pre{padding:.85rem;overflow-x:auto;font-size:.74rem;line-height:1.6;color:#cdd6f4;font-family:var(--fm);margin:0;white-space:pre}
|
||||
.tf-code-header .btn{color:#7b42bc;border-color:#d4c8ef;background:transparent;font-size:.66rem;padding:2px 8px}
|
||||
.tf-code-header .btn:hover{background:#e2ddf5}
|
||||
.tf-pre{padding:.85rem;overflow-x:auto;font-size:.74rem;line-height:1.6;color:#3b2e58;font-family:var(--fm);margin:0;white-space:pre}
|
||||
.tf-plan-block{background:var(--gnl);border:1px solid rgba(22,163,74,.15);border-radius:var(--r);margin:.6rem 0;overflow:hidden}
|
||||
.tf-plan-header{padding:.45rem .7rem;font-weight:700;font-size:.76rem;color:var(--gn);border-bottom:1px solid rgba(22,163,74,.12)}
|
||||
.tf-plan-list{padding:.4rem .7rem}
|
||||
@@ -285,7 +313,6 @@ tbody tr:last-child td{border-bottom:none}
|
||||
.tf-plan-add{color:var(--gn);font-weight:700;font-family:var(--fm)}
|
||||
.tf-plan-type{font-family:var(--fm);font-weight:600;color:var(--t1)}
|
||||
.tf-plan-desc{color:var(--t3);font-size:.68rem}
|
||||
.tf-output{background:#1a1a2e;color:#cdd6f4;font-family:var(--fm);font-size:.7rem;line-height:1.5;padding:.6rem;overflow:auto;white-space:pre-wrap;word-break:break-word;border-radius:var(--r);height:100%}
|
||||
.tf-badge{font-size:.58rem;padding:1px 6px;border-radius:10px;font-weight:700;display:inline-flex;align-items:center;gap:3px}
|
||||
.tf-badge-draft{background:#7b42bc18;color:#7b42bc}
|
||||
.tf-badge-ok{background:var(--gnl);color:var(--gn)}
|
||||
@@ -298,7 +325,7 @@ tbody tr:last-child td{border-bottom:none}
|
||||
.tf-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--t3);font-size:.76rem;gap:.3rem}
|
||||
.tf-empty svg{width:40px;height:40px;fill:#7b42bc;opacity:.3}
|
||||
@media(max-width:768px){
|
||||
.tf-bottom{height:200px}
|
||||
.tf-bottom{min-height:0}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -310,7 +337,7 @@ const LOGO_W=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width=
|
||||
const LOGO_R=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="52" height="52"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.08)" stroke="#C74634" stroke-width="1.8"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.4"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#C74634" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#C74634" opacity="0.25" stroke="#C74634" stroke-width="0.7"/><line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.4"/><line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.4"/><circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.35"/><circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.35"/></svg>`;
|
||||
|
||||
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],models:{},regions:[],embModels:{},expData:null,expCfg:'',expSelRegions:[],expRegDdOpen:false,expTree:[],expSelComp:'',expCat:'Compute',expResType:'instances',expTreeOpen:{},expLoading:false,expRegions:[],expCounts:{},editing:null,
|
||||
chatModel:'',chatOci:'',chatRegion:'',chatCompartment:'',
|
||||
chatModel:'',chatOci:'',chatRegion:'',chatCompartment:'',chatHistOpen:false,chatHistory:[],tfHistOpen:false,tfHistory:[],
|
||||
chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0},chatPanel:'',chatUseTools:true,
|
||||
chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'',
|
||||
rptOciOpen:false,rptOciFilter:'',rptOciVal:'',rptRselOpen:false,rptRselFilter:'',rptRselVal:'',ociFormRegOpen:false,ociFormRegFilter:'',ociFormRegVal:'',
|
||||
@@ -318,7 +345,7 @@ const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],g
|
||||
cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:'',chatFiles:[],
|
||||
tfMsgs:[],tfSid:null,tfModel:'',tfOci:'',tfRegion:'',tfCompartment:'',tfPlan:[],tfCode:'',tfPanel:'',
|
||||
tfWs:null,tfWsList:[],tfPlanOut:'',tfApplyOut:'',tfDestroyOut:'',tfStatus:'draft',tfRunning:false,tfConfirmDest:false,tfMdOpen:false,
|
||||
tfComps:[],tfCompLoading:false,tfFiles:[],tfBtab:'files',tfResources:null,tfResLoading:false};
|
||||
tfComps:[],tfCompLoading:false,tfFiles:[],tfBtab:'files',tfEditIdx:-1,tfResources:null,tfResLoading:false};
|
||||
const API='/api';
|
||||
|
||||
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
|
||||
@@ -340,9 +367,19 @@ async function loadData(){try{
|
||||
if(!S.chatModel){const dc=S.genaiCfg.find(g=>g.is_default);if(dc)S.chatModel='cfg:'+dc.id;else if(S.models['openai.gpt-4.1'])S.chatModel='openai.gpt-4.1'}
|
||||
if(S.user.role==='admin'){try{S.users=await $api('/users')}catch(e){}}
|
||||
try{S.cisVer=await $api('/cis-engine/version')}catch(e){}
|
||||
loadHistory('chat');
|
||||
}catch(e){console.error(e)}}
|
||||
function R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();if(S.editing?.type==='mcp')setTimeout(mcpTypeFields,0)}
|
||||
function switchTab(t){S.tab=t;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();if(t==='explorer'&&S.ociCfg.length&&!S.expTree.length){if(!S.expCfg)S.expCfg=S.ociCfg[0].id;expLoadTree();expLoadRegions()}
|
||||
function R(){try{
|
||||
// Preserve textarea values before re-render
|
||||
const chiEl=document.getElementById('chi');if(chiEl)S._chiVal=chiEl.value;
|
||||
const tfiEl=document.getElementById('tfi');if(tfiEl)S._tfiVal=tfiEl.value;
|
||||
document.getElementById('app').innerHTML=S.user?rApp():rLogin();
|
||||
// Restore textarea values after re-render
|
||||
const chi2=document.getElementById('chi');if(chi2&&S._chiVal){chi2.value=S._chiVal;autoGrow(chi2)}
|
||||
const tfi2=document.getElementById('tfi');if(tfi2&&S._tfiVal){tfi2.value=S._tfiVal;autoGrow(tfi2)}
|
||||
if(S.editing?.type==='mcp')setTimeout(mcpTypeFields,0)
|
||||
}catch(e){console.error('Render error:',e);document.getElementById('app').innerHTML='<div style="padding:2rem;color:red"><h3>Render Error</h3><pre>'+e.message+'</pre></div>'}}
|
||||
function switchTab(t){S.tab=t;R();if(t==='chat')loadHistory('chat');if(t==='terraform')loadHistory('terraform');if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();if(t==='explorer'&&S.ociCfg.length&&!S.expTree.length){if(!S.expCfg)S.expCfg=S.ociCfg[0].id;expLoadTree();expLoadRegions()}
|
||||
const tm={'oci-config':'oci','genai':'genai','adb':'adb','mcp':'mcp'};if(tm[t])setTimeout(()=>refreshCLogs(tm[t]),100)}
|
||||
|
||||
/* ── Login ── */
|
||||
@@ -403,18 +440,24 @@ function rChat(){
|
||||
if(S.chatPanel==='config')sideContent=rChatConfig(isDirect,toolCount);
|
||||
else if(S.chatPanel==='logs')sideContent=rChatLogs();
|
||||
return`<div class="ch-c">
|
||||
<div class="ch-hp${S.chatHistOpen?' open':''}"><div class="ch-hp-inner">
|
||||
<div class="ch-hp-hdr"><span>🕘 Histórico</span><div class="ch-hp-new" onclick="newChat()">+ Novo Chat</div></div>
|
||||
<div class="ch-hp-list">${rHistList('chat',S.chatHistory,S.sid)}</div>
|
||||
</div></div>
|
||||
<div class="ch-side${S.chatPanel?' open':''}"><div class="ch-side-inner">${sideContent}</div></div>
|
||||
<div class="ch-main">
|
||||
<div class="ch-t"><label>🧠 Modelo:</label><div class="mdrop"><div class="mdrop-btn" onclick="toggleModelDrop()">${curLabel} <span class="arr">▼</span></div><div class="mdrop-dd" id="mdd"><input type="text" id="mds" placeholder="Buscar modelo..." oninput="filterModels(this.value)"><div class="mdrop-list" id="mdl">${ddItems}</div></div></div>
|
||||
<div class="ch-t">
|
||||
<div class="ch-icon-btn${S.chatHistOpen?' active':''}" onclick="S.chatHistOpen=!S.chatHistOpen;if(S.chatHistOpen)loadHistory('chat');R()" title="Histórico">☰</div>
|
||||
<label>🧠 Modelo:</label><div class="mdrop"><div class="mdrop-btn" onclick="toggleModelDrop()">${curLabel} <span class="arr">▼</span></div><div class="mdrop-dd" id="mdd"><input type="text" id="mds" placeholder="Buscar modelo..." oninput="filterModels(this.value)"><div class="mdrop-list" id="mdl">${ddItems}</div></div></div>
|
||||
${ociSel}${ragBadge}<div style="flex:1"></div>
|
||||
<div class="ch-icon-btn${S.chatPanel==='config'?' active':''}" onclick="togglePanel('config')" title="Configurações">⚙️</div>
|
||||
<div class="ch-icon-btn${S.chatPanel==='logs'?' active':''}" onclick="togglePanel('logs')" title="Chat Logs">📋</div>
|
||||
<div class="ch-icon-btn" onclick="clrChat()" title="Limpar conversa">🗑️</div></div>
|
||||
<div class="ch-icon-btn" onclick="newChat()" title="Nova conversa">✨</div></div>
|
||||
<div class="ch-m" id="chm">${ms}</div>
|
||||
${S.chatFiles.length?`<div style="display:flex;gap:.4rem;flex-wrap:wrap;padding:.4rem .85rem;background:var(--bg1);border-top:1px solid var(--bd)">${S.chatFiles.map((f,i)=>f.type==='image'?`<div style="position:relative;width:48px;height:48px"><img src="${f.preview}" style="width:48px;height:48px;object-fit:cover;border-radius:6px;border:1px solid var(--bd)"><span onclick="rmChatFile(${i})" style="position:absolute;top:-4px;right:-4px;background:var(--err);color:#fff;width:16px;height:16px;border-radius:50%;font-size:.6rem;display:flex;align-items:center;justify-content:center;cursor:pointer">×</span></div>`:`<div style="position:relative;display:flex;align-items:center;gap:.25rem;background:var(--bg2);border:1px solid var(--bd);border-radius:6px;padding:.25rem .5rem;font-size:.68rem"><span>📄 ${f.name}</span><span onclick="rmChatFile(${i})" style="color:var(--err);cursor:pointer;font-weight:700">×</span></div>`).join('')}</div>`:''}
|
||||
<div class="ch-i"><input type="file" id="chf" multiple accept="image/*,.pdf,.txt,.csv,.json,.log,.xml,.yaml,.yml,.md,.py,.js,.html,.css,.sql" style="display:none" onchange="addChatFiles(this)">
|
||||
<button class="btn bs" onclick="document.getElementById('chf').click()" title="Anexar arquivo" style="font-size:1rem;padding:.3rem .5rem">📎</button>
|
||||
<input type="text" id="chi" placeholder="Digite sua mensagem..." onkeydown="if(event.key==='Enter')sChat()">
|
||||
<textarea id="chi" placeholder="Digite sua mensagem... (Shift+Enter para nova linha)" rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sChat()}" oninput="autoGrow(this)" onpaste="setTimeout(()=>autoGrow(this),0)" style="resize:none;overflow-y:auto"></textarea>
|
||||
<button class="btn bp" onclick="sChat()">Enviar →</button></div></div></div>`}
|
||||
function rChatConfig(isDirect,toolCount){
|
||||
const pp=S.chatParams;
|
||||
@@ -464,13 +507,14 @@ async function savePrompt(){const name=document.getElementById('spname').value;c
|
||||
async function activatePrompt(id){try{await $api('/prompts/'+id,{method:'PUT',body:{is_active:true}});S.chatPrompts=await $api('/prompts/chat');R()}catch(e){sm('spm',e.message,'e')}}
|
||||
async function delPrompt(id){if(!confirm('Excluir prompt?'))return;try{await $api('/prompts/'+id,{method:'DELETE'});S.editingPrompt=null;S.chatPrompts=await $api('/prompts/chat');R()}catch(e){sm('spm',e.message,'e')}}
|
||||
function fm(t){return t.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>').replace(/`(.*?)`/g,'<code>$1</code>').replace(/\n/g,'<br>')}
|
||||
function autoGrow(el){el.style.height='auto';el.style.height=Math.min(el.scrollHeight,200)+'px'}
|
||||
function addChatFiles(input){for(const f of input.files){if(S.chatFiles.length>=5){alert('Máximo 5 arquivos');break}
|
||||
const entry={file:f,name:f.name,type:f.type.startsWith('image/')?'image':'file',preview:null};
|
||||
if(entry.type==='image'){const r=new FileReader();r.onload=e=>{entry.preview=e.target.result;R()};r.readAsDataURL(f)}
|
||||
S.chatFiles.push(entry)}input.value='';R()}
|
||||
function rmChatFile(i){S.chatFiles.splice(i,1);R()}
|
||||
function tstamp(){const d=new Date();return d.toLocaleTimeString('pt-BR',{hour:'2-digit',minute:'2-digit'})}
|
||||
async function sChat(){const el=document.getElementById('chi');const m=el.value.trim();
|
||||
async function sChat(){const el=document.getElementById('chi');const m=el.value.trim();el.style.height='auto';S._chiVal='';
|
||||
if(!m&&!S.chatFiles.length)return;
|
||||
if(!S.chatModel){S.msgs.push({r:'assistant',c:'⚠️ Selecione um modelo antes de enviar uma mensagem.'});R();return}
|
||||
el.value='';
|
||||
@@ -511,6 +555,7 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
|
||||
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
|
||||
d=await $api('/chat',{method:'POST',body})}
|
||||
S.sid=d.session_id;
|
||||
if(S.chatHistOpen)loadHistory('chat');
|
||||
if(d.status==='processing'&&d.message_id){
|
||||
await pollChatResult(d.message_id)
|
||||
}else{
|
||||
@@ -530,6 +575,69 @@ async function pollChatResult(mid){
|
||||
S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:'⏰ Timeout: a resposta está demorando muito. Tente novamente.',t:tstamp()});R()}
|
||||
function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)}
|
||||
async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()}
|
||||
function newChat(){S.msgs=[];S.sid=null;S.chatFiles=[];R()}
|
||||
function newTfChat(){S.tfMsgs=[];S.tfSid=null;S.tfPlan=[];S.tfCode='';S.tfFiles=[];S.tfWs=null;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfStatus='draft';S.tfRunning=false;S.tfConfirmDest=false;S.tfEditIdx=-1;R()}
|
||||
|
||||
async function loadHistory(type){
|
||||
try{const h=await $api('/chat/sessions?agent_type='+type+'&limit=50');
|
||||
if(type==='chat')S.chatHistory=h;else S.tfHistory=h}
|
||||
catch(e){if(type==='chat')S.chatHistory=[];else S.tfHistory=[]}
|
||||
R();
|
||||
}
|
||||
async function loadSession(sid,type){
|
||||
try{const d=await $api('/chat/sessions/'+sid+'/messages');
|
||||
if(type==='chat'){
|
||||
S.sid=sid;S.msgs=d.messages.map(m=>({r:m.role,c:m.content,t:m.created_at?.slice(11,16)||''}));R();scCh();
|
||||
}else{
|
||||
S.tfSid=sid;S.tfMsgs=d.messages.map(m=>({r:m.role,c:m.content}));
|
||||
S.tfFiles=[];S.tfCode='';S.tfPlan=[];
|
||||
const last=[...d.messages].reverse().find(m=>m.role==='assistant'&&m.content&&m.content.includes('```'));
|
||||
if(last)fmTf(last.content);
|
||||
try{const wsList=await $api('/terraform/workspaces');const ws=wsList.find(w=>w.session_id===sid);
|
||||
if(ws){S.tfWs=ws.id;S.tfStatus=ws.status}else{S.tfWs=null;S.tfStatus='draft'}
|
||||
}catch(e){S.tfWs=null;S.tfStatus='draft'}
|
||||
R();
|
||||
}
|
||||
}catch(e){alert('Erro ao carregar sessão: '+e.message)}
|
||||
}
|
||||
async function delSession(sid,type){
|
||||
if(!confirm('Excluir esta conversa?'))return;
|
||||
try{await $api('/chat/'+sid,{method:'DELETE'})}catch(e){}
|
||||
if(type==='chat'){if(S.sid===sid)newChat();S.chatHistory=S.chatHistory.filter(s=>s.id!==sid)}
|
||||
else{if(S.tfSid===sid)newTfChat();S.tfHistory=S.tfHistory.filter(s=>s.id!==sid)}
|
||||
R();
|
||||
}
|
||||
async function renameSession(sid,type){
|
||||
const cur=(type==='chat'?S.chatHistory:S.tfHistory).find(s=>s.id===sid);
|
||||
const t=prompt('Renomear conversa:',cur?.title||'');
|
||||
if(t===null)return;
|
||||
try{await $api('/chat/sessions/'+sid+'/title',{method:'PUT',body:{title:t}});
|
||||
if(cur)cur.title=t;R()}catch(e){}
|
||||
}
|
||||
function _histDateGroup(dateStr){
|
||||
if(!dateStr)return'Outras';
|
||||
const d=new Date(dateStr+'Z'),now=new Date(),diff=(now-d)/86400000;
|
||||
if(diff<1)return'Hoje';if(diff<2)return'Ontem';if(diff<7)return'Últimos 7 dias';if(diff<30)return'Últimos 30 dias';return'Anteriores';
|
||||
}
|
||||
|
||||
function rHistList(type,hist,curSid){
|
||||
if(!hist.length)return'<div style="text-align:center;padding:1rem .5rem;font-size:.7rem;color:var(--t4)">Nenhuma conversa</div>';
|
||||
let html='';let lastGroup='';
|
||||
hist.forEach(s=>{
|
||||
const g=_histDateGroup(s.updated_at||s.created_at);
|
||||
if(g!==lastGroup){lastGroup=g;html+=`<div class="ch-hp-grp">${g}</div>`}
|
||||
const active=s.id===curSid?' active':'';
|
||||
const title=s.title||'Nova conversa';
|
||||
html+=`<div class="ch-hp-item${active}" onclick="loadSession('${s.id}','${type}')">
|
||||
<span class="ch-hp-ttl" title="${title.replace(/"/g,'"')}">${escHtml(title)}</span>
|
||||
<span class="ch-hp-del" onclick="event.stopPropagation();delSession('${s.id}','${type}')" title="Excluir">
|
||||
<svg viewBox="0 0 16 16" width="11" height="11" fill="currentColor"><path d="M5.5 5.5A.5.5 0 016 6v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm2.5 0a.5.5 0 01.5.5v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm3 .5a.5.5 0 00-1 0v6a.5.5 0 001 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 01-1 1H13v9a2 2 0 01-2 2H5a2 2 0 01-2-2V4h-.5a1 1 0 010-2H6a1 1 0 011-1h2a1 1 0 011 1h3.5a1 1 0 011 1zM4.118 4L4 4.059V13a1 1 0 001 1h6a1 1 0 001-1V4.059L11.882 4H4.118z"/></svg>
|
||||
</span>
|
||||
</div>`;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
async function loadChatLogs(){
|
||||
const sev=document.getElementById('cl-sev-chat')?.value||'';
|
||||
const qs='limit=50'+(sev?'&severity='+sev:'')+(S.sid?'&session_id='+S.sid:'');
|
||||
@@ -551,30 +659,42 @@ const TF_ICON=`<svg viewBox="0 0 16 16" width="16" height="16"><path d="M5.6 1v4
|
||||
function escHtml(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
||||
|
||||
function fmTf(t){
|
||||
// Extract ALL hcl blocks and build files list
|
||||
// Extract ALL hcl blocks, parse // filename: comments, build separate files
|
||||
const codeBlocks=[];
|
||||
let r=t.replace(/```(?:hcl|terraform)\s*\n([\s\S]*?)```/g,(m,code)=>{
|
||||
const trimmed=code.trim();
|
||||
codeBlocks.push(trimmed);
|
||||
S.tfCode=trimmed;
|
||||
return '<div class="tf-code-block"><div class="tf-code-header"><span>'+TF_ICON+' main.tf</span>'+
|
||||
'<button class="btn" onclick="copyTfCode()">Copy</button></div>'+
|
||||
'<pre class="tf-pre"><code>'+escHtml(trimmed)+'</code></pre></div>';
|
||||
// Check for // filename: comment on first line
|
||||
const fnMatch=trimmed.match(/^\/\/\s*filename:\s*(\S+)/);
|
||||
const fileName=fnMatch?fnMatch[1]:'main.tf';
|
||||
// Content without the filename comment line
|
||||
const content=fnMatch?trimmed.replace(/^\/\/\s*filename:\s*\S+\s*\n?/,'').trim():trimmed;
|
||||
codeBlocks.push({name:fileName,content:content});
|
||||
S.tfCode=content;
|
||||
return '<div class="tf-code-block"><div class="tf-code-header"><span>'+TF_ICON+' '+escHtml(fileName)+'</span>'+
|
||||
'<button class="btn" onclick="tfCopyBlock('+( codeBlocks.length-1)+')">Copy</button></div>'+
|
||||
'<pre class="tf-pre"><code>'+escHtml(content)+'</code></pre></div>';
|
||||
});
|
||||
r=r.replace(/```plan\s*\n([\s\S]*?)```/g,(m,plan)=>{
|
||||
const lines=plan.trim().split('\n');
|
||||
S.tfPlan=lines.map(l=>{const m2=l.match(/^\+\s+(\S+)\s+\((.+)\)$/);return m2?{type:m2[1],desc:m2[2]}:{type:l.replace(/^\+\s*/,'').trim(),desc:''}}).filter(x=>x.type);
|
||||
S.tfPlan=lines.map(l=>{const m2=l.match(/^[+~]\s+(\S+)\s+\((.+)\)$/);return m2?{type:m2[1],desc:m2[2]}:{type:l.replace(/^[+~]\s*/,'').trim(),desc:''}}).filter(x=>x.type);
|
||||
return '<div class="tf-plan-block"><div class="tf-plan-header">'+TF_ICON+' Resource Plan — '+S.tfPlan.length+' resource(s)</div><div class="tf-plan-list">'+
|
||||
S.tfPlan.map(x=>'<div class="tf-plan-item"><span class="tf-plan-add">+</span><span class="tf-plan-type">'+escHtml(x.type)+'</span><span class="tf-plan-desc">'+escHtml(x.desc)+'</span></div>').join('')+'</div></div>';
|
||||
});
|
||||
// Update files list
|
||||
// Build files list — merge blocks with same filename, keep separate files
|
||||
if(codeBlocks.length){
|
||||
S.tfFiles=[{name:'main.tf',content:codeBlocks.join('\n\n'),size:codeBlocks.join('\n\n').length,type:'hcl'}];
|
||||
// Detect if the code has variables -> also create variables.tf reference
|
||||
const fileMap={};
|
||||
codeBlocks.forEach(b=>{
|
||||
if(fileMap[b.name])fileMap[b.name]+='\n\n'+b.content;
|
||||
else fileMap[b.name]=b.content;
|
||||
});
|
||||
S.tfFiles=Object.entries(fileMap).map(([name,content])=>({name,content,size:content.length,type:'hcl'}));
|
||||
// Set tfCode to combined content for backward compat
|
||||
S.tfCode=S.tfFiles.map(f=>'// filename: '+f.name+'\n'+f.content).join('\n\n');
|
||||
}
|
||||
r=r.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>').replace(/`(.*?)`/g,'<code>$1</code>').replace(/\n/g,'<br>');
|
||||
return r;
|
||||
}
|
||||
function tfCopyBlock(i){const b=S.tfFiles[i];if(b)navigator.clipboard.writeText(b.content).then(()=>alert('Copiado!'))}
|
||||
|
||||
function rTerraform(){
|
||||
const ms=S.tfMsgs.length===0
|
||||
@@ -593,14 +713,20 @@ function rTerraform(){
|
||||
|
||||
const isDirect=S.tfModel&&!S.tfModel.startsWith('cfg:');
|
||||
const ociSel=isDirect?`<select id="tfoci" onchange="S.tfOci=this.value;const c=S.ociCfg.find(x=>x.id===this.value);if(c){S.tfRegion=c.region;S.tfCompartment=c.compartment_id||''}tfLoadComps()" style="max-width:180px"><option value="">OCI Config...</option>${S.ociCfg.map(c=>`<option value="${c.id}"${S.tfOci===c.id?' selected':''}>${c.tenancy_name} (${c.region})</option>`).join('')}</select>`:'';
|
||||
const compSel=(S.tfComps.length||S.tfCompLoading)?`<select onchange="S.tfCompartment=this.value;tfLoadResources()" style="max-width:220px">${S.tfCompLoading?'<option>Carregando...</option>':'<option value="">Compartment...</option>'+tfFlatComps(S.tfComps,0).map(c=>`<option value="${c.id}"${S.tfCompartment===c.id?' selected':''}>${c.name}</option>`).join('')}</select>`:'';
|
||||
const compSel=(S.tfComps.length||S.tfCompLoading||S.tfOci)?`<select onchange="S.tfCompartment=this.value;tfLoadResources()" style="max-width:220px">${S.tfCompLoading?'<option>Carregando...</option>':S.tfComps.length?'<option value="">Compartment...</option>'+tfFlatComps(S.tfComps,0).map(c=>`<option value="${c.id}"${S.tfCompartment===c.id?' selected':''}>${c.name}</option>`).join(''):'<option value="">Selecione Compartment...</option>'}</select>`:'';
|
||||
|
||||
// Status badge
|
||||
const stBadge=_tfBadge(S.tfStatus);
|
||||
|
||||
return `<div class="tf-wrap">
|
||||
<div style="display:flex;flex:1;min-height:0;overflow:hidden">
|
||||
<div class="ch-hp${S.tfHistOpen?' open':''}"><div class="ch-hp-inner">
|
||||
<div class="ch-hp-hdr"><span>🕘 Histórico</span><div class="ch-hp-new" onclick="newTfChat()">+ Novo Chat</div></div>
|
||||
<div class="ch-hp-list">${rHistList('terraform',S.tfHistory,S.tfSid)}</div>
|
||||
</div></div>
|
||||
<div class="tf-chat">
|
||||
<div class="tf-toolbar">
|
||||
<div class="ch-icon-btn${S.tfHistOpen?' active':''}" onclick="S.tfHistOpen=!S.tfHistOpen;if(S.tfHistOpen)loadHistory('terraform');R()" title="Histórico" style="width:24px;height:24px;font-size:.85rem">☰</div>
|
||||
${TF_ICON}
|
||||
<div class="mdrop"><div class="mdrop-btn" onclick="S.tfMdOpen=!S.tfMdOpen;R()">${curLabel} <span class="arr">▼</span></div>
|
||||
${S.tfMdOpen?`<div class="mdrop-dd open"><input type="text" id="tfmds" placeholder="Buscar..." oninput="tfFilterModels(this.value)"><div class="mdrop-list" id="tfmdl">${ddItems}</div></div>`:''}
|
||||
@@ -608,15 +734,15 @@ function rTerraform(){
|
||||
${ociSel}${compSel}
|
||||
${stBadge}
|
||||
<div style="flex:1"></div>
|
||||
${rTfActions()}
|
||||
<div class="tf-file-btn" onclick="tfClear()" title="Nova conversa" style="width:24px;height:24px"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M2 3h12v1H2zm1.5-1h9l-.5-1h-8zM3 4v9.5a1.5 1.5 0 001.5 1.5h7a1.5 1.5 0 001.5-1.5V4z"/></svg></div>
|
||||
<div class="tf-file-btn" onclick="newTfChat()" title="Nova conversa" style="width:24px;height:24px"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M8 2v12M2 8h12" stroke="var(--t3)" stroke-width="2" stroke-linecap="round"/></svg></div>
|
||||
</div>
|
||||
<div class="tf-msgs" id="tfchm">${ms}</div>
|
||||
<div class="tf-input">
|
||||
<input type="text" id="tfi" placeholder="Descreva a infraestrutura desejada..." onkeydown="if(event.key==='Enter')tfSend()">
|
||||
<textarea id="tfi" placeholder="Descreva a infraestrutura desejada... (Shift+Enter para nova linha)" rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();tfSend()}" oninput="autoGrow(this)" onpaste="setTimeout(()=>autoGrow(this),0)" style="resize:none;overflow-y:auto"></textarea>
|
||||
<button class="btn bp" onclick="tfSend()" style="background:#7b42bc;border-color:#7b42bc">Gerar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tf-resize" onmousedown="tfStartResize(event)"></div>
|
||||
<div class="tf-bottom" id="tfBottom">
|
||||
<div class="tf-bottom-tabs">
|
||||
@@ -632,15 +758,15 @@ function rTerraform(){
|
||||
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 1L1 5l7 4 7-4zm0 6L1 11l7 4 7-4z"/></svg>
|
||||
Resources${S.tfResources?(' ('+Object.values(S.tfResources).reduce((a,b)=>a+(Array.isArray(b)?b.length:0),0)+')'):''}
|
||||
</div>
|
||||
<div class="tf-btab${S.tfBtab==='output'?' active':''}" onclick="S.tfBtab='output';R()">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M2 1h12a1 1 0 011 1v12a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1zm1 3v9h10V4zm2 1l3 2.5L5 10z"/></svg>
|
||||
Output
|
||||
</div>
|
||||
<div class="tf-bottom-actions">
|
||||
${S.tfFiles.length?'<button class="btn bs bsm" onclick="tfDlAll()" style="font-size:.66rem;padding:2px 8px">Download All</button>':''}
|
||||
${rTfActions()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tf-bottom-content">${rTfBottomContent()}</div>
|
||||
<div class="tf-bottom-content">
|
||||
<div class="tf-bc-left">${rTfBottomContent()}</div>
|
||||
${rTfTerminal()}
|
||||
</div>
|
||||
</div>
|
||||
${S.tfConfirmDest?rTfConfirmDestroy():''}
|
||||
</div>`;
|
||||
@@ -653,13 +779,13 @@ function _tfBadge(st){
|
||||
}
|
||||
|
||||
function rTfActions(){
|
||||
if(S.tfRunning)return'<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.68rem;padding:2px 10px" onclick="tfCancel()">Cancel</button>';
|
||||
if(S.tfRunning)return'<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px" onclick="tfCancel()">Cancel</button>';
|
||||
const st=S.tfStatus;let btns='';
|
||||
if(S.tfCode&&(st==='draft'||st==='failed'))btns+=`<button class="btn bsm" style="background:#7b42bc;color:#fff;border-color:#7b42bc;font-size:.68rem;padding:2px 10px" onclick="tfSaveAndPlan()">${TF_ICON} Plan</button>`;
|
||||
if(st==='planned'||st==='failed')btns+=`<button class="btn bsm" style="background:#7b42bc;color:#fff;border-color:#7b42bc;font-size:.68rem;padding:2px 10px" onclick="tfSaveAndPlan()">${TF_ICON} Re-plan</button>`;
|
||||
if(st==='planned')btns+='<button class="btn bsm" style="background:var(--gn);color:#fff;border-color:var(--gn);font-size:.68rem;padding:2px 10px" onclick="tfApply()">Apply</button>';
|
||||
if(st==='applied')btns+=`<button class="btn bsm" style="background:#7b42bc;color:#fff;border-color:#7b42bc;font-size:.68rem;padding:2px 10px" onclick="tfSaveAndPlan()">${TF_ICON} Re-plan</button>`;
|
||||
if(st==='applied'||st==='planned')btns+='<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.68rem;padding:2px 10px" onclick="S.tfConfirmDest=true;R()">Destroy</button>';
|
||||
if(S.tfCode&&(st==='draft'||st==='failed'))btns+=`<button class="btn bsm" style="background:#7b42bc;color:#fff;border-color:#7b42bc;font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfSaveAndPlan()">${TF_ICON} Plan</button>`;
|
||||
if(st==='planned'||st==='failed')btns+=`<button class="btn bsm" style="background:#7b42bc;color:#fff;border-color:#7b42bc;font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfSaveAndPlan()">${TF_ICON} Re-plan</button>`;
|
||||
if(st==='planned')btns+='<button class="btn bsm" style="background:var(--gn);color:#fff;border-color:var(--gn);font-size:.72rem;padding:4px 12px" onclick="tfApply()">✓ Apply</button>';
|
||||
if(st==='applied')btns+=`<button class="btn bsm" style="background:#7b42bc;color:#fff;border-color:#7b42bc;font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfSaveAndPlan()">${TF_ICON} Re-plan</button>`;
|
||||
if(st==='applied'||st==='planned')btns+='<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px" onclick="S.tfConfirmDest=true;R()">✕ Destroy</button>';
|
||||
return btns;
|
||||
}
|
||||
|
||||
@@ -667,13 +793,27 @@ function rTfBottomContent(){
|
||||
if(S.tfBtab==='files')return rTfFiles();
|
||||
if(S.tfBtab==='plan')return rTfPlanPanel();
|
||||
if(S.tfBtab==='resources')return rTfResourcesPanel();
|
||||
if(S.tfBtab==='output')return rTfOutputPanel();
|
||||
return'';
|
||||
}
|
||||
|
||||
function rTfTerminal(){
|
||||
const out=S.tfPlanOut||S.tfApplyOut||S.tfDestroyOut;
|
||||
let label='Terminal',color='#cdd6f4',content='';
|
||||
if(out){
|
||||
if(S.tfPlanOut)content=S.tfPlanOut;
|
||||
if(S.tfApplyOut)content=S.tfApplyOut;
|
||||
if(S.tfDestroyOut)content=S.tfDestroyOut;
|
||||
label=S.tfDestroyOut?'Destroy Output':S.tfApplyOut?'Apply Output':'Plan Output';
|
||||
color=S.tfDestroyOut?'#f38ba8':S.tfApplyOut?'#a6e3a1':'#cba6f7';
|
||||
}
|
||||
return`<div class="tf-bc-right">
|
||||
<div class="tf-term-hdr"><svg viewBox="0 0 16 16"><path d="M2 1h12a1 1 0 011 1v12a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1zm1 3v9h10V4zm2 1l3 2.5L5 10z"/></svg><span style="color:${color}">${label}</span>${S.tfRunning?'<span style="margin-left:auto;font-size:.6rem;color:#f9e2af">● Executando...</span>':''}</div>
|
||||
<div class="tf-term-body" id="tfTermOut">${out?escHtml(content):'<span style="opacity:.4">Aguardando execução...</span>'}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function rTfFiles(){
|
||||
if(!S.tfFiles.length)return`<div class="tf-empty">${TF_ICON}<p>Nenhum arquivo gerado ainda</p><p style="font-size:.66rem;opacity:.5">Descreva a infraestrutura desejada no chat acima</p></div>`;
|
||||
// Auto-add provider.tf and terraform.tfvars as virtual files
|
||||
const files=[...S.tfFiles];
|
||||
if(S.tfWs||S.tfOci){
|
||||
const oc=S.ociCfg.find(x=>x.id===S.tfOci);
|
||||
@@ -682,18 +822,29 @@ function rTfFiles(){
|
||||
if(!files.find(f=>f.name==='terraform.tfvars'))files.push({name:'terraform.tfvars',content:`compartment_id = "${comp}"\nregion = "${oc.region}"`,size:0,type:'tfvars',auto:true});
|
||||
}
|
||||
}
|
||||
return files.map((f,i)=>{
|
||||
let html='';
|
||||
for(let i=0;i<files.length;i++){
|
||||
const f=files[i];const isEdit=S.tfEditIdx===i;
|
||||
const sz=f.content?f.content.length:f.size;
|
||||
const sizeStr=sz>1024?(sz/1024).toFixed(1)+' KB':sz+' B';
|
||||
const iconFill=f.auto?'#888':'#7b42bc';
|
||||
return`<div class="tf-file">
|
||||
html+=`<div class="tf-file${isEdit?' editing':''}" onclick="tfOpenFile(${i})">
|
||||
<div class="tf-file-icon"><svg viewBox="0 0 16 16" fill="${iconFill}"><path d="M4 1a1 1 0 00-1 1v12a1 1 0 001 1h8a1 1 0 001-1V4.5L9.5 1H4zm5 0v3.5h3.5"/></svg></div>
|
||||
<div class="tf-file-info"><div class="tf-file-name">${f.name}</div><div class="tf-file-meta">${sizeStr}${f.auto?' · auto-generated':''}</div></div>
|
||||
<div class="tf-file-actions">
|
||||
<div class="tf-file-btn" onclick="tfCopyFile(${i})" title="Copiar"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M4 2a2 2 0 012-2h4.586L14 3.414V10a2 2 0 01-2 2H6a2 2 0 01-2-2zm6-1.414L13.414 4H10zM2 6a2 2 0 00-2 2v6a2 2 0 002 2h4a2 2 0 002-2v-1H6a3 3 0 01-3-3V6z"/></svg></div>
|
||||
<div class="tf-file-btn" onclick="tfDlFile(${i})" title="Download"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M8 1v9m0 0L5 7m3 3l3-3M2 12v1.5A1.5 1.5 0 003.5 15h9a1.5 1.5 0 001.5-1.5V12" stroke="var(--t3)" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg></div>
|
||||
${!f.auto?`<div class="tf-file-btn" onclick="event.stopPropagation();tfOpenFile(${isEdit?-1:i})" title="${isEdit?'Fechar':'Editar'}"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M12.146.854a.5.5 0 01.708 0l2.292 2.292a.5.5 0 010 .708L5.854 13.146a.5.5 0 01-.22.128l-3.5 1.166a.5.5 0 01-.632-.632l1.166-3.5a.5.5 0 01.128-.22z"/></svg></div>`:''}
|
||||
<div class="tf-file-btn" onclick="event.stopPropagation();tfCopyFile(${i})" title="Copiar"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M4 2a2 2 0 012-2h4.586L14 3.414V10a2 2 0 01-2 2H6a2 2 0 01-2-2zm6-1.414L13.414 4H10zM2 6a2 2 0 00-2 2v6a2 2 0 002 2h4a2 2 0 002-2v-1H6a3 3 0 01-3-3V6z"/></svg></div>
|
||||
<div class="tf-file-btn" onclick="event.stopPropagation();tfDlFile(${i})" title="Download"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M8 1v9m0 0L5 7m3 3l3-3M2 12v1.5A1.5 1.5 0 003.5 15h9a1.5 1.5 0 001.5-1.5V12" stroke="var(--t3)" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg></div>
|
||||
</div>
|
||||
</div>`}).join('');
|
||||
</div>`;
|
||||
if(isEdit){
|
||||
html+=`<div class="tf-editor">
|
||||
<div class="tf-editor-hdr"><svg viewBox="0 0 16 16" width="12" height="12" fill="#7b42bc"><path d="M12.146.854a.5.5 0 01.708 0l2.292 2.292a.5.5 0 010 .708L5.854 13.146a.5.5 0 01-.22.128l-3.5 1.166a.5.5 0 01-.632-.632l1.166-3.5a.5.5 0 01.128-.22z"/></svg><span class="tf-ed-name">${f.name}</span>${f.auto?'<span style="font-size:.58rem;color:var(--t3);font-weight:400">somente leitura</span>':'<button class="btn bs" onclick="tfSaveFile(${i})" style="background:#7b42bc;color:#fff;border-color:#7b42bc;font-size:.6rem;padding:1px 8px">Salvar</button>'}<button class="btn bs" onclick="S.tfEditIdx=-1;R()" style="font-size:.6rem;padding:1px 8px">Fechar</button></div>
|
||||
<textarea class="tf-editor-ta" id="tfEdTa" ${f.auto?'readonly':''}onkeydown="if(event.key==='Tab'){event.preventDefault();const s=this.selectionStart,e=this.selectionEnd;this.value=this.value.substring(0,s)+' '+this.value.substring(e);this.selectionStart=this.selectionEnd=s+2}">${escHtml(f.content||'')}</textarea>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function rTfPlanPanel(){
|
||||
@@ -704,15 +855,6 @@ function rTfPlanPanel(){
|
||||
<div style="font-size:.64rem;color:var(--t3)">${escHtml(r.desc)}</div></div></div>`).join('');
|
||||
}
|
||||
|
||||
function rTfOutputPanel(){
|
||||
let out='';
|
||||
if(S.tfPlanOut)out+=`<div style="font-size:.7rem;font-weight:600;color:#7b42bc;margin-bottom:.3rem">${TF_ICON} Plan Output</div><div class="tf-output">${escHtml(S.tfPlanOut)}</div>`;
|
||||
if(S.tfApplyOut)out+=`<div style="font-size:.7rem;font-weight:600;color:var(--gn);margin-top:.5rem;margin-bottom:.3rem">Apply Output</div><div class="tf-output">${escHtml(S.tfApplyOut)}</div>`;
|
||||
if(S.tfDestroyOut)out+=`<div style="font-size:.7rem;font-weight:600;color:var(--rd);margin-top:.5rem;margin-bottom:.3rem">Destroy Output</div><div class="tf-output">${escHtml(S.tfDestroyOut)}</div>`;
|
||||
if(!out)return`<div class="tf-empty"><svg viewBox="0 0 16 16" width="36" height="36" fill="#7b42bc" opacity=".2"><path d="M2 1h12a1 1 0 011 1v12a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1zm1 3v9h10V4zm2 1l3 2.5L5 10z"/></svg><p>Nenhuma execução ainda</p></div>`;
|
||||
return out;
|
||||
}
|
||||
|
||||
function rTfConfirmDestroy(){
|
||||
return`<div class="tf-modal-overlay" onclick="S.tfConfirmDest=false;R()"><div class="tf-modal" onclick="event.stopPropagation()">
|
||||
<h3 style="display:flex;align-items:center;gap:.4rem"><svg viewBox="0 0 16 16" width="18" height="18" fill="var(--rd)"><path d="M8 1a7 7 0 100 14A7 7 0 008 1zm0 2.5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 018 3.5zM8 10a1 1 0 100 2 1 1 0 000-2z"/></svg> Confirmar Destroy</h3>
|
||||
@@ -735,6 +877,8 @@ function tfStartResize(e){
|
||||
document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);
|
||||
}
|
||||
|
||||
function tfOpenFile(i){S.tfEditIdx=S.tfEditIdx===i?-1:i;R();if(S.tfEditIdx>=0)setTimeout(()=>{const ta=document.getElementById('tfEdTa');if(ta){ta.style.height='auto';ta.style.height=Math.min(Math.max(ta.scrollHeight,120),400)+'px'}},20)}
|
||||
function tfSaveFile(i){const ta=document.getElementById('tfEdTa');if(!ta||i<0||i>=S.tfFiles.length)return;S.tfFiles[i].content=ta.value;S.tfFiles[i].size=ta.value.length;S.tfCode=S.tfFiles.map(f=>'// filename: '+f.name+'\n'+f.content).join('\n\n');S.tfStatus='draft';R()}
|
||||
function tfCopyFile(i){
|
||||
const files=[...S.tfFiles];
|
||||
const oc=S.ociCfg.find(x=>x.id===S.tfOci);
|
||||
@@ -787,12 +931,12 @@ function rTfResourcesPanel(){
|
||||
const cats=[
|
||||
{key:'vcns',label:'VCNs',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M1 3h14v2H1zm2 4h10v2H3zm3 4h4v2H6z"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${(r.cidr_blocks||[]).join(', ')})</span>`},
|
||||
{key:'subnets',label:'Subnets',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M2 2h5v5H2zm7 0h5v5H9zm-7 7h5v5H2zm7 0h5v5H9z"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.cidr_block}, ${r.public?'pública':'privada'})</span>`},
|
||||
{key:'instances',label:'Compute',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M2 2h12v12H2z"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.shape})</span>`},
|
||||
{key:'instances',label:'Compute',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M2 2h12v12H2z"/></svg>',fmt:r=>{const stopped=r.state==='STOPPED';const running=r.state==='RUNNING';const stC=stopped?'var(--rd)':running?'var(--gn)':'var(--t3)';return`${r.display_name} <span style="opacity:.6">(${r.shape})</span> <span style="color:${stC};font-weight:600;font-size:.62rem">${r.state}</span>${stopped?` <button class="btn bsm" style="font-size:.58rem;padding:1px 6px;background:var(--gn);color:#fff;border-color:var(--gn);margin-left:4px" onclick="event.stopPropagation();ociStartInstance('${r.id}')">▶ Start</button>`:running?` <button class="btn bsm" style="font-size:.58rem;padding:1px 6px;background:var(--rd);color:#fff;border-color:var(--rd);margin-left:4px" onclick="event.stopPropagation();ociStopInstance('${r.id}')">■ Stop</button>`:''}`}},
|
||||
{key:'internet_gateways',label:'Internet GWs',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><circle cx="8" cy="8" r="6" fill="none" stroke="#7b42bc" stroke-width="1.5"/><path d="M1 8h14M8 2c-2 2-2 10 0 12m0-12c2 2 2 10 0 12"/></svg>',fmt:r=>r.display_name},
|
||||
{key:'nat_gateways',label:'NAT GWs',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M3 3l5 5-5 5"/><path d="M8 8h6" stroke="#7b42bc" stroke-width="1.5"/></svg>',fmt:r=>r.display_name},
|
||||
{key:'route_tables',label:'Route Tables',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M2 3h12v2H2zm0 4h8v2H2zm0 4h10v2H2z"/></svg>',fmt:r=>r.display_name},
|
||||
{key:'security_lists',label:'Security Lists',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M8 1l6 3v4c0 3.5-2.5 6.5-6 7.5C4.5 14.5 2 11.5 2 8V4z"/></svg>',fmt:r=>r.display_name},
|
||||
{key:'autonomous_databases',label:'Databases',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><ellipse cx="8" cy="4" rx="6" ry="2.5"/><path d="M2 4v8c0 1.4 2.7 2.5 6 2.5s6-1.1 6-2.5V4"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.db_name}${r.is_free_tier?' Free':''})</span>`},
|
||||
{key:'autonomous_databases',label:'Databases',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><ellipse cx="8" cy="4" rx="6" ry="2.5"/><path d="M2 4v8c0 1.4 2.7 2.5 6 2.5s6-1.1 6-2.5V4"/></svg>',fmt:r=>{const stopped=r.state==='STOPPED';const avail=r.state==='AVAILABLE';const stC=stopped?'var(--rd)':avail?'var(--gn)':'var(--t3)';return`${r.display_name} <span style="opacity:.6">(${r.db_name}${r.is_free_tier?' Free':''})</span> <span style="color:${stC};font-weight:600;font-size:.62rem">${r.state}</span>${stopped?` <button class="btn bsm" style="font-size:.58rem;padding:1px 6px;background:var(--gn);color:#fff;border-color:var(--gn);margin-left:4px" onclick="event.stopPropagation();ociStartAdb('${r.id}')">▶ Start</button>`:avail?` <button class="btn bsm" style="font-size:.58rem;padding:1px 6px;background:var(--rd);color:#fff;border-color:var(--rd);margin-left:4px" onclick="event.stopPropagation();ociStopAdb('${r.id}')">■ Stop</button>`:''}`}},
|
||||
{key:'block_volumes',label:'Block Volumes',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M3 2h10v12H3z"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.size_in_gbs||'?'} GB)</span>`},
|
||||
{key:'load_balancers',label:'Load Balancers',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M8 2v12M2 8h12M4 4l8 8M12 4l-8 8"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.shape_name||'flexible'})</span>`}
|
||||
];
|
||||
@@ -815,16 +959,22 @@ function rTfResourcesPanel(){
|
||||
html+='</div>';
|
||||
return html;
|
||||
}
|
||||
function _ociResOid(){return S.tfOci||(S.tfModel.startsWith('cfg:')?((S.genaiCfg.find(x=>x.id===S.tfModel.slice(4))||{}).oci_config_id||''):'')}
|
||||
async function ociStartInstance(id){if(!confirm('Iniciar esta instância?'))return;try{await $api('/oci/instances/'+id+'/action',{method:'POST',body:{action:'START',oci_config_id:_ociResOid(),region:S.tfRegion||null}});alert('Instância iniciando...');tfLoadResources()}catch(e){alert('Erro: '+e.message)}}
|
||||
async function ociStopInstance(id){if(!confirm('Parar esta instância?'))return;try{await $api('/oci/instances/'+id+'/action',{method:'POST',body:{action:'STOP',oci_config_id:_ociResOid(),region:S.tfRegion||null}});alert('Instância parando...');tfLoadResources()}catch(e){alert('Erro: '+e.message)}}
|
||||
async function ociStartAdb(id){if(!confirm('Iniciar este Autonomous Database?'))return;try{await $api('/oci/autonomous-databases/'+id+'/action',{method:'POST',body:{action:'start',oci_config_id:_ociResOid(),region:S.tfRegion||null}});alert('Autonomous DB iniciando...');tfLoadResources()}catch(e){alert('Erro: '+e.message)}}
|
||||
async function ociStopAdb(id){if(!confirm('Parar este Autonomous Database?'))return;try{await $api('/oci/autonomous-databases/'+id+'/action',{method:'POST',body:{action:'stop',oci_config_id:_ociResOid(),region:S.tfRegion||null}});alert('Autonomous DB parando...');tfLoadResources()}catch(e){alert('Erro: '+e.message)}}
|
||||
function tfFilterModels(q){const list=document.getElementById('tfmdl');if(!list)return;const items=list.querySelectorAll('.itm,.grp');const ql=q.toLowerCase();let lastGrp=null,vis=false;items.forEach(el=>{if(el.classList.contains('grp')){if(lastGrp)lastGrp.style.display=vis?'':'none';lastGrp=el;vis=false}else{const show=!q||el.textContent.toLowerCase().includes(ql);el.style.display=show?'':'none';if(show)vis=true}});if(lastGrp)lastGrp.style.display=vis?'':'none'}
|
||||
function tfTogglePanel(p){S.tfPanel=S.tfPanel===p?'':p;R()}
|
||||
async function tfClear(){if(S.tfSid)try{await $api('/chat/'+S.tfSid,{method:'DELETE'})}catch(e){}S.tfMsgs=[];S.tfSid=null;S.tfPlan=[];S.tfCode='';S.tfWs=null;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfStatus='draft';S.tfRunning=false;S.tfConfirmDest=false;S.tfComps=[];S.tfCompartment='';S.tfResources=null;S.tfResLoading=false;R()}
|
||||
async function tfClear(){if(S.tfSid)try{await $api('/chat/'+S.tfSid,{method:'DELETE'})}catch(e){}S.tfMsgs=[];S.tfSid=null;S.tfPlan=[];S.tfCode='';S.tfFiles=[];S.tfWs=null;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfStatus='draft';S.tfRunning=false;S.tfConfirmDest=false;S.tfComps=[];S.tfCompartment='';S.tfResources=null;S.tfResLoading=false;R()}
|
||||
function tfScroll(){setTimeout(()=>{const e=document.getElementById('tfchm');if(e)e.scrollTop=e.scrollHeight},50)}
|
||||
function copyTfCode(){navigator.clipboard.writeText(S.tfCode).then(()=>alert('Código copiado!'))}
|
||||
function dlTfCode(){if(!S.tfCode)return;const b=new Blob([S.tfCode],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='main.tf';a.click();URL.revokeObjectURL(a.href)}
|
||||
function dlTfCode(){if(!S.tfFiles.length)return;if(S.tfFiles.length===1){const f=S.tfFiles[0];const b=new Blob([f.content],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=f.name;a.click();URL.revokeObjectURL(a.href)}else{tfDlAll()}}
|
||||
|
||||
async function tfSend(){
|
||||
const el=document.getElementById('tfi');const m=el.value.trim();if(!m)return;
|
||||
const el=document.getElementById('tfi');const m=el.value.trim();if(!m)return;el.style.height='auto';S._tfiVal='';
|
||||
if(!S.tfModel){S.tfMsgs.push({r:'assistant',c:'⚠️ Selecione um modelo antes de enviar.'});R();return}
|
||||
if(!S.tfCompartment){S.tfMsgs.push({r:'assistant',c:'⚠️ Selecione um compartment antes de enviar.'});R();return}
|
||||
el.value='';S.tfMsgs.push({r:'user',c:m});R();tfScroll();
|
||||
S.tfMsgs.push({r:'assistant',c:'⏳ _Gerando Terraform..._',thinking:true});R();tfScroll();
|
||||
try{
|
||||
@@ -836,6 +986,7 @@ async function tfSend(){
|
||||
}
|
||||
const d=await $api('/terraform/chat',{method:'POST',body});
|
||||
S.tfSid=d.session_id;
|
||||
if(S.tfHistOpen)loadHistory('terraform');
|
||||
if(d.status==='processing'&&d.message_id){await tfPollResult(d.message_id)}
|
||||
else{S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:d.response||d.content||'Sem resposta'});R();tfScroll()}
|
||||
}catch(e){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:'❌ Erro: '+e.message});R()}
|
||||
@@ -870,7 +1021,7 @@ async function tfSaveAndPlan(){
|
||||
}
|
||||
// Run plan
|
||||
await $api('/terraform/workspaces/'+S.tfWs+'/plan',{method:'POST'});
|
||||
S.tfStatus='planning';S.tfRunning=true;S.tfPlanOut='';S.tfPanel='output';R();
|
||||
S.tfStatus='planning';S.tfRunning=true;S.tfPlanOut='';S.tfBtab='plan';R();
|
||||
await tfPollExec();
|
||||
}catch(e){alert('Erro: '+e.message)}
|
||||
}
|
||||
@@ -879,7 +1030,7 @@ async function tfApply(){
|
||||
if(!S.tfWs){alert('Execute Plan primeiro.');return}
|
||||
try{
|
||||
await $api('/terraform/workspaces/'+S.tfWs+'/apply',{method:'POST'});
|
||||
S.tfStatus='applying';S.tfRunning=true;S.tfApplyOut='';S.tfPanel='output';R();
|
||||
S.tfStatus='applying';S.tfRunning=true;S.tfApplyOut='';S.tfBtab='plan';R();
|
||||
await tfPollExec();
|
||||
}catch(e){alert('Erro: '+e.message)}
|
||||
}
|
||||
@@ -891,7 +1042,7 @@ async function tfConfirmDestroy(){
|
||||
if(!S.tfWs)return;
|
||||
try{
|
||||
await $api('/terraform/workspaces/'+S.tfWs+'/destroy',{method:'POST',body:{confirm:'DESTROY'}});
|
||||
S.tfStatus='destroying';S.tfRunning=true;S.tfDestroyOut='';S.tfPanel='output';R();
|
||||
S.tfStatus='destroying';S.tfRunning=true;S.tfDestroyOut='';S.tfBtab='plan';R();
|
||||
await tfPollExec();
|
||||
}catch(e){alert('Erro: '+e.message)}
|
||||
}
|
||||
@@ -914,7 +1065,7 @@ async function tfPollExec(){
|
||||
if(!['planning','applying','destroying'].includes(r.status)){S.tfRunning=false;R();return}
|
||||
R();
|
||||
// Auto-scroll output
|
||||
const outs=document.querySelectorAll('.tf-output');outs.forEach(o=>o.scrollTop=o.scrollHeight);
|
||||
const t=document.getElementById('tfTermOut');if(t)t.scrollTop=t.scrollHeight;
|
||||
}catch(e){}
|
||||
}
|
||||
S.tfRunning=false;R();
|
||||
@@ -1235,7 +1386,7 @@ function _dlFmtSize(b){if(b>=1048576)return(b/1048576).toFixed(1)+' MB';if(b>=10
|
||||
function _dlFileIcon(name){
|
||||
const ext=name.split('.').pop().toLowerCase();
|
||||
const colors={html:'#e44d26',htm:'#e44d26',csv:'#217346',json:'#f5a623',xlsx:'#217346',xls:'#217346',txt:'#6b7280',pdf:'#dc2626',png:'#8b5cf6',svg:'#8b5cf6'};
|
||||
const c=colors[ext]||'var(--ac)';
|
||||
const c=colors[ext]||'#c74634';
|
||||
return`<div style="width:28px;height:28px;border-radius:6px;background:${c}14;display:flex;align-items:center;justify-content:center;flex-shrink:0"><span style="font-size:.58rem;font-weight:700;color:${c};text-transform:uppercase;letter-spacing:.3px">${ext}</span></div>`;
|
||||
}
|
||||
function rDl(){
|
||||
|
||||
Reference in New Issue
Block a user