minicpm5-2b-webgpu / src /artifacts.js
ProCreations's picture
Launch MiniCPM5 Q8 WebGPU playground with local HTML, DOCX, XLSX and PDF tools
a9a3715 verified
Raw History Blame Contribute Delete
7.89 kB
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();}