json-object-editor 0.10.653 → 0.10.654

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.
@@ -5351,7 +5351,7 @@ this.renderHTMLContent = function(specs){
5351
5351
  '<div class="joe-uploader-preview">'+dz_message+'</div>'+
5352
5352
  '</div>'+
5353
5353
  '<div class="joe-uploader-message">add a file</div>'+
5354
- '<div class="joe-button joe-green-button joe-upload-cofirm-button hidden" onclick="_joe.uploaderConfirm(\''+uploader_id+'\',\''+prop.name+'\');">Upload File</div>'+__clearDiv__+
5354
+ '<div class="joe-button joe-green-button joe-upload-cofirm-button ui-helper-hidden hidden" onclick="_joe.uploaderConfirm(\''+uploader_id+'\',\''+prop.name+'\');">Upload File</div>'+__clearDiv__+
5355
5355
  '</div>'
5356
5356
  ;
5357
5357
  //var idprop = prop.idprop || '_id';
@@ -5378,7 +5378,7 @@ this.renderHTMLContent = function(specs){
5378
5378
  var jup_template,html= '';
5379
5379
  var alink ="<a href='${url}${base64}' class='file-link' target='_blank'></a>";
5380
5380
  var delete_btn = '<joe-uploader-file-delete-btn class="svg-shadow" onclick="_joe.Fields.uploader.remove(\''+cuid+'\',\'${filename}\');">'+self.SVG.icon.close+'</joe-uploader-delete-btn>';
5381
- var label = '<joe-uploader-file-label>${filename}</joe-uploader-file-label>';
5381
+ var label = '';
5382
5382
 
5383
5383
 
5384
5384
  files.map(function(file){
@@ -5403,6 +5403,12 @@ this.renderHTMLContent = function(specs){
5403
5403
 
5404
5404
 
5405
5405
 
5406
+ // Build the label with filename and optional OpenAI file id on a new line
5407
+ label = '<joe-uploader-file-label>'+(file.filename || '')+'</joe-uploader-file-label>';
5408
+ if(file.openai_file_id){
5409
+ label += '<joe-uploader-file-oaid >'+file.openai_file_id+'</joe-uploader-file-oaid>';
5410
+ }
5411
+
5406
5412
  if((file.type && file.type.contains('image')) || (!file.type && (file.url||file.base64).split('.').contains(['jpg','jpeg','png','gif','svg']))){
5407
5413
  jup_template = '<joe-uploader-file class="'+(file.uploaded && 'uploaded'||'')+'" style="background-image:url(${url}${base64})">'+alink+label+filesize+delete_btn+'</joe-uploader-file>';
5408
5414
  }else if (docTypes.includes((file.url || file.base64).split('.').pop())) {
@@ -5414,7 +5420,16 @@ this.renderHTMLContent = function(specs){
5414
5420
  '<joe-uploader-file-extension >.'+(file.type.split('/')[1] || '???')+'</joe-uploader-file-extension>'+
5415
5421
  alink+label+filesize+delete_btn+'</joe-uploader-file>';
5416
5422
  }
5417
- html += fillTemplate(jup_template,file);
5423
+ // Build optional OpenAI retry/upload button
5424
+ var openaiBtn = '';
5425
+ var needRetry = (!file.openai_file_id || file.openai_status == 'error');
5426
+ var hasUrl = !!file.url;
5427
+ if(hasUrl && needRetry){
5428
+ var openaiLabel = (file.openai_status == 'error') ? 'Retry OpenAI' : 'Upload to OpenAI';
5429
+ var safeFilename = (file.filename || '').replace(/'/g,"\\'");
5430
+ openaiBtn = '<joe-uploader-openai-btn class="joe-button" style="margin-left:6px;" onclick="_joe.SERVER.Plugins.openaiRetryFromUrl(\''+cuid+'\',\''+safeFilename+'\');">'+openaiLabel+'</joe-uploader-openai-btn>';
5431
+ }
5432
+ html += fillTemplate(jup_template,file) + openaiBtn;
5418
5433
  })
5419
5434
  return html+'<div class="clear"></div>';
5420
5435
  }
@@ -5431,7 +5446,7 @@ this.renderHTMLContent = function(specs){
5431
5446
  $(joe_uploader.dropzone).find('img,div').replaceWith('<img src="' + base64 + '">')
5432
5447
  //joe_uploader.dropzone.html('<img src="' + base64 + '">');
5433
5448
  joe_uploader.message.html('<b>'+file.name+'</b> selected');
5434
- joe_uploader.confirmBtn.removeClass('hidden');
5449
+ joe_uploader.confirmBtn.removeClass('ui-helper-hidden hidden');
5435
5450
  }else {
5436
5451
  results.innerHTML = 'Nothing to upload.';
5437
5452
  }
@@ -5462,7 +5477,7 @@ this.renderHTMLContent = function(specs){
5462
5477
  }
5463
5478
  joe_uploader.files.push(temp_file);
5464
5479
  $(joe_uploader.preview).html(_renderUploaderFilePreviews(joe_uploader.files,joe_uploader.cuid));
5465
- joe_uploader.confirmBtn.removeClass('hidden');
5480
+ joe_uploader.confirmBtn.removeClass('ui-helper-hidden hidden');
5466
5481
  }else {
5467
5482
  results.innerHTML = 'Nothing to upload.';
5468
5483
  }
@@ -5530,6 +5545,20 @@ this.renderHTMLContent = function(specs){
5530
5545
  var fileobj = joe_uploader.files.where({filename:filename})[0];
5531
5546
  fileobj.uploaded = new Date();
5532
5547
  fileobj.url = url;
5548
+ // Apply captured OpenAI info if present
5549
+ var key = self.current.object._id + '/' + filename;
5550
+ var oi = _joe._lastOpenAI && _joe._lastOpenAI[key];
5551
+ fileobj.openai_purpose = 'assistants';
5552
+ if(oi){
5553
+ fileobj.openai_file_id = oi.openai_file_id || null;
5554
+ fileobj.openai_status = (oi.openai_file_id && 'ok') || (oi.openai_error && 'error') || 'not_uploaded';
5555
+ fileobj.openai_error = oi.openai_error || null;
5556
+ if(oi.openai_error){
5557
+ joe_uploader.message.append('<div>OpenAI error: '+oi.openai_error+'</div>');
5558
+ }
5559
+ }else{
5560
+ fileobj.openai_status = fileobj.openai_status || 'not_uploaded';
5561
+ }
5533
5562
  delete fileobj.base64;
5534
5563
  }
5535
5564
 
@@ -5600,6 +5629,14 @@ this.renderHTMLContent = function(specs){
5600
5629
  self.uploaders[id].dropzone = $(this).find('.joe-uploader-dropzone');
5601
5630
  self.uploaders[id].confirmBtn = $(this).find('.joe-upload-cofirm-button');
5602
5631
 
5632
+ // If there are pending files (not uploaded or missing url), show the confirm button
5633
+ var pending = (self.uploaders[id].files || []).some(function(file){
5634
+ return $c.isObject(file) && (!file.uploaded || !file.url);
5635
+ });
5636
+ if(pending){
5637
+ self.uploaders[id].confirmBtn.removeClass('ui-helper-hidden hidden');
5638
+ }
5639
+
5603
5640
  });
5604
5641
  };
5605
5642
  encapsulateFieldType('uploader',self.renderUploaderField,
@@ -9936,6 +9973,13 @@ logit(intent)
9936
9973
  url = prefix+response.Key;
9937
9974
  }
9938
9975
  var filename = response.Key.replace(self.current.object._id+'/','');
9976
+ // Stash OpenAI info for use during finalization
9977
+ _joe._lastOpenAI = _joe._lastOpenAI || {};
9978
+ _joe._lastOpenAI[response.Key] = {
9979
+ openai_file_id: response.openai_file_id,
9980
+ openai_purpose: response.openai_purpose,
9981
+ openai_error: response.openai_error
9982
+ };
9939
9983
  if(field.url_field){
9940
9984
  var nprop = {};
9941
9985
  _joe.current.object[field.url_field] = url;
@@ -9960,6 +10004,53 @@ logit(intent)
9960
10004
  file: { base64:data.base64, extension:data.extension, type:data.contentType, filename: (_joe.current.object._id+data.extension), name: (_joe.current.object._id+data.extension) },
9961
10005
  field: data.field
9962
10006
  }, callback);
