File size: 7,885 Bytes
a9a3715
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import {safeFilename, validateArtifact, escapeHTML, sandboxHTML} from './core.js';
export const renderMarkdown = text => DOMPurify.sanitize(marked.parse(text||'',{breaks:true}),{ADD_ATTR:['target'],FORBID_TAGS:['style','iframe','form','input','button','img','video','audio','source','object','embed'],FORBID_ATTR:['style']});
function plainInline(s) {return s.replace(/\[([^\]]+)\]\([^)]*\)/g,'$1').replace(/[*`_]/g,'');}
function blocks(markdown) {
 return marked.lexer(markdown).filter(t=>t.type!=='space');
}
export async function createArtifact(name,args) {
 validateArtifact(name,args);
 const type=name.slice(7),filename=safeFilename(args.title);
 let blob;
 if(type==='html') blob=new Blob([args.html],{type:'text/html;charset=utf-8'});
 if(type==='document') {
  const {Document,Packer,Paragraph,TextRun,HeadingLevel,Table,TableRow,TableCell,WidthType,BorderStyle}=await import('docx');
  const runs=s=>marked.lexer(s).flatMap(t=>t.tokens||[{type:'text',text:s}]).map(t=>new TextRun({text:t.text||t.raw||'',bold:t.type==='strong',italics:t.type==='em',font:t.type==='codespan'?'Consolas':'Calibri'}));
  const children=[new Paragraph({text:args.title,heading:HeadingLevel.TITLE,spacing:{after:320}})];
  for(const t of blocks(args.markdown)) {
   if(t.type==='heading')children.push(new Paragraph({children:runs(t.text),heading:HeadingLevel[`HEADING_${Math.min(t.depth,3)}`],spacing:{before:220,after:140}}));
   else if(t.type==='list')t.items.forEach((item,i)=>children.push(new Paragraph({children:runs(t.ordered?`${i+1}. ${item.text}`:item.text),...(!t.ordered?{bullet:{level:0}}:{}),spacing:{after:120}})));
   else if(t.type==='table')children.push(new Table({width:{size:100,type:WidthType.PERCENTAGE},rows:[t.header,...t.rows].map((row,i)=>new TableRow({children:row.map(c=>new TableCell({shading:i===0?{fill:'E8EAFE'}:undefined,children:[new Paragraph({children:[new TextRun({text:plainInline(c.text),bold:i===0})]})]}))}))}));
   else children.push(new Paragraph({children:t.type==='code'?[new TextRun({text:t.text,font:'Consolas',size:19})]:runs(t.text||t.raw),spacing:{after:160,line:290}}));
  }
  const document=new Document({creator:'MiniCPM5 · ProCreations',title:args.title,styles:{default:{document:{run:{font:'Calibri',size:22,color:'182039'}}},paragraphStyles:[{id:'Title',name:'Title',basedOn:'Normal',run:{size:52,bold:true,color:'343FB2'}},{id:'Heading1',name:'Heading 1',basedOn:'Normal',run:{size:32,bold:true,color:'343FB2'}},{id:'Heading2',name:'Heading 2',basedOn:'Normal',run:{size:27,bold:true,color:'343FB2'}}]},sections:[{properties:{page:{margin:{top:1100,bottom:1100,left:1100,right:1100}}},children}]});
  blob=await Packer.toBlob(document);
 }
 if(type==='spreadsheet') {
  const mod=await import('exceljs');const ExcelJS=mod.default;
  const book=new ExcelJS.Workbook();book.creator='MiniCPM5 · ProCreations';book.calcProperties.fullCalcOnLoad=true;
  const names=new Set();
  args.sheets.forEach((s,index)=>{
   let n=String(s.name||`Sheet ${index+1}`).replace(/[\\/*?:\[\]]/g,' ').slice(0,27)||`Sheet ${index+1}`;
   if(names.has(n.toLowerCase()))n+=` ${index+1}`;names.add(n.toLowerCase());
   const sheet=book.addWorksheet(n,{views:[{state:'frozen',ySplit:1}],properties:{tabColor:{argb:'FF6268F1'}}});
   sheet.columns=s.columns.map((c,i)=>({header:String(c),key:String(i),width:Math.min(48,Math.max(17,String(c).length+4))}));
   s.rows.forEach(r=>sheet.addRow(r.map(v=>typeof v==='string'&&v.startsWith('=')?{formula:v.slice(1)}:v)));
   sheet.getRow(1).height=30;sheet.getRow(1).eachCell(c=>{c.fill={type:'pattern',pattern:'solid',fgColor:{argb:'FF424BC6'}};c.font={bold:true,color:{argb:'FFFFFFFF'}};c.alignment={vertical:'middle'};});
   sheet.eachRow((r,idx)=>{if(idx===1)return;r.height=25;r.eachCell(c=>{c.font={name:'Calibri',size:11,color:{argb:'FF243047'}};c.alignment={vertical:'middle',wrapText:true};if(idx%2===0)c.fill={type:'pattern',pattern:'solid',fgColor:{argb:'FFF1F3FC'}};});});
   sheet.autoFilter={from:{row:1,column:1},to:{row:Math.max(1,s.rows.length+1),column:s.columns.length}};
  });
  blob=new Blob([await book.xlsx.writeBuffer()],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
 }
 if(type==='pdf') {
  const {jsPDF}=await import('jspdf');
  const pdf=new jsPDF({unit:'mm',format:'a4',compress:true});
  // Embed a real Unicode font (loaded only when a PDF is requested).
  const fontName=/[\u2e80-\u9fff]/.test(args.title+args.markdown)?'NotoSansSC-Regular.ttf':'NotoSans-Regular.ttf';
  const font=await fetch('/fonts/'+fontName);
  if(!font.ok)throw new Error('PDF font could not load. Please try again.');
  const bytes=new Uint8Array(await font.arrayBuffer());let binary='';for(let i=0;i<bytes.length;i+=8192)binary+=String.fromCharCode(...bytes.subarray(i,i+8192));
  pdf.addFileToVFS('NotoSans.ttf',btoa(binary));pdf.addFont('NotoSans.ttf','NotoSans','normal');pdf.setFont('NotoSans');
  let y=24;
  const write=(text,size=11,color='#25324b',gap=5)=>{
   pdf.setFontSize(size);pdf.setTextColor(color);
   const lines=pdf.splitTextToSize(plainInline(text),166);
   const height=size*0.47;
   for(const line of lines){if(y+height>274){pdf.addPage();y=24;}pdf.text(line,22,y);y+=height;}y+=gap;
  };
  write(args.title,25,'#444cc4',10);
  for(const t of blocks(args.markdown)) {
   if(t.type==='heading')write(t.text,t.depth===1?18:14,'#444cc4',5);
   else if(t.type==='list') t.items.forEach((item,i)=>write(`${t.ordered?`${i+1}.`:'•'} ${item.text}`,11,'#25324b',3));
   else if(t.type==='table') {write(t.header.map(c=>c.text).join('  |  '),10,'#444cc4',3);t.rows.forEach(r=>write(r.map(c=>c.text).join('  |  '),10,'#25324b',3));}
   else write(t.text||t.raw,11);
  }
  const pages=pdf.getNumberOfPages();for(let i=1;i<=pages;i++){pdf.setPage(i);pdf.setDrawColor('#e0e4ed');pdf.line(22,282,188,282);pdf.setFontSize(8);pdf.setTextColor('#7c879b');pdf.text('MiniCPM5 · Created on your device',22,288);pdf.text(`${i} / ${pages}`,188,288,{align:'right'});}
  pdf.setProperties({title:args.title,creator:'MiniCPM5 · ProCreations'});blob=pdf.output('blob');
 }
 const ext={html:'html',document:'docx',spreadsheet:'xlsx',pdf:'pdf'}[type];
 return {id:crypto.randomUUID(),type,title:args.title,filename:`${filename}.${ext}`,args,blob,url:URL.createObjectURL(blob),createdAt:Date.now()};
}
export function previewMarkup(artifact) {
 if(artifact.type==='html')return `<iframe title="${escapeHTML(artifact.title)} preview" sandbox="allow-scripts" referrerpolicy="no-referrer" srcdoc="${escapeHTML(sandboxHTML(artifact.args.html))}"></iframe>`;
 if(artifact.type==='pdf')return `<iframe title="${escapeHTML(artifact.title)} PDF preview" src="${artifact.url}#toolbar=0&view=FitH"></iframe>`;
 if(artifact.type==='document')return `<div class="paper"><div class="paper-brand">MINICPM5 / DOCUMENT</div><h1>${escapeHTML(artifact.title)}</h1>${renderMarkdown(artifact.args.markdown)}</div>`;
 return `<div class="workbook">${artifact.args.sheets.map((s,i)=>`<section class="worksheet"><div class="sheet-label"><span class="sheet-dot"></span>${escapeHTML(s.name||`Sheet ${i+1}`)}<small>${s.rows.length} rows</small></div><div class="table-scroll"><table><thead><tr><th></th>${s.columns.map(c=>`<th>${escapeHTML(c)}</th>`).join('')}</tr></thead><tbody>${s.rows.slice(0,200).map((r,ri)=>`<tr><th>${ri+1}</th>${s.columns.map((_,ci)=>`<td class="${typeof r[ci]==='number'?'numeric':''}">${escapeHTML(r[ci]??'')}</td>`).join('')}</tr>`).join('')}</tbody></table></div>${s.rows.length>200?'<p>Preview shows the first 200 rows. Download contains all rows.</p>':''}</section>`).join('')}<p class="sheet-note">Formula cells are shown as formulas here and recalculate in Excel.</p></div>`;
}
export function downloadArtifact(a) {const link=document.createElement('a');link.href=a.url;link.download=a.filename;link.click();}