feat: multimodal chat, region-specific MCP scanning, and resilience improvements
Add image/PDF/text file upload in chat via new /api/chat/upload endpoint with OCI GenAI ImageContent and DocumentContent support. Frontend shows attach button, file preview, and handles multipart upload seamlessly. Add optional regions parameter to all CIS MCP scan tools for targeting specific OCI regions instead of full tenancy. Session cache scoped by config+regions for concurrent multi-region scans. Add orphaned report auto-detection on progress poll (marks as failed if process not found). Increase nginx proxy timeout to 15 minutes. Improve $api error handling for non-JSON responses (504 HTML pages). Update README to v1.9.
This commit is contained in:
@@ -218,12 +218,14 @@ const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],g
|
||||
chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'',
|
||||
rptOciOpen:false,rptOciFilter:'',rptOciVal:'',rptRselOpen:false,rptRselFilter:'',rptRselVal:'',ociFormRegOpen:false,ociFormRegFilter:'',ociFormRegVal:'',
|
||||
rptLevel:2,rptObp:false,rptRaw:false,rptRedact:false,reportFiles:{},dlExpandedRid:null,dlTenancyFilter:'',
|
||||
cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:''};
|
||||
cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:'',chatFiles:[]};
|
||||
const API='/api';
|
||||
|
||||
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
|
||||
if(o.body&&!(o.body instanceof FormData)){h['Content-Type']='application/json';o.body=JSON.stringify(o.body)}
|
||||
const r=await fetch(API+p,{...o,headers:h});if(r.status===401){S.user=null;S.token=null;localStorage.removeItem('t');R();throw new Error('Unauthorized')}
|
||||
const ct=r.headers.get('content-type')||'';
|
||||
if(!ct.includes('application/json')){const txt=await r.text();throw new Error(r.ok?'Resposta inesperada do servidor':`HTTP ${r.status}: ${txt.slice(0,120).replace(/<[^>]*>/g,'').trim()||'Erro do servidor'}`)}
|
||||
const d=await r.json();if(!r.ok)throw new Error(d.detail||d.message||'Erro');return d;}
|
||||
async function doLogin(u,pw,totp){const b={username:u,password:pw};if(totp)b.totp_code=totp;
|
||||
const d=await $api('/auth/login',{method:'POST',body:b});if(d.mfa_required)return d;
|
||||
@@ -320,7 +322,10 @@ ${S.mcpSvr.filter(m=>m.is_active&&Array.isArray(m.tools)&&m.tools.length).length
|
||||
<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>
|
||||
${ociSel}${gearBtn}${ragBadge}<div style="flex:1"></div><button class="btn bd bsm" onclick="clrChat()">Limpar</button></div>
|
||||
${paramsPanel}<div class="ch-m" id="chm">${ms}</div>
|
||||
<div class="ch-i"><input type="text" id="chi" placeholder="Digite sua mensagem..." onkeydown="if(event.key==='Enter')sChat()">
|
||||
${S.chatFiles.length?`<div style="display:flex;gap:.4rem;flex-wrap:wrap;padding:.4rem .85rem;background:var(--bg1);border:1px solid var(--bd);border-bottom:none">${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()">
|
||||
<button class="btn bp" onclick="sChat()">Enviar →</button></div></div>`}
|
||||
function toggleModelDrop(){const dd=document.getElementById('mdd');dd.classList.toggle('open');if(dd.classList.contains('open')){const inp=document.getElementById('mds');inp.value='';inp.focus();filterModels('')}}
|
||||
function pickModel(v){S.chatModel=v;document.getElementById('mdd').classList.remove('open');
|
||||
@@ -344,32 +349,59 @@ 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>')}
|
||||
async function sChat(){const el=document.getElementById('chi');const m=el.value.trim();if(!m)return;
|
||||
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()}
|
||||
async function sChat(){const el=document.getElementById('chi');const m=el.value.trim();
|
||||
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='';
|
||||
S.msgs.push({r:'user',c:m});R();scCh();
|
||||
const hasFiles=S.chatFiles.length>0;
|
||||
const fileNames=S.chatFiles.map(f=>f.name);
|
||||
let userDisplay=m||'';
|
||||
if(hasFiles)userDisplay+=(userDisplay?'\n':'')+'📎 '+fileNames.join(', ');
|
||||
S.msgs.push({r:'user',c:userDisplay});R();scCh();
|
||||
// Disable input and show thinking indicator
|
||||
const btn=document.querySelector('.ch-i .btn');
|
||||
el.disabled=true;if(btn){btn.disabled=true;btn.dataset.origText=btn.textContent;btn.innerHTML='<span class="spinner" style="width:14px;height:14px;border-width:2px;margin-right:6px"></span>Pensando...'}
|
||||
const btns=document.querySelectorAll('.ch-i .btn');
|
||||
el.disabled=true;btns.forEach(b=>{b.disabled=true});
|
||||
const sendBtn=btns[btns.length-1];if(sendBtn){sendBtn.dataset.origText=sendBtn.textContent;sendBtn.innerHTML='<span class="spinner" style="width:14px;height:14px;border-width:2px;margin-right:6px"></span>Pensando...'}
|
||||
S.msgs.push({r:'assistant',c:'⏳ _Pensando..._',thinking:true});R();scCh();
|
||||
try{const body={message:m,session_id:S.sid,use_tools:S.chatUseTools};
|
||||
if(S.chatModel.startsWith('cfg:')){
|
||||
body.genai_config_id=S.chatModel.slice(4);
|
||||
body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p;
|
||||
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
|
||||
else if(S.chatModel){
|
||||
if(!S.chatOci){S.msgs.pop();S.msgs.push({r:'assistant',c:'⚠️ Selecione uma credencial OCI para usar o modelo direto.'});R();el.disabled=false;if(btn){btn.disabled=false;btn.textContent=btn.dataset.origText}return}
|
||||
body.model_id=S.chatModel;body.oci_config_id=S.chatOci;body.genai_region=S.chatRegion;body.compartment_id=S.chatCompartment;
|
||||
body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p;
|
||||
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
|
||||
const d=await $api('/chat',{method:'POST',body});S.sid=d.session_id;
|
||||
// Remove thinking indicator
|
||||
try{let d;
|
||||
if(hasFiles){
|
||||
const fd=new FormData();
|
||||
fd.append('message',m||'Analise os arquivos anexados.');
|
||||
if(S.sid)fd.append('session_id',S.sid);
|
||||
fd.append('use_tools',S.chatUseTools);
|
||||
if(S.chatModel.startsWith('cfg:')){fd.append('genai_config_id',S.chatModel.slice(4))}
|
||||
else if(S.chatModel){
|
||||
if(!S.chatOci){S.msgs.pop();S.msgs.push({r:'assistant',c:'⚠️ Selecione uma credencial OCI.'});R();el.disabled=false;btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText;return}
|
||||
fd.append('model_id',S.chatModel);fd.append('oci_config_id',S.chatOci);fd.append('genai_region',S.chatRegion);fd.append('compartment_id',S.chatCompartment)}
|
||||
fd.append('temperature',S.chatParams.temperature);fd.append('max_tokens',S.chatParams.max_tokens);
|
||||
fd.append('top_p',S.chatParams.top_p);fd.append('top_k',S.chatParams.top_k);
|
||||
fd.append('frequency_penalty',S.chatParams.frequency_penalty);fd.append('presence_penalty',S.chatParams.presence_penalty);
|
||||
S.chatFiles.forEach(f=>fd.append('files',f.file));
|
||||
d=await $api('/chat/upload',{method:'POST',body:fd});S.chatFiles=[]
|
||||
}else{
|
||||
const body={message:m,session_id:S.sid,use_tools:S.chatUseTools};
|
||||
if(S.chatModel.startsWith('cfg:')){
|
||||
body.genai_config_id=S.chatModel.slice(4);
|
||||
body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p;
|
||||
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
|
||||
else if(S.chatModel){
|
||||
if(!S.chatOci){S.msgs.pop();S.msgs.push({r:'assistant',c:'⚠️ Selecione uma credencial OCI.'});R();el.disabled=false;btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText;return}
|
||||
body.model_id=S.chatModel;body.oci_config_id=S.chatOci;body.genai_region=S.chatRegion;body.compartment_id=S.chatCompartment;
|
||||
body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p;
|
||||
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;
|
||||
S.msgs=S.msgs.filter(x=>!x.thinking);
|
||||
let resp=d.response;
|
||||
if(d.tools_used&&d.tools_used.length){resp+='\n\n🔧 **Tools utilizadas:** '+d.tools_used.map(t=>t.name).join(', ')}
|
||||
S.msgs.push({r:'assistant',c:resp});R();scCh()}
|
||||
catch(e){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:'❌ Erro: '+e.message});R()}
|
||||
finally{el.disabled=false;el.focus();if(btn){btn.disabled=false;btn.textContent=btn.dataset.origText||'Enviar →'}}}
|
||||
finally{el.disabled=false;el.focus();btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText||'Enviar →'}}
|
||||
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()}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user