10007
+ },
10008
+ // Retry uploading a specific file to OpenAI using its URL.
10009
+ // Invoked by the uploader row action.
10010
+ openaiRetryFromUrl:function(uploader_id, filename){
10011
+ try{
10012
+ var joe_uploader = self.uploaders[uploader_id];
10013
+ if(!joe_uploader){ return alert('Uploader not found'); }
10014
+ var file = joe_uploader.files.where({filename:filename})[0];
10015
+ if(!file){ return alert('File not found'); }
10016
+ if(!file.url){ return alert('No URL to upload to OpenAI'); }
10017
+ var btn = $(joe_uploader.preview).find("joe-uploader-openai-btn:contains('"+filename+"')");
10018
+ joe_uploader.message.append('<div>Uploading to OpenAI...</div>');
10019
+ $.ajax({
10020
+ url: (location.port && '//'+__jsc.hostname+':'+__jsc.port+'/API/plugin/chatgpt/filesRetryFromUrl/') || '//'+__jsc.hostname+'/API/plugin/chatgpt/filesRetryFromUrl/',
10021
+ type:'POST',
10022
+ data:{
10023
+ url:file.url,
10024
+ filename:file.filename,
10025
+ contentType:file.type
10026
+ },
10027
+ success:function(resp){
10028
+ if(resp && resp.success && resp.openai_file_id){
10029
+ file.openai_file_id = resp.openai_file_id;
10030
+ file.openai_purpose = resp.openai_purpose || 'assistants';
10031
+ file.openai_status = 'ok';
10032
+ file.openai_error = null;
10033
+ joe_uploader.message.append('<div>OpenAI file attached</div>');
10034
+ }else{
10035
+ file.openai_status = 'error';
10036
+ file.openai_error = (resp && resp.error) || 'Upload failed';
10037
+ joe_uploader.message.append('<div>OpenAI error: '+(file.openai_error)+'</div>');
10038
+ }
10039
+ // Refresh preview to reflect new status/button
10040
+ $(joe_uploader.preview).html(_renderUploaderFilePreviews(joe_uploader.files,joe_uploader.cuid));
10041
+ // Save object so file metadata is persisted
10042
+ self.updateObject(null, null, true);
10043
+ },
10044
+ error:function(xhr,status,err){
10045
+ var message = (xhr && (xhr.responseJSON && (xhr.responseJSON.error || xhr.responseJSON.code) || xhr.responseText)) || err || status || 'Retry failed';
10046
+ file.openai_status = 'error';
10047
+ file.openai_error = message;
10048
+ joe_uploader.message.append('<div>OpenAI error: '+message+'</div>');
10049
+ }
10050
+ });
10051
+ }catch(e){
10052
+ alert(e && e.message || e);
10053
+ }
9963
10054
  }
9964
10055
  },
9965
10056
  ready:{
package/js/joe-ai.js CHANGED
@@ -1679,26 +1679,54 @@
1679
1679
  * Run the Thought agent for the current schema object.
1680
1680
  * Used by the core `proposeThought` field.
1681
1681
  */
1682
- Ai.runProposeThought = function(scopeId, textareaId, model){
1682
+ Ai.runProposeThought = function(btnOrScopeId, textareaIdOrModel, maybeModel){
1683
1683
  try{
1684
+ // Back-compat: allow old signature (scopeId, textareaId, model)
1685
+ var btn = (btnOrScopeId && btnOrScopeId.tagName) ? btnOrScopeId : null;
1686
+ var scopeId = btn ? textareaIdOrModel : btnOrScopeId;
1687
+ var textareaId = btn ? maybeModel : textareaIdOrModel;
1688
+ var model = btn ? undefined : maybeModel;
1689
+
1690
+ if (btn) {
1691
+ if (btn.dataset.running === '1') { return; }
1692
+ btn.dataset.running = '1';
1693
+ btn.classList.add('joe-loading');
1694
+ btn.setAttribute('disabled','disabled');
1695
+ }
1684
1696
  if (!scopeId) {
1685
1697
  alert('Save the item before proposing thoughts.');
1698
+ if (btn) { btn.dataset.running=''; btn.classList.remove('joe-loading'); btn.removeAttribute('disabled'); }
1686
1699
  return;
1687
1700
  }
1688
1701
  var ta = document.getElementById(textareaId);
1689
1702
  if (!ta) {
1690
1703
  alert('Prompt field not found.');
1704
+ if (btn) { btn.dataset.running=''; btn.classList.remove('joe-loading'); btn.removeAttribute('disabled'); }
1691
1705
  return;
1692
1706
  }
1693
1707
  var prompt = (ta.value || '').trim();
1694
1708
  if (!prompt) {
1695
1709
  alert('Please enter a prompt for the Thought run.');
1710
+ if (btn) { btn.dataset.running=''; btn.classList.remove('joe-loading'); btn.removeAttribute('disabled'); }
1696
1711
  return;
1697
1712
  }
1713
+ // Collect selected OpenAI file ids (trim to 10)
1714
+ var selId = 'propose_thought_files_' + scopeId;
1715
+ var fileIds = [];
1716
+ try{
1717
+ var sel = document.getElementById(selId);
1718
+ if(sel){
1719
+ var selected = Array.prototype.slice.call(sel.selectedOptions || []);
1720
+ fileIds = selected.map(function(o){ return o.value; }).filter(Boolean);
1721
+ if(fileIds.length > 10){ fileIds = fileIds.slice(0,10); }
1722
+ }
1723
+ }catch(_e){/*noop*/}
1724
+
1698
1725
  var params = {
1699
1726
  agent_id: 'thought_agent_default',
1700
1727
  scope_id: scopeId,
1701
- user_input: prompt
1728
+ user_input: prompt,
1729
+ openai_file_ids: fileIds
1702
1730
  };
1703
1731
  if (model) {
1704
1732
  params.model = model;
@@ -1723,12 +1751,15 @@
1723
1751
  } else {
1724
1752
  alert('Thought agent run complete. Proposed ' + count + ' thoughts.');
1725
1753
  }
1754
+ if (btn) { btn.dataset.running=''; btn.classList.remove('joe-loading'); btn.removeAttribute('disabled'); }
1726
1755
  }).catch(function(err){
1727
1756
  console.error('Ai.runProposeThought error', err);
1728
1757
  alert('Failed to run Thought agent: ' + (err && err.message || err));
1758
+ if (btn) { btn.dataset.running=''; btn.classList.remove('joe-loading'); btn.removeAttribute('disabled'); }
1729
1759
  });
1730
1760
  }catch(e){
1731
1761
  console.error('Ai.runProposeThought error', e);
1762
+ try{ if (btn) { btn.dataset.running=''; btn.classList.remove('joe-loading'); btn.removeAttribute('disabled'); } }catch(_e){}
1732
1763
  }
1733
1764
  };
1734
1765
 
@@ -1813,7 +1844,203 @@
1813
1844
  return false;
1814
1845
  }
1815
1846
  };
1847
+
1848
+ // --------------------------------------------------
1849
+ // File helpers for AI integrations
1850
+ // --------------------------------------------------
1851
+ Ai.getFilesForObject = function(options = {}){
1852
+ try{
1853
+ var onlyWithOpenAI = options.onlyWithOpenAI === true;
1854
+ var includeNested = options.includeNested === true; // not used in v1
1855
+ var obj = (_joe && _joe.current && _joe.current.object) || {};
1856
+ var schema = (_joe && _joe.current && _joe.current.schema) || {};
1857
+ // Prefer the live, constructed field defs; fall back to schema.fields
1858
+ var liveFieldDefs = (_joe && _joe.current && Array.isArray(_joe.current.fields) && _joe.current.fields) || [];
1859
+ var schemaFields = (schema && schema.fields) || [];
1860
+ var fieldNames = [];
1861
+ // collect from live constructed defs
1862
+ liveFieldDefs.forEach(function(fd){
1863
+ if(fd && fd.name && fd.type === 'uploader'){ fieldNames.push(fd.name); }
1864
+ });
1865
+ // also collect from raw schema (string or object), dedupe
1866
+ schemaFields.forEach(function(f){
1867
+ var fname = (typeof f === 'string') ? f : (f && (f.name || f.field || f.prop));
1868
+ if(!fname){ return; }
1869
+ try{
1870
+ var fd = _joe.getField(fname);
1871
+ if(fd && fd.type === 'uploader' && fieldNames.indexOf(fname) === -1){
1872
+ fieldNames.push(fname);
1873
+ }
1874
+ }catch(_e){ /* ignore */ }
1875
+ });
1876
+
1877
+ var out = [];
1878
+ fieldNames.forEach(function(fname){
1879
+ var val = obj[fname];
1880
+ if(!val){ return; }
1881
+ var list = Array.isArray(val) ? val : [val];
1882
+ list.forEach(function(file){
1883
+ if(!file || typeof file !== 'object'){ return; }
1884
+ var item = {
1885
+ fieldName: fname,
1886
+ filename: file.filename || (file.url && String(file.url).split('/').pop()) || '',
1887
+ url: file.url || null,
1888
+ openai_file_id: file.openai_file_id || null,
1889
+ openai_status: file.openai_status || null,
1890
+ size: file.size || null,
1891
+ type: file.type || null,
1892
+ path: fname
1893
+ };
1894
+ if(onlyWithOpenAI && !item.openai_file_id){ return; }
1895
+ out.push(item);
1896
+ });
1897
+ });
1898
+ return out;
1899
+ }catch(e){
1900
+ console.warn('Ai.getFilesForObject error', e);
1901
+ return [];
1902
+ }
1903
+ };
1904
+
1905
+ Ai.renderFilesSelector = function(selectId, opts = {}){
1906
+ try{
1907
+ var cap = typeof opts.cap === 'number' ? opts.cap : 10;
1908
+ var disableWithoutOpenAI = opts.disableWithoutOpenAI !== false; // default true
1909
+ var onlyWithOpenAI = false; // show all; disable those missing ids
1910
+ var files = Ai.getFilesForObject({ onlyWithOpenAI, includeNested:false });
1911
+ // Fallback: scan top-level arrays for file-shaped objects if nothing found
1912
+ if(!files || !files.length){
1913
+ try{
1914
+ var obj = (_joe && _joe.current && _joe.current.object) || {};
1915
+ Object.keys(obj || {}).forEach(function(k){
1916
+ var v = obj[k];
1917
+ if(Array.isArray(v)){
1918
+ v.forEach(function(it){
1919
+ if(it && typeof it === 'object' && (it.filename || it.url)){
1920
+ files.push({
1921
+ fieldName: k,
1922
+ filename: it.filename || (it.url && String(it.url).split('/').pop()) || '',
1923
+ url: it.url || null,
1924
+ openai_file_id: it.openai_file_id || null,
1925
+ openai_status: it.openai_status || null,
1926
+ size: it.size || null,
1927
+ type: it.type || null,
1928
+ path: k
1929
+ });
1930
+ }
1931
+ });
1932
+ }
1933
+ });
1934
+ }catch(_e){ /* noop */ }
1935
+ }
1936
+ var sel = document.getElementById(selectId);
1937
+ if(!sel){ return; }
1938
+ sel.innerHTML = '';
1939
+ files.forEach(function(f){
1940
+ var opt = document.createElement('option');
1941
+ opt.value = f.openai_file_id || ''; // empty when missing
1942
+ opt.textContent = f.filename || f.openai_file_id || '(unnamed file)';
1943
+ if(disableWithoutOpenAI && !f.openai_file_id){
1944
+ opt.disabled = true;
1945
+ opt.textContent += ' (not uploaded to OpenAI)';
1946
+ }
1947
+ sel.appendChild(opt);
1948
+ });
1949
+ // Enforce cap on submit: caller will trim again before sending
1950
+ // If still empty, retry shortly in case the panel hasn't fully constructed fields yet
1951
+ if(!files.length){
1952
+ setTimeout(function(){ try{ Ai.renderFilesSelector(selectId, opts); }catch(_e){} }, 200);
1953
+ }
1954
+ }catch(e){
1955
+ console.warn('Ai.renderFilesSelector error', e);
1956
+ }
1957
+ };
1816
1958
 
1959
+ Ai.runPromptSelection = function(btnOrObjectId, selectId, filesSelectId){
1960
+ try{
1961
+ // Back-compat: allow old signature (objectId, selectId, filesSelectId)
1962
+ var btn = (btnOrObjectId && btnOrObjectId.tagName) ? btnOrObjectId : null;
1963
+ var objectId = btn ? selectId : btnOrObjectId;
1964
+ var realSelectId = btn ? filesSelectId : selectId;
1965
+ var realFilesSelectId = btn ? arguments[3] : filesSelectId; // handle both signatures
1966
+
1967
+ if (btn) {
1968
+ if (btn.dataset.running === '1') { return; }
1969
+ btn.dataset.running = '1';
1970
+ btn.classList.add('joe-loading');
1971
+ btn.setAttribute('disabled','disabled');
1972
+ }
1973
+
1974
+ var sel = document.getElementById(realSelectId);
1975
+ if(!sel || !sel.value){
1976
+ alert('Select a prompt first.');
1977
+ if (btn) { btn.dataset.running=''; btn.classList.remove('joe-loading'); btn.removeAttribute('disabled'); }
1978
+ return;
1979
+ }
1980
+ var promptId = sel.value;
1981
+ // Collect selected OpenAI file ids (trim to 10)
1982
+ var fileIds = [];
1983
+ try{
1984
+ var fsel = document.getElementById(realFilesSelectId);
1985
+ if(fsel){
1986
+ var selected = Array.prototype.slice.call(fsel.selectedOptions || []);
1987
+ fileIds = selected.map(function(o){ return o.value; }).filter(Boolean);
1988
+ if(fileIds.length > 10){ fileIds = fileIds.slice(0,10); }
1989
+ }
1990
+ }catch(_e){}
1991
+
1992
+ // Build references for this object when the prompt expects the current itemtype
1993
+ var obj = (_joe && _joe.current && _joe.current.object) || {};
1994
+ var prompt = $J.get(promptId,'ai_prompt');
1995
+ var params = {};
1996
+ try{
1997
+ (prompt && prompt.content_items || []).forEach(function(ci){
1998
+ if(ci && ci.itemtype === obj.itemtype && ci.reference){
1999
+ params[ci.reference] = obj._id;
2000
+ }
2001
+ });
2002
+ }catch(_e){}
2003
+
2004
+ $.ajax('/API/plugin/chatgpt/executeJOEAiPrompt',{
2005
+ method:'POST',
2006
+ data:{
2007
+ ai_prompt: promptId,
2008
+ params: params,
2009
+ openai_file_ids: fileIds
2010
+ },
2011
+ success:function(resp){
2012
+ try{
2013
+ // Try to apply a patch if the response contains one
2014
+ var text = (resp && resp.response) || '';
2015
+ if(text){
2016
+ try{
2017
+ var parsed = JSON.parse(text);
2018
+ if(parsed && parsed.patch){
2019
+ _joe.Ai.applyAutofillPatch(parsed.patch);
2020
+ }
2021
+ }catch(_e){ /* ignore parse errors */ }
2022
+ }
2023
+ }catch(_e){}
2024
+ // Refresh AI responses field
2025
+ try{ _joe.Fields.rerender && _joe.Fields.rerender('ai_responses'); }catch(_e){}
2026
+ if(_joe.toast){
2027
+ _joe.toast('AI prompt run complete.');
2028
+ }
2029
+ if (btn) { btn.dataset.running=''; btn.classList.remove('joe-loading'); btn.removeAttribute('disabled'); }
2030
+ },
2031
+ error:function(xhr,status,err){
2032
+ var message = (xhr && (xhr.responseJSON && (xhr.responseJSON.error || xhr.responseJSON.code) || xhr.responseText)) || err || status || 'Prompt run failed';
2033
+ alert(message);
2034
+ if (btn) { btn.dataset.running=''; btn.classList.remove('joe-loading'); btn.removeAttribute('disabled'); }
2035
+ }
2036
+ });
2037
+ }catch(e){
2038
+ console.warn('runPromptSelection error', e);
2039
+ alert('Failed to run prompt.');
2040
+ try{ if (btn) { btn.dataset.running=''; btn.classList.remove('joe-loading'); btn.removeAttribute('disabled'); } }catch(_e){}
2041
+ }
2042
+ };
2043
+
1817
2044
 
1818
2045
  // Attach AI to _joe
1819
2046
  if (window._joe) {
package/js/joe.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /* --------------------------------------------------------
2
2
  *
3
- * json-object-editor - v0.10.653
3
+ * json-object-editor - v0.10.654
4
4
  * Created by: Corey Hadden
5
5
  *
6
6
  * -------------------------------------------------------- */
@@ -5357,7 +5357,7 @@ this.renderHTMLContent = function(specs){
5357
5357
  '<div class="joe-uploader-preview">'+dz_message+'</div>'+
5358
5358
  '</div>'+
5359
5359
  '<div class="joe-uploader-message">add a file</div>'+
5360
- '<div class="joe-button joe-green-button joe-upload-cofirm-button hidden" onclick="_joe.uploaderConfirm(\''+uploader_id+'\',\''+prop.name+'\');">Upload File</div>'+__clearDiv__+
5360
+ '<div class="joe-button joe-green-button joe-upload-cofirm-button ui-helper-hidden hidden" onclick="_joe.uploaderConfirm(\''+uploader_id+'\',\''+prop.name+'\');">Upload File</div>'+__clearDiv__+
5361
5361
  '</div>'
5362
5362
  ;
5363
5363
  //var idprop = prop.idprop || '_id';
@@ -5384,7 +5384,7 @@ this.renderHTMLContent = function(specs){
5384
5384
  var jup_template,html= '';
5385
5385
  var alink ="<a href='${url}${base64}' class='file-link' target='_blank'></a>";
5386
5386
  var delete_btn = '<joe-uploader-file-delete-btn class="svg-shadow" onclick="_joe.Fields.uploader.remove(\''+cuid+'\',\'${filename}\');">'+self.SVG.icon.close+'</joe-uploader-delete-btn>';
5387
- var label = '<joe-uploader-file-label>${filename}</joe-uploader-file-label>';
5387
+ var label = '';
5388
5388
 
5389
5389
 
5390
5390
  files.map(function(file){
@@ -5409,6 +5409,12 @@ this.renderHTMLContent = function(specs){
5409
5409
 
5410
5410
 
5411
5411
 
5412
+ // Build the label with filename and optional OpenAI file id on a new line
5413
+ label = '<joe-uploader-file-label>'+(file.filename || '')+'</joe-uploader-file-label>';
5414
+ if(file.openai_file_id){
5415
+ label += '<joe-uploader-file-oaid >'+file.openai_file_id+'</joe-uploader-file-oaid>';
5416
+ }
5417
+
5412
5418
  if((file.type && file.type.contains('image')) || (!file.type && (file.url||file.base64).split('.').contains(['jpg','jpeg','png','gif','svg']))){
5413
5419
  jup_template = '<joe-uploader-file class="'+(file.uploaded && 'uploaded'||'')+'" style="background-image:url(${url}${base64})">'+alink+label+filesize+delete_btn+'</joe-uploader-file>';
5414
5420
  }else if (docTypes.includes((file.url || file.base64).split('.').pop())) {
@@ -5420,7 +5426,16 @@ this.renderHTMLContent = function(specs){
5420
5426
  '<joe-uploader-file-extension >.'+(file.type.split('/')[1] || '???')+'</joe-uploader-file-extension>'+
5421
5427
  alink+label+filesize+delete_btn+'</joe-uploader-file>';
5422
5428
  }
5423
- html += fillTemplate(jup_template,file);
5429
+ // Build optional OpenAI retry/upload button
5430
+ var openaiBtn = '';
5431
+ var needRetry = (!file.openai_file_id || file.openai_status == 'error');
5432
+ var hasUrl = !!file.url;
5433
+ if(hasUrl && needRetry){
5434
+ var openaiLabel = (file.openai_status == 'error') ? 'Retry OpenAI' : 'Upload to OpenAI';
5435
+ var safeFilename = (file.filename || '').replace(/'/g,"\\'");
5436
+ openaiBtn = '<joe-uploader-openai-btn class="joe-button" style="margin-left:6px;" onclick="_joe.SERVER.Plugins.openaiRetryFromUrl(\''+cuid+'\',\''+safeFilename+'\');">'+openaiLabel+'</joe-uploader-openai-btn>';
5437
+ }
5438
+ html += fillTemplate(jup_template,file) + openaiBtn;
5424
5439
  })
5425
5440
  return html+'<div class="clear"></div>';
5426
5441
  }
@@ -5437,7 +5452,7 @@ this.renderHTMLContent = function(specs){
5437
5452
  $(joe_uploader.dropzone).find('img,div').replaceWith('<img src="' + base64 + '">')
5438
5453
  //joe_uploader.dropzone.html('<img src="' + base64 + '">');
5439
5454
  joe_uploader.message.html('<b>'+file.name+'</b> selected');
5440
- joe_uploader.confirmBtn.removeClass('hidden');
5455
+ joe_uploader.confirmBtn.removeClass('ui-helper-hidden hidden');
5441
5456
  }else {
5442
5457
  results.innerHTML = 'Nothing to upload.';
5443
5458
  }
@@ -5468,7 +5483,7 @@ this.renderHTMLContent = function(specs){
5468
5483
  }
5469
5484
  joe_uploader.files.push(temp_file);
5470
5485
  $(joe_uploader.preview).html(_renderUploaderFilePreviews(joe_uploader.files,joe_uploader.cuid));
5471
- joe_uploader.confirmBtn.removeClass('hidden');
5486
+ joe_uploader.confirmBtn.removeClass('ui-helper-hidden hidden');
5472
5487
  }else {
5473
5488
  results.innerHTML = 'Nothing to upload.';
5474
5489
  }
@@ -5536,6 +5551,20 @@ this.renderHTMLContent = function(specs){
5536
5551
  var fileobj = joe_uploader.files.where({filename:filename})[0];
5537
5552
  fileobj.uploaded = new Date();
5538
5553
  fileobj.url = url;
5554
+ // Apply captured OpenAI info if present
5555
+ var key = self.current.object._id + '/' + filename;
5556
+ var oi = _joe._lastOpenAI && _joe._lastOpenAI[key];
5557
+ fileobj.openai_purpose = 'assistants';
5558
+ if(oi){
5559
+ fileobj.openai_file_id = oi.openai_file_id || null;
5560
+ fileobj.openai_status = (oi.openai_file_id && 'ok') || (oi.openai_error && 'error') || 'not_uploaded';
5561
+ fileobj.openai_error = oi.openai_error || null;
5562
+ if(oi.openai_error){
5563
+ joe_uploader.message.append('<div>OpenAI error: '+oi.openai_error+'</div>');
5564
+ }
5565
+ }else{
5566
+ fileobj.openai_status = fileobj.openai_status || 'not_uploaded';
5567
+ }
5539
5568
  delete fileobj.base64;
5540
5569
  }
5541
5570
 
@@ -5606,6 +5635,14 @@ this.renderHTMLContent = function(specs){
5606
5635
  self.uploaders[id].dropzone = $(this).find('.joe-uploader-dropzone');
5607
5636
  self.uploaders[id].confirmBtn = $(this).find('.joe-upload-cofirm-button');
5608
5637
 
5638
+ // If there are pending files (not uploaded or missing url), show the confirm button
5639
+ var pending = (self.uploaders[id].files || []).some(function(file){
5640
+ return $c.isObject(file) && (!file.uploaded || !file.url);
5641
+ });
5642
+ if(pending){
5643
+ self.uploaders[id].confirmBtn.removeClass('ui-helper-hidden hidden');
5644
+ }
5645
+
5609
5646
  });
5610
5647
  };
5611
5648
  encapsulateFieldType('uploader',self.renderUploaderField,
@@ -9942,6 +9979,13 @@ logit(intent)
9942
9979
  url = prefix+response.Key;
9943
9980
  }
9944
9981
  var filename = response.Key.replace(self.current.object._id+'/','');
9982
+ // Stash OpenAI info for use during finalization
9983
+ _joe._lastOpenAI = _joe._lastOpenAI || {};
9984
+ _joe._lastOpenAI[response.Key] = {
9985
+ openai_file_id: response.openai_file_id,
9986
+ openai_purpose: response.openai_purpose,
9987
+ openai_error: response.openai_error
9988
+ };
9945
9989
  if(field.url_field){
9946
9990
  var nprop = {};
9947
9991
  _joe.current.object[field.url_field] = url;
@@ -9966,6 +10010,53 @@ logit(intent)
9966
10010
  file: { base64:data.base64, extension:data.extension, type:data.contentType, filename: (_joe.current.object._id+data.extension), name: (_joe.current.object._id+data.extension) },
9967
10011
  field: data.field
9968
10012
  }, callback);
10013
+ },
10014
+ // Retry uploading a specific file to OpenAI using its URL.
10015
+ // Invoked by the uploader row action.
10016
+ openaiRetryFromUrl:function(uploader_id, filename){
10017
+ try{
10018
+ var joe_uploader = self.uploaders[uploader_id];
10019
+ if(!joe_uploader){ return alert('Uploader not found'); }
10020
+ var file = joe_uploader.files.where({filename:filename})[0];
10021
+ if(!file){ return alert('File not found'); }
10022
+ if(!file.url){ return alert('No URL to upload to OpenAI'); }
10023
+ var btn = $(joe_uploader.preview).find("joe-uploader-openai-btn:contains('"+filename+"')");
10024
+ joe_uploader.message.append('<div>Uploading to OpenAI...</div>');
10025
+ $.ajax({
10026
+ url: (location.port && '//'+__jsc.hostname+':'+__jsc.port+'/API/plugin/chatgpt/filesRetryFromUrl/') || '//'+__jsc.hostname+'/API/plugin/chatgpt/filesRetryFromUrl/',
10027
+ type:'POST',
10028
+ data:{
10029
+ url:file.url,
10030
+ filename:file.filename,
10031
+ contentType:file.type
10032
+ },
10033
+ success:function(resp){
10034
+ if(resp && resp.success && resp.openai_file_id){
10035
+ file.openai_file_id = resp.openai_file_id;
10036
+ file.openai_purpose = resp.openai_purpose || 'assistants';
10037
+ file.openai_status = 'ok';
10038
+ file.openai_error = null;
10039
+ joe_uploader.message.append('<div>OpenAI file attached</div>');
10040
+ }else{
10041
+ file.openai_status = 'error';
10042
+ file.openai_error = (resp && resp.error) || 'Upload failed';
10043
+ joe_uploader.message.append('<div>OpenAI error: '+(file.openai_error)+'</div>');
10044
+ }
10045
+ // Refresh preview to reflect new status/button
10046
+ $(joe_uploader.preview).html(_renderUploaderFilePreviews(joe_uploader.files,joe_uploader.cuid));
10047
+ // Save object so file metadata is persisted
10048
+ self.updateObject(null, null, true);
10049
+ },
10050
+ error:function(xhr,status,err){
10051
+ var message = (xhr && (xhr.responseJSON && (xhr.responseJSON.error || xhr.responseJSON.code) || xhr.responseText)) || err || status || 'Retry failed';
10052
+ file.openai_status = 'error';
10053
+ file.openai_error = message;
10054
+ joe_uploader.message.append('<div>OpenAI error: '+message+'</div>');
10055
+ }
10056
+ });
10057
+ }catch(e){
10058
+ alert(e && e.message || e);
10059
+ }
9969
10060
  }
9970
10061
  },
9971
10062
  ready:{