@urmzd/github-insights 2.1.0 → 2.3.0
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.
- package/.githooks/.sr-hooks-hash +1 -0
- package/.githooks/commit-msg +3 -0
- package/.githooks/pre-commit +3 -0
- package/AGENTS.md +32 -19
- package/CHANGELOG.md +62 -0
- package/CONTRIBUTING.md +18 -19
- package/README.md +21 -24
- package/action.yml +1 -1
- package/assets/insights/index.svg +45 -4
- package/assets/insights/metrics-constellation.svg +55 -0
- package/assets/insights/metrics-growth.svg +55 -0
- package/assets/insights/metrics-heatmap.svg +55 -0
- package/assets/insights/metrics-impact.svg +55 -0
- package/assets/insights/metrics-rhythm.svg +55 -0
- package/assets/insights/metrics-velocity.svg +55 -0
- package/examples/classic/README.md +36 -2
- package/examples/classic/index.svg +45 -4
- package/examples/classic/metrics-constellation.svg +55 -0
- package/examples/classic/metrics-growth.svg +55 -0
- package/examples/classic/metrics-heatmap.svg +55 -0
- package/examples/classic/metrics-impact.svg +55 -0
- package/examples/classic/metrics-rhythm.svg +55 -0
- package/examples/classic/metrics-velocity.svg +55 -0
- package/examples/ecosystem/README.md +39 -28
- package/examples/ecosystem/index.svg +45 -4
- package/examples/ecosystem/metrics-constellation.svg +55 -0
- package/examples/ecosystem/metrics-growth.svg +55 -0
- package/examples/ecosystem/metrics-heatmap.svg +55 -0
- package/examples/ecosystem/metrics-impact.svg +55 -0
- package/examples/ecosystem/metrics-rhythm.svg +55 -0
- package/examples/ecosystem/metrics-velocity.svg +55 -0
- package/examples/minimal/README.md +36 -2
- package/examples/minimal/index.svg +45 -4
- package/examples/minimal/metrics-constellation.svg +55 -0
- package/examples/minimal/metrics-growth.svg +55 -0
- package/examples/minimal/metrics-heatmap.svg +55 -0
- package/examples/minimal/metrics-impact.svg +55 -0
- package/examples/minimal/metrics-rhythm.svg +55 -0
- package/examples/minimal/metrics-velocity.svg +55 -0
- package/examples/modern/README.md +62 -50
- package/examples/modern/index.svg +45 -4
- package/examples/modern/metrics-constellation.svg +55 -0
- package/examples/modern/metrics-growth.svg +55 -0
- package/examples/modern/metrics-heatmap.svg +55 -0
- package/examples/modern/metrics-impact.svg +55 -0
- package/examples/modern/metrics-rhythm.svg +55 -0
- package/examples/modern/metrics-velocity.svg +55 -0
- package/llms.txt +4 -4
- package/package.json +1 -1
- package/skills/github-insights/SKILL.md +35 -81
- package/sr.yaml +9 -0
- package/src/api.ts +2 -140
- package/src/components/contribution-heatmap.tsx +43 -0
- package/src/components/contribution-rhythm.tsx +152 -0
- package/src/components/full-svg.test.tsx +4 -1
- package/src/components/full-svg.tsx +14 -7
- package/src/components/growth-arc.tsx +119 -0
- package/src/components/impact-trail.tsx +90 -0
- package/src/components/language-velocity.tsx +181 -0
- package/src/components/project-constellation.tsx +97 -0
- package/src/components/section.test.tsx +5 -3
- package/src/components/section.tsx +5 -13
- package/src/components/style-defs.tsx +44 -3
- package/src/index.ts +28 -47
- package/src/metrics.test.ts +50 -57
- package/src/metrics.ts +277 -95
- package/src/readme.test.ts +2 -4
- package/src/templates.test.ts +19 -16
- package/src/templates.ts +30 -16
- package/src/theme.ts +11 -1
- package/src/types.ts +34 -7
- package/assets/insights/metrics-calendar.svg +0 -14
- package/assets/insights/metrics-complexity.svg +0 -14
- package/assets/insights/metrics-contributions.svg +0 -14
- package/assets/insights/metrics-expertise.svg +0 -14
- package/assets/insights/metrics-languages.svg +0 -14
- package/assets/insights/metrics-pulse.svg +0 -14
- package/examples/classic/metrics-calendar.svg +0 -14
- package/examples/classic/metrics-complexity.svg +0 -14
- package/examples/classic/metrics-contributions.svg +0 -14
- package/examples/classic/metrics-expertise.svg +0 -14
- package/examples/classic/metrics-languages.svg +0 -14
- package/examples/classic/metrics-pulse.svg +0 -14
- package/examples/ecosystem/metrics-calendar.svg +0 -14
- package/examples/ecosystem/metrics-complexity.svg +0 -14
- package/examples/ecosystem/metrics-contributions.svg +0 -14
- package/examples/ecosystem/metrics-expertise.svg +0 -14
- package/examples/ecosystem/metrics-languages.svg +0 -14
- package/examples/ecosystem/metrics-pulse.svg +0 -14
- package/examples/minimal/metrics-calendar.svg +0 -14
- package/examples/minimal/metrics-complexity.svg +0 -14
- package/examples/minimal/metrics-contributions.svg +0 -14
- package/examples/minimal/metrics-expertise.svg +0 -14
- package/examples/minimal/metrics-languages.svg +0 -14
- package/examples/minimal/metrics-pulse.svg +0 -14
- package/examples/modern/metrics-calendar.svg +0 -14
- package/examples/modern/metrics-complexity.svg +0 -14
- package/examples/modern/metrics-contributions.svg +0 -14
- package/examples/modern/metrics-expertise.svg +0 -14
- package/examples/modern/metrics-languages.svg +0 -14
- package/examples/modern/metrics-pulse.svg +0 -14
- package/src/components/bar-chart.test.tsx +0 -38
- package/src/components/bar-chart.tsx +0 -54
- package/src/components/contribution-calendar.test.tsx +0 -44
- package/src/components/contribution-calendar.tsx +0 -94
- package/src/components/contribution-cards.test.tsx +0 -36
- package/src/components/contribution-cards.tsx +0 -58
- package/src/components/donut-chart.test.tsx +0 -36
- package/src/components/donut-chart.tsx +0 -102
- package/src/components/project-cards.test.tsx +0 -46
- package/src/components/project-cards.tsx +0 -66
- package/src/components/stat-cards.test.tsx +0 -32
- package/src/components/stat-cards.tsx +0 -57
- package/src/components/tech-highlights.test.tsx +0 -63
- package/src/components/tech-highlights.tsx +0 -109
- package/teasr.toml +0 -14
|
@@ -8,9 +8,9 @@ description: >-
|
|
|
8
8
|
argument-hint: [setup | customize | generate | debug | extend]
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
# GitHub
|
|
11
|
+
# GitHub Insights — Agent Skill
|
|
12
12
|
|
|
13
|
-
Generate beautiful
|
|
13
|
+
Generate beautiful SVG metrics for GitHub profile READMEs. Sections include language velocity streamgraph, contribution rhythm radar chart, project constellation map, and open source impact trail. SVGs support light/dark theme switching and CSS animations.
|
|
14
14
|
|
|
15
15
|
## Quick Reference
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@ Generate beautiful dark-themed SVG metrics for GitHub profile READMEs. Sections
|
|
|
18
18
|
|------|---------|
|
|
19
19
|
| Generate locally | `npm run generate` (requires `gh auth login`) |
|
|
20
20
|
| Full CI check | `npm run ci` (fmt + lint + typecheck + test + build) |
|
|
21
|
-
| Build bundle | `npm run build` (ncc
|
|
21
|
+
| Build bundle | `npm run build` (ncc -> `dist/`) |
|
|
22
22
|
| Run tests | `npm test` (vitest) |
|
|
23
23
|
| Type-check | `npm run typecheck` |
|
|
24
24
|
| Lint | `npm run lint` (biome) |
|
|
@@ -66,8 +66,7 @@ npm run generate
|
|
|
66
66
|
Local mode differences:
|
|
67
67
|
- `commit-push` defaults to `false` (no git operations)
|
|
68
68
|
- README writes to `_README.md` (not `README.md`)
|
|
69
|
-
- All
|
|
70
|
-
- Both preamble variants (full + short) are generated for preview
|
|
69
|
+
- All four template previews are generated in `examples/{classic,modern,minimal,ecosystem}/`
|
|
71
70
|
|
|
72
71
|
## Configuration
|
|
73
72
|
|
|
@@ -76,18 +75,16 @@ Local mode differences:
|
|
|
76
75
|
```yaml
|
|
77
76
|
name: Display Name # overrides GitHub profile name
|
|
78
77
|
pronunciation: pronunciation # shown as subscript in heading
|
|
79
|
-
title: Software Engineer # blockquote under heading
|
|
80
|
-
desired_title: Senior SWE # AI context only
|
|
78
|
+
title: Software Engineer # blockquote under heading
|
|
79
|
+
desired_title: Senior SWE # AI context only
|
|
81
80
|
bio: Short bio text. # footer text in classic template
|
|
82
81
|
preamble: PREAMBLE.md # path to custom preamble (bypasses AI generation)
|
|
83
|
-
template: classic # "classic" | "modern" | "minimal"
|
|
82
|
+
template: classic # "classic" | "modern" | "minimal" | "ecosystem"
|
|
84
83
|
sections:
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
- contributions
|
|
90
|
-
- calendar
|
|
84
|
+
- velocity
|
|
85
|
+
- rhythm
|
|
86
|
+
- constellation
|
|
87
|
+
- impact
|
|
91
88
|
```
|
|
92
89
|
|
|
93
90
|
### Action inputs
|
|
@@ -109,41 +106,38 @@ sections:
|
|
|
109
106
|
|
|
110
107
|
| Key | SVG filename | What it renders |
|
|
111
108
|
|-----|-------------|-----------------|
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `calendar` | `metrics-calendar.svg` | GitHub contribution heatmap (1 year) |
|
|
117
|
-
| `contributions` | `metrics-contributions.svg` | External repos contributed to |
|
|
109
|
+
| `velocity` | `metrics-velocity.svg` | Streamgraph of language usage over 12 months |
|
|
110
|
+
| `rhythm` | `metrics-rhythm.svg` | 7-spoke radar chart + contribution stats |
|
|
111
|
+
| `constellation` | `metrics-constellation.svg` | Project map by language and complexity |
|
|
112
|
+
| `impact` | `metrics-impact.svg` | External contributions with impact bars |
|
|
118
113
|
|
|
119
114
|
### Templates
|
|
120
115
|
|
|
121
|
-
- **`classic`** (default): Formal layout — `# Name`, blockquote title,
|
|
122
|
-
- **`modern`**: Friendly — `# Hi, I'm {firstName}
|
|
123
|
-
- **`minimal`**: Clean — `# {firstName}`,
|
|
116
|
+
- **`classic`** (default): Formal layout — `# Name`, blockquote title, preamble, social badges, SVG metrics, bio footer.
|
|
117
|
+
- **`modern`**: Friendly — `# Hi, I'm {firstName}`, projects by activity, Project Map, GitHub Stats, Impact.
|
|
118
|
+
- **`minimal`**: Clean — `# {firstName}`, preamble, social badges, SVG metrics.
|
|
119
|
+
- **`ecosystem`**: Categorized — projects by purpose (Developer Tools/SDKs/Applications/Research), Project Map, GitHub Stats, Impact.
|
|
124
120
|
|
|
125
121
|
## Architecture
|
|
126
122
|
|
|
127
123
|
### Execution flow
|
|
128
124
|
|
|
129
125
|
```
|
|
130
|
-
Inputs
|
|
126
|
+
Inputs -> Fetch (parallel) -> AI calls -> Transform -> Compute velocity/rhythm/constellation -> Render sections -> Write SVGs -> Generate README -> Commit
|
|
131
127
|
```
|
|
132
128
|
|
|
133
129
|
### Key source files
|
|
134
130
|
|
|
135
131
|
| File | Role |
|
|
136
132
|
|------|------|
|
|
137
|
-
| `src/index.ts` | Orchestration: fetch
|
|
133
|
+
| `src/index.ts` | Orchestration: fetch -> transform -> render -> write -> commit |
|
|
138
134
|
| `src/api.ts` | GitHub GraphQL queries + GitHub Models AI calls |
|
|
139
|
-
| `src/metrics.ts` | Data aggregation,
|
|
140
|
-
| `src/config.ts` | TOML config loading |
|
|
141
|
-
| `src/types.ts` | All TypeScript interfaces
|
|
142
|
-
| `src/templates.ts` |
|
|
143
|
-
| `src/theme.ts` | `THEME` colors, `LAYOUT` dimensions, `BAR_COLORS` palette |
|
|
144
|
-
| `src/
|
|
145
|
-
| `src/components/` | SVG rendering components (custom JSX → SVG strings) |
|
|
146
|
-
| `src/jsx-factory.ts` | Custom `h()` / `Fragment()` JSX runtime (no React) |
|
|
135
|
+
| `src/metrics.ts` | Data aggregation, velocity/rhythm/constellation computation, section building |
|
|
136
|
+
| `src/config.ts` | YAML/TOML config loading |
|
|
137
|
+
| `src/types.ts` | All TypeScript interfaces |
|
|
138
|
+
| `src/templates.ts` | Four README template functions + social badge builder |
|
|
139
|
+
| `src/theme.ts` | `THEME`/`THEME_LIGHT` colors, `LAYOUT` dimensions, `BAR_COLORS` palette |
|
|
140
|
+
| `src/components/` | SVG rendering components (custom JSX -> SVG strings) |
|
|
147
141
|
|
|
148
142
|
### Component rendering pattern
|
|
149
143
|
|
|
@@ -155,25 +149,6 @@ function renderXxx(data: ..., y: number): { svg: string; height: number }
|
|
|
155
149
|
|
|
156
150
|
Components return SVG string fragments and their rendered height. The `y` parameter is the vertical cursor; heights are accumulated to stack sections vertically.
|
|
157
151
|
|
|
158
|
-
### Theme constants (hardcoded in `src/theme.ts`)
|
|
159
|
-
|
|
160
|
-
```
|
|
161
|
-
THEME.bg = "#0d1117" (dark background)
|
|
162
|
-
THEME.cardBg = "#161b22" (card backgrounds)
|
|
163
|
-
THEME.border = "#30363d" (card borders)
|
|
164
|
-
THEME.link = "#58a6ff" (card titles - blue)
|
|
165
|
-
THEME.text = "#c9d1d9" (primary text)
|
|
166
|
-
THEME.secondary= "#8b949e" (labels)
|
|
167
|
-
THEME.muted = "#6e7681" (values)
|
|
168
|
-
|
|
169
|
-
LAYOUT.width = 808 (fixed SVG canvas width)
|
|
170
|
-
LAYOUT.padX = 24
|
|
171
|
-
LAYOUT.padY = 24
|
|
172
|
-
LAYOUT.sectionGap = 30
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
These are not configurable via inputs or TOML — changing them requires editing source.
|
|
176
|
-
|
|
177
152
|
## Extending
|
|
178
153
|
|
|
179
154
|
### Add a new section
|
|
@@ -183,16 +158,7 @@ These are not configurable via inputs or TOML — changing them requires editing
|
|
|
183
158
|
3. Add the section key to `SECTION_KEYS` map in `src/metrics.ts`
|
|
184
159
|
4. Add a `SectionDef` entry in the `buildSections()` function in `src/metrics.ts`
|
|
185
160
|
5. Fetch any new data needed in `src/index.ts`
|
|
186
|
-
6. Add a `*.test.
|
|
187
|
-
7. Update `action.yml` if new inputs are needed
|
|
188
|
-
|
|
189
|
-
### Add a new dependency parser
|
|
190
|
-
|
|
191
|
-
1. Implement `PackageParser` interface in `src/parsers.ts`:
|
|
192
|
-
```ts
|
|
193
|
-
{ filenames: string[]; parseDependencies(text: string): string[] }
|
|
194
|
-
```
|
|
195
|
-
2. Add it to the `PARSERS` array — it auto-registers in `PARSER_MAP`
|
|
161
|
+
6. Add a `*.test.tsx` file alongside the component
|
|
196
162
|
|
|
197
163
|
### Add a new README template
|
|
198
164
|
|
|
@@ -202,36 +168,24 @@ These are not configurable via inputs or TOML — changing them requires editing
|
|
|
202
168
|
|
|
203
169
|
## Troubleshooting
|
|
204
170
|
|
|
205
|
-
### "
|
|
206
|
-
The
|
|
207
|
-
- Token has `models: read` permission
|
|
208
|
-
- The workflow has `permissions: models: read`
|
|
209
|
-
- GitHub Models endpoint is reachable
|
|
171
|
+
### "Velocity section is flat"
|
|
172
|
+
The streamgraph distributes per-repo commits using the contribution calendar's monthly activity weights. If there's no calendar data, velocity will be empty.
|
|
210
173
|
|
|
211
|
-
### "
|
|
212
|
-
Only rendered when
|
|
174
|
+
### "Constellation section missing"
|
|
175
|
+
Only rendered when there are projects with language data. Ensure repos have detectable languages.
|
|
213
176
|
|
|
214
|
-
### "
|
|
215
|
-
Only rendered when the user has contributed to external (non-owned) repositories
|
|
177
|
+
### "Impact section missing"
|
|
178
|
+
Only rendered when the user has contributed to external (non-owned) repositories.
|
|
216
179
|
|
|
217
180
|
### AI preamble is empty or generic
|
|
218
181
|
- The AI call uses `gpt-4.1` via GitHub Models — it needs diverse profile data to generate good output
|
|
219
|
-
- Provide `title`
|
|
182
|
+
- Provide `title` in config for better results
|
|
220
183
|
- Create a custom `PREAMBLE.md` to bypass AI entirely
|
|
221
184
|
|
|
222
185
|
### Local generation fails
|
|
223
186
|
- Ensure `gh auth login` is done and `gh auth token` returns a valid token
|
|
224
187
|
- Ensure Node.js 22+ (`node --version`)
|
|
225
|
-
- The `GITHUB_TOKEN` and `GITHUB_REPOSITORY_OWNER` env vars are set automatically by `npm run generate` via `gh`
|
|
226
188
|
|
|
227
189
|
### SVG looks wrong after changes
|
|
228
190
|
- Run `npm run build` to rebuild the `dist/` bundle — the action runs `dist/index.js`, not source directly
|
|
229
191
|
- SVG width is fixed at 808px; all layout math depends on this
|
|
230
|
-
|
|
231
|
-
## Code Style Rules
|
|
232
|
-
|
|
233
|
-
- TypeScript strict mode, ES modules
|
|
234
|
-
- Biome for formatting and linting (not ESLint/Prettier)
|
|
235
|
-
- Tests colocated as `*.test.ts` / `*.test.tsx` alongside source
|
|
236
|
-
- Custom JSX factory (`h`, `Fragment`) — NOT React
|
|
237
|
-
- All AI calls and contribution fetches are non-fatal (catch → return empty)
|
package/sr.yaml
CHANGED
package/src/api.ts
CHANGED
|
@@ -7,7 +7,6 @@ import type {
|
|
|
7
7
|
RepoClassificationInput,
|
|
8
8
|
RepoClassificationOutput,
|
|
9
9
|
RepoNode,
|
|
10
|
-
TechHighlight,
|
|
11
10
|
UserConfig,
|
|
12
11
|
UserProfile,
|
|
13
12
|
} from "./types.js";
|
|
@@ -320,7 +319,6 @@ export interface PreambleContext {
|
|
|
320
319
|
profile: UserProfile;
|
|
321
320
|
userConfig: UserConfig;
|
|
322
321
|
languages: { name: string; percent: string }[];
|
|
323
|
-
techHighlights: TechHighlight[];
|
|
324
322
|
activeProjects: ProjectItem[];
|
|
325
323
|
complexProjects: ProjectItem[];
|
|
326
324
|
}
|
|
@@ -330,21 +328,12 @@ export const fetchAIPreamble = async (
|
|
|
330
328
|
context: PreambleContext,
|
|
331
329
|
): Promise<string | undefined> => {
|
|
332
330
|
try {
|
|
333
|
-
const {
|
|
334
|
-
|
|
335
|
-
userConfig,
|
|
336
|
-
languages,
|
|
337
|
-
techHighlights,
|
|
338
|
-
activeProjects,
|
|
339
|
-
complexProjects,
|
|
340
|
-
} = context;
|
|
331
|
+
const { profile, userConfig, languages, activeProjects, complexProjects } =
|
|
332
|
+
context;
|
|
341
333
|
|
|
342
334
|
const langLines = languages
|
|
343
335
|
.map((l) => `- ${l.name}: ${l.percent}%`)
|
|
344
336
|
.join("\n");
|
|
345
|
-
const techLines = techHighlights
|
|
346
|
-
.map((h) => `- ${h.category}: ${h.items.join(", ")} (score: ${h.score})`)
|
|
347
|
-
.join("\n");
|
|
348
337
|
|
|
349
338
|
const formatProject = (p: ProjectItem): string => {
|
|
350
339
|
const langs = p.languages?.length ? ` [${p.languages.join(", ")}]` : "";
|
|
@@ -373,9 +362,6 @@ ${profileLines}
|
|
|
373
362
|
Languages (by code volume):
|
|
374
363
|
${langLines}
|
|
375
364
|
|
|
376
|
-
Expertise areas:
|
|
377
|
-
${techLines}
|
|
378
|
-
|
|
379
365
|
Most technically complex projects (by language diversity, codebase size, and depth):
|
|
380
366
|
${complexProjectLines || "None"}
|
|
381
367
|
|
|
@@ -474,130 +460,6 @@ Generate 1-2 sentences that:
|
|
|
474
460
|
}
|
|
475
461
|
};
|
|
476
462
|
|
|
477
|
-
export const fetchExpertiseAnalysis = async (
|
|
478
|
-
token: string,
|
|
479
|
-
languages: { name: string; percent: string }[],
|
|
480
|
-
allDeps: string[],
|
|
481
|
-
allTopics: string[],
|
|
482
|
-
repos: RepoNode[],
|
|
483
|
-
readmeMap: ReadmeMap,
|
|
484
|
-
userConfig: UserConfig = {},
|
|
485
|
-
): Promise<TechHighlight[]> => {
|
|
486
|
-
try {
|
|
487
|
-
const langLines = languages
|
|
488
|
-
.map((l) => `- ${l.name}: ${l.percent}%`)
|
|
489
|
-
.join("\n");
|
|
490
|
-
|
|
491
|
-
const repoSummaries = repos
|
|
492
|
-
.slice(0, 20)
|
|
493
|
-
.map((r) => {
|
|
494
|
-
const readme = readmeMap.get(r.name) || "";
|
|
495
|
-
const snippet = readme.slice(0, 500).replace(/\n/g, " ");
|
|
496
|
-
const desc = r.description || "";
|
|
497
|
-
return `- ${r.name}: ${desc} | ${snippet}`;
|
|
498
|
-
})
|
|
499
|
-
.join("\n");
|
|
500
|
-
|
|
501
|
-
const desiredTitle = userConfig.desired_title || userConfig.title;
|
|
502
|
-
let titleContext = "";
|
|
503
|
-
if (userConfig.title) {
|
|
504
|
-
titleContext = `\nDeveloper context:\n- Current title: ${userConfig.title}`;
|
|
505
|
-
if (desiredTitle && desiredTitle !== userConfig.title) {
|
|
506
|
-
titleContext += `\n- Desired title: ${desiredTitle}`;
|
|
507
|
-
}
|
|
508
|
-
titleContext += `\n- Tailor the expertise categories to highlight skills most relevant to ${desiredTitle}. Prioritize domains and technologies that align with this role.\n`;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const prompt = `You are analyzing a developer's GitHub profile to create a curated expertise showcase.
|
|
512
|
-
${titleContext}
|
|
513
|
-
Languages (by code volume):
|
|
514
|
-
${langLines}
|
|
515
|
-
|
|
516
|
-
Dependencies found across repositories:
|
|
517
|
-
${allDeps.join(", ")}
|
|
518
|
-
|
|
519
|
-
Repository topics:
|
|
520
|
-
${allTopics.join(", ")}
|
|
521
|
-
|
|
522
|
-
Repository descriptions and README excerpts:
|
|
523
|
-
${repoSummaries}
|
|
524
|
-
|
|
525
|
-
From this data, produce a curated expertise profile:
|
|
526
|
-
- Group the most notable technologies into 3-6 expertise categories
|
|
527
|
-
- Use domain-oriented category names (e.g., "Machine Learning", "Web Development", "DevOps", "Backend & APIs", "Data Science", "Systems Programming")
|
|
528
|
-
- Include 3-6 of the most relevant technologies/tools per category
|
|
529
|
-
- Normalize names to their common display form (e.g., "pg" → "PostgreSQL", "torch" → "PyTorch", "boto3" → "AWS SDK")
|
|
530
|
-
- Skip trivial utility libraries (lodash, uuid, etc.) that don't showcase meaningful expertise
|
|
531
|
-
- Only include categories where there's meaningful evidence of usage
|
|
532
|
-
- Assign each category a proficiency score from 0 to 100 based on evidence strength:
|
|
533
|
-
language code volume, dependency count, topic mentions, and README depth.
|
|
534
|
-
Use the full range (e.g. 80-95 for primary stack, 50-70 for secondary, 30-50 for minor).`;
|
|
535
|
-
|
|
536
|
-
const res = await fetchWithRetry(
|
|
537
|
-
"https://models.github.ai/inference/chat/completions",
|
|
538
|
-
{
|
|
539
|
-
method: "POST",
|
|
540
|
-
headers: {
|
|
541
|
-
Authorization: `bearer ${token}`,
|
|
542
|
-
"Content-Type": "application/json",
|
|
543
|
-
},
|
|
544
|
-
body: JSON.stringify({
|
|
545
|
-
model: "gpt-4.1",
|
|
546
|
-
messages: [{ role: "user", content: prompt }],
|
|
547
|
-
temperature: 0.1,
|
|
548
|
-
response_format: {
|
|
549
|
-
type: "json_schema",
|
|
550
|
-
json_schema: {
|
|
551
|
-
name: "tech_highlights",
|
|
552
|
-
strict: true,
|
|
553
|
-
schema: {
|
|
554
|
-
type: "object",
|
|
555
|
-
properties: {
|
|
556
|
-
highlights: {
|
|
557
|
-
type: "array",
|
|
558
|
-
items: {
|
|
559
|
-
type: "object",
|
|
560
|
-
properties: {
|
|
561
|
-
category: { type: "string" },
|
|
562
|
-
items: { type: "array", items: { type: "string" } },
|
|
563
|
-
score: { type: "number" },
|
|
564
|
-
},
|
|
565
|
-
required: ["category", "items", "score"],
|
|
566
|
-
additionalProperties: false,
|
|
567
|
-
},
|
|
568
|
-
},
|
|
569
|
-
},
|
|
570
|
-
required: ["highlights"],
|
|
571
|
-
additionalProperties: false,
|
|
572
|
-
},
|
|
573
|
-
},
|
|
574
|
-
},
|
|
575
|
-
}),
|
|
576
|
-
},
|
|
577
|
-
"Expertise",
|
|
578
|
-
);
|
|
579
|
-
|
|
580
|
-
if (!res?.ok) {
|
|
581
|
-
if (res) console.warn(`GitHub Models API error: ${res.status}`);
|
|
582
|
-
return [];
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
const json = (await res.json()) as {
|
|
586
|
-
choices?: { message?: { content?: string } }[];
|
|
587
|
-
};
|
|
588
|
-
const content = json.choices?.[0]?.message?.content || "{}";
|
|
589
|
-
const parsed = JSON.parse(content) as { highlights?: TechHighlight[] };
|
|
590
|
-
return (parsed.highlights || [])
|
|
591
|
-
.filter((h) => h.category && Array.isArray(h.items) && h.items.length > 0)
|
|
592
|
-
.map((h) => ({ ...h, score: Math.max(0, Math.min(100, h.score || 0)) }))
|
|
593
|
-
.sort((a, b) => b.score - a.score);
|
|
594
|
-
} catch (err: unknown) {
|
|
595
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
596
|
-
console.warn(`Expertise analysis failed (non-fatal): ${msg}`);
|
|
597
|
-
return [];
|
|
598
|
-
}
|
|
599
|
-
};
|
|
600
|
-
|
|
601
463
|
export const fetchProjectClassifications = async (
|
|
602
464
|
token: string,
|
|
603
465
|
repos: RepoClassificationInput[],
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Fragment, h } from "../jsx-factory.js";
|
|
2
|
+
import { LAYOUT } from "../theme.js";
|
|
3
|
+
import type { ContributionCalendar, RenderResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export function renderContributionHeatmap(
|
|
6
|
+
calendar: ContributionCalendar,
|
|
7
|
+
y: number,
|
|
8
|
+
): RenderResult {
|
|
9
|
+
if (calendar.weeks.length === 0) return { svg: "", height: 0 };
|
|
10
|
+
|
|
11
|
+
const { padX } = LAYOUT;
|
|
12
|
+
const cellSize = 11;
|
|
13
|
+
const cellGap = 2;
|
|
14
|
+
const step = cellSize + cellGap;
|
|
15
|
+
|
|
16
|
+
const rows = 7;
|
|
17
|
+
const chartHeight = rows * step;
|
|
18
|
+
const totalHeight = chartHeight + 4;
|
|
19
|
+
|
|
20
|
+
const svg = (
|
|
21
|
+
<>
|
|
22
|
+
{/* Heatmap cells */}
|
|
23
|
+
{calendar.weeks.map((week, wi) =>
|
|
24
|
+
week.contributionDays.map((day, di) => (
|
|
25
|
+
<rect
|
|
26
|
+
x={padX + wi * step}
|
|
27
|
+
y={y + di * step}
|
|
28
|
+
width={cellSize}
|
|
29
|
+
height={cellSize}
|
|
30
|
+
rx="2"
|
|
31
|
+
fill={day.color}
|
|
32
|
+
opacity="0.85"
|
|
33
|
+
className={`fade-${Math.min((wi % 6) + 1, 6)}`}
|
|
34
|
+
/>
|
|
35
|
+
)),
|
|
36
|
+
)}
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return { svg, height: totalHeight };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
void Fragment;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Fragment, h } from "../jsx-factory.js";
|
|
2
|
+
import { escapeXml } from "../svg-utils.js";
|
|
3
|
+
import { BAR_COLORS, LAYOUT, THEME } from "../theme.js";
|
|
4
|
+
import type { ContributionRhythm, RenderResult } from "../types.js";
|
|
5
|
+
|
|
6
|
+
const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
7
|
+
|
|
8
|
+
export function renderContributionRhythm(
|
|
9
|
+
rhythm: ContributionRhythm,
|
|
10
|
+
y: number,
|
|
11
|
+
): RenderResult {
|
|
12
|
+
const { padX } = LAYOUT;
|
|
13
|
+
|
|
14
|
+
// Radar chart dimensions
|
|
15
|
+
const radarCx = padX + 120;
|
|
16
|
+
const radarCy = y + 120;
|
|
17
|
+
const radarR = 90;
|
|
18
|
+
const maxVal = Math.max(...rhythm.dayTotals, 1);
|
|
19
|
+
|
|
20
|
+
// Guide circles
|
|
21
|
+
const guides = [0.25, 0.5, 0.75, 1.0];
|
|
22
|
+
const guidesSvg = guides.map((pct) => (
|
|
23
|
+
<circle
|
|
24
|
+
cx={radarCx}
|
|
25
|
+
cy={radarCy}
|
|
26
|
+
r={radarR * pct}
|
|
27
|
+
fill="none"
|
|
28
|
+
stroke={THEME.border}
|
|
29
|
+
stroke-width="1"
|
|
30
|
+
stroke-opacity="0.4"
|
|
31
|
+
/>
|
|
32
|
+
));
|
|
33
|
+
|
|
34
|
+
// Spoke lines
|
|
35
|
+
const spokesSvg = DAY_NAMES.map((_, i) => {
|
|
36
|
+
const angle = (i * 2 * Math.PI) / 7 - Math.PI / 2;
|
|
37
|
+
const x2 = radarCx + radarR * Math.cos(angle);
|
|
38
|
+
const y2 = radarCy + radarR * Math.sin(angle);
|
|
39
|
+
return (
|
|
40
|
+
<line
|
|
41
|
+
x1={radarCx}
|
|
42
|
+
y1={radarCy}
|
|
43
|
+
x2={x2}
|
|
44
|
+
y2={y2}
|
|
45
|
+
stroke={THEME.border}
|
|
46
|
+
stroke-width="1"
|
|
47
|
+
stroke-opacity="0.3"
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Day labels
|
|
53
|
+
const labelsSvg = DAY_NAMES.map((name, i) => {
|
|
54
|
+
const angle = (i * 2 * Math.PI) / 7 - Math.PI / 2;
|
|
55
|
+
const labelR = radarR + 16;
|
|
56
|
+
const lx = radarCx + labelR * Math.cos(angle);
|
|
57
|
+
const ly = radarCy + labelR * Math.sin(angle) + 4;
|
|
58
|
+
return (
|
|
59
|
+
<text x={lx} y={ly} className="t t-value" text-anchor="middle">
|
|
60
|
+
{escapeXml(name)}
|
|
61
|
+
</text>
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Data polygon
|
|
66
|
+
const points = rhythm.dayTotals
|
|
67
|
+
.map((val, i) => {
|
|
68
|
+
const angle = (i * 2 * Math.PI) / 7 - Math.PI / 2;
|
|
69
|
+
const r = (val / maxVal) * radarR;
|
|
70
|
+
const px = radarCx + r * Math.cos(angle);
|
|
71
|
+
const py = radarCy + r * Math.sin(angle);
|
|
72
|
+
return `${px},${py}`;
|
|
73
|
+
})
|
|
74
|
+
.join(" ");
|
|
75
|
+
|
|
76
|
+
// Stats section (right side)
|
|
77
|
+
const statsX = padX + 300;
|
|
78
|
+
const statsStartY = y + 30;
|
|
79
|
+
const statColors = [
|
|
80
|
+
BAR_COLORS[0],
|
|
81
|
+
BAR_COLORS[1],
|
|
82
|
+
BAR_COLORS[2],
|
|
83
|
+
BAR_COLORS[4],
|
|
84
|
+
BAR_COLORS[5],
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const statsSvg = rhythm.stats.map((stat, i) => {
|
|
88
|
+
const sy = statsStartY + i * 42;
|
|
89
|
+
const color = statColors[i % statColors.length];
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
92
|
+
<text x={statsX} y={sy} className="t t-stat-label">
|
|
93
|
+
{escapeXml(stat.label)}
|
|
94
|
+
</text>
|
|
95
|
+
<text x={statsX} y={sy + 22} fill={color} className="t t-stat-value">
|
|
96
|
+
{escapeXml(stat.value)}
|
|
97
|
+
</text>
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const height = 250;
|
|
103
|
+
|
|
104
|
+
const svg = (
|
|
105
|
+
<>
|
|
106
|
+
{/* Guide circles */}
|
|
107
|
+
{guidesSvg.join("")}
|
|
108
|
+
|
|
109
|
+
{/* Spokes */}
|
|
110
|
+
{spokesSvg.join("")}
|
|
111
|
+
|
|
112
|
+
{/* Data polygon */}
|
|
113
|
+
<polygon
|
|
114
|
+
points={points}
|
|
115
|
+
fill={BAR_COLORS[0]}
|
|
116
|
+
fill-opacity="0.2"
|
|
117
|
+
stroke={BAR_COLORS[0]}
|
|
118
|
+
stroke-width="2"
|
|
119
|
+
stroke-opacity="0.8"
|
|
120
|
+
className="fade-2"
|
|
121
|
+
style={`transform-origin: ${radarCx}px ${radarCy}px`}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
{/* Data points */}
|
|
125
|
+
{rhythm.dayTotals.map((val, i) => {
|
|
126
|
+
const angle = (i * 2 * Math.PI) / 7 - Math.PI / 2;
|
|
127
|
+
const r = (val / maxVal) * radarR;
|
|
128
|
+
const px = radarCx + r * Math.cos(angle);
|
|
129
|
+
const py = radarCy + r * Math.sin(angle);
|
|
130
|
+
return (
|
|
131
|
+
<circle
|
|
132
|
+
cx={px}
|
|
133
|
+
cy={py}
|
|
134
|
+
r="3"
|
|
135
|
+
fill={BAR_COLORS[0]}
|
|
136
|
+
className={`fade-${Math.min(i + 1, 6)}`}
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
})}
|
|
140
|
+
|
|
141
|
+
{/* Day labels */}
|
|
142
|
+
{labelsSvg.join("")}
|
|
143
|
+
|
|
144
|
+
{/* Stats */}
|
|
145
|
+
{statsSvg.join("")}
|
|
146
|
+
</>
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return { svg, height };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
void Fragment;
|
|
@@ -35,7 +35,10 @@ describe("generateFullSvg", () => {
|
|
|
35
35
|
filename: "b.svg",
|
|
36
36
|
title: "Section B",
|
|
37
37
|
subtitle: "Subtitle B",
|
|
38
|
-
|
|
38
|
+
renderBody: (y) => ({
|
|
39
|
+
svg: `<text y="${y}">Go</text>`,
|
|
40
|
+
height: 30,
|
|
41
|
+
}),
|
|
39
42
|
},
|
|
40
43
|
];
|
|
41
44
|
const result = generateFullSvg(sections);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Fragment, h } from "../jsx-factory.js";
|
|
2
2
|
import { LAYOUT, THEME } from "../theme.js";
|
|
3
3
|
import type { SectionDef } from "../types.js";
|
|
4
|
-
import { renderBarChart } from "./bar-chart.js";
|
|
5
4
|
import { renderSectionHeader } from "./section.js";
|
|
6
5
|
import { StyleDefs } from "./style-defs.js";
|
|
7
6
|
|
|
@@ -15,7 +14,13 @@ export function wrapSectionSvg(bodySvg: string, height: number): string {
|
|
|
15
14
|
viewBox={`0 0 ${width} ${height}`}
|
|
16
15
|
>
|
|
17
16
|
<StyleDefs />
|
|
18
|
-
<rect
|
|
17
|
+
<rect
|
|
18
|
+
width={width}
|
|
19
|
+
height={height}
|
|
20
|
+
rx="12"
|
|
21
|
+
className="bg-fill"
|
|
22
|
+
fill={THEME.bg}
|
|
23
|
+
/>
|
|
19
24
|
{bodySvg}
|
|
20
25
|
</svg>
|
|
21
26
|
);
|
|
@@ -35,10 +40,6 @@ export function generateFullSvg(sections: SectionDef[]): string {
|
|
|
35
40
|
const body = section.renderBody(y);
|
|
36
41
|
bodySvg += body.svg;
|
|
37
42
|
y += body.height + sectionGap;
|
|
38
|
-
} else if (section.items) {
|
|
39
|
-
const bars = renderBarChart(section.items, y, section.options || {});
|
|
40
|
-
bodySvg += bars.svg;
|
|
41
|
-
y += bars.height + sectionGap;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -52,7 +53,13 @@ export function generateFullSvg(sections: SectionDef[]): string {
|
|
|
52
53
|
viewBox={`0 0 ${width} ${totalHeight}`}
|
|
53
54
|
>
|
|
54
55
|
<StyleDefs />
|
|
55
|
-
<rect
|
|
56
|
+
<rect
|
|
57
|
+
width={width}
|
|
58
|
+
height={totalHeight}
|
|
59
|
+
rx="12"
|
|
60
|
+
className="bg-fill"
|
|
61
|
+
fill={THEME.bg}
|
|
62
|
+
/>
|
|
56
63
|
{bodySvg}
|
|
57
64
|
</svg>
|
|
58
65
|
);
|