@growthub/cli 0.13.7 → 0.13.9
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/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/codex-sites/route.js +13 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +98 -34
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +106 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceContributionGraph.jsx +119 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +357 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +488 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensWalkthrough.jsx +69 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +105 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +37 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +382 -32
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/codex-sites-data-model-card.jsx +81 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/page.jsx +31 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/settings-accordion-section.jsx +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +192 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-lens/page.jsx +76 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +140 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-local-state.js +139 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-workspace-adapter.js +156 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +1025 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +24 -8
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
- package/dist/index.js +5224 -5225
- package/package.json +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { listLocalCodexSites } from "@/lib/codex-sites-local-state";
|
|
3
|
+
|
|
4
|
+
async function GET() {
|
|
5
|
+
const sites = await listLocalCodexSites();
|
|
6
|
+
return NextResponse.json({
|
|
7
|
+
ok: true,
|
|
8
|
+
source: "codex-local-session-state",
|
|
9
|
+
sites
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { GET };
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* Execution model:
|
|
12
12
|
* 1. Read and sanitize the live growthub.config.json (or accept a snapshot).
|
|
13
13
|
* 2. Build a workspace-grammar-injected system prompt via workspace-helper.js.
|
|
14
|
-
* 3. Dispatch through the
|
|
15
|
-
* 4. Parse the
|
|
14
|
+
* 3. Dispatch through the configured workspace-helper-sandbox adapter.
|
|
15
|
+
* 4. Parse the helper JSON envelope.
|
|
16
16
|
* 5. Validate proposals, write a run record to source-records, return response.
|
|
17
17
|
*
|
|
18
18
|
* Request body (WorkspaceHelperQuery):
|
|
@@ -73,11 +73,38 @@ const VALID_INTENTS = [
|
|
|
73
73
|
];
|
|
74
74
|
|
|
75
75
|
const HELPER_SOURCE_KEY_PREFIX = "helper";
|
|
76
|
+
const HELPER_SANDBOX_OBJECT_ID = "workspace-helper-sandbox";
|
|
76
77
|
|
|
77
78
|
function helperSourceId(intent, runId) {
|
|
78
79
|
return `${HELPER_SOURCE_KEY_PREFIX}:${intent}:${runId}`;
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
function findHelperSandboxRow(config) {
|
|
83
|
+
const objects = Array.isArray(config?.dataModel?.objects) ? config.dataModel.objects : [];
|
|
84
|
+
const helperObject = objects.find((o) => o?.id === HELPER_SANDBOX_OBJECT_ID && o?.objectType === "sandbox-environment");
|
|
85
|
+
const rows = Array.isArray(helperObject?.rows) ? helperObject.rows : [];
|
|
86
|
+
return rows[0] && typeof rows[0] === "object" ? rows[0] : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildAgentHostCommand(messages) {
|
|
90
|
+
const transcript = messages
|
|
91
|
+
.map((message) => {
|
|
92
|
+
const role = typeof message?.role === "string" ? message.role.toUpperCase() : "MESSAGE";
|
|
93
|
+
const content = typeof message?.content === "string" ? message.content : "";
|
|
94
|
+
return `### ${role}\n${content}`;
|
|
95
|
+
})
|
|
96
|
+
.join("\n\n");
|
|
97
|
+
|
|
98
|
+
return [
|
|
99
|
+
"You are the Growthub Workspace Helper running through the user's selected local agent host.",
|
|
100
|
+
"Use the full transcript below. Return only one JSON object with this exact shape:",
|
|
101
|
+
JSON.stringify({ summary: "", proposals: [], warnings: [] }),
|
|
102
|
+
"Do not wrap the JSON in markdown. Do not execute tools or mutate files.",
|
|
103
|
+
"",
|
|
104
|
+
transcript,
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
81
108
|
async function POST(request) {
|
|
82
109
|
let body;
|
|
83
110
|
try {
|
|
@@ -117,6 +144,13 @@ async function POST(request) {
|
|
|
117
144
|
snapshot = sanitizeWorkspaceSnapshot(liveConfig);
|
|
118
145
|
liveConfigForThread = liveConfig;
|
|
119
146
|
}
|
|
147
|
+
if (!liveConfigForThread) {
|
|
148
|
+
try {
|
|
149
|
+
liveConfigForThread = await readWorkspaceConfig();
|
|
150
|
+
} catch {
|
|
151
|
+
liveConfigForThread = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
120
154
|
|
|
121
155
|
// Thread continuity — pull prior messages from the governed helper-threads
|
|
122
156
|
// row when the caller supplied a threadId so the chat completion gets the
|
|
@@ -170,12 +204,17 @@ async function POST(request) {
|
|
|
170
204
|
const userIntent = userPrompt;
|
|
171
205
|
|
|
172
206
|
await ensureSandboxAdaptersLoaded();
|
|
173
|
-
const
|
|
207
|
+
const helperSandboxRow = findHelperSandboxRow(liveConfigForThread);
|
|
208
|
+
const helperAdapterId = String(helperSandboxRow?.adapter || "").trim();
|
|
209
|
+
const helperAgentHost = String(helperSandboxRow?.agentHost || "").trim();
|
|
210
|
+
const useAgentHost = helperAdapterId === "local-agent-host" && helperAgentHost;
|
|
211
|
+
const adapterId = useAgentHost ? "local-agent-host" : "local-intelligence";
|
|
212
|
+
const adapter = getSandboxAdapter(adapterId);
|
|
174
213
|
if (!adapter) {
|
|
175
214
|
return NextResponse.json(
|
|
176
215
|
{
|
|
177
216
|
ok: false,
|
|
178
|
-
error:
|
|
217
|
+
error: `${adapterId} adapter not registered. Ensure sandbox adapters are loaded.`,
|
|
179
218
|
},
|
|
180
219
|
{ status: 503 }
|
|
181
220
|
);
|
|
@@ -186,32 +225,50 @@ async function POST(request) {
|
|
|
186
225
|
|
|
187
226
|
let adapterResult;
|
|
188
227
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
228
|
+
if (useAgentHost) {
|
|
229
|
+
adapterResult = await adapter.run({
|
|
230
|
+
runId,
|
|
231
|
+
name: `workspace-helper-${resolvedIntent}`,
|
|
232
|
+
runtime: "node",
|
|
233
|
+
agentHost: helperAgentHost,
|
|
234
|
+
command: buildAgentHostCommand(chatMessages),
|
|
235
|
+
timeoutMs: Number(helperSandboxRow?.timeoutMs) || 120000,
|
|
236
|
+
networkAllow: true,
|
|
237
|
+
allowList: [],
|
|
238
|
+
env: {},
|
|
239
|
+
envRefSlugs: [],
|
|
240
|
+
envRefsMissing: [],
|
|
241
|
+
workdir: "/tmp",
|
|
242
|
+
ranAt,
|
|
243
|
+
});
|
|
244
|
+
} else {
|
|
245
|
+
adapterResult = await adapter.run({
|
|
246
|
+
runId,
|
|
247
|
+
name: `workspace-helper-${resolvedIntent}`,
|
|
248
|
+
runtime: "node",
|
|
249
|
+
agentHost: "",
|
|
250
|
+
command: userIntent,
|
|
251
|
+
timeoutMs: 90000,
|
|
252
|
+
networkAllow: true,
|
|
253
|
+
allowList: [],
|
|
254
|
+
env: {},
|
|
255
|
+
envRefSlugs: [],
|
|
256
|
+
envRefsMissing: [],
|
|
257
|
+
workdir: "/tmp",
|
|
258
|
+
ranAt,
|
|
259
|
+
intelligenceSandbox: {
|
|
260
|
+
// Structured multi-turn message array — the adapter passes this
|
|
261
|
+
// straight through to the chat completions endpoint so the model
|
|
262
|
+
// sees the full conversation history with stable system prefix
|
|
263
|
+
// (KV-cache friendly).
|
|
264
|
+
messages: chatMessages,
|
|
265
|
+
userIntent,
|
|
266
|
+
localModel: modelOverride || process.env.NATIVE_INTELLIGENCE_LOCAL_MODEL || process.env.OLLAMA_MODEL || "gemma3:4b",
|
|
267
|
+
localEndpoint: localEndpointOverride || "",
|
|
268
|
+
intelligenceAdapterMode: adapterModeOverride || "ollama",
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
215
272
|
} catch (err) {
|
|
216
273
|
return NextResponse.json(
|
|
217
274
|
{
|
|
@@ -226,10 +283,10 @@ async function POST(request) {
|
|
|
226
283
|
return NextResponse.json(
|
|
227
284
|
{
|
|
228
285
|
ok: false,
|
|
229
|
-
error: adapterResult.error ||
|
|
286
|
+
error: adapterResult.error || `${adapterId} adapter returned error`,
|
|
230
287
|
receipts: {
|
|
231
|
-
model: "unknown",
|
|
232
|
-
adapterMode: adapterModeOverride || "ollama",
|
|
288
|
+
model: useAgentHost ? helperAgentHost : "unknown",
|
|
289
|
+
adapterMode: useAgentHost ? "agent-host" : (adapterModeOverride || "ollama"),
|
|
233
290
|
endpoint: "",
|
|
234
291
|
confidence: 0,
|
|
235
292
|
latencyMs: adapterResult.durationMs || 0,
|
|
@@ -241,7 +298,14 @@ async function POST(request) {
|
|
|
241
298
|
);
|
|
242
299
|
}
|
|
243
300
|
|
|
244
|
-
const
|
|
301
|
+
const parsedInput = useAgentHost
|
|
302
|
+
? {
|
|
303
|
+
result: { text: adapterResult.stdout },
|
|
304
|
+
adapter: { mode: "agent-host", modelId: helperAgentHost, endpoint: "local-agent-host" },
|
|
305
|
+
latencyMs: adapterResult.durationMs || 0,
|
|
306
|
+
}
|
|
307
|
+
: adapterResult.stdout;
|
|
308
|
+
const parsed = parseHelperEnvelope(parsedInput, intent);
|
|
245
309
|
const { valid: validProposals, errors: validationErrors } = validateProposals(parsed.proposals);
|
|
246
310
|
|
|
247
311
|
const warnings = [...parsed.warnings];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/workspace/swarm-condition
|
|
3
|
+
*
|
|
4
|
+
* Growthub Workspace Swarm Condition Packet V1 — read-only projection.
|
|
5
|
+
*
|
|
6
|
+
* Composes a registered workspace state lens into the agent-swarm assignment
|
|
7
|
+
* shape: instead of a vague prompt, an agent (or a swarm) gets a workspace
|
|
8
|
+
* *condition* — goal, current state, the blocked step, its prerequisite, the
|
|
9
|
+
* tools available, and the evidence it must produce. The human activation
|
|
10
|
+
* panel and this packet read the identical derived state.
|
|
11
|
+
*
|
|
12
|
+
* Optional query parameter:
|
|
13
|
+
* - lensId: "activation" (default) | "persistence" | "observability" |
|
|
14
|
+
* "deploy" | "tasks" | "app-build". Unknown ids fall back to
|
|
15
|
+
* "activation".
|
|
16
|
+
*
|
|
17
|
+
* Authority invariants:
|
|
18
|
+
* - GET only. PATCH / POST / PUT / DELETE are not exposed. Writes still flow
|
|
19
|
+
* through the existing governed routes (`PATCH /api/workspace`,
|
|
20
|
+
* `POST /api/workspace/sandbox-run`, etc.).
|
|
21
|
+
* - growthub.config.json remains the authoritative artifact.
|
|
22
|
+
* - No secrets, connection IDs, or tokens are returned. The runtime block is
|
|
23
|
+
* assembled from safe descriptors (persistence mode + adapter booleans).
|
|
24
|
+
* - Read OR derivation failures fall back to a typed packet with warnings —
|
|
25
|
+
* this route never throws.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { NextResponse } from "next/server";
|
|
29
|
+
import { readWorkspaceConfig, readWorkspaceSourceRecords, describePersistenceMode } from "@/lib/workspace-config";
|
|
30
|
+
import { readAdapterConfig } from "@/lib/adapters/env";
|
|
31
|
+
import { deriveSwarmConditionPacket } from "@/lib/workspace-activation";
|
|
32
|
+
|
|
33
|
+
const SWARM_PACKET_KIND = "growthub-swarm-condition-packet-v1";
|
|
34
|
+
|
|
35
|
+
/** Assemble the safe runtime descriptor the lenses read (no secrets/values). */
|
|
36
|
+
function safeRuntime(warnings) {
|
|
37
|
+
const runtime = { persistenceMode: "", persistenceAdapter: null, allowFsWrite: false, nangoConfigured: false, deploy: {} };
|
|
38
|
+
try {
|
|
39
|
+
const persistence = describePersistenceMode();
|
|
40
|
+
runtime.persistenceMode = persistence.mode;
|
|
41
|
+
runtime.allowFsWrite = persistence.mode === "filesystem" && persistence.canSave === true;
|
|
42
|
+
const adapter = readAdapterConfig();
|
|
43
|
+
runtime.persistenceAdapter = persistence.mode === "database" ? (adapter.dataAdapter || null) : null;
|
|
44
|
+
runtime.nangoConfigured = Boolean(adapter?.nango?.hasSecretKey);
|
|
45
|
+
runtime.deploy = { target: adapter.deployTarget || "" };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
warnings.push(`Failed to read runtime descriptor: ${error?.message || "unknown error"}`);
|
|
48
|
+
}
|
|
49
|
+
return runtime;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function GET(request) {
|
|
53
|
+
const warnings = [];
|
|
54
|
+
|
|
55
|
+
let lensId = "activation";
|
|
56
|
+
try {
|
|
57
|
+
const url = request && request.url ? new URL(request.url) : null;
|
|
58
|
+
const requested = url ? (url.searchParams.get("lensId") || "").trim() : "";
|
|
59
|
+
if (requested) lensId = requested;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
warnings.push(`Failed to parse query: ${error?.message || "unknown error"}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let workspaceConfig = {};
|
|
65
|
+
try {
|
|
66
|
+
workspaceConfig = (await readWorkspaceConfig()) || {};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
warnings.push(`Failed to read workspace config: ${error?.message || "unknown error"}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let workspaceSourceRecords = {};
|
|
72
|
+
try {
|
|
73
|
+
workspaceSourceRecords = (await readWorkspaceSourceRecords()) || {};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
warnings.push(`Failed to read source records sidecar: ${error?.message || "unknown error"}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const metadataGraph = { runtime: safeRuntime(warnings) };
|
|
79
|
+
|
|
80
|
+
let packet;
|
|
81
|
+
try {
|
|
82
|
+
packet = deriveSwarmConditionPacket(
|
|
83
|
+
{ workspaceConfig, workspaceSourceRecords, metadataGraph },
|
|
84
|
+
{ lensId },
|
|
85
|
+
);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
warnings.push(`Failed to derive swarm packet: ${error?.message || "unknown error"}`);
|
|
88
|
+
packet = {
|
|
89
|
+
kind: SWARM_PACKET_KIND,
|
|
90
|
+
version: 1,
|
|
91
|
+
lensId: "activation",
|
|
92
|
+
goal: "Activate this workspace.",
|
|
93
|
+
currentState: "0/0",
|
|
94
|
+
complete: false,
|
|
95
|
+
nextAction: null,
|
|
96
|
+
blockedStep: null,
|
|
97
|
+
prerequisite: null,
|
|
98
|
+
availableTools: [],
|
|
99
|
+
expectedEvidence: [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return NextResponse.json({ ...packet, warnings });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { GET };
|
|
@@ -52,6 +52,7 @@ export function WorkspaceActivationPanel({
|
|
|
52
52
|
metadataGraph,
|
|
53
53
|
onOpenHelper,
|
|
54
54
|
compact = false,
|
|
55
|
+
showLenses = false,
|
|
55
56
|
}) {
|
|
56
57
|
const state = deriveWorkspaceActivationState({
|
|
57
58
|
workspaceConfig,
|
|
@@ -59,6 +60,11 @@ export function WorkspaceActivationPanel({
|
|
|
59
60
|
metadataGraph,
|
|
60
61
|
});
|
|
61
62
|
|
|
63
|
+
// Onboarding stays pure. Once the primary loop completes we don't render
|
|
64
|
+
// operating-state cards inside the checklist — we hand the user off to the
|
|
65
|
+
// dedicated Workspace Lens surface (the "you levelled up" moment).
|
|
66
|
+
const showLensTeaser = showLenses && !compact && state.complete;
|
|
67
|
+
|
|
62
68
|
const stepsToRender = compact
|
|
63
69
|
? state.steps.filter((step) => step.status !== "optional")
|
|
64
70
|
: state.steps;
|
|
@@ -148,6 +154,17 @@ export function WorkspaceActivationPanel({
|
|
|
148
154
|
})}
|
|
149
155
|
</ol>
|
|
150
156
|
|
|
157
|
+
{showLensTeaser ? (
|
|
158
|
+
<div className="workspace-activation-lens-teaser">
|
|
159
|
+
<span className="workspace-activation-lens-teaser-text">
|
|
160
|
+
Workspace Lens is now available — your live operating surface for state, blockers, and agent-assignable work.
|
|
161
|
+
</span>
|
|
162
|
+
<Link href="/workspace-lens" className="workspace-activation-lens-teaser-link">
|
|
163
|
+
Open Workspace Lens
|
|
164
|
+
</Link>
|
|
165
|
+
</div>
|
|
166
|
+
) : null}
|
|
167
|
+
|
|
151
168
|
{onOpenHelper ? (
|
|
152
169
|
<div className="workspace-activation-helper-cta">
|
|
153
170
|
<button
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WorkspaceContributionGraph — GitHub-style activity heatmap for the workspace.
|
|
5
|
+
*
|
|
6
|
+
* Renders the pure `deriveWorkspaceContributions` grid (53 weeks × 7 days) as a
|
|
7
|
+
* calm, developer-familiar contribution graph. The chrome is neutral; only the
|
|
8
|
+
* cells use the recognizable green intensity scale. On hover each day shows a
|
|
9
|
+
* minimal tooltip (count + date) with a link into the matching filtered lens
|
|
10
|
+
* view — so the graph becomes the entry point to a daily ritual: open the lens,
|
|
11
|
+
* read your activity, drill into the day, start work.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
|
|
16
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
17
|
+
|
|
18
|
+
function formatDay(iso) {
|
|
19
|
+
const d = new Date(iso);
|
|
20
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
21
|
+
return d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function WorkspaceContributionGraph({ data, onSelectDay, buildDayHref }) {
|
|
25
|
+
const [hover, setHover] = useState(null);
|
|
26
|
+
if (!data || !Array.isArray(data.weeks) || data.weeks.length === 0) return null;
|
|
27
|
+
|
|
28
|
+
// Month labels: show a label above the first week whose first day starts a
|
|
29
|
+
// new month versus the previous column.
|
|
30
|
+
let prevMonth = -1;
|
|
31
|
+
const monthLabels = data.weeks.map((week) => {
|
|
32
|
+
const first = week.days[0];
|
|
33
|
+
const month = first ? new Date(first.date).getUTCMonth() : -1;
|
|
34
|
+
if (month !== prevMonth) { prevMonth = month; return MONTHS[month] || ""; }
|
|
35
|
+
return "";
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<section className="workspace-contrib" aria-label="Workspace contribution graph">
|
|
40
|
+
<div className="workspace-contrib-head">
|
|
41
|
+
<span className="workspace-contrib-total">{data.total} workspace contributions in the last year</span>
|
|
42
|
+
<span className="workspace-contrib-legend" aria-hidden="true">
|
|
43
|
+
Less
|
|
44
|
+
<i className="workspace-contrib-cell lvl-0" />
|
|
45
|
+
<i className="workspace-contrib-cell lvl-1" />
|
|
46
|
+
<i className="workspace-contrib-cell lvl-2" />
|
|
47
|
+
<i className="workspace-contrib-cell lvl-3" />
|
|
48
|
+
<i className="workspace-contrib-cell lvl-4" />
|
|
49
|
+
More
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="workspace-contrib-grid-wrap">
|
|
54
|
+
<div className="workspace-contrib-months" aria-hidden="true">
|
|
55
|
+
{monthLabels.map((label, i) => (
|
|
56
|
+
<span key={i} className="workspace-contrib-month">{label}</span>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
<div className="workspace-contrib-body">
|
|
60
|
+
<div className="workspace-contrib-weekdays" aria-hidden="true">
|
|
61
|
+
<span />
|
|
62
|
+
<span>Mon</span>
|
|
63
|
+
<span />
|
|
64
|
+
<span>Wed</span>
|
|
65
|
+
<span />
|
|
66
|
+
<span>Fri</span>
|
|
67
|
+
<span />
|
|
68
|
+
</div>
|
|
69
|
+
<div className="workspace-contrib-weeks" role="img" aria-label={`${data.total} workspace contributions`}>
|
|
70
|
+
{data.weeks.map((week, wi) => (
|
|
71
|
+
<div key={wi} className="workspace-contrib-week">
|
|
72
|
+
{week.days.map((day) => (
|
|
73
|
+
<button
|
|
74
|
+
key={day.date}
|
|
75
|
+
type="button"
|
|
76
|
+
className={"workspace-contrib-cell lvl-" + (day.future ? "future" : day.level) + (day.count > 0 ? " has-count" : "")}
|
|
77
|
+
disabled={day.future}
|
|
78
|
+
data-count={day.count}
|
|
79
|
+
aria-label={day.future ? "" : `${day.count} on ${formatDay(day.date)}`}
|
|
80
|
+
onMouseEnter={(e) => {
|
|
81
|
+
if (day.future) return;
|
|
82
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
83
|
+
setHover({ date: day.date, count: day.count, x: r.left + r.width / 2, y: r.top });
|
|
84
|
+
}}
|
|
85
|
+
onMouseLeave={() => setHover(null)}
|
|
86
|
+
onClick={() => { if (!day.future && onSelectDay) onSelectDay(day.date); }}
|
|
87
|
+
/>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{hover ? (
|
|
96
|
+
<div
|
|
97
|
+
className="workspace-contrib-tooltip"
|
|
98
|
+
style={{ left: hover.x, top: hover.y }}
|
|
99
|
+
role="tooltip"
|
|
100
|
+
>
|
|
101
|
+
<span className="workspace-contrib-tooltip-count">
|
|
102
|
+
{hover.count} contribution{hover.count === 1 ? "" : "s"}
|
|
103
|
+
</span>
|
|
104
|
+
<span className="workspace-contrib-tooltip-date">{formatDay(hover.date)}</span>
|
|
105
|
+
{hover.count > 0 && buildDayHref ? (
|
|
106
|
+
<a
|
|
107
|
+
className="workspace-contrib-tooltip-link"
|
|
108
|
+
href={buildDayHref(hover.date)}
|
|
109
|
+
target="_blank"
|
|
110
|
+
rel="noopener noreferrer"
|
|
111
|
+
>
|
|
112
|
+
Open day in Workspace Lens ↗
|
|
113
|
+
</a>
|
|
114
|
+
) : null}
|
|
115
|
+
</div>
|
|
116
|
+
) : null}
|
|
117
|
+
</section>
|
|
118
|
+
);
|
|
119
|
+
}
|