mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 17:41:09 +02:00
feat(cards-web): PDF input for AI generator + study activity heatmap
PDF input:
• lib/ai/pdf.ts wraps pdfjs-dist (Apache-2.0). Worker is bound via
Vite's `?worker` suffix so the heavy parsing runs off-main-thread.
• AiCardGen gains a "📄 PDF laden" button that pipes extracted text
into the same textarea — the user can review/trim before
generation. Reading state shows file name + page count + chars.
Heatmap:
• queries.useStudyHeatmap(weeks=12) fills gaps with count=0 so the
grid renders without holes.
• StudyHeatmap.svelte: 7 rows × N columns (Monday-anchored), 5
intensity buckets (neutral → emerald-300), tooltip per cell with
date + count, legend strip.
• Mounted on the dashboard between the deck list and the Anki import
so the user lands on a quick visual progress receipt every visit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d8a35afd99
commit
1f2206f10b
7 changed files with 375 additions and 10 deletions
|
|
@ -41,6 +41,7 @@
|
|||
"@mana/shared-utils": "workspace:*",
|
||||
"dexie": "^4.4.1",
|
||||
"jszip": "^3.10.1",
|
||||
"pdfjs-dist": "^5.7.284",
|
||||
"sql.js": "^1.14.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
apps/cards/apps/web/src/lib/ai/pdf.ts
Normal file
56
apps/cards/apps/web/src/lib/ai/pdf.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* PDF text extraction using pdfjs-dist.
|
||||
*
|
||||
* Loads each page, walks the text layer, joins items with spaces and
|
||||
* pages with double newlines so the LLM gets a structured input. We
|
||||
* don't try to preserve columns / tables — the use case is "feed me
|
||||
* the prose so I can make cards", not document fidelity.
|
||||
*
|
||||
* Worker is wired via Vite's `?worker` suffix so the heavy parsing
|
||||
* happens off the main thread (PDF extraction is CPU-heavy).
|
||||
*/
|
||||
|
||||
import * as pdfjs from 'pdfjs-dist';
|
||||
import PdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?worker';
|
||||
|
||||
let workerWired = false;
|
||||
function ensureWorker() {
|
||||
if (workerWired) return;
|
||||
pdfjs.GlobalWorkerOptions.workerPort = new PdfjsWorker();
|
||||
workerWired = true;
|
||||
}
|
||||
|
||||
export interface PdfExtractResult {
|
||||
text: string;
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
export async function extractTextFromPdf(file: File | Blob): Promise<PdfExtractResult> {
|
||||
ensureWorker();
|
||||
const buffer = await file.arrayBuffer();
|
||||
const doc = await pdfjs.getDocument({ data: new Uint8Array(buffer) }).promise;
|
||||
|
||||
const pages: string[] = [];
|
||||
for (let i = 1; i <= doc.numPages; i++) {
|
||||
const page = await doc.getPage(i);
|
||||
const content = await page.getTextContent();
|
||||
const pieces: string[] = [];
|
||||
for (const item of content.items) {
|
||||
if (typeof (item as { str?: string }).str === 'string') {
|
||||
pieces.push((item as { str: string }).str);
|
||||
}
|
||||
}
|
||||
pages.push(
|
||||
pieces
|
||||
.join(' ')
|
||||
.replace(/[ \t]+/g, ' ')
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
await doc.destroy();
|
||||
return {
|
||||
text: pages.filter(Boolean).join('\n\n'),
|
||||
pageCount: doc.numPages,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { generateCardsFromText, type GeneratedCard } from '$lib/ai/generate';
|
||||
import { extractTextFromPdf } from '$lib/ai/pdf';
|
||||
import { cardStore } from '$lib/stores/cards.svelte';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -10,8 +11,12 @@
|
|||
|
||||
let { deckId, currentCardCount, onCreated }: Props = $props();
|
||||
|
||||
let stage = $state<'idle' | 'generating' | 'preview' | 'creating' | 'done' | 'error'>('idle');
|
||||
let stage = $state<
|
||||
'idle' | 'reading-pdf' | 'generating' | 'preview' | 'creating' | 'done' | 'error'
|
||||
>('idle');
|
||||
let source = $state('');
|
||||
let pdfPicker = $state<HTMLInputElement | null>(null);
|
||||
let pdfStatus = $state<string | null>(null);
|
||||
let generated = $state<GeneratedCard[]>([]);
|
||||
let selected = $state<boolean[]>([]);
|
||||
let error = $state<string | null>(null);
|
||||
|
|
@ -72,6 +77,27 @@
|
|||
source = '';
|
||||
error = null;
|
||||
createdCount = 0;
|
||||
pdfStatus = null;
|
||||
}
|
||||
|
||||
async function handlePdfPick(e: Event) {
|
||||
const input = e.currentTarget as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
input.value = '';
|
||||
if (!file) return;
|
||||
error = null;
|
||||
stage = 'reading-pdf';
|
||||
pdfStatus = `Lese ${file.name}…`;
|
||||
try {
|
||||
const result = await extractTextFromPdf(file);
|
||||
source = result.text;
|
||||
pdfStatus = `${file.name} · ${result.pageCount} Seiten · ${result.text.length} Zeichen`;
|
||||
stage = 'idle';
|
||||
} catch (e: any) {
|
||||
error = e?.message ?? 'PDF konnte nicht gelesen werden.';
|
||||
stage = 'error';
|
||||
pdfStatus = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -97,16 +123,36 @@
|
|||
{#if stage === 'error' && error}
|
||||
<p class="mt-2 text-sm text-red-400">{error}</p>
|
||||
{/if}
|
||||
<div class="mt-2 flex items-center justify-between text-xs text-neutral-500">
|
||||
<span>{source.length} Zeichen</span>
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
onclick={handleGenerate}
|
||||
disabled={!source.trim()}
|
||||
>
|
||||
Generieren
|
||||
</button>
|
||||
<div class="mt-2 flex items-center justify-between gap-3 text-xs text-neutral-500">
|
||||
<div class="flex items-center gap-3">
|
||||
<span>{source.length} Zeichen</span>
|
||||
{#if pdfStatus}<span class="text-indigo-300">📄 {pdfStatus}</span>{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="rounded-lg border border-neutral-700 px-3 py-1.5 text-neutral-300 hover:bg-neutral-800"
|
||||
onclick={() => pdfPicker?.click()}
|
||||
>
|
||||
📄 PDF laden
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-indigo-500 px-4 py-1.5 text-sm text-white hover:bg-indigo-400 disabled:opacity-50"
|
||||
onclick={handleGenerate}
|
||||
disabled={!source.trim()}
|
||||
>
|
||||
Generieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
bind:this={pdfPicker}
|
||||
type="file"
|
||||
accept="application/pdf,.pdf"
|
||||
class="hidden"
|
||||
onchange={handlePdfPick}
|
||||
/>
|
||||
{:else if stage === 'reading-pdf'}
|
||||
<div class="py-6 text-center text-sm text-neutral-400">{pdfStatus ?? 'Lese PDF…'}</div>
|
||||
{:else if stage === 'generating'}
|
||||
<div class="py-6 text-center text-sm text-neutral-400">Modell denkt nach…</div>
|
||||
{:else if stage === 'preview'}
|
||||
|
|
|
|||
93
apps/cards/apps/web/src/lib/components/StudyHeatmap.svelte
Normal file
93
apps/cards/apps/web/src/lib/components/StudyHeatmap.svelte
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* GitHub-style activity grid: 7 rows (weekdays) × N columns (weeks).
|
||||
* Each cell encodes one day's review count via 5 color steps.
|
||||
* Tooltips on hover show the date + count.
|
||||
*
|
||||
* Week-start convention: we group by ISO week starting Monday so the
|
||||
* top row is always Mondays — matches the European calendar convention.
|
||||
*/
|
||||
|
||||
import { useStudyHeatmap } from '$lib/queries';
|
||||
|
||||
interface Props {
|
||||
weeks?: number;
|
||||
}
|
||||
let { weeks = 12 }: Props = $props();
|
||||
|
||||
const dataQuery = $derived(useStudyHeatmap(weeks));
|
||||
const rawDays = $derived(($dataQuery as { date: string; count: number }[] | undefined) ?? []);
|
||||
|
||||
// Pad to align the first day to a Monday so columns are full weeks.
|
||||
const grid = $derived.by(() => {
|
||||
if (rawDays.length === 0) return [] as { date: string | null; count: number }[];
|
||||
const first = new Date(rawDays[0].date);
|
||||
const dow = (first.getDay() + 6) % 7; // 0=Mon, 6=Sun
|
||||
const padded: { date: string | null; count: number }[] = [];
|
||||
for (let i = 0; i < dow; i++) padded.push({ date: null, count: 0 });
|
||||
padded.push(...rawDays);
|
||||
return padded;
|
||||
});
|
||||
|
||||
const columns = $derived.by(() => {
|
||||
const cols: { date: string | null; count: number }[][] = [];
|
||||
for (let i = 0; i < grid.length; i += 7) cols.push(grid.slice(i, i + 7));
|
||||
return cols;
|
||||
});
|
||||
|
||||
const max = $derived(rawDays.reduce((m, d) => Math.max(m, d.count), 0));
|
||||
|
||||
function bucket(count: number): string {
|
||||
if (count === 0) return 'bg-neutral-800';
|
||||
if (count <= Math.max(1, max * 0.25)) return 'bg-emerald-900';
|
||||
if (count <= max * 0.5) return 'bg-emerald-700';
|
||||
if (count <= max * 0.75) return 'bg-emerald-500';
|
||||
return 'bg-emerald-300';
|
||||
}
|
||||
|
||||
function fmt(date: string): string {
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString('de-DE', {
|
||||
weekday: 'short',
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
const total = $derived(rawDays.reduce((sum, d) => sum + d.count, 0));
|
||||
const activeDays = $derived(rawDays.filter((d) => d.count > 0).length);
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-neutral-800 bg-neutral-900 p-4">
|
||||
<div class="mb-3 flex items-center justify-between text-sm">
|
||||
<span class="font-medium">Lernaktivität</span>
|
||||
<span class="text-xs text-neutral-500">
|
||||
{total} Karten · {activeDays} aktive {activeDays === 1 ? 'Tag' : 'Tage'} · letzte {weeks} Wochen
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-1 overflow-x-auto">
|
||||
{#each columns as col, ci (ci)}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each col as cell, ri (ri)}
|
||||
{#if cell.date === null}
|
||||
<div class="h-3 w-3"></div>
|
||||
{:else}
|
||||
<div
|
||||
class="h-3 w-3 rounded-sm {bucket(cell.count)}"
|
||||
title="{fmt(cell.date)}: {cell.count} {cell.count === 1 ? 'Karte' : 'Karten'}"
|
||||
></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mt-3 flex items-center gap-1 text-xs text-neutral-500">
|
||||
<span>weniger</span>
|
||||
<span class="ml-1 h-3 w-3 rounded-sm bg-neutral-800"></span>
|
||||
<span class="h-3 w-3 rounded-sm bg-emerald-900"></span>
|
||||
<span class="h-3 w-3 rounded-sm bg-emerald-700"></span>
|
||||
<span class="h-3 w-3 rounded-sm bg-emerald-500"></span>
|
||||
<span class="h-3 w-3 rounded-sm bg-emerald-300"></span>
|
||||
<span class="ml-1">mehr</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -185,6 +185,41 @@ export function useDueCountByDeck() {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-day review counts for the last `weeks * 7` days (default 12 weeks
|
||||
* = 84 days). Used by the GitHub-style heatmap on the dashboard. Days
|
||||
* with no row in cardStudyBlocks come back as count=0 so the renderer
|
||||
* doesn't have to fill gaps itself.
|
||||
*/
|
||||
export function useStudyHeatmap(weeks: number = 12) {
|
||||
return liveQuery(async () => {
|
||||
const today = new Date();
|
||||
const localKey = (d: Date) => {
|
||||
const y = d.getFullYear();
|
||||
const m = `${d.getMonth() + 1}`.padStart(2, '0');
|
||||
const day = `${d.getDate()}`.padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
};
|
||||
|
||||
const days = weeks * 7;
|
||||
const rows = await cardStudyBlockTable.toArray();
|
||||
const byDate = new Map<string, number>();
|
||||
for (const r of rows) {
|
||||
if (r.deletedAt) continue;
|
||||
byDate.set(r.date, (byDate.get(r.date) ?? 0) + r.cardsReviewed);
|
||||
}
|
||||
|
||||
const out: { date: string; count: number }[] = [];
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
const key = localKey(d);
|
||||
out.push({ date: key, count: byDate.get(key) ?? 0 });
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Days-in-a-row with at least one review. Walks back from today; the
|
||||
* first day with no row (or a soft-deleted/empty one) ends the count.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useAllDecks, useDueCountByDeck } from '$lib/queries';
|
||||
import { deckStore } from '$lib/stores/decks.svelte';
|
||||
import AnkiImport from '$lib/components/AnkiImport.svelte';
|
||||
import StudyHeatmap from '$lib/components/StudyHeatmap.svelte';
|
||||
import type { Deck } from '@mana/cards-core';
|
||||
|
||||
const decksQuery = $derived(useAllDecks());
|
||||
|
|
@ -142,6 +143,10 @@
|
|||
{/if}
|
||||
|
||||
<div class="mt-10">
|
||||
<StudyHeatmap />
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<AnkiImport />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
129
pnpm-lock.yaml
generated
129
pnpm-lock.yaml
generated
|
|
@ -232,6 +232,9 @@ importers:
|
|||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
pdfjs-dist:
|
||||
specifier: ^5.7.284
|
||||
version: 5.7.284
|
||||
sql.js:
|
||||
specifier: ^1.14.1
|
||||
version: 1.14.1
|
||||
|
|
@ -6215,6 +6218,76 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.100':
|
||||
resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.100':
|
||||
resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.100':
|
||||
resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
|
||||
resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
|
||||
resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.100':
|
||||
resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
|
||||
resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
|
||||
resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas@0.1.100':
|
||||
resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@nestjs/cli@10.4.9':
|
||||
resolution: {integrity: sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==}
|
||||
engines: {node: '>= 16.14'}
|
||||
|
|
@ -13921,6 +13994,10 @@ packages:
|
|||
pdf-lib@1.17.1:
|
||||
resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==}
|
||||
|
||||
pdfjs-dist@5.7.284:
|
||||
resolution: {integrity: sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==}
|
||||
engines: {node: '>=22.13.0 || >=24'}
|
||||
|
||||
pend@1.2.0:
|
||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||
|
||||
|
|
@ -20733,6 +20810,54 @@ snapshots:
|
|||
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas@0.1.100':
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas-android-arm64': 0.1.100
|
||||
'@napi-rs/canvas-darwin-arm64': 0.1.100
|
||||
'@napi-rs/canvas-darwin-x64': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.100
|
||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-x64-musl': 0.1.100
|
||||
'@napi-rs/canvas-win32-arm64-msvc': 0.1.100
|
||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.100
|
||||
optional: true
|
||||
|
||||
'@nestjs/cli@10.4.9':
|
||||
dependencies:
|
||||
'@angular-devkit/core': 17.3.11(chokidar@3.6.0)
|
||||
|
|
@ -31164,6 +31289,10 @@ snapshots:
|
|||
pako: 1.0.11
|
||||
tslib: 1.14.1
|
||||
|
||||
pdfjs-dist@5.7.284:
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 0.1.100
|
||||
|
||||
pend@1.2.0: {}
|
||||
|
||||
performance-now@2.1.0:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue