diff --git a/apps/x/apps/main/forge.config.cjs b/apps/x/apps/main/forge.config.cjs index c79a8c43..178cb7e1 100644 --- a/apps/x/apps/main/forge.config.cjs +++ b/apps/x/apps/main/forge.config.cjs @@ -11,6 +11,9 @@ module.exports = { icon: './icons/icon', // .icns extension added automatically appBundleId: 'com.rowboat.app', appCategoryType: 'public.app-category.productivity', + extendInfo: { + NSAudioCaptureUsageDescription: 'Rowboat needs access to system audio to transcribe meetings from other apps (Zoom, Meet, etc.)', + }, osxSign: { batchCodesignCalls: true, optionsForFile: () => ({ diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index b92e3fe9..0fa0de79 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -1,4 +1,4 @@ -import { ipcMain, BrowserWindow, shell, dialog } from 'electron'; +import { ipcMain, BrowserWindow, shell, dialog, systemPreferences, desktopCapturer } from 'electron'; import { ipc } from '@x/shared'; import path from 'node:path'; import os from 'node:os'; @@ -719,6 +719,24 @@ export function setupIpcHandlers() { return { success: false, error: 'Unknown format' }; }, + 'meeting:checkScreenPermission': async () => { + if (process.platform !== 'darwin') return { granted: true }; + const status = systemPreferences.getMediaAccessStatus('screen'); + console.log('[meeting] Screen recording permission status:', status); + if (status === 'granted') return { granted: true }; + // Not granted — call desktopCapturer.getSources() to register the app + // in the macOS Screen Recording list. On first call this shows the + // native permission prompt (signed apps are remembered across restarts). + try { await desktopCapturer.getSources({ types: ['screen'] }); } catch { /* ignore */ } + // Re-check after the native prompt was dismissed + const statusAfter = systemPreferences.getMediaAccessStatus('screen'); + console.log('[meeting] Screen recording permission status after prompt:', statusAfter); + return { granted: statusAfter === 'granted' }; + }, + 'meeting:openScreenRecordingSettings': async () => { + await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'); + return { success: true }; + }, 'meeting:summarize': async (_event, args) => { const notes = await summarizeMeeting(args.transcript, args.meetingStartTime, args.calendarEventJson); return { notes }; diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index f83ea5cb..b2bc9d7f 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -484,7 +484,7 @@ function FixedSidebarToggle({ )} style={{ marginLeft: TITLEBAR_BUTTON_GAP_PX }} > - {meetingSummarizing ? ( + {meetingSummarizing || meetingState === 'connecting' ? ( ) : meetingState === 'recording' ? ( @@ -494,7 +494,7 @@ function FixedSidebarToggle({ - {meetingSummarizing ? 'Generating meeting notes...' : meetingState === 'recording' ? 'Stop meeting notes' : 'Take new meeting notes'} + {meetingSummarizing ? 'Generating meeting notes...' : meetingState === 'connecting' ? 'Starting transcription...' : meetingState === 'recording' ? 'Stop meeting notes' : 'Take new meeting notes'} )} @@ -3417,9 +3417,9 @@ function App() { const [meetingSummarizing, setMeetingSummarizing] = useState(false) const [showMeetingPermissions, setShowMeetingPermissions] = useState(false) - const startMeetingAfterPermissions = useCallback(async () => { - setShowMeetingPermissions(false) - localStorage.setItem('meeting-permissions-acknowledged', '1') + const [checkingPermission, setCheckingPermission] = useState(false) + + const startMeetingNow = useCallback(async () => { const calEvent = pendingCalendarEventRef.current pendingCalendarEventRef.current = undefined const notePath = await meetingTranscription.start(calEvent) @@ -3429,6 +3429,23 @@ function App() { } }, [meetingTranscription, handleVoiceNoteCreated]) + const handleCheckPermissionAndRetry = useCallback(async () => { + setCheckingPermission(true) + try { + const { granted } = await window.ipc.invoke('meeting:checkScreenPermission', null) + if (granted) { + setShowMeetingPermissions(false) + await startMeetingNow() + } + } finally { + setCheckingPermission(false) + } + }, [startMeetingNow]) + + const handleOpenScreenRecordingSettings = useCallback(async () => { + await window.ipc.invoke('meeting:openScreenRecordingSettings', null) + }, []) + const handleToggleMeeting = useCallback(async () => { if (meetingTranscription.state === 'recording') { await meetingTranscription.stop() @@ -3450,16 +3467,15 @@ function App() { const calendarEventJson = calEventMatch?.[1]?.replace(/''/g, "'") const { notes } = await window.ipc.invoke('meeting:summarize', { transcript: fileContent, meetingStartTime, calendarEventJson }) if (notes) { - // Prepend meeting notes below the title but above the transcript - const { raw: fm, body: transcriptBody } = splitFrontmatter(fileContent) - // Use frontmatter title as the heading (set from calendar event summary) + // Prepend meeting notes above the existing transcript block + const { raw: fm, body } = splitFrontmatter(fileContent) const fmTitleMatch = fileContent.match(/^title:\s*(.+)$/m) - const noteTitle = fmTitleMatch?.[1]?.trim() || 'Meeting note' - // Strip any existing top-level heading from body - const bodyWithoutTitle = transcriptBody.replace(/^#\s+.+\s*\n*/, '') - // Also strip any title/heading the LLM may have generated + const noteTitle = fmTitleMatch?.[1]?.trim() || 'Meeting Notes' const cleanedNotes = notes.replace(/^#{1,2}\s+.+\n+/, '') - const newBody = `# ${noteTitle}\n\n` + cleanedNotes + '\n\n---\n\n## Raw transcript\n\n' + bodyWithoutTitle + // Extract the existing transcript block and preserve it as-is + const transcriptBlockMatch = body.match(/(```transcript\n[\s\S]*?\n```)/) + const transcriptBlock = transcriptBlockMatch?.[1] || '' + const newBody = `# ${noteTitle}\n\n` + cleanedNotes + (transcriptBlock ? '\n\n' + transcriptBlock : '') const newContent = fm ? `${fm}\n${newBody}` : newBody await window.ipc.invoke('workspace:writeFile', { path: notePath, @@ -3477,20 +3493,18 @@ function App() { meetingNotePathRef.current = null } } else if (meetingTranscription.state === 'idle') { - // Show permissions modal on first use (macOS only — Windows works out of the box) - if (isMac && !localStorage.getItem('meeting-permissions-acknowledged')) { - setShowMeetingPermissions(true) - return - } - const calEvent = pendingCalendarEventRef.current - pendingCalendarEventRef.current = undefined - const notePath = await meetingTranscription.start(calEvent) - if (notePath) { - meetingNotePathRef.current = notePath - await handleVoiceNoteCreated(notePath) + // On macOS, check screen recording permission before starting + if (isMac) { + const result = await window.ipc.invoke('meeting:checkScreenPermission', null) + console.log('[meeting] Permission check result:', result) + if (!result.granted) { + setShowMeetingPermissions(true) + return + } } + await startMeetingNow() } - }, [meetingTranscription, handleVoiceNoteCreated]) + }, [meetingTranscription, handleVoiceNoteCreated, startMeetingNow]) handleToggleMeetingRef.current = handleToggleMeeting // Listen for calendar block "join meeting & take notes" events @@ -4421,23 +4435,25 @@ function App() { - Meeting transcription setup + Screen recording permission required - Rowboat needs Screen Recording permission to capture meeting audio from other apps (Zoom, Meet, etc.). + Rowboat needs Screen Recording permission to capture meeting audio from other apps (Zoom, Meet, etc.). This feature won't work without it.

To enable this:

    -
  1. Open System SettingsPrivacy & Security
  2. -
  3. Click Screen Recording
  4. +
  5. Open System SettingsPrivacy & SecurityScreen Recording
  6. Toggle on Rowboat
  7. You may need to restart the app after granting permission
- + +
diff --git a/apps/x/apps/renderer/src/components/markdown-editor.tsx b/apps/x/apps/renderer/src/components/markdown-editor.tsx index 2592dec3..f3ccba2d 100644 --- a/apps/x/apps/renderer/src/components/markdown-editor.tsx +++ b/apps/x/apps/renderer/src/components/markdown-editor.tsx @@ -15,6 +15,7 @@ import { ChartBlockExtension } from '@/extensions/chart-block' import { TableBlockExtension } from '@/extensions/table-block' import { CalendarBlockExtension } from '@/extensions/calendar-block' import { EmailBlockExtension } from '@/extensions/email-block' +import { TranscriptBlockExtension } from '@/extensions/transcript-block' import { Markdown } from 'tiptap-markdown' import { useEffect, useCallback, useMemo, useRef, useState } from 'react' import { Calendar, ChevronDown, ExternalLink } from 'lucide-react' @@ -155,6 +156,8 @@ function getMarkdownWithBlankLines(editor: Editor): string { blocks.push('```calendar\n' + (node.attrs?.data as string || '{}') + '\n```') } else if (node.type === 'emailBlock') { blocks.push('```email\n' + (node.attrs?.data as string || '{}') + '\n```') + } else if (node.type === 'transcriptBlock') { + blocks.push('```transcript\n' + (node.attrs?.data as string || '{}') + '\n```') } else if (node.type === 'codeBlock') { const lang = (node.attrs?.language as string) || '' blocks.push('```' + lang + '\n' + nodeToText(node) + '\n```') @@ -567,6 +570,7 @@ export function MarkdownEditor({ TableBlockExtension, CalendarBlockExtension, EmailBlockExtension, + TranscriptBlockExtension, WikiLink.configure({ onCreate: wikiLinks?.onCreate ? (path) => { diff --git a/apps/x/apps/renderer/src/extensions/transcript-block.tsx b/apps/x/apps/renderer/src/extensions/transcript-block.tsx new file mode 100644 index 00000000..9b76f568 --- /dev/null +++ b/apps/x/apps/renderer/src/extensions/transcript-block.tsx @@ -0,0 +1,177 @@ +import { mergeAttributes, Node } from '@tiptap/react' +import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react' +import { ChevronDown, FileText } from 'lucide-react' +import { blocks } from '@x/shared' +import { useState, useMemo } from 'react' + +interface TranscriptEntry { + speaker: string + text: string +} + +function parseTranscript(raw: string): TranscriptEntry[] { + const entries: TranscriptEntry[] = [] + const lines = raw.split('\n') + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed) continue + // Match **Speaker Name:** text or **You:** text + const match = trimmed.match(/^\*\*(.+?):\*\*\s*(.*)$/) + if (match) { + entries.push({ speaker: match[1], text: match[2] }) + } else if (entries.length > 0) { + // Continuation line — append to last entry + entries[entries.length - 1].text += ' ' + trimmed + } + } + return entries +} + +function speakerColor(speaker: string): string { + // Simple hash to pick a consistent color per speaker + let hash = 0 + for (let i = 0; i < speaker.length; i++) { + hash = speaker.charCodeAt(i) + ((hash << 5) - hash) + } + const colors = [ + '#3b82f6', // blue + '#06b6d4', // cyan + '#6366f1', // indigo + '#8b5cf6', // purple + '#0ea5e9', // sky + '#2563eb', // blue darker + '#7c3aed', // violet + ] + return colors[Math.abs(hash) % colors.length] +} + +function TranscriptBlockView({ node, getPos, editor }: { + node: { attrs: Record } + getPos: () => number | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + editor: any +}) { + const raw = node.attrs.data as string + let config: blocks.TranscriptBlock | null = null + + try { + config = blocks.TranscriptBlockSchema.parse(JSON.parse(raw)) + } catch { + // fallback below + } + + // Auto-detect: expand if this is the first real block (live recording), + // collapse if there's other content above (notes have been generated) + const isFirstBlock = useMemo(() => { + try { + const pos = getPos() + if (pos === undefined) return false + const firstChild = editor?.state?.doc?.firstChild + if (!firstChild) return true + // If the transcript block is right after the first node (heading), it's the main content + return pos <= (firstChild.nodeSize ?? 0) + 1 + } catch { + return false + } + }, [getPos, editor]) + + const [expanded, setExpanded] = useState(isFirstBlock) + + const entries = useMemo(() => { + if (!config) return [] + return parseTranscript(config.transcript) + }, [config]) + + if (!config) { + return ( + +
+ + Invalid transcript block +
+
+ ) + } + + return ( + +
e.stopPropagation()}> + + {expanded && ( +
+ {entries.length > 0 ? ( + entries.map((entry, i) => ( +
+ + {entry.speaker} + + {entry.text} +
+ )) + ) : ( +
{config.transcript}
+ )} +
+ )} +
+
+ ) +} + +export const TranscriptBlockExtension = Node.create({ + name: 'transcriptBlock', + group: 'block', + atom: true, + selectable: true, + draggable: false, + + addAttributes() { + return { + data: { default: '{}' }, + } + }, + + parseHTML() { + return [{ + tag: 'pre', + priority: 60, + getAttrs(element) { + const code = element.querySelector('code') + if (!code) return false + const cls = code.className || '' + if (cls.includes('language-transcript')) { + return { data: code.textContent || '{}' } + } + return false + }, + }] + }, + + renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) { + return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'transcript-block' })] + }, + + addNodeView() { + return ReactNodeViewRenderer(TranscriptBlockView) + }, + + addStorage() { + return { + markdown: { + serialize(state: { write: (text: string) => void; closeBlock: (node: unknown) => void }, node: { attrs: { data: string } }) { + state.write('```transcript\n' + node.attrs.data + '\n```') + state.closeBlock(node) + }, + parse: {}, + }, + } + }, +}) diff --git a/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts b/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts index 35a0a703..50d89a57 100644 --- a/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts +++ b/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts @@ -60,7 +60,7 @@ export interface CalendarEventMeta { } function formatTranscript(entries: TranscriptEntry[], date: string, calendarEvent?: CalendarEventMeta): string { - const noteTitle = calendarEvent?.summary || 'Meeting note'; + const noteTitle = calendarEvent?.summary || 'Meeting Notes'; const lines = [ '---', 'type: meeting', @@ -89,13 +89,18 @@ function formatTranscript(entries: TranscriptEntry[], date: string, calendarEven `# ${noteTitle}`, '', ); + // Build the raw transcript text + const transcriptLines: string[] = []; for (let i = 0; i < entries.length; i++) { if (i > 0 && entries[i].speaker !== entries[i - 1].speaker) { - lines.push(''); + transcriptLines.push(''); } - lines.push(`**${entries[i].speaker}:** ${entries[i].text}`); - lines.push(''); + transcriptLines.push(`**${entries[i].speaker}:** ${entries[i].text}`); + transcriptLines.push(''); } + const transcriptText = transcriptLines.join('\n').trim(); + const transcriptData = JSON.stringify({ transcript: transcriptText }); + lines.push('```transcript', transcriptData, '```'); return lines.join('\n'); } @@ -187,52 +192,83 @@ export function useMeetingTranscription(onAutoStop?: () => void) { if (state !== 'idle') return null; setState('connecting'); - // Detect headphones vs speakers - const usingHeadphones = await detectHeadphones(); - console.log(`[meeting] Audio output mode: ${usingHeadphones ? 'headphones' : 'speakers'}`); - - // Rowboat WebSocket + bearer token when signed in; else local Deepgram API key - let ws: WebSocket; - try { - const account = await refreshRowboatAccount(); - if ( - account?.signedIn && - account.accessToken && - account.config?.websocketApiUrl - ) { - const listenUrl = buildDeepgramListenUrl(account.config.websocketApiUrl, DEEPGRAM_PARAMS); - console.log('[meeting] Using Rowboat WebSocket'); - ws = new WebSocket(listenUrl, ['bearer', account.accessToken]); - } else { - const config = await window.ipc.invoke('voice:getConfig', null); - if (!config?.deepgram) { - console.error('[meeting] No Deepgram config available'); - setState('idle'); - return null; + // Run independent setup steps in parallel for faster startup + const [headphoneResult, wsResult, micResult, systemResult] = await Promise.allSettled([ + // 1. Detect headphones vs speakers + detectHeadphones(), + // 2. Set up Deepgram WebSocket (account refresh + connect + wait for open) + (async () => { + const account = await refreshRowboatAccount(); + let ws: WebSocket; + if ( + account?.signedIn && + account.accessToken && + account.config?.websocketApiUrl + ) { + const listenUrl = buildDeepgramListenUrl(account.config.websocketApiUrl, DEEPGRAM_PARAMS); + console.log('[meeting] Using Rowboat WebSocket'); + ws = new WebSocket(listenUrl, ['bearer', account.accessToken]); + } else { + const config = await window.ipc.invoke('voice:getConfig', null); + if (!config?.deepgram) { + throw new Error('No Deepgram config available'); + } + console.log('[meeting] Using Deepgram API key'); + ws = new WebSocket(DEEPGRAM_LISTEN_URL, ['token', config.deepgram.apiKey]); } - console.log('[meeting] Using Deepgram API key'); - ws = new WebSocket(DEEPGRAM_LISTEN_URL, ['token', config.deepgram.apiKey]); - } - } catch (err) { - console.error('[meeting] Failed to connect Deepgram:', err); - setState('idle'); - return null; - } - wsRef.current = ws; + const ok = await new Promise((resolve) => { + ws.onopen = () => resolve(true); + ws.onerror = () => resolve(false); + setTimeout(() => resolve(false), 5000); + }); + if (!ok) throw new Error('WebSocket failed to connect'); + console.log('[meeting] WebSocket connected'); + return ws; + })(), + // 3. Get mic stream + navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + }, + }), + // 4. Get system audio via getDisplayMedia (loopback) + (async () => { + const stream = await navigator.mediaDevices.getDisplayMedia({ audio: true, video: true }); + stream.getVideoTracks().forEach(t => t.stop()); + if (stream.getAudioTracks().length === 0) { + stream.getTracks().forEach(t => t.stop()); + throw new Error('No audio track from getDisplayMedia'); + } + console.log('[meeting] System audio captured'); + return stream; + })(), + ]); - // Wait for WS open - const wsOk = await new Promise((resolve) => { - ws.onopen = () => resolve(true); - ws.onerror = () => resolve(false); - setTimeout(() => resolve(false), 5000); - }); - if (!wsOk) { - console.error('[meeting] WebSocket failed to connect'); + // Check for failures — clean up any successful resources if something failed + const failed = wsResult.status === 'rejected' + || micResult.status === 'rejected' + || systemResult.status === 'rejected'; + + if (failed) { + if (wsResult.status === 'rejected') console.error('[meeting] WebSocket setup failed:', wsResult.reason); + if (micResult.status === 'rejected') console.error('[meeting] Microphone access denied:', micResult.reason); + if (systemResult.status === 'rejected') console.error('[meeting] System audio access denied:', systemResult.reason); + // Clean up any resources that did succeed + if (wsResult.status === 'fulfilled') { wsResult.value.close(); } + if (micResult.status === 'fulfilled') { micResult.value.getTracks().forEach(t => t.stop()); } + if (systemResult.status === 'fulfilled') { systemResult.value.getTracks().forEach(t => t.stop()); } cleanup(); setState('idle'); return null; } - console.log('[meeting] WebSocket connected'); + + const usingHeadphones = headphoneResult.status === 'fulfilled' ? headphoneResult.value : false; + console.log(`[meeting] Audio output mode: ${usingHeadphones ? 'headphones' : 'speakers'}`); + + const ws = wsResult.value; + wsRef.current = ws; // Set up WS message handler transcriptRef.current = []; @@ -283,43 +319,10 @@ export function useMeetingTranscription(onAutoStop?: () => void) { wsRef.current = null; }; - // Get mic stream - let micStream: MediaStream; - try { - micStream = await navigator.mediaDevices.getUserMedia({ - audio: { - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - }, - }); - } catch (err) { - console.error('[meeting] Microphone access denied:', err); - cleanup(); - setState('idle'); - return null; - } + const micStream = micResult.value; micStreamRef.current = micStream; - // Get system audio via getDisplayMedia (loopback) - let systemStream: MediaStream; - try { - systemStream = await navigator.mediaDevices.getDisplayMedia({ audio: true, video: true }); - systemStream.getVideoTracks().forEach(t => t.stop()); - } catch (err) { - console.error('[meeting] System audio access denied:', err); - cleanup(); - setState('idle'); - return null; - } - if (systemStream.getAudioTracks().length === 0) { - console.error('[meeting] No audio track from getDisplayMedia'); - systemStream.getTracks().forEach(t => t.stop()); - cleanup(); - setState('idle'); - return null; - } - console.log('[meeting] System audio captured'); + const systemStream = systemResult.value; systemStreamRef.current = systemStream; // ----- Audio pipeline ----- diff --git a/apps/x/apps/renderer/src/styles/editor.css b/apps/x/apps/renderer/src/styles/editor.css index 8701099b..efa481c1 100644 --- a/apps/x/apps/renderer/src/styles/editor.css +++ b/apps/x/apps/renderer/src/styles/editor.css @@ -618,7 +618,8 @@ .tiptap-editor .ProseMirror .chart-block-wrapper, .tiptap-editor .ProseMirror .table-block-wrapper, .tiptap-editor .ProseMirror .calendar-block-wrapper, -.tiptap-editor .ProseMirror .email-block-wrapper { +.tiptap-editor .ProseMirror .email-block-wrapper, +.tiptap-editor .ProseMirror .transcript-block-wrapper { margin: 8px 0; } @@ -628,7 +629,8 @@ .tiptap-editor .ProseMirror .table-block-card, .tiptap-editor .ProseMirror .calendar-block-card, .tiptap-editor .ProseMirror .email-block-card, -.tiptap-editor .ProseMirror .email-draft-block-card { +.tiptap-editor .ProseMirror .email-draft-block-card, +.tiptap-editor .ProseMirror .transcript-block-card { position: relative; padding: 12px 14px; border: 1px solid var(--border); @@ -644,7 +646,8 @@ .tiptap-editor .ProseMirror .table-block-card:hover, .tiptap-editor .ProseMirror .calendar-block-card:hover, .tiptap-editor .ProseMirror .email-block-card:hover, -.tiptap-editor .ProseMirror .email-draft-block-card:hover { +.tiptap-editor .ProseMirror .email-draft-block-card:hover, +.tiptap-editor .ProseMirror .transcript-block-card:hover { background-color: color-mix(in srgb, var(--muted) 70%, transparent); } @@ -1488,6 +1491,74 @@ margin-left: 4px; } +/* Transcript block */ +.tiptap-editor .ProseMirror .transcript-block-toggle { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + padding: 0; + font-size: 13px; + font-weight: 500; + color: color-mix(in srgb, var(--foreground) 60%, transparent); + background: none; + border: none; + cursor: pointer; + transition: color 0.12s ease; +} + +.tiptap-editor .ProseMirror .transcript-block-toggle:hover { + color: var(--foreground); +} + +.tiptap-editor .ProseMirror .transcript-block-chevron { + transition: transform 0.15s ease; + flex-shrink: 0; +} + +.tiptap-editor .ProseMirror .transcript-block-chevron-open { + transform: rotate(180deg); +} + +.tiptap-editor .ProseMirror .transcript-block-content { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); + display: flex; + flex-direction: column; + gap: 6px; +} + +.tiptap-editor .ProseMirror .transcript-entry { + font-size: 13px; + line-height: 1.5; +} + +.tiptap-editor .ProseMirror .transcript-speaker { + font-weight: 600; + margin-right: 6px; +} + +.tiptap-editor .ProseMirror .transcript-text { + color: color-mix(in srgb, var(--foreground) 75%, transparent); +} + +.tiptap-editor .ProseMirror .transcript-raw { + font-size: 13px; + line-height: 1.6; + color: color-mix(in srgb, var(--foreground) 70%, transparent); + white-space: pre-wrap; + word-break: break-word; +} + +.tiptap-editor .ProseMirror .transcript-block-error { + display: flex; + align-items: center; + gap: 6px; + color: color-mix(in srgb, var(--foreground) 55%, transparent); + font-size: 13px; +} + /* Meeting event banner */ .meeting-event-banner { position: relative; diff --git a/apps/x/packages/core/src/knowledge/summarize_meeting.ts b/apps/x/packages/core/src/knowledge/summarize_meeting.ts index 534b6655..30e3c5d4 100644 --- a/apps/x/packages/core/src/knowledge/summarize_meeting.ts +++ b/apps/x/packages/core/src/knowledge/summarize_meeting.ts @@ -15,7 +15,8 @@ const SYSTEM_PROMPT = `You are a meeting notes assistant. Given a raw meeting tr ## Calendar matching You will be given the transcript (with a timestamp of when recording started) and recent calendar events with their titles, times, and attendees. If a calendar event clearly matches this meeting (overlapping time + content aligns), then: - Do NOT output a title or heading — the title is already set by the caller. -- Replace generic speaker labels ("Speaker 0", "Speaker 1", "System audio") with actual attendee names, but ONLY if you have HIGH CONFIDENCE about which speaker is which based on the discussion content. If unsure, use "They" instead of "Speaker 0" etc. +- ONLY use names from the calendar event attendee list. Do NOT introduce names that are not in the attendee list — any unrecognized names in the transcript are transcription errors. +- Replace generic speaker labels ("Speaker 0", "Speaker 1", "System audio") with actual attendee names from the list, but ONLY if you have HIGH CONFIDENCE about which speaker is which based on the discussion content. If unsure, use "They" instead of "Speaker 0" etc. - "You" in the transcript is the local user — if the calendar event has an organizer or you can identify who "You" is from context, use their name. If no calendar event matches with high confidence, or if no calendar events are provided, use "They" for all non-"You" speakers. diff --git a/apps/x/packages/shared/src/blocks.ts b/apps/x/packages/shared/src/blocks.ts index 55d1cd3e..68209051 100644 --- a/apps/x/packages/shared/src/blocks.ts +++ b/apps/x/packages/shared/src/blocks.ts @@ -74,3 +74,9 @@ export const EmailBlockSchema = z.object({ }); export type EmailBlock = z.infer; + +export const TranscriptBlockSchema = z.object({ + transcript: z.string(), +}); + +export type TranscriptBlock = z.infer; diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 5f4988f4..28718db3 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -501,6 +501,16 @@ const ipcSchemas = { mimeType: z.string(), }), }, + 'meeting:checkScreenPermission': { + req: z.null(), + res: z.object({ + granted: z.boolean(), + }), + }, + 'meeting:openScreenRecordingSettings': { + req: z.null(), + res: z.object({ success: z.boolean() }), + }, 'meeting:summarize': { req: z.object({ transcript: z.string(), diff --git a/apps/x/pnpm-lock.yaml b/apps/x/pnpm-lock.yaml index e59ce990..01a9240f 100644 --- a/apps/x/pnpm-lock.yaml +++ b/apps/x/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: electron-squirrel-startup: specifier: ^1.0.1 version: 1.0.1 + html-to-docx: + specifier: ^1.8.0 + version: 1.8.0(encoding@0.1.13) mammoth: specifier: ^1.11.0 version: 1.11.0 @@ -235,6 +238,9 @@ importers: react-dom: specifier: ^19.2.0 version: 19.2.3(react@19.2.3) + recharts: + specifier: ^3.8.0 + version: 3.8.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1) sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -1578,6 +1584,46 @@ packages: '@octokit/types@6.41.0': resolution: {integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==} + '@oozcitak/dom@1.15.5': + resolution: {integrity: sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A==} + engines: {node: '>=8.0'} + + '@oozcitak/dom@1.15.6': + resolution: {integrity: sha512-k4uEIa6DI3FCrFJMGq/05U/59WnS9DjME0kaPqBRCJAqBTkmopbYV1Xs4qFKbDJ/9wOg8W97p+1E0heng/LH7g==} + engines: {node: '>=8.0'} + + '@oozcitak/infra@1.0.3': + resolution: {integrity: sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag==} + engines: {node: '>=6.0'} + + '@oozcitak/infra@1.0.5': + resolution: {integrity: sha512-o+zZH7M6l5e3FaAWy3ojaPIVN5eusaYPrKm6MZQt0DKNdgXa2wDYExjpP0t/zx+GoQgQKzLu7cfD8rHCLt8JrQ==} + engines: {node: '>=6.0'} + + '@oozcitak/url@1.0.0': + resolution: {integrity: sha512-LGrMeSxeLzsdaitxq3ZmBRVOrlRRQIgNNci6L0VRnOKlJFuRIkNm4B+BObXPCJA6JT5bEJtrrwjn30jueHJYZQ==} + engines: {node: '>=8.0'} + + '@oozcitak/util@1.0.1': + resolution: {integrity: sha512-dFwFqcKrQnJ2SapOmRD1nQWEZUtbtIy9Y6TyJquzsalWNJsKIPxmTI0KG6Ypyl8j7v89L2wixH9fQDNrF78hKg==} + engines: {node: '>=6.0'} + + '@oozcitak/util@1.0.2': + resolution: {integrity: sha512-4n8B1cWlJleSOSba5gxsMcN4tO8KkkcvXhNWW+ADqvq9Xj+Lrl9uCa90GRpjekqQJyt84aUX015DG81LFpZYXA==} + engines: {node: '>=6.0'} + + '@oozcitak/util@8.0.0': + resolution: {integrity: sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==} + engines: {node: '>=6.0'} + + '@oozcitak/util@8.3.3': + resolution: {integrity: sha512-Ufpab7G5PfnEhQyy5kDg9C8ltWJjsVT1P/IYqacjstaqydG4Q21HAT2HUZQYBrC/a1ZLKCz87pfydlDvv8y97w==} + engines: {node: '>=6.0'} + + '@oozcitak/util@8.3.4': + resolution: {integrity: sha512-6gH/bLQJSJEg7OEpkH4wGQdA8KXHRbzL1YkGyUO12YNAgV3jxKy4K9kvfXj4+9T0OLug5k58cnPCKSSIKzp7pg==} + engines: {node: '>=8.0'} + '@openrouter/ai-sdk-provider@1.5.4': resolution: {integrity: sha512-xrSQPUIH8n9zuyYZR0XK7Ba0h2KsjJcMkxnwaYfmv13pKs3sDkjPzVPPhlhzqBGddHb5cFEwJ9VFuFeDcxCDSw==} engines: {node: '>=18'} @@ -2516,6 +2562,17 @@ packages: '@react-pdf/types@2.9.2': resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==} + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} @@ -2876,6 +2933,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/helpers@0.5.18': resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} @@ -3775,6 +3835,9 @@ packages: brotli@1.3.3: resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + browser-split@0.0.1: + resolution: {integrity: sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==} + browserify-zlib@0.2.0: resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} @@ -3836,6 +3899,9 @@ packages: camel-case@4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + caniuse-lite@1.0.30001761: resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} @@ -4263,6 +4329,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} @@ -4331,12 +4400,24 @@ packages: dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + dom-serializer@0.2.2: + resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + + domelementtype@1.3.1: + resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} + domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + domhandler@2.4.2: + resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==} + domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} @@ -4344,6 +4425,9 @@ packages: dompurify@3.3.1: resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + domutils@1.7.0: + resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -4428,6 +4512,16 @@ packages: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} + ent@2.2.2: + resolution: {integrity: sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==} + engines: {node: '>= 0.4'} + + entities@1.1.2: + resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -4446,6 +4540,9 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error@4.4.0: + resolution: {integrity: sha512-SNDKualLUtT4StGFP7xNfuFybL2f6iJujFtrWuvJqGbVQGaN+adE23veqzPz1hjUjTunLi2EnJ+0SJxtbJreKw==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -4465,6 +4562,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} @@ -4565,6 +4665,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + ev-store@7.0.0: + resolution: {integrity: sha512-otazchNRnGzp2YarBJ+GXKVGvhxVATB1zmaStxJBYet0Dyq7A9VhH8IUEB/gRcL6Ch52lfpgPTRJ2m49epyMsQ==} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -4907,6 +5010,9 @@ packages: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} + global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -5050,12 +5156,24 @@ packages: hsl-to-rgb-for-reals@1.1.1: resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==} + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-to-docx@1.8.0: + resolution: {integrity: sha512-IiMBWIqXM4+cEsW//RKoonWV7DlXAJBmmKI73XJSVWTIXjGUaxSr2ck1jqzVRZknpvO8xsFnVicldKVAWrBYBA==} + + html-to-vdom@0.7.0: + resolution: {integrity: sha512-k+d2qNkbx0JO00KezQsNcn6k2I/xSBP4yXYFLvXbcasTTDh+RDLUJS3puxqyNnpdyXWRHFGoKU7cRmby8/APcQ==} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@3.10.1: + resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -5113,9 +5231,23 @@ packages: engines: {node: '>=6.9.0'} hasBin: true + image-size@1.2.1: + resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} + engines: {node: '>=16.x'} + hasBin: true + + image-to-base64@2.2.0: + resolution: {integrity: sha512-Z+aMwm/91UOQqHhrz7Upre2ytKhWejZlWV/JxUTD1sT7GWWKFDJUEV5scVQKnkzSgPHFuQBUEWcanO+ma0PSVw==} + immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -5132,6 +5264,9 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + individual@3.0.0: + resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} + infer-owner@1.0.4: resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} @@ -5232,6 +5367,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-object@1.0.2: + resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -5242,6 +5380,10 @@ packages: is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + is-stream@1.1.0: resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} engines: {node: '>=0.10.0'} @@ -5836,6 +5978,9 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + min-document@2.19.2: + resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==} + minimatch@10.1.1: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} @@ -5965,6 +6110,9 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + next-tick@0.2.2: + resolution: {integrity: sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==} + nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} @@ -6425,6 +6573,9 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -6484,6 +6635,18 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -6549,10 +6712,26 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + recharts@3.8.1: + resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + rechoir@0.8.0: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -6625,6 +6804,9 @@ packages: resolution: {integrity: sha512-oTeemxwoMuxxTYxXUwjkrOPfngTQehlv0/HoYFNkB4uzsP1Un1A9nI8JQKGOFkxpqkC7qkMs0lUsGrvUlbLNUA==} engines: {node: '>=14', npm: '>=7'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -6713,6 +6895,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -6908,6 +7094,9 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 + string-template@0.2.1: + resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -7026,6 +7215,9 @@ packages: tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -7297,6 +7489,12 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + + virtual-dom@2.1.1: + resolution: {integrity: sha512-wb6Qc9Lbqug0kRqo/iuApfBpJJAq14Sk1faAnSmtqXiwahg7PVTvWMs9L02Z8nNIMqbwsxzBAA90bbtRLbw0zg==} + vite-compatible-readable-stream@3.6.1: resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==} engines: {node: '>= 6'} @@ -7446,11 +7644,21 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + x-is-array@0.1.0: + resolution: {integrity: sha512-goHPif61oNrr0jJgsXRfc8oqtYzvfiMJpTqwE7Z4y9uH+T3UozkGqQ4d2nX9mB9khvA8U2o/UbPOFjgC7hLWIA==} + + x-is-string@0.1.0: + resolution: {integrity: sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==} + xlsx@0.18.5: resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} engines: {node: '>=0.8'} hasBin: true + xmlbuilder2@2.1.2: + resolution: {integrity: sha512-PI710tmtVlQ5VmwzbRTuhmVhKnj9pM8Si+iOZCV2g2SNo3gCrpzR2Ka9wNzZtqfD+mnP+xkrqoNy0sjKZqP4Dg==} + engines: {node: '>=8.0'} + xmlbuilder@10.1.1: resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} engines: {node: '>=4.0'} @@ -9230,6 +9438,41 @@ snapshots: dependencies: '@octokit/openapi-types': 12.11.0 + '@oozcitak/dom@1.15.5': + dependencies: + '@oozcitak/infra': 1.0.5 + '@oozcitak/url': 1.0.0 + '@oozcitak/util': 8.0.0 + + '@oozcitak/dom@1.15.6': + dependencies: + '@oozcitak/infra': 1.0.5 + '@oozcitak/url': 1.0.0 + '@oozcitak/util': 8.3.4 + + '@oozcitak/infra@1.0.3': + dependencies: + '@oozcitak/util': 1.0.1 + + '@oozcitak/infra@1.0.5': + dependencies: + '@oozcitak/util': 8.0.0 + + '@oozcitak/url@1.0.0': + dependencies: + '@oozcitak/infra': 1.0.3 + '@oozcitak/util': 1.0.2 + + '@oozcitak/util@1.0.1': {} + + '@oozcitak/util@1.0.2': {} + + '@oozcitak/util@8.0.0': {} + + '@oozcitak/util@8.3.3': {} + + '@oozcitak/util@8.3.4': {} + '@openrouter/ai-sdk-provider@1.5.4(ai@5.0.151(zod@4.2.1))(zod@4.2.1)': dependencies: '@openrouter/sdk': 0.1.27 @@ -10259,6 +10502,18 @@ snapshots: '@react-pdf/primitives': 4.1.1 '@react-pdf/stylesheet': 6.1.2 + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.4 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.3 + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1) + '@remirror/core-constants@3.0.0': {} '@rolldown/pluginutils@1.0.0-beta.53': {} @@ -10704,6 +10959,8 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} + '@swc/helpers@0.5.18': dependencies: tslib: 2.8.1 @@ -11729,6 +11986,8 @@ snapshots: dependencies: base64-js: 1.5.1 + browser-split@0.0.1: {} + browserify-zlib@0.2.0: dependencies: pako: 1.0.11 @@ -11823,6 +12082,8 @@ snapshots: pascal-case: 3.1.2 tslib: 2.8.1 + camelize@1.0.1: {} + caniuse-lite@1.0.30001761: {} ccount@2.0.1: {} @@ -12243,6 +12504,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 @@ -12306,14 +12569,27 @@ snapshots: minimatch: 3.1.2 p-limit: 3.1.0 + dom-serializer@0.2.2: + dependencies: + domelementtype: 2.3.0 + entities: 2.2.0 + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 entities: 4.5.0 + dom-walk@0.1.2: {} + + domelementtype@1.3.1: {} + domelementtype@2.3.0: {} + domhandler@2.4.2: + dependencies: + domelementtype: 1.3.1 + domhandler@5.0.3: dependencies: domelementtype: 2.3.0 @@ -12322,6 +12598,11 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 + domutils@1.7.0: + dependencies: + dom-serializer: 0.2.2 + domelementtype: 1.3.1 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -12462,6 +12743,17 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + ent@2.2.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + punycode: 1.4.1 + safe-regex-test: 1.1.0 + + entities@1.1.2: {} + + entities@2.2.0: {} + entities@4.5.0: {} entities@6.0.1: {} @@ -12474,6 +12766,12 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error@4.4.0: + dependencies: + camelize: 1.0.1 + string-template: 0.2.1 + xtend: 4.0.2 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -12491,6 +12789,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es-toolkit@1.45.1: {} + es6-error@4.1.1: optional: true @@ -12655,6 +12955,10 @@ snapshots: etag@1.8.1: {} + ev-store@7.0.0: + dependencies: + individual: 3.0.0 + event-target-shim@5.0.1: {} eventemitter3@5.0.1: {} @@ -13098,6 +13402,11 @@ snapshots: dependencies: ini: 2.0.0 + global@4.4.0: + dependencies: + min-document: 2.19.2 + process: 0.11.10 + globals@14.0.0: {} globals@16.5.0: {} @@ -13344,10 +13653,44 @@ snapshots: hsl-to-rgb-for-reals@1.1.1: {} + html-entities@2.6.0: {} + + html-to-docx@1.8.0(encoding@0.1.13): + dependencies: + '@oozcitak/dom': 1.15.6 + '@oozcitak/util': 8.3.4 + color-name: 1.1.4 + html-entities: 2.6.0 + html-to-vdom: 0.7.0 + image-size: 1.2.1 + image-to-base64: 2.2.0(encoding@0.1.13) + jszip: 3.10.1 + lodash: 4.17.21 + mime-types: 2.1.35 + nanoid: 3.3.11 + virtual-dom: 2.1.1 + xmlbuilder2: 2.1.2 + transitivePeerDependencies: + - encoding + + html-to-vdom@0.7.0: + dependencies: + ent: 2.2.2 + htmlparser2: 3.10.1 + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} + htmlparser2@3.10.1: + dependencies: + domelementtype: 1.3.1 + domhandler: 2.4.2 + domutils: 1.7.0 + entities: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + http-cache-semantics@4.2.0: {} http-errors@2.0.1: @@ -13412,8 +13755,22 @@ snapshots: image-size@0.7.5: optional: true + image-size@1.2.1: + dependencies: + queue: 6.0.2 + + image-to-base64@2.2.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + immediate@3.0.6: {} + immer@10.2.0: {} + + immer@11.1.4: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -13426,6 +13783,8 @@ snapshots: indent-string@4.0.0: {} + individual@3.0.0: {} + infer-owner@1.0.4: {} inflight@1.0.6: @@ -13500,6 +13859,8 @@ snapshots: is-number@7.0.0: {} + is-object@1.0.2: {} + is-plain-obj@4.1.0: {} is-promise@4.0.0: {} @@ -13507,6 +13868,13 @@ snapshots: is-property@1.0.2: optional: true + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + is-stream@1.1.0: {} is-stream@2.0.1: {} @@ -14352,6 +14720,10 @@ snapshots: mimic-response@3.1.0: {} + min-document@2.19.2: + dependencies: + dom-walk: 0.1.2 + minimatch@10.1.1: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -14467,6 +14839,8 @@ snapshots: neo-async@2.6.2: {} + next-tick@0.2.2: {} + nice-try@1.0.5: {} no-case@3.0.4: @@ -14955,6 +15329,8 @@ snapshots: punycode.js@2.3.1: {} + punycode@1.4.1: {} + punycode@2.3.1: {} pusher-js@8.4.0: @@ -15064,6 +15440,15 @@ snapshots: react-is@16.13.1: {} + react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.7 + redux: 5.0.1 + react-refresh@0.18.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3): @@ -15138,10 +15523,36 @@ snapshots: readdirp@4.1.2: {} + recharts@3.8.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.45.1 + eventemitter3: 5.0.1 + immer: 10.2.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-is: 16.13.1 + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.3) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + rechoir@0.8.0: dependencies: resolve: 1.22.11 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -15248,6 +15659,8 @@ snapshots: dependencies: pe-library: 1.0.1 + reselect@5.1.1: {} + resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} @@ -15366,6 +15779,12 @@ snapshots: safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} scheduler@0.25.0-rc-603e6108-20241029: {} @@ -15616,6 +16035,8 @@ snapshots: - micromark-util-types - supports-color + string-template@0.2.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -15733,6 +16154,8 @@ snapshots: tiny-inflate@1.0.3: {} + tiny-invariant@1.3.3: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -16004,6 +16427,34 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + virtual-dom@2.1.1: + dependencies: + browser-split: 0.0.1 + error: 4.4.0 + ev-store: 7.0.0 + global: 4.4.0 + is-object: 1.0.2 + next-tick: 0.2.2 + x-is-array: 0.1.0 + x-is-string: 0.1.0 + vite-compatible-readable-stream@3.6.1: dependencies: inherits: 2.0.4 @@ -16155,6 +16606,10 @@ snapshots: wrappy@1.0.2: {} + x-is-array@0.1.0: {} + + x-is-string@0.1.0: {} + xlsx@0.18.5: dependencies: adler-32: 1.3.1 @@ -16165,12 +16620,17 @@ snapshots: wmf: 1.0.2 word: 0.3.0 + xmlbuilder2@2.1.2: + dependencies: + '@oozcitak/dom': 1.15.5 + '@oozcitak/infra': 1.0.5 + '@oozcitak/util': 8.3.3 + xmlbuilder@10.1.1: {} xmlbuilder@15.1.1: {} - xtend@4.0.2: - optional: true + xtend@4.0.2: {} y18n@5.0.8: {}