UNPKG

react-github-timeline

Version:

3D visualization of GitHub repository evolution over time using React Three Fiber

111 lines (109 loc) • 55.7 kB
(function(_,o){typeof exports=="object"&&typeof module<"u"?o(exports,require("react/jsx-runtime"),require("react"),require("@react-three/drei"),require("@react-three/fiber"),require("three")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","@react-three/drei","@react-three/fiber","three"],o):(_=typeof globalThis<"u"?globalThis:_||self,o(_.RepoTimeline={},_["react/jsx-runtime"],_.React,_.ReactThreeDrei,_.ReactThreeFiber,_.THREE))})(this,function(_,o,f,G,W,ie){"use strict";var tt=Object.defineProperty;var st=(_,o,f)=>o in _?tt(_,o,{enumerable:!0,configurable:!0,writable:!0,value:f}):_[o]=f;var T=(_,o,f)=>st(_,typeof o!="symbol"?o+"":o,f);function ne(i){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(i){for(const e in i)if(e!=="default"){const s=Object.getOwnPropertyDescriptor(i,e);Object.defineProperty(t,e,s.get?s:{enumerable:!0,get:()=>i[e]})}}return t.default=i,Object.freeze(t)}const z=ne(ie);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const ce=i=>i.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),le=i=>i.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,e,s)=>s?s.toUpperCase():e.toLowerCase()),B=i=>{const t=le(i);return t.charAt(0).toUpperCase()+t.slice(1)},K=(...i)=>i.filter((t,e,s)=>!!t&&t.trim()!==""&&s.indexOf(t)===e).join(" ").trim(),he=i=>{for(const t in i)if(t.startsWith("aria-")||t==="role"||t==="title")return!0};/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */var de={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const me=f.forwardRef(({color:i="currentColor",size:t=24,strokeWidth:e=2,absoluteStrokeWidth:s,className:a="",children:r,iconNode:n,...c},d)=>f.createElement("svg",{ref:d,...de,width:t,height:t,stroke:i,strokeWidth:s?Number(e)*24/Number(t):e,className:K("lucide",a),...!r&&!he(c)&&{"aria-hidden":"true"},...c},[...n.map(([m,l])=>f.createElement(m,l)),...Array.isArray(r)?r:[r]]));/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const N=(i,t)=>{const e=f.forwardRef(({className:s,...a},r)=>f.createElement(me,{ref:r,iconNode:t,className:K(`lucide-${ce(B(i))}`,`lucide-${i}`,s),...a}));return e.displayName=B(i),e};/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const ue=N("arrow-left",[["path",{d:"m12 19-7-7 7-7",key:"1l729n"}],["path",{d:"M19 12H5",key:"x3x0zl"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const pe=N("chevron-down",[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const ge=N("chevron-up",[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const fe=N("chevrons-left",[["path",{d:"m11 17-5-5 5-5",key:"13zhaf"}],["path",{d:"m18 17-5-5 5-5",key:"h8a8et"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const ye=N("chevrons-right",[["path",{d:"m6 17 5-5-5-5",key:"xnjwq"}],["path",{d:"m13 17 5-5-5-5",key:"17xmmf"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const X=N("circle-alert",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const Se=N("circle-check-big",[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335",key:"yps3ct"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const ve=N("fast-forward",[["path",{d:"M12 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 12 18z",key:"b19h5q"}],["path",{d:"M2 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 2 18z",key:"h7h5ge"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const we=N("loader-circle",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const Ce=N("pause",[["rect",{x:"14",y:"3",width:"5",height:"18",rx:"1",key:"kaeet6"}],["rect",{x:"5",y:"3",width:"5",height:"18",rx:"1",key:"1wsw3u"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const be=N("play",[["path",{d:"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z",key:"10ikf1"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const xe=N("refresh-cw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const Te=N("rewind",[["path",{d:"M12 6a2 2 0 0 0-3.414-1.414l-6 6a2 2 0 0 0 0 2.828l6 6A2 2 0 0 0 12 18z",key:"2a1g8i"}],["path",{d:"M22 6a2 2 0 0 0-3.414-1.414l-6 6a2 2 0 0 0 0 2.828l6 6A2 2 0 0 0 22 18z",key:"rg3s36"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const _e=N("rotate-ccw",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const Ne=N("skip-forward",[["path",{d:"M21 4v16",key:"7j8fe9"}],["path",{d:"M6.029 4.285A2 2 0 0 0 3 6v12a2 2 0 0 0 3.029 1.715l9.997-5.998a2 2 0 0 0 .003-3.432z",key:"zs4d6"}]]);/** * @license lucide-react v0.552.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */const Ee=N("skip-back",[["path",{d:"M17.971 4.285A2 2 0 0 1 21 6v12a2 2 0 0 1-3.029 1.715l-9.997-5.998a2 2 0 0 1-.003-3.432z",key:"15892j"}],["path",{d:"M3 20V4",key:"1ptbpl"}]]),Me=!1;function ke({isPlaying:i,playbackSpeed:t,playbackDirection:e,timeRange:s,hasCommits:a,onTimeChange:r,onPlayingChange:n}){const c=f.useRef(null);f.useEffect(()=>{if(i&&a){const m=100*t;return c.current=setInterval(()=>{r(l=>{let h;if(e==="forward"){if(h=l+m,h>=s.end)return n(!1),s.end}else if(h=l-m,h<=s.start)return n(!1),s.start;return h})},100),()=>{c.current&&clearInterval(c.current)}}},[i,t,e,a,s.start,s.end,r,n])}const Ae="2025-11-04-v2";function U(i){console.log(`šŸ”Ø buildFileTree called with ${i.length} files`);const t=[],e=new Map,s=new Set;if(i.forEach(a=>{const r={id:a.path,path:a.path,name:a.path.split("/").pop()||a.path,size:a.size,type:a.type||"file"};t.push(r),e.set(a.path,r);const n=a.path.split("/");for(let c=1;c<n.length;c++){const d=n.slice(0,c).join("/");s.add(d)}}),s.forEach(a=>{if(!e.has(a)){const r={id:a,path:a,name:a.split("/").pop()||a,size:0,type:"directory"};t.push(r),e.set(a,r)}}),i.length>0&&!e.has("/")){const a={id:"/",path:"/",name:"root",size:0,type:"directory"};t.push(a),e.set("/",a),console.log("āœ“ Created root node")}else i.length===0?console.log("āœ— No root node: no files"):e.has("/")&&console.log("āœ— No root node: already exists in pathMap");return t}function j(i){console.log(`šŸ”— buildEdges called with ${i.length} files`);const t=[],e=new Set;i.forEach(a=>{const r=a.path.split("/");if(r.length>1){const n=r.slice(0,-1).join("/");console.log(` File edge: ${n} → ${a.path}`),t.push({source:n,target:a.path,type:"parent"});for(let c=0;c<r.length-1;c++){const d=r.slice(0,c+1).join("/"),m=c>0?r.slice(0,c).join("/"):"",l=`${m||"/"}->${d}`;e.has(l)||(e.add(l),m===""?t.push({source:"/",target:d,type:"parent"}):t.push({source:m,target:d,type:"parent"}))}}else t.push({source:"/",target:a.path,type:"parent"})});const s=t.filter(a=>a.source==="/");return console.log(` āœ“ Created ${t.length} total edges, ${s.length} from root`),s.length>0&&console.log(" Root edge targets:",s.map(a=>a.target)),t}function O(i,t,e,s,a){const r=a.getFileData(),n=U(r),c=j(r),d=new Set([...c.map(l=>l.target),...c.map(l=>l.source)]),m=n.filter(l=>!d.has(l.id));return m.length>0&&(console.warn(`āš ļø Commit ${i} has ${m.length} orphaned nodes:`,m.map(l=>`${l.path} (${l.type})`)),console.warn(" File data:",r),console.warn(" Nodes:",n.map(l=>`${l.path} (${l.type})`)),console.warn(" Edges:",c.map(l=>`${l.source} → ${l.target}`))),{hash:i,message:t,author:e,date:new Date(s),files:n,edges:c}}class F{constructor(){T(this,"fileState",new Map)}updateFromPRFiles(t){for(const e of t)if(e.status==="removed")this.fileState.delete(e.filename);else if(e.status==="renamed"&&e.previous_filename){const s=this.fileState.get(e.previous_filename)||0;this.fileState.delete(e.previous_filename),this.fileState.set(e.filename,s+e.additions-e.deletions)}else{const s=this.fileState.get(e.filename)||0;this.fileState.set(e.filename,s+e.additions-e.deletions)}}getFileState(){return Array.from(this.fileState.entries())}getFileData(){return this.getFileState().filter(([,t])=>t>0).map(([t,e])=>({path:t,size:e}))}clear(){this.fileState.clear()}}class Le{constructor(t,e,s){T(this,"workerUrl");T(this,"owner");T(this,"repo");this.workerUrl=t,this.owner=e,this.repo=s}async fetchMetadata(){const t=`${this.workerUrl}/api/repo/${this.owner}/${this.repo}/metadata`,e=await fetch(t);if(!e.ok){const a=await e.json().catch(()=>({error:"Unknown error"}));throw new Error(a.error||`Worker request failed: ${e.status}`)}return await e.json()}async fetchCacheStatus(){const t=`${this.workerUrl}/api/repo/${this.owner}/${this.repo}/cache`,e=await fetch(t);if(!e.ok){const r=await e.json().catch(()=>({error:"Unknown error"}));throw new Error(r.error||`Cache status request failed: ${e.status}`)}const s=await e.json();return{cache:s.cache,status:s.status}}async fetchRepoSummary(){const t=`${this.workerUrl}/api/repo/${this.owner}/${this.repo}/summary`,e=await fetch(t);if(!e.ok){const a=await e.json().catch(()=>({error:"Unknown error"}));throw new Error(a.error||`Summary request failed: ${e.status}`)}return{github:(await e.json()).github}}async fetchSinglePR(t){const e=`${this.workerUrl}/api/repo/${this.owner}/${this.repo}/pr/${t}`,s=await fetch(e);if(s.status===404)return null;if(!s.ok){const r=await s.json().catch(()=>({error:"Unknown error"}));throw new Error(r.error||`Single PR request failed: ${s.status}`)}return await s.json()}async fetchCommits(t=0,e=40){const s=`${this.workerUrl}/api/repo/${this.owner}/${this.repo}?offset=${t}&limit=${e}`,a=await fetch(s);if(!a.ok){const l=await a.json().catch(()=>({error:"Unknown error"}));throw new Error(l.error||`Worker request failed: ${a.status}`)}const r=await a.json(),n=Number.parseInt(a.headers.get("X-Total-Count")||"0",10),c=a.headers.get("X-Has-More")==="true",d=Number.parseInt(a.headers.get("X-Offset")||"0",10),m=Number.parseInt(a.headers.get("X-Limit")||"40",10);return{commits:r,totalCount:n,hasMore:c,offset:d,limit:m}}}console.log(`šŸ“¦ Loaded fileTreeBuilder version: ${Ae}`);class P{constructor(t,e,s){T(this,"owner");T(this,"repo");T(this,"baseUrl","https://api.github.com");T(this,"requestDelay",1e3);T(this,"token");T(this,"lastRateLimit",null);T(this,"workerService");const[a,r]=t.split("/");this.owner=a,this.repo=r,this.token=e,s&&(this.workerService=new Le(s,a,r))}shouldUseWorker(){return!!this.workerService}async fetchMetadata(){if(!this.workerService)throw new Error("Worker URL not configured");return this.workerService.fetchMetadata()}async fetchCacheStatus(){if(!this.workerService)throw new Error("Worker URL required for cache status");return this.workerService.fetchCacheStatus()}async fetchRepoSummary(){if(!this.workerService)throw new Error("Worker URL required for summary");return this.workerService.fetchRepoSummary()}async fetchSinglePR(t){if(!this.workerService)throw new Error("Worker URL required for single PR fetch");return this.workerService.fetchSinglePR(t)}async fetchCommitsFromWorker(t=0,e=40){if(!this.workerService)throw new Error("Worker URL not configured");return this.workerService.fetchCommits(t,e)}getRateLimitInfo(){return this.lastRateLimit}async fetchRepoInfo(){return this.fetchGitHub(`/repos/${this.owner}/${this.repo}`)}sleep(t){return new Promise(e=>setTimeout(e,t))}async fetchGitHub(t,e={}){const s=`${this.baseUrl}${t}`,a={Accept:"application/vnd.github.v3+json"};this.token&&(a.Authorization=`Bearer ${this.token}`);const r=await fetch(s,{...e,headers:{...a,...e.headers}}),n=r.headers.get("X-RateLimit-Remaining"),c=r.headers.get("X-RateLimit-Limit"),d=r.headers.get("X-RateLimit-Reset");if(n&&c&&d&&(this.lastRateLimit={remaining:Number.parseInt(n),limit:Number.parseInt(c),resetTime:new Date(Number.parseInt(d)*1e3)}),!r.ok){if(r.status===403&&n==="0"){const m=d?new Date(Number.parseInt(d)*1e3):new Date;throw new Error(`GitHub API rate limit exceeded. Resets at ${m.toLocaleTimeString()}. Unauthenticated limit is 60/hour. For higher limits, wait or contact the developer.`)}throw r.status===404?new Error(`Unable to access repository: ${this.owner}/${this.repo} This could mean: • The repository is private (GitHub returns 404 for private repos) • The repository doesn't exist • The repository name is misspelled To access private repositories, authentication is required but not currently supported in this demo.`):new Error(`GitHub API error: ${r.status} ${r.statusText}`)}return r.json()}async fetchMergedPRs(t){const e=[];let s=1;const a=100;for(t&&t({loaded:0,total:-1,percentage:0,message:"Fetching pull requests..."});;){const r=await this.fetchGitHub(`/repos/${this.owner}/${this.repo}/pulls?state=closed&per_page=${a}&page=${s}&sort=created&direction=asc`);if(r.length===0)break;const n=r.filter(c=>c.merged_at!==null);if(e.push(...n),t&&t({loaded:e.length,total:-1,percentage:0,message:`Found ${e.length} merged pull requests...`}),r.length<a)break;s++,await this.sleep(this.requestDelay)}return e}async fetchPRFiles(t){const e=[];let s=1;const a=100;for(;;){const r=await this.fetchGitHub(`/repos/${this.owner}/${this.repo}/pulls/${t}/files?per_page=${a}&page=${s}`);if(r.length===0||(e.push(...r),r.length<a))break;s++,await this.sleep(this.requestDelay)}return e}async loadMoreCommits(t,e=40,s=new Map,a,r){if(!this.shouldUseWorker())throw new Error("loadMoreCommits requires worker URL");r&&r({loaded:0,total:-1,percentage:0,message:`Fetching commits ${t}-${t+e}...`});const n=await this.fetchCommitsFromWorker(t,e);r&&r({loaded:n.commits.length,total:n.commits.length,percentage:50,message:`Processing ${n.commits.length} additional commits...`});const c=[],d=new F;for(const[m,l]of s.entries())d.updateFromPRFiles([{filename:m,status:"added",additions:l,deletions:0,changes:l}]);for(let m=0;m<n.commits.length;m++){const l=n.commits[m];if(r&&r({loaded:m+1,total:n.commits.length,percentage:50+Math.round(m/n.commits.length*50),message:`Processing commit ${m+1}/${n.commits.length}`}),l.files&&l.files.length>0){const u=l.files.map(y=>({filename:y.filename,status:y.status,additions:y.additions,deletions:y.deletions,changes:y.changes,previous_filename:y.previous_filename}));d.updateFromPRFiles(u)}const h=O(l.sha.substring(0,7),l.commit.message.split(` `)[0],l.commit.author.name,l.commit.author.date,d);c.push(h),a&&a(h)}return{commits:c,hasMore:n.hasMore,totalCount:n.totalCount}}async buildTimelineFromPRsIncremental(t,e,s){if(this.shouldUseWorker()){e&&e({loaded:0,total:-1,percentage:0,message:"Fetching data from cache..."});const c=await this.fetchCommitsFromWorker();console.log("[AUTOLOAD] Initial fetch response:",{commits:c.commits.length,totalCount:c.totalCount,hasMore:c.hasMore}),e&&e({loaded:c.commits.length,total:c.totalCount,percentage:50,message:`Loaded ${c.commits.length} commits from cache`});const d=[],m=new F;for(let l=0;l<c.commits.length;l++){const h=c.commits[l];if(e&&e({loaded:l+1,total:c.commits.length,percentage:50+Math.round(l/c.commits.length*50),message:`Processing commit ${l+1}/${c.commits.length}`}),h.files&&h.files.length>0){const y=h.files.map(p=>({filename:p.filename,status:p.status,additions:p.additions,deletions:p.deletions,changes:p.changes,previous_filename:p.previous_filename}));m.updateFromPRFiles(y)}const u=O(h.sha.substring(0,7),h.commit.message.split(` `)[0],h.commit.author.name,h.commit.author.date,m);d.push(u),t&&t(u),s&&(l%5===0||l===c.commits.length-1)&&s([...d])}return{commits:d,hasMore:c.hasMore,totalCount:c.totalCount}}const a=await this.fetchMergedPRs(c=>{e&&e({...c,percentage:10})});if(a.length===0)return this.buildTimelineFromCommits(t,e);const r=[],n=new F;for(let c=0;c<a.length;c++){const d=a[c];e&&e({loaded:c+1,total:a.length,percentage:10+Math.round(c/a.length*90),message:`Processing PR #${d.number}: ${d.title}`});const m=d.files||await this.fetchPRFiles(d.number);n.updateFromPRFiles(m);const l=O(d.merge_commit_sha?d.merge_commit_sha.substring(0,7):`pr-${d.number}`,d.title,d.user.login,d.merged_at||new Date(Date.now()).toISOString(),n);r.push(l),t&&t(l),s&&(c%5===0||c===a.length-1)&&s([...r]),c<a.length-1&&await this.sleep(this.requestDelay)}return{commits:r}}async buildTimelineFromCommits(t,e){const a=(await this.fetchRepoInfo()).default_branch;e&&e({loaded:0,total:-1,percentage:0,message:`Fetching commits from ${a} branch...`});const r=[];let n=1;const c=100,d=100;for(;r.length<d;){const h=await this.fetchGitHub(`/repos/${this.owner}/${this.repo}/commits?sha=${a}&per_page=${c}&page=${n}`);if(h.length===0||(r.push(...h.slice(0,d-r.length)),h.length<c))break;n++,await this.sleep(this.requestDelay)}if(r.length===0)throw new Error("No commits found. Repository may be empty or private.");const m=[],l=new F;for(let h=0;h<r.length;h++){const u=r[h];e&&e({loaded:h+1,total:r.length,percentage:Math.round((h+1)/r.length*100),message:`Processing commit ${h+1}/${r.length}`});const y=await this.fetchGitHub(`/repos/${this.owner}/${this.repo}/commits/${u.sha}`);if(y.files&&y.files.length>0){const g=y.files.map(S=>({filename:S.filename,status:S.status,additions:S.additions,deletions:S.deletions,changes:S.changes,previous_filename:S.previous_filename}));l.updateFromPRFiles(g)}const p=O(y.sha.substring(0,7),y.commit.message.split(` `)[0],y.commit.author.name,y.commit.author.date,l);m.push(p),t&&t(p),h<r.length-1&&await this.sleep(this.requestDelay)}return{commits:m}}async buildTimelineFromPRs(t){return(await this.buildTimelineFromPRsIncremental(void 0,t)).commits}}const ze=Object.freeze(Object.defineProperty({__proto__:null,GitHubApiService:P},Symbol.toStringTag,{value:"Module"})),V=1,R="github-timeline:",$e=24*60*60*1e3;class L{static getStorageKey(t){return`${R}${t}`}static saveCommits(t,e){try{const s={repoKey:t,commits:e,lastUpdated:Date.now(),version:V},a=JSON.stringify(s,(r,n)=>n instanceof Date?n.toISOString():n);return localStorage.setItem(this.getStorageKey(t),a),!0}catch(s){return console.error("Failed to save to localStorage:",s),s instanceof Error&&s.name==="QuotaExceededError"&&this.clearOldestCache(),!1}}static loadCommits(t){try{const e=localStorage.getItem(this.getStorageKey(t));if(!e)return null;const s=JSON.parse(e,(a,r)=>typeof r=="string"&&/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(r)?new Date(r):r);return s.version!==V?(this.clearCache(t),null):Date.now()-s.lastUpdated>$e?(this.clearCache(t),null):s.commits}catch(e){return console.error("Failed to load from localStorage:",e),null}}static clearCache(t){try{localStorage.removeItem(this.getStorageKey(t))}catch(e){console.error("Failed to clear cache:",e)}}static clearAllCaches(){try{const t=Object.keys(localStorage);for(const e of t)e.startsWith(R)&&localStorage.removeItem(e)}catch(t){console.error("Failed to clear all caches:",t)}}static getCacheInfo(t){try{const e=localStorage.getItem(this.getStorageKey(t));if(!e)return{exists:!1};const s=JSON.parse(e);return{exists:!0,age:Date.now()-s.lastUpdated,commitCount:s.commits.length}}catch{return{exists:!1}}}static clearOldestCache(){try{const t=Object.keys(localStorage),e=[];for(const s of t)if(s.startsWith(R)){const a=JSON.parse(localStorage.getItem(s)||"{}");a.lastUpdated&&e.push({key:s,timestamp:a.lastUpdated})}e.sort((s,a)=>s.timestamp-a.timestamp),e.length>0&&localStorage.removeItem(e[0].key)}catch(t){console.error("Failed to clear oldest cache:",t)}}static getStorageStats(){try{const t=Object.keys(localStorage);let e=0,s=0;for(const a of t)if(a.startsWith(R)){s++;const r=localStorage.getItem(a);r&&(e+=a.length+r.length)}return{totalCaches:s,estimatedSize:e*2}}catch{return{totalCaches:0,estimatedSize:0}}}}class Y{constructor(t,e,s){T(this,"repoPath");T(this,"token");T(this,"workerUrl");T(this,"githubService");this.repoPath=t,this.token=e,this.workerUrl=s}getRateLimitInfo(){var t;return((t=this.githubService)==null?void 0:t.getRateLimitInfo())||null}async loadMoreCommits(t,e=40,s=new Map,a,r){this.githubService||(this.githubService=new P(this.repoPath,this.token,this.workerUrl));const n=await this.githubService.loadMoreCommits(t,e,s,a,r),c=this.calculateSizeChanges(n.commits),d=this.getCacheKey(),l=[...L.loadCommits(d)||[],...c];return L.saveCommits(d,l),{...n,commits:c}}async getMetadata(){this.githubService=new P(this.repoPath,this.token,this.workerUrl);const e=(await this.githubService.fetchMetadata()).map((r,n)=>({number:n+1,title:r.message,author:r.author,date:new Date(r.date)})),s=e.map(r=>r.date.getTime()),a={start:Math.min(...s),end:Math.max(...s)};return{prs:e,timeRange:a}}getCacheKey(){return this.repoPath.toLowerCase().replace(/[^a-z0-9]/g,"-")}async getCommitHistory(t,e=!1,s){const a=this.getCacheKey();if(!e){const r=L.loadCommits(a);if(r)return{commits:r}}try{const r=await this.fetchCommitsWithProgress(t,s);return L.saveCommits(a,r.commits),r}catch(r){console.error("Error fetching commits:",r);const n=L.loadCommits(a);if(n)return{commits:n};throw r}}async fetchCommitsWithProgress(t,e){if(/^[^/]+\/[^/]+$/.test(this.repoPath))try{this.githubService=new P(this.repoPath,this.token,this.workerUrl);const s=this.getCacheKey(),a=await this.githubService.buildTimelineFromPRsIncremental(e?r=>{const n=this.calculateSizeChanges([r]);e(n[0])}:void 0,t,r=>{const n=this.calculateSizeChanges(r);L.saveCommits(s,n)});return console.log("[AUTOLOAD] GitService initial load result:",{commits:a.commits.length,hasMore:a.hasMore,totalCount:a.totalCount}),{commits:this.calculateSizeChanges(a.commits),hasMore:a.hasMore,totalCount:a.totalCount}}catch(s){throw console.error("GitHub API error:",s),s}try{const s=await fetch(`/api/commits?path=${encodeURIComponent(this.repoPath)}`);if(!s.ok)throw new Error("Failed to fetch commits");const a=await s.json();return{commits:this.parseCommitsWithProgress(a,t)}}catch(s){throw new Error(`Failed to fetch repository data. Please check the repository path and try again. ${s instanceof Error?s.message:""}`)}}parseCommitsWithProgress(t,e){const s=[];for(let a=0;a<t.length;a++){const r=t[a];s.push({hash:r.hash,message:r.message,author:r.author,date:new Date(r.date),files:U(r.files.map(n=>({path:n.path,size:n.size||100,type:n.type}))),edges:j(r.files.map(n=>({path:n.path,size:n.size||100})))}),e&&a%10===0&&e({loaded:a+1,total:t.length,percentage:Math.round((a+1)/t.length*100)})}return this.calculateSizeChanges(s)}clearCache(){L.clearCache(this.getCacheKey())}getCacheInfo(){return L.getCacheInfo(this.getCacheKey())}calculateSizeChanges(t){for(let e=0;e<t.length;e++)if(e===0)t[e].files.forEach(s=>{s.sizeChange="unchanged",s.fileStatus="added"});else{const s=t[e-1],a=new Map(s.files.map(c=>[c.path,c])),r=new Map(t[e].files.map(c=>[c.path,c]));t[e].files.forEach(c=>{const d=a.get(c.path);if(d)c.previousSize=d.size,c.fileStatus="unchanged",c.size>d.size?c.sizeChange="increase":c.size<d.size?c.sizeChange="decrease":c.sizeChange="unchanged";else{const m=c.path.split("/").pop();let l=!1;for(const[h,u]of a)if(h.split("/").pop()===m&&!r.has(h)&&c.size===u.size){c.fileStatus="moved",c.previousPath=h,c.previousSize=u.size,c.sizeChange="unchanged",l=!0;break}l||(c.fileStatus="added",c.sizeChange="increase")}});const n=[];for(const[c,d]of a)if(!r.has(c)){let m=!1;for(const l of t[e].files)if(l.previousPath===c){m=!0;break}if(!m){const l={...d,size:0,previousSize:d.size,fileStatus:"deleted",sizeChange:"decrease"};t[e].files.push(l),n.push(l)}}if(n.length>0){const c=t[e].files.map(l=>({path:l.path,size:l.size})),d=U(c),m=new Map(t[e].files.map(l=>[l.path,l]));t[e].files=d.map(l=>{const h=m.get(l.path);return h||l}),t[e].edges=j(c)}}return t}}function Ie(i,t){switch(t.type){case"SET_COMMITS":{if(t.commits.length>0){const e=t.commits.map(r=>r.date.getTime()),s={start:Math.min(...e),end:Math.max(...e)},a=s.start;return{...i,commits:t.commits,timeRange:s,currentTime:a}}return{...i,commits:t.commits}}case"ADD_COMMIT":{const e=[...i.commits,t.commit],s=t.commit.date.getTime(),a={start:i.commits.length===0?s:Math.min(s,i.timeRange.start),end:i.commits.length===0?s:Math.max(s,i.timeRange.end)},r=i.currentTime===0?s:i.currentTime;return{...i,commits:e,timeRange:a,currentTime:r}}case"SET_CURRENT_TIME":return{...i,currentTime:t.time};case"SET_TIME_RANGE":return{...i,timeRange:t.range};case"SET_TOTAL_PRS":return{...i,totalPRs:t.count};case"SET_LOADING":return{...i,loading:t.loading};case"SET_BACKGROUND_LOADING":return{...i,backgroundLoading:t.loading};case"SET_LOAD_PROGRESS":return{...i,loadProgress:t.progress};case"SET_ERROR":return{...i,error:t.error};case"SET_RATE_LIMIT":return{...i,rateLimit:t.rateLimit};case"SET_FROM_CACHE":return{...i,fromCache:t.fromCache};case"SET_RATE_LIMITED_CACHE":return{...i,rateLimitedCache:t.rateLimitedCache};case"SET_REPO_STATUS":return{...i,repoStatus:t.status};case"SET_CACHE_STATUS":return{...i,cacheStatus:t.status};case"SET_REPO_SUMMARY":return{...i,repoSummary:t.summary};case"SET_LOADING_STAGE":return{...i,loadingStage:t.stage};case"RESET_COMMITS":return{...i,commits:[]};case"SET_PAGINATION":return{...i,hasMoreCommits:t.hasMore,totalCommitsAvailable:t.totalAvailable};case"APPEND_COMMITS":{const e=[...i.commits,...t.commits],s=t.commits.map(r=>r.date.getTime()),a={start:Math.min(i.timeRange.start,...s),end:Math.max(i.timeRange.end,...s)};return{...i,commits:e,timeRange:a}}default:return i}}function De({repoPath:i,githubToken:t,workerUrl:e,testMode:s=!1,onError:a}){const[r,n]=f.useReducer(Ie,{commits:[],currentTime:0,timeRange:{start:0,end:Date.now()},totalPRs:0,loading:!s,backgroundLoading:!1,loadProgress:null,error:null,rateLimit:null,fromCache:!1,rateLimitedCache:!1,repoStatus:null,cacheStatus:null,repoSummary:null,loadingStage:"initial",hasMoreCommits:!1,totalCommitsAvailable:0}),c=f.useRef(null);f.useEffect(()=>{if(!e||s)return;(async()=>{n({type:"SET_LOADING_STAGE",stage:"cache-check"});try{const{GitHubApiService:h}=await Promise.resolve().then(()=>ze),u=new h(i,t,e),[y,p]=await Promise.all([u.fetchCacheStatus(),u.fetchRepoSummary()]);n({type:"SET_CACHE_STATUS",status:y.cache}),n({type:"SET_REPO_SUMMARY",summary:p.github});const g={cache:y.cache,github:p.github,recommendation:y.status};n({type:"SET_REPO_STATUS",status:g})}catch(h){console.error("[Stage 1] Error loading instant feedback:",h)}})()},[i,e,s,t]),f.useEffect(()=>{(async()=>{n({type:"SET_LOADING_STAGE",stage:"metadata"});try{const u=await new Y(i,void 0,e).getMetadata();n({type:"SET_TOTAL_PRS",count:u.prs.length}),n({type:"SET_TIME_RANGE",range:u.timeRange}),n({type:"SET_CURRENT_TIME",time:u.timeRange.start})}catch(h){console.error("[Stage 2] Error loading metadata:",h)}})()},[i,e]);const d=f.useCallback(async(l=!1)=>{const h=new Y(i,void 0,e);c.current=h;const u=h.getCacheInfo();if(u.exists&&!l){n({type:"SET_LOADING",loading:!0}),n({type:"SET_LOAD_PROGRESS",progress:null});try{const p=await h.getCommitHistory(g=>{n({type:"SET_LOAD_PROGRESS",progress:g})},l);console.log("[AUTOLOAD] useRepoData received initial result:",{commits:p.commits.length,hasMore:p.hasMore,totalCount:p.totalCount}),n({type:"SET_COMMITS",commits:p.commits}),p.hasMore!==void 0&&p.totalCount!==void 0&&n({type:"SET_PAGINATION",hasMore:p.hasMore,totalAvailable:p.totalCount}),n({type:"SET_FROM_CACHE",fromCache:!0}),n({type:"SET_RATE_LIMITED_CACHE",rateLimitedCache:!1}),n({type:"SET_LOADING",loading:!1}),n({type:"SET_ERROR",error:null}),n({type:"SET_LOADING_STAGE",stage:"complete"}),n({type:"SET_RATE_LIMIT",rateLimit:h.getRateLimitInfo()})}catch(p){console.error("Error loading commits:",p);const g=p instanceof Error?p:new Error("Failed to load repository");n({type:"SET_ERROR",error:g.message}),n({type:"SET_LOADING",loading:!1}),n({type:"SET_RATE_LIMITED_CACHE",rateLimitedCache:!1}),n({type:"SET_RATE_LIMIT",rateLimit:h.getRateLimitInfo()}),a&&a(g)}}else{n({type:"SET_LOADING_STAGE",stage:"incremental"}),n({type:"SET_LOADING",loading:!1}),n({type:"SET_BACKGROUND_LOADING",loading:!0}),n({type:"SET_LOAD_PROGRESS",progress:null}),n({type:"RESET_COMMITS"}),n({type:"SET_FROM_CACHE",fromCache:!1});try{await h.getCommitHistory(p=>{n({type:"SET_LOAD_PROGRESS",progress:p})},l,p=>{n({type:"ADD_COMMIT",commit:p})})}catch(p){if(console.error("Error loading commits:",p),u.exists)try{const g=await h.getCommitHistory(void 0,!1);n({type:"SET_COMMITS",commits:g.commits}),n({type:"SET_FROM_CACHE",fromCache:!0}),n({type:"SET_RATE_LIMITED_CACHE",rateLimitedCache:!0}),n({type:"SET_ERROR",error:null}),console.warn("Using cached data due to API error:",p instanceof Error?p.message:"Unknown error")}catch{n({type:"SET_ERROR",error:p instanceof Error?p.message:"Failed to load repository"}),n({type:"SET_RATE_LIMITED_CACHE",rateLimitedCache:!1})}else n({type:"SET_ERROR",error:p instanceof Error?p.message:"Failed to load repository"}),n({type:"SET_RATE_LIMITED_CACHE",rateLimitedCache:!1});n({type:"SET_RATE_LIMIT",rateLimit:h.getRateLimitInfo()})}finally{n({type:"SET_BACKGROUND_LOADING",loading:!1}),n({type:"SET_LOAD_PROGRESS",progress:null}),n({type:"SET_LOADING_STAGE",stage:"complete"})}}},[i,e,a]);f.useEffect(()=>{d()},[d]);const m=f.useCallback(async()=>{if(console.log("[AUTOLOAD] loadMore() called",{hasGitService:!!c.current,backgroundLoading:r.backgroundLoading,commitsLength:r.commits.length}),!c.current||r.backgroundLoading){console.log("[AUTOLOAD] loadMore() skipped - no service or already loading");return}console.log("[AUTOLOAD] Starting background load..."),n({type:"SET_BACKGROUND_LOADING",loading:!0});try{const l=new Map;if(r.commits.length>0){const u=r.commits[r.commits.length-1];for(const y of u.files)l.set(y.path,y.size)}console.log("[AUTOLOAD] Calling gitService.loadMoreCommits with offset:",r.commits.length);const h=await c.current.loadMoreCommits(r.commits.length,40,l,u=>{console.log("[AUTOLOAD] Received commit:",u.hash),n({type:"APPEND_COMMITS",commits:[u]})},u=>{n({type:"SET_LOAD_PROGRESS",progress:u})});console.log("[AUTOLOAD] Load complete:",{newCommits:h.commits.length,hasMore:h.hasMore,totalCount:h.totalCount}),n({type:"SET_PAGINATION",hasMore:h.hasMore,totalAvailable:h.totalCount}),n({type:"SET_BACKGROUND_LOADING",loading:!1}),n({type:"SET_LOAD_PROGRESS",progress:null})}catch(l){console.error("[AUTOLOAD] Error loading more commits:",l),n({type:"SET_BACKGROUND_LOADING",loading:!1}),n({type:"SET_LOAD_PROGRESS",progress:null})}},[r.commits,r.backgroundLoading]);return{...r,loadCommits:d,loadMore:m,setCurrentTime:l=>{n(typeof l=="function"?{type:"SET_CURRENT_TIME",time:l(r.currentTime)}:{type:"SET_CURRENT_TIME",time:l})}}}function J(i,t){if(i.length===0)return 0;for(let e=i.length-1;e>=0;e--)if(i[e].date.getTime()<=t)return e;return 0}function Oe({repoPath:i}){return o.jsx("div",{className:"w-full h-full flex items-center justify-center bg-slate-900 text-white",children:o.jsxs("div",{className:"text-center",children:[o.jsx("div",{className:"text-xl mb-2",children:"No commits found"}),o.jsxs("div",{className:"text-gray-400",children:["Unable to load repository data for: ",i]})]})})}function Z({remaining:i,limit:t,resetTime:e}){if(i===null||t===null)return null;const s=i/t*100,a=s<20,r=s<5;return o.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[r?o.jsx(X,{size:14,className:"text-red-400"}):a?o.jsx(X,{size:14,className:"text-yellow-400"}):o.jsx(Se,{size:14,className:"text-green-400"}),o.jsxs("div",{className:r?"text-red-400":a?"text-yellow-400":"text-gray-400",children:["API: ",i,"/",t,e&&i<t/2&&o.jsxs("span",{className:"ml-1",children:["(resets ",e.toLocaleTimeString(),")"]})]})]})}function Fe({error:i,repoPath:t,rateLimit:e,onBack:s,onRetry:a}){const r=i.includes("rate limit");return o.jsx("div",{className:"w-full h-full flex items-center justify-center bg-slate-900 text-white",children:o.jsxs("div",{className:"text-center max-w-2xl px-8",children:[o.jsx("div",{className:"text-xl mb-4 text-red-400",children:r?"āš ļø GitHub API Rate Limit Exceeded":"Error Loading Repository"}),o.jsx("div",{className:"text-gray-300 mb-4 whitespace-pre-line text-left max-w-xl mx-auto",children:i}),o.jsxs("div",{className:"text-sm text-gray-500 mb-6",children:["Repository: ",t]}),e&&o.jsx("div",{className:"mb-6 text-sm text-gray-400",children:o.jsx(Z,{remaining:e.remaining,limit:e.limit,resetTime:e.resetTime})}),o.jsxs("div",{className:"flex gap-3 justify-center",children:[s&&o.jsx("button",{onClick:s,className:"px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded transition-colors",children:"Back"}),o.jsx("button",{onClick:a,className:"px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors",children:"Retry"})]})]})})}function Q({loadProgress:i,fromCache:t}){return o.jsx("div",{className:"w-full h-full flex items-center justify-center bg-slate-900 text-white",children:o.jsxs("div",{className:"text-center max-w-md",children:[o.jsx("div",{className:"text-xl mb-4",children:"Loading repository..."}),i?o.jsxs(o.Fragment,{children:[o.jsx("div",{className:"mb-2 text-gray-400",children:i.message||`Loading commits: ${i.loaded} / ${i.total}`}),o.jsx("div",{className:"w-full bg-gray-700 rounded-full h-2.5 mb-2",children:o.jsx("div",{className:"bg-blue-600 h-2.5 rounded-full transition-all duration-300",style:{width:`${i.percentage}%`}})}),o.jsxs("div",{className:"text-sm text-gray-500",children:[i.percentage,"%"]})]}):o.jsx("div",{className:"text-gray-400",children:t?"Loading from cache...":"Analyzing commit history..."})]})})}class Pe{constructor(t,e,s={}){T(this,"nodes");T(this,"edges");this.nodes=t,this.edges=e,this.initializePositions()}initializePositions(){this.nodes.forEach(t=>{if(t.x===void 0||t.y===void 0||t.z===void 0)if(t.id==="/"||t.path==="/")t.x=0,t.y=0,t.z=0,t.vx=0,t.vy=0,t.vz=0;else{const e=Math.random()*Math.PI*2,s=Math.random()*Math.PI*2,a=100+Math.random()*100;t.x=a*Math.sin(e)*Math.cos(s),t.y=a*Math.sin(e)*Math.sin(s),t.z=a*Math.cos(e),t.vx=0,t.vy=0,t.vz=0}})}tick(){this.applySpringForces(),this.applyRepulsionForces(),this.applyCenteringForce();const t=.85;this.nodes.forEach(e=>{if(e.id==="/"||e.path==="/"){e.x=0,e.y=0,e.z=0,e.vx=0,e.vy=0,e.vz=0;return}if(e.vx!==void 0&&e.vy!==void 0&&e.vz!==void 0){e.vx*=t,e.vy*=t,e.vz*=t;const s=10,a=Math.sqrt(e.vx*e.vx+e.vy*e.vy+e.vz*e.vz);if(a>s){const r=s/a;e.vx*=r,e.vy*=r,e.vz*=r}e.x+=e.vx,e.y+=e.vy,e.z+=e.vz}})}applySpringForces(){const t=new Map(this.nodes.map(a=>[a.id,a])),e=.1,s=30;this.edges.forEach(a=>{const r=t.get(a.source),n=t.get(a.target);if(!r||!n)return;const c=Math.max(.5,Math.min(5,Math.log10(r.size+1)*1.2)),d=Math.max(.5,Math.min(5,Math.log10(n.size+1)*1.2)),m=n.x-r.x,l=n.y-r.y,h=n.z-r.z,u=Math.sqrt(m*m+l*l+h*h)||1,y=c+d+s,g=(u-y)*e,S=m/u*g,v=l/u*g,w=h/u*g;r.vx+=S,r.vy+=v,r.vz+=w,n.vx-=S,n.vy-=v,n.vz-=w})}applyRepulsionForces(){for(let e=0;e<this.nodes.length;e++)for(let s=e+1;s<this.nodes.length;s++){const a=this.nodes[e],r=this.nodes[s],n=Math.max(.5,Math.min(5,Math.log10(a.size+1)*1.2)),c=Math.max(.5,Math.min(5,Math.log10(r.size+1)*1.2)),d=r.x-a.x,m=r.y-a.y,l=r.z-a.z,h=d*d+m*m+l*l,u=Math.sqrt(h)||1,y=n+c,p=Math.max(u-y,1),g=5e3*(n+c)/(p*p),S=d/u*g,v=m/u*g,w=l/u*g;a.vx-=S,a.vy-=v,a.vz-=w,r.vx+=S,r.vy+=v,r.vz+=w}}applyCenteringForce(){this.nodes.forEach(e=>{e.vx-=e.x*.01,e.vy-=e.y*.01,e.vz-=e.z*.01})}getNodes(){return this.nodes}getEdges(){return this.edges}}function Re({edge:i,nodes:t}){const e=t.get(i.source),s=t.get(i.target);if(!e||!s)return null;const a=e.type==="directory"?3:Math.max(2,Math.min(15,Math.log10(e.size+1)*4)),r=s.type==="directory"?3:Math.max(2,Math.min(15,Math.log10(s.size+1)*4)),n=new z.Vector3(e.x??0,e.y??0,e.z??0),c=new z.Vector3(s.x??0,s.y??0,s.z??0),d=new z.Vector3().subVectors(c,n);if(d.length()<.1&&e.x!==void 0&&s.x!==void 0)return null;d.normalize();const l=n.clone().add(d.clone().multiplyScalar(a)),h=c.clone().sub(d.clone().multiplyScalar(r)),u=new z.LineCurve3(l,h),y=new z.TubeGeometry(u,1,.3,8,!1),p=i.type==="parent"?"#ffffff":"#22d3ee";return y?o.jsx("mesh",{geometry:y,children:o.jsx("meshBasicMaterial",{color:p,opacity:.8,transparent:!0})}):null}function Ge({node:i,onClick:t}){const e=f.useRef(null),[s,a]=f.useState(1),[r,n]=f.useState(0),c=f.useRef(null),d=f.useRef(null),l=i.id==="/"||i.path==="/"?5:i.type==="directory"?3:Math.max(2,Math.min(15,Math.log10(i.size+1)*4));f.useEffect(()=>{if(i.fileStatus==="added")n(0);else if(i.fileStatus==="deleted"){const w=Math.max(2,Math.min(15,Math.log10((i.previousSize||100)+1)*4));n(w),d.current=Date.now()}else n(l)},[i.fileStatus,i.previousSize,l]);const h=i.id==="/"||i.path==="/",u=h?"#ffffff":i.type==="directory"?"#60a5fa":"#10b981";let y=null;i.fileStatus==="moved"?y="#eab308":i.sizeChange==="increase"?y="#ef4444":i.sizeChange==="decrease"&&(y="#22c55e"),f.useEffect(()=>{(i.sizeChange&&i.sizeChange!=="unchanged"||i.fileStatus==="moved")&&(c.current=Date.now(),a(1))},[i.sizeChange,i.fileStatus]),W.useFrame(()=>{if(c.current){const x=Date.now()-c.current;x<3e3?a(1-x/3e3):(a(0),c.current=null)}if(d.current){const x=Date.now()-d.current,C=2e3;if(x<C){const M=Math.max(2,Math.min(15,Math.log10((i.previousSize||100)+1)*4));n(M*(1-x/C))}else n(0),d.current=null}else i.fileStatus==="added"&&r<l&&n(C=>{const M=C+l/1e3*16;return Math.min(M,l)})});const p=()=>{t&&t(i)},g=y&&s>0?new z.Color(u).lerp(new z.Color(y),s):new z.Color(u),S=r;if(S<.01)return null;const v=i.type==="directory";return o.jsxs("group",{position:[i.x||0,i.y||0,i.z||0],children:[v?o.jsxs("mesh",{ref:e,onClick:p,children:[o.jsx("octahedronGeometry",{args:[S,0]}),o.jsx("meshStandardMaterial",{color:g,emissive:g,emissiveIntensity:h?1:s>0?.5:.3,roughness:.4,metalness:.6,transparent:i.fileStatus==="deleted",opacity:i.fileStatus==="deleted"?.5:1})]}):o.jsx(G.Sphere,{ref:e,args:[S,16,16],onClick:p,children:o.jsx("meshStandardMaterial",{color:g,emissive:g,emissiveIntensity:s>0?.5:.2,roughness:.5,metalness:.5,transparent:i.fileStatus==="deleted",opacity:i.fileStatus==="deleted"?.5:1})}),S>.3&&o.jsx(G.Text,{position:[0,S+1,0],fontSize:.8,color:"white",anchorX:"center",anchorY:"middle",children:i.name})]})}const Ue=f.forwardRef(function({nodes:t,edges:e,onNodeClick:s},a){const[r,n]=f.useState(t),[c,d]=f.useState(!1),m=f.useRef(null),l=f.useRef(),h=f.useRef(null),u=f.useRef(new Map),y=f.useRef(null);f.useImperativeHandle(a,()=>({resetCamera:()=>{y.current&&y.current.reset()}})),f.useEffect(()=>{const g=t.map(b=>({...b})),S=[],v=[],w=u.current;let x=!1;g.forEach(b=>{const E=w.get(b.id);E?(b.x=E.x,b.y=E.y,b.z=E.z,b.vx=E.vx,b.vy=E.vy,b.vz=E.vz):x=!0,b.fileStatus==="deleted"?S.push(b):v.push(b)}),m.current=new Pe(v,e,{strength:1,distance:15,iterations:500});let C=0;const M=x||w.size===0?500:200,k=()=>{if(!m.current||C>=M){if(l.current&&cancelAnimationFrame(l.current),m.current){const I=[...m.current.getNodes(),...S];u.current=new Map(I.map(D=>[D.id,{...D}]))}return}m.current.tick();const E=[...m.current.getNodes(),...S];n(E),C++,C<150?l.current=requestAnimationFrame(k):C<300?C%2===0&&(l.current=requestAnimationFrame(k)):C%3===0&&(l.current=requestAnimationFrame(k))};return k(),()=>{if(l.current&&cancelAnimationFrame(l.current),m.current){const E=[...m.current.getNodes(),...S];u.current=new Map(E.map($=>[$.id,{...$}]))}}},[t,e]),f.useEffect(()=>{const g=h.current;if(!g)return;const S=w=>{w.preventDefault(),console.warn("WebGL context lost, attempting recovery..."),d(!0)},v=()=>{console.log("WebGL context restored"),d(!1)};return g.addEventListener("webglcontextlost",S),g.addEventListener("webglcontextrestored",v),()=>{g.removeEventListener("webglcontextlost",S),g.removeEventListener("webglcontextrestored",v)}},[]);const p=new Map(r.map(g=>[g.id,g]));return c?o.jsx("div",{style:{width:"100%",height:"100%",background:"#0f172a",display:"flex",alignItems:"center",justifyContent:"center",color:"#94a3b8"},children:o.jsxs("div",{style:{textAlign:"center"},children:[o.jsx("div",{style:{fontSize:"1.2rem",marginBottom:"0.5rem"},children:"āš ļø WebGL Context Lost"}),o.jsx("div",{style:{fontSize:"0.9rem"},children:"The 3D visualization is recovering..."}),o.jsx("button",{onClick:()=>window.location.reload(),style:{marginTop:"1rem",padding:"0.5rem 1rem",background:"#3b82f6",color:"white",border:"none",borderRadius:"0.375rem",cursor:"pointer"},children:"Reload Page"})]})}):o.jsxs(W.Canvas,{ref:g=>{g&&(h.current=g)},camera:{position:[0,0,200],fov:75},style:{background:"#0f172a"},gl:{powerPreference:"high-performance",antialias:!0,alpha:!1,preserveDrawingBuffer:!1},children:[o.jsx("ambientLight",{intensity:.5}),o.jsx("pointLight",{position:[100,100,100],intensity:1}),o.jsx("pointLight",{position:[-100,-100,-100],intensity:.5}),e.map((g,S)=>{var C,M,k,b;const v=p.get(g.source),w=p.get(g.target),x=`edge-${S}-${((C=v==null?void 0:v.x)==null?void 0:C.toFixed(1))??0}-${((M=v==null?void 0:v.y)==null?void 0:M.toFixed(1))??0}-${((k=w==null?void 0:w.x)==null?void 0:k.toFixed(1))??0}-${((b=w==null?void 0:w.y)==null?void 0:b.toFixed(1))??0}`;return o.jsx(Re,{edge:g,nodes:p},x)}),r.map(g=>o.jsx(Ge,{node:g,onClick:s},g.id)),o.jsx(G.OrbitControls,{ref:y,enableDamping:!0,dampingFactor:.05,rotateSpeed:.5,zoomSpeed:.5})]})});function je({github:i,cache:t,recommendation:e,backgroundLoading:s=!1,loadProgress:a=null,onClearCache:r,isVisible:n,onVisibilityChange:c,toggleButton:d}){const m={ready:"bg-green-900 border-green-600 text-green-200",partial:"bg-yellow-900 border-yellow-600 text-yellow-200",fetching:"bg-blue-900 border-blue-600 text-blue-200"},l={ready:"Ready to visualize",partial:"Partially cached - visualizing available data",fetching:"Collecting data in background..."},h={ready:"āœ“",partial:"⚔",fetching:"ā³"},u=e==="fetching"&&!s?"ready":e;return f.useEffect(()=>{if(u==="ready"&&!s&&n){const y=setTimeout(()=>{c(!1)},3e3);return()=>clearTimeout(y)}(u!=="ready"||s)&&!n&&c(!0)},[u,s,n,c]),o.jsxs("div",{className:`relative z-10 transition-transform duration-500 ease-in-out ${n?"translate-y-0":"translate-y-full"}`,style:{willChange:"transform"},children:[d,o.jsx("div",{className:`py-2 px-4 border-b ${m[u]}`,children:o.jsxs("div",{className:"flex items-center justify-between gap-4 text-sm",children:[o.jsxs("div",{className:"flex items-center gap-4 flex-wrap",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("span",{className:"text-lg",children:h[u]}),o.jsx("span",{className:"font-semibold",children:l[u]}),s&&a&&o.jsxs("span",{className:"text-xs opacity-75",children:["(",a.loaded,"/",a.total!==-1?a.total:"?"," PRs -"," ",a.percentage,"%)"]})]}),o.jsxs("div",{children:[o.jsx("strong",{children:"GitHub:"})," ~",i.estimatedTotalPRs," PRs",i.firstMergedPR&&o.jsxs("span",{className:"ml-1 text-xs opacity-75",children:["(from #",i.firstMergedPR.number,")"]})]}),o.jsxs("div",{children:[o.jsx("strong",{children:"Cached:"})," ",t.cachedCommits," commits",t.exists&&i.estimatedTotalPRs>0&&o.jsxs("span",{className:"ml-1 text-xs opacity-75",children:["(",Math.round(t.cachedCommits/i.estimatedTotalPRs*100),"%)"]}),t.ageSeconds&&o.jsxs("span",{className:"ml-1 text-xs opacity-75",children:["• ",Math.round(t.ageSeconds/60),"m ago"]})]}),t.firstCommit&&t.lastCommit&&o.jsxs("div",{className:"text-xs opacity-75",children:[new Date(t.firstCommit.date).toLocaleDateString()," -"," ",new Date(t.lastCommit.date).toLocaleDateString()]})]}),r&&t.exists&&o.jsx("button",{onClick:r,className:"px-2 py-1 text-xs bg-red-600 hover:bg-red-700 rounded transition-colors flex-shrink-0",title:"Clear local cache and reload data",children:"Clear"})]})})]})}const He=f.memo(function({commit:t,currentTime:e,isPlaying:s}){return o.jsx("div",{className:"mb-3",children:o.jsxs("div",{className:"flex items-center justify-between mb-2",children:[o.jsxs("div",{className:"flex-1",children:[o.jsx("div",{className:"font-semibold text-lg",children:t.message}),o.jsxs("div",{className:"text-sm text-gray-400",children:[t.author," • ",t.date.toLocaleDateString()]})]}),s&&o.jsxs("div",{className:"text-lg font-mono text-blue-400 tabular-nums",children:[new Date(e).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})," ",new Date(e).toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",second:"2-digit"})]})]})})}),qe=f.memo(function({isPlaying:t,onPlayPause:e,playbackSpeed:s,onSpeedChange:a,playbackDirection:r,onDirectionChange:n,currentIndex:c,totalCommits:d,onSkipToStart:m,onPrevious:l,onNext:h,onSkipToEnd:u,onResetView:y}){const p=()=>{const w=[1,60,300,1800],C=(w.indexOf(s)+1)%w.length;a(w[C])},g=()=>{n(r==="forward"?"reverse":"forward")},S="px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded transition-colors text-sm font-mono",v="p-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded transition-colors";return o.jsxs("div",{className:"flex items-center gap-3 mb-3",children:[o.jsx("button",{onClick:m,disabled:c===0,className:v,title:"Skip to first commit",children:o.jsx(fe,{size:18})}),o.jsx("button",{onClick:l,disabled:c===0,className:v,title:"Previous commit",children:o.jsx(Ee,{size:18})}),o.jsx("button",{onClick:g,className:`${S} ${r==="reverse"?"bg-blue-600":""}`,title:"Toggle reverse playback",children:o.jsx(Te,{size:18,className:"inline"})}),o.jsx("button",{onClick:e,className:"p-3 bg-blue-600 hover:bg-blue-700 rounded-full transition-colors",title:t?"Pause":"Play",children:t?o.jsx(Ce,{size:24}):o.jsx(be,{size:24})}),o.jsxs("button",{onClick:p,className:S,title:`Playback speed: ${s}x`,children:[o.jsx(ve,{size:18,className:"inline mr-1"}),s,"x"]}),o.jsx("button",{onClick:h,disabled:c===d-1,className:v,title:"Next commit",children:o.jsx(Ne,{size:18})}),o.jsx("button",{onClick:u,disabled:c===d-1,className:v,title:"Skip to last commit",children:o.jsx(ye,{size:18})}),y&&o.jsx("button",{onClick:y,className:"p-2 bg-gray-700 hover:bg-gray-600 rounded transition-colors ml-2",title:"Reset camera view",children:o.jsx(_e,{size:18})})]})});function We({commits:i,currentTime:t,onTimeChange:e,timeRange:s,isPlaying:a,onPlayPause:r,playbackSpeed:n,onSpeedChange:c,playbackDirection:d,onDirectionChange:m,onResetView:l}){const h=J(i,t),u=w=>{const x=Number.parseFloat(w.target.value),C=s.start+(s.end-s.start)*(x/100);e(C)},y=()=>{if(h>0){const w=i[h-1].date.getTime();e(w)}},p=()=>{if(h<i.length-1){const w=i[h+1].date.getTime();e(w)}},g=()=>{e(s.start)},S=()=>{e(s.end)},v=i[h];return v?o.jsxs("div",{className:"w-full bg-gray-900 text-white p-4 border-t border-gray-700 relative z-20",children:[o.jsxs("div",{className:"max-w-7xl mx-auto",children:[o.jsx(He,{commit:v,currentTime:t,isPlaying:a}),o.jsxs("div",{className:"flex items-center gap-3 mb-3",children:[o.jsx(qe,{isPlaying:a,onPlayPause:r,playbackSpeed:n,onSpeedChange:c,playbackDirection:d,onDirectionChange:m,currentIndex:h,totalCommits:i.length,onSkipToStart:g,onPrevious:y,onNext:p,onSkipToEnd:S,onResetView:l}),o.jsx("div",{className:"flex-1 flex items-center gap-4 ml-4",children:o.jsxs("div",{className:"flex-1 relative",children:[o.jsx("div",{className:"absolute inset-0 pointer-events-none flex items-center",children:i.map((w,x)=>{const C=s.end-s.start,k=(w.date.getTime()-s.start)/C*100;return o.jsx("div",{className:"absolute w-0.5 h-4 bg-gray-500",style:{left:`${k}%`,transform:"translateX(-50%)"}},x)})}),o.jsx("input",{type:"range",min:0,max:100,step:.1,value:(t-s.start)/(s.end-s.start)*100,onChange:u,className:"w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider relative z-10"})]})})]}),o.jsxs("div",{className:"flex gap-6 text-sm text-gray-300",children:[o.jsxs("div",{children:[o.jsx("span",{className:"text-gray-400",children:"Fil