Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>ConvoReader CodeMap Studio</title> | |
| <script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script> | |
| <script src="chat-git.js"></script> | |
| <style> | |
| :root{--bg:#090a0b;--panel:#121417;--card:#191c20;--line:#2b3036;--text:#f1f3f5;--muted:#8d949d;--lime:#c9ff57;--blue:#66a3ff;--purple:#a98bff;--red:#ff6b78;--orange:#ffb454}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 70% -10%,#18272a 0,transparent 33%),var(--bg);color:var(--text);font:14px/1.5 Inter,ui-sans-serif,system-ui,sans-serif}button,input,select{font:inherit}.shell{min-height:100vh;display:grid;grid-template-rows:64px 1fr}.top{display:flex;align-items:center;gap:18px;padding:0 24px;border-bottom:1px solid var(--line);background:rgba(9,10,11,.86);backdrop-filter:blur(18px);position:sticky;top:0;z-index:5}.brand{font-weight:800;letter-spacing:-.04em;font-size:20px}.brand b{color:var(--lime)}.pill{border:1px solid var(--line);border-radius:999px;padding:5px 9px;color:var(--muted);font:11px ui-monospace,monospace}.spacer{flex:1}a{color:var(--muted);text-decoration:none}a:hover{color:var(--text)}main{display:grid;grid-template-columns:280px 1fr;min-height:0}.rail{border-right:1px solid var(--line);padding:22px 16px;background:#0e1012}.rail h3,.label{text-transform:uppercase;letter-spacing:.12em;font-size:10px;color:var(--muted);margin:0 0 10px}.drop{border:1px dashed #424952;border-radius:12px;padding:20px 14px;text-align:center;background:var(--panel);cursor:pointer;margin-bottom:12px}.drop:hover{border-color:var(--lime)}.drop strong{display:block;margin-bottom:4px}.tiny{font-size:11px;color:var(--muted)}.project{padding:11px;border:1px solid var(--line);border-radius:10px;margin:8px 0;background:var(--card);display:grid;grid-template-columns:20px 1fr;gap:8px}.project strong{display:block;font-size:13px}.work{padding:28px;overflow:auto}.hero{display:grid;grid-template-columns:1.45fr .75fr;gap:18px;margin-bottom:18px}.hero-card,.panel{border:1px solid var(--line);border-radius:16px;background:linear-gradient(145deg,rgba(28,32,36,.95),rgba(15,17,19,.95));padding:22px}.eyebrow{color:var(--lime);font:11px ui-monospace,monospace;letter-spacing:.13em;text-transform:uppercase}.hero h1{font-size:clamp(28px,4vw,52px);letter-spacing:-.055em;line-height:1;margin:12px 0 14px;max-width:720px}.hero p{color:var(--muted);max-width:720px;font-size:15px}.metric{font:700 38px ui-monospace,monospace;color:var(--lime)}.metric-label{color:var(--muted);font-size:12px}.actions{display:flex;gap:9px;flex-wrap:wrap;margin-top:18px}.btn{border:1px solid var(--line);background:#22262b;color:var(--text);border-radius:8px;padding:9px 13px;cursor:pointer}.btn.primary{background:var(--lime);color:#0a0c08;border-color:var(--lime);font-weight:700}.btn:disabled{opacity:.35;cursor:not-allowed}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.panel h2{font-size:17px;margin:0 0 4px}.sub{color:var(--muted);margin-bottom:18px}.intent{white-space:pre-wrap;max-height:220px;overflow:auto;color:#cbd0d6;font:12px/1.6 ui-monospace,monospace;background:#0c0e10;border-radius:10px;padding:14px}.map{display:flex;flex-wrap:wrap;gap:10px;min-height:120px}.node{border:1px solid var(--line);border-radius:10px;padding:10px 12px;background:#111419;min-width:140px;max-width:240px}.node .type{font:9px ui-monospace,monospace;color:var(--blue);text-transform:uppercase}.node b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.node small{color:var(--muted)}.files{max-height:360px;overflow:auto;font:11px/1.7 ui-monospace,monospace}.file{display:grid;grid-template-columns:1fr auto auto;gap:12px;border-bottom:1px solid #21252a;padding:4px 0}.tag{font:9px ui-monospace,monospace;padding:2px 6px;border-radius:99px;background:#232832;color:var(--purple)}.conflict{color:var(--orange)}.good{color:var(--lime)}.empty{color:var(--muted);padding:34px 0;text-align:center}.notice{border-left:3px solid var(--orange);padding:9px 12px;background:rgba(255,180,84,.08);color:#d8c8b1;margin-top:16px}.status{margin-top:12px;color:var(--muted);font:11px ui-monospace,monospace}@media(max-width:900px){main{grid-template-columns:1fr}.rail{border-right:0;border-bottom:1px solid var(--line)}.hero,.grid{grid-template-columns:1fr}} | |
| </style> | |
| </head> | |
| <body><div class="shell"> | |
| <header class="top"><div class="brand">ConvoReader <b>CodeMap</b></div><div class="pill">BROWSER-LOCAL</div><div class="pill" id="gitBranch">⑂ main</div><div class="spacer"></div><a href="index.html?view=versions">Version graph</a><a href="index.html">← Conversations</a></header> | |
| <main> | |
| <aside class="rail"> | |
| <h3>Project intake</h3> | |
| <label class="drop" for="folderInput"><strong>+ Add project folder</strong><span class="tiny">Source stays in this browser tab</span></label> | |
| <input id="folderInput" type="file" webkitdirectory multiple hidden> | |
| <label class="drop" for="zipInput"><strong>+ Add project ZIP</strong><span class="tiny">Unpacked in memory</span></label> | |
| <input id="zipInput" type="file" accept=".zip" multiple hidden> | |
| <div id="projectList"></div> | |
| <div class="notice">Merging never pushes to Git. You receive a ZIP with a manifest, provenance, dependency union, and namespaced conflicts.</div> | |
| </aside> | |
| <section class="work"> | |
| <div class="hero"> | |
| <div class="hero-card"><div class="eyebrow">Conversation → architecture → symbiosis</div><h1 id="heroTitle">Map intent onto code. Merge without erasing lineage.</h1><p id="heroText">Open a conversation from ConvoReader or add projects here. CodeMap finds modules, symbols, dependencies, and collisions. Symbiotic Merge preserves both sources and generates a runnable merge substrate.</p><div class="actions"><button class="btn primary" id="mergeBtn" disabled>Build symbiotic ZIP</button><button class="btn" id="manifestBtn" disabled>Download map JSON</button></div><div class="status" id="status">Waiting for project input.</div></div> | |
| <div class="hero-card"><div class="label">Active conversation</div><div class="metric" id="messageCount">0</div><div class="metric-label">messages grounding this map</div><div style="height:18px"></div><div class="label">Loaded projects</div><div class="metric" id="projectCount">0</div><div class="metric-label" id="projectMeta">0 files · 0 symbols</div></div> | |
| </div> | |
| <div class="grid"> | |
| <article class="panel"><h2>Conversation intent</h2><div class="sub">Requirements and file references extracted from the selected chat.</div><div id="intent" class="intent">Select “CodeMap” from a conversation to ground the project map.</div></article> | |
| <article class="panel"><h2>Architecture map</h2><div class="sub">Language, entrypoint, dependency, and module signals.</div><div id="map" class="map"><div class="empty">Add a project to generate the map.</div></div></article> | |
| <article class="panel"><h2>Files and symbols</h2><div class="sub">Static browser analysis; generated/vendor/binary files are excluded.</div><div id="files" class="files"><div class="empty">No source files loaded.</div></div></article> | |
| <article class="panel"><h2>Merge plan</h2><div class="sub">Compatible files dedupe; conflicts remain explicit and namespaced.</div><div id="mergePlan"><div class="empty">Select at least two projects.</div></div></article> | |
| </div> | |
| </section> | |
| </main> | |
| </div> | |
| <script> | |
| ; | |
| const TEXT_EXT=/\.(js|jsx|ts|tsx|py|go|rs|java|kt|swift|rb|php|c|cc|cpp|h|hpp|cs|html|css|scss|md|json|toml|ya?ml|txt|sh|sql)$/i; | |
| const EXCLUDED=/(^|\/)(node_modules|\.git|dist|build|coverage|vendor|\.next|__pycache__)(\/|$)/; | |
| const projects=[];let conversation=null; | |
| const $=id=>document.getElementById(id); | |
| function safe(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML} | |
| function slug(s){return(s||'project').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'').slice(0,50)||'project'} | |
| async function digest(text){const bytes=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(text));return[...new Uint8Array(bytes)].map(x=>x.toString(16).padStart(2,'0')).join('')} | |
| function symbols(text,ext){const patterns=ext==='.py'?[/^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm,/^class\s+([A-Za-z_]\w*)/gm]:[/\b(?:function|class|interface|type|const|let|var|struct|enum|func)\s+([A-Za-z_$][\w$]*)/g];return[...new Set(patterns.flatMap(re=>[...text.matchAll(re)].map(m=>m[1])))].slice(0,100)} | |
| function imports(text,ext){const found=[];const patterns=ext==='.py'?[/^(?:from|import)\s+([\w.]+)/gm]:[/\b(?:import[^'";]*from\s*|require\s*\(|import\s*\()['"]([^'"]+)/g];patterns.forEach(re=>{for(const m of text.matchAll(re))found.push(m[1])});return[...new Set(found)].slice(0,100)} | |
| function language(path){const ext=path.match(/\.[^.]+$/)?.[0]?.toLowerCase()||'';return({'.ts':'TypeScript','.tsx':'TypeScript','.js':'JavaScript','.jsx':'JavaScript','.py':'Python','.go':'Go','.rs':'Rust','.swift':'Swift','.java':'Java','.kt':'Kotlin','.html':'HTML','.css':'CSS','.md':'Docs','.json':'JSON'}[ext]||ext.slice(1)||'text')} | |
| async function makeProject(name,rawFiles){const files=[];for(const raw of rawFiles){const path=raw.path.replace(/^\/+/, '');if(!TEXT_EXT.test(path)||EXCLUDED.test(path)||raw.text.length>1_000_000)continue;const ext=path.match(/\.[^.]+$/)?.[0]?.toLowerCase()||'';files.push({path,text:raw.text,bytes:new Blob([raw.text]).size,hash:await digest(raw.text),language:language(path),symbols:symbols(raw.text,ext),imports:imports(raw.text,ext)})}return{id:crypto.randomUUID(),name:slug(name),files,selected:true}} | |
| async function addFolder(list){if(!list.length)return;const root=list[0].webkitRelativePath.split('/')[0]||'project';const raws=[];for(const file of list){if(file.size>1_000_000)continue;const path=file.webkitRelativePath.split('/').slice(1).join('/')||file.name;if(TEXT_EXT.test(path)&&!EXCLUDED.test(path))raws.push({path,text:await file.text()})}projects.push(await makeProject(root,raws));render();commitMapVersion('project-folder','Map '+root)} | |
| async function addZips(list){for(const file of list){const zip=await JSZip.loadAsync(file);const raws=[];for(const [path,entry] of Object.entries(zip.files)){if(!entry.dir&&TEXT_EXT.test(path)&&!EXCLUDED.test(path))raws.push({path:path.split('/').slice(1).join('/')||path,text:await entry.async('string')})}projects.push(await makeProject(file.name.replace(/\.zip$/i,''),raws))}render();commitMapVersion('project-zip','Map '+list.length+' project archive(s)')} | |
| function allSelected(){return projects.filter(p=>p.selected)} | |
| function mergeAnalysis(){const selected=allSelected(),byPath=new Map;selected.forEach(p=>p.files.forEach(f=>{if(!byPath.has(f.path))byPath.set(f.path,[]);byPath.get(f.path).push({project:p,file:f})}));let unique=0,deduped=0,conflicts=0;for(const entries of byPath.values()){if(entries.length===1)unique++;else if(new Set(entries.map(e=>e.file.hash)).size===1)deduped++;else conflicts++}return{selected,byPath,unique,deduped,conflicts}} | |
| function render(){const chosen=allSelected(),fileCount=projects.reduce((n,p)=>n+p.files.length,0),symbolCount=projects.reduce((n,p)=>n+p.files.reduce((m,f)=>m+f.symbols.length,0),0);$('gitBranch').textContent='⑂ '+ChatGit.read().activeBranch;$('projectCount').textContent=projects.length;$('projectMeta').textContent=`${fileCount} files · ${symbolCount} symbols`;$('projectList').innerHTML=projects.map((p,i)=>`<label class="project"><input type="checkbox" ${p.selected?'checked':''} onchange="projects[${i}].selected=this.checked;render()"><span><strong>${safe(p.name)}</strong><span class="tiny">${p.files.length} source files</span></span></label>`).join('');const langs={};projects.forEach(p=>p.files.forEach(f=>langs[f.language]=(langs[f.language]||0)+1));const modules=projects.flatMap(p=>p.files.filter(f=>/(^|\/)(index|main|app|server|cli)\.[^.]+$/i.test(f.path)).map(f=>({p,f}))).slice(0,12);$('map').innerHTML=projects.length?[...Object.entries(langs).sort((a,b)=>b[1]-a[1]).slice(0,8).map(([lang,n])=>`<div class="node"><span class="type">language</span><b>${safe(lang)}</b><small>${n} files</small></div>`),...modules.map(({p,f})=>`<div class="node"><span class="type">entry signal · ${safe(p.name)}</span><b>${safe(f.path)}</b><small>${f.symbols.length} symbols · ${f.imports.length} imports</small></div>`)].join(''):'<div class="empty">Add a project to generate the map.</div>';$('files').innerHTML=projects.length?projects.flatMap(p=>p.files.slice(0,80).map(f=>`<div class="file"><span>${safe(p.name)}/${safe(f.path)}</span><span>${f.symbols.length} sym</span><span class="tag">${safe(f.language)}</span></div>`)).join(''):'<div class="empty">No source files loaded.</div>';const m=mergeAnalysis();$('mergePlan').innerHTML=chosen.length<2?'<div class="empty">Select at least two projects.</div>':`<div class="node"><span class="type">safe union</span><b class="good">${m.unique} unique paths</b><small>copied directly</small></div><br><div class="node"><span class="type">deduplication</span><b>${m.deduped} identical paths</b><small>one copy, all provenance retained</small></div><br><div class="node"><span class="type">explicit conflicts</span><b class="${m.conflicts?'conflict':'good'}">${m.conflicts} divergent paths</b><small>namespaced under symbiotic-sources/</small></div>`;$('mergeBtn').disabled=chosen.length<2;$('manifestBtn').disabled=!projects.length;$('status').textContent=projects.length?`${projects.length} projects mapped locally. No source uploaded.`:'Waiting for project input.'} | |
| function intentFromMessages(messages){const user=messages.filter(m=>(m.r||m.role)==='user').map(m=>m.t||m.content||'').join('\n');const paths=[...new Set(user.match(/[\w.-]+\/(?:[\w.-]+\/)*[\w.-]+\.[A-Za-z0-9]+/g)||[])].slice(0,30);const req=user.split(/\n|[.!?]\s+/).map(x=>x.trim()).filter(x=>x.length>24&&x.length<260).slice(0,12);return{paths,requirements:req,summary:req.slice(0,5).join('\n• ')} } | |
| async function loadConversation(){const id=new URLSearchParams(location.search).get('conversation');if(!id)return;try{const r=await fetch(`chunks/${encodeURIComponent(id)}.json`);if(!r.ok)throw new Error(`HTTP ${r.status}`);conversation=await r.json();const intent=intentFromMessages(conversation.messages||[]);$('heroTitle').textContent=conversation.title||'Conversation CodeMap';$('messageCount').textContent=(conversation.messages||[]).length;$('intent').textContent=(intent.requirements.length?'• '+intent.requirements.join('\n• '):'No explicit user requirements found.')+(intent.paths.length?'\n\nReferenced paths:\n'+intent.paths.join('\n'):'');$('heroText').textContent='This map is grounded in the selected conversation. Add one or more repositories to compare implemented code with requested intent.'}catch(e){$('intent').textContent='Could not load conversation: '+e.message}} | |
| function manifest(){const m=mergeAnalysis();return{schemaVersion:1,createdAt:new Date().toISOString(),conversation:conversation?{id:conversation.id||new URLSearchParams(location.search).get('conversation'),title:conversation.title,messageCount:(conversation.messages||[]).length,intent:intentFromMessages(conversation.messages||[])}:null,projects:m.selected.map(p=>({name:p.name,fileCount:p.files.length,files:p.files.map(f=>({path:f.path,sha256:f.hash,bytes:f.bytes,language:f.language,symbols:f.symbols,imports:f.imports}))})),merge:{uniquePaths:m.unique,dedupedPaths:m.deduped,conflictingPaths:m.conflicts,policy:'unique files copied; identical paths deduplicated; divergent paths preserved under symbiotic-sources/<project>/'}}} | |
| async function buildMerge(){const m=mergeAnalysis();if(m.selected.length<2)return;const zip=new JSZip();const provenance=[];for(const [path,entries] of m.byPath){if(entries.length===1||new Set(entries.map(e=>e.file.hash)).size===1){zip.file(path,entries[0].file.text);provenance.push({output:path,sources:entries.map(e=>`${e.project.name}/${e.file.path}`),resolution:entries.length===1?'unique':'identical-dedup'})}else{for(const e of entries){const out=`symbiotic-sources/${e.project.name}/${path}`;zip.file(out,e.file.text);provenance.push({output:out,sources:[`${e.project.name}/${path}`],resolution:'namespaced-conflict'})}}}const packageFiles=m.selected.flatMap(p=>p.files.filter(f=>f.path==='package.json').map(f=>f));if(packageFiles.length){const merged={name:'symbiotic-project',private:true,version:'0.1.0',scripts:{},dependencies:{},devDependencies:{}};for(const f of packageFiles){try{const p=JSON.parse(f.text);Object.assign(merged.scripts,p.scripts||{});Object.assign(merged.dependencies,p.dependencies||{});Object.assign(merged.devDependencies,p.devDependencies||{})}catch{}}zip.file('package.symbiotic.json',JSON.stringify(merged,null,2)+'\n')}const requirements=[...new Set(m.selected.flatMap(p=>p.files.filter(f=>f.path==='requirements.txt').flatMap(f=>f.text.split('\n').map(x=>x.trim()).filter(x=>x&&!x.startsWith('#')))))].sort();if(requirements.length)zip.file('requirements.symbiotic.txt',requirements.join('\n')+'\n');const man=manifest();man.provenance=provenance;zip.file('SYMBIOSIS.json',JSON.stringify(man,null,2)+'\n');zip.file('README.symbiotic.md',`# Symbiotic project\n\nGenerated locally by ConvoReader CodeMap Studio from ${m.selected.map(p=>p.name).join(', ')}.\n\n## Merge policy\n\nUnique files are copied at their original paths. Identical collisions are deduplicated. Divergent collisions are preserved under \`symbiotic-sources/<project>/\` and must be reconciled deliberately. No remote repository was modified.\n\n## Next gate\n\nReview SYMBIOSIS.json, reconcile namespaced conflicts, choose one build/runtime entrypoint, then run each source project's tests before claiming compatibility.\n`);const blob=await zip.generateAsync({type:'blob',compression:'DEFLATE'});download(`symbiotic-${m.selected.map(p=>p.name).join('-')}.zip`,blob);await ChatGit.commit({streamId:'codemap-'+(new URLSearchParams(location.search).get('conversation')||'workspace'),type:'codemap-merge',message:'Merge '+m.selected.map(p=>p.name).join(' + '),snapshot:man,codemap:{unique:m.unique,deduped:m.deduped,conflicts:m.conflicts}});$('status').textContent=`Merged ZIP created and versioned: ${m.unique} unique, ${m.deduped} deduped, ${m.conflicts} namespaced conflicts.`} | |
| function commitMapVersion(type,message){const summary=manifest();ChatGit.commit({streamId:'codemap-'+(new URLSearchParams(location.search).get('conversation')||'workspace'),type,message,snapshot:summary,codemap:{projects:projects.map(p=>p.name),files:projects.reduce((n,p)=>n+p.files.length,0)}})} | |
| function download(name,content){const blob=content instanceof Blob?content:new Blob([content],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000)} | |
| $('folderInput').addEventListener('change',e=>addFolder([...e.target.files]));$('zipInput').addEventListener('change',e=>addZips([...e.target.files]));$('mergeBtn').addEventListener('click',buildMerge);$('manifestBtn').addEventListener('click',()=>download('convoreader-codemap.json',JSON.stringify(manifest(),null,2)+'\n'));loadConversation();render(); | |
| </script></body></html> | |