@peaske7/readit 0.1.4 → 0.1.6
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/.agents/skills/remotion-best-practices/SKILL.md +61 -0
- package/.agents/skills/remotion-best-practices/rules/3d.md +86 -0
- package/.agents/skills/remotion-best-practices/rules/animations.md +27 -0
- package/.agents/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +178 -0
- package/.agents/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
- package/.agents/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
- package/.agents/skills/remotion-best-practices/rules/assets.md +78 -0
- package/.agents/skills/remotion-best-practices/rules/audio-visualization.md +198 -0
- package/.agents/skills/remotion-best-practices/rules/audio.md +169 -0
- package/.agents/skills/remotion-best-practices/rules/calculate-metadata.md +134 -0
- package/.agents/skills/remotion-best-practices/rules/can-decode.md +75 -0
- package/.agents/skills/remotion-best-practices/rules/charts.md +120 -0
- package/.agents/skills/remotion-best-practices/rules/compositions.md +154 -0
- package/.agents/skills/remotion-best-practices/rules/display-captions.md +184 -0
- package/.agents/skills/remotion-best-practices/rules/extract-frames.md +229 -0
- package/.agents/skills/remotion-best-practices/rules/ffmpeg.md +38 -0
- package/.agents/skills/remotion-best-practices/rules/fonts.md +152 -0
- package/.agents/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
- package/.agents/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
- package/.agents/skills/remotion-best-practices/rules/get-video-duration.md +60 -0
- package/.agents/skills/remotion-best-practices/rules/gifs.md +141 -0
- package/.agents/skills/remotion-best-practices/rules/images.md +134 -0
- package/.agents/skills/remotion-best-practices/rules/import-srt-captions.md +69 -0
- package/.agents/skills/remotion-best-practices/rules/light-leaks.md +73 -0
- package/.agents/skills/remotion-best-practices/rules/lottie.md +70 -0
- package/.agents/skills/remotion-best-practices/rules/maps.md +412 -0
- package/.agents/skills/remotion-best-practices/rules/measuring-dom-nodes.md +34 -0
- package/.agents/skills/remotion-best-practices/rules/measuring-text.md +140 -0
- package/.agents/skills/remotion-best-practices/rules/parameters.md +109 -0
- package/.agents/skills/remotion-best-practices/rules/sequencing.md +118 -0
- package/.agents/skills/remotion-best-practices/rules/sfx.md +26 -0
- package/.agents/skills/remotion-best-practices/rules/subtitles.md +36 -0
- package/.agents/skills/remotion-best-practices/rules/tailwind.md +11 -0
- package/.agents/skills/remotion-best-practices/rules/text-animations.md +20 -0
- package/.agents/skills/remotion-best-practices/rules/timing.md +179 -0
- package/.agents/skills/remotion-best-practices/rules/transcribe-captions.md +70 -0
- package/.agents/skills/remotion-best-practices/rules/transitions.md +197 -0
- package/.agents/skills/remotion-best-practices/rules/transparent-videos.md +106 -0
- package/.agents/skills/remotion-best-practices/rules/trimming.md +51 -0
- package/.agents/skills/remotion-best-practices/rules/videos.md +171 -0
- package/.agents/skills/remotion-best-practices/rules/voiceover.md +99 -0
- package/.agents/skills/simple/SKILL.md +52 -0
- package/.agents/skills/vercel-react-best-practices/AGENTS.md +3254 -0
- package/.agents/skills/vercel-react-best-practices/README.md +123 -0
- package/.agents/skills/vercel-react-best-practices/SKILL.md +141 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +142 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.claude/CLAUDE.md +142 -0
- package/.claude/commands/review.md +120 -0
- package/.claude/commands/sync-docs.md +71 -0
- package/.claude/roadmap.md +98 -0
- package/.claude/rules/style-guide.md +830 -0
- package/.claude/settings.json +18 -0
- package/.claude/user-stories.md +248 -0
- package/AGENTS.md +64 -0
- package/README.md +7 -7
- package/biome.json +69 -0
- package/bun.lock +1124 -0
- package/docs/design.md +563 -0
- package/docs/plans/2026-03-13-client-mode-design.md +86 -0
- package/docs/plans/2026-03-13-client-mode-plan.md +605 -0
- package/docs/plans/2026-03-13-keyboard-shortcuts-design.md +129 -0
- package/docs/plans/2026-03-13-keyboard-shortcuts-plan.md +1471 -0
- package/docs/plans/2026-03-13-multi-document-design.md +183 -0
- package/docs/plans/2026-03-13-performance-benchmarks-design.md +121 -0
- package/e2e/comments.spec.ts +125 -0
- package/e2e/document-load.spec.ts +54 -0
- package/e2e/export.spec.ts +58 -0
- package/e2e/fixtures/sample.html +13 -0
- package/e2e/fixtures/sample.md +7 -0
- package/e2e/persistence-file.spec.ts +342 -0
- package/e2e/utils/cli.ts +84 -0
- package/e2e/utils/selection.ts +135 -0
- package/{dist/index.html → index.html} +8 -2
- package/lefthook.yml +8 -0
- package/package.json +17 -39
- package/playwright.config.ts +22 -0
- package/skills-lock.json +20 -0
- package/src/App.tsx +396 -0
- package/src/cli/index.ts +467 -0
- package/src/components/ActionsMenu.tsx +110 -0
- package/src/components/DocumentViewer/CodeBlock.tsx +83 -0
- package/src/components/DocumentViewer/DocumentViewer.tsx +257 -0
- package/src/components/DocumentViewer/IframeContainer.tsx +251 -0
- package/src/components/DocumentViewer/MermaidDiagram.tsx +137 -0
- package/src/components/DocumentViewer/index.ts +1 -0
- package/src/components/FloatingTOC.tsx +59 -0
- package/src/components/Header.tsx +63 -0
- package/src/components/InlineEditor.tsx +72 -0
- package/src/components/MarginNote.tsx +198 -0
- package/src/components/MarginNotes.tsx +50 -0
- package/src/components/RawModal.tsx +141 -0
- package/src/components/ReanchorConfirm.tsx +33 -0
- package/src/components/SettingsModal.tsx +221 -0
- package/src/components/ShortcutCapture.tsx +45 -0
- package/src/components/ShortcutList.tsx +157 -0
- package/src/components/TabBar.tsx +60 -0
- package/src/components/TableOfContents.tsx +108 -0
- package/src/components/comments/CommentBadge.tsx +43 -0
- package/src/components/comments/CommentInput.tsx +119 -0
- package/src/components/comments/CommentListItem.tsx +82 -0
- package/src/components/comments/CommentManager.tsx +106 -0
- package/src/components/comments/CommentMinimap.tsx +62 -0
- package/src/components/comments/CommentNav.tsx +104 -0
- package/src/components/ui/ActionBar.tsx +16 -0
- package/src/components/ui/ActionLink.tsx +32 -0
- package/src/components/ui/Button.tsx +55 -0
- package/src/components/ui/Dialog.tsx +156 -0
- package/src/components/ui/DropdownMenu.tsx +114 -0
- package/src/components/ui/SeparatorDot.tsx +9 -0
- package/src/components/ui/Text.tsx +54 -0
- package/src/contexts/CommentContext.tsx +222 -0
- package/src/contexts/LayoutContext.tsx +76 -0
- package/src/hooks/useClickOutside.ts +35 -0
- package/src/hooks/useClipboard.ts +79 -0
- package/src/hooks/useCommentNavigation.ts +130 -0
- package/src/hooks/useComments.ts +323 -0
- package/src/hooks/useDocument.ts +141 -0
- package/src/hooks/useFontPreference.ts +76 -0
- package/src/hooks/useHeadings.test.ts +159 -0
- package/src/hooks/useHeadings.ts +129 -0
- package/src/hooks/useKeybindings.ts +120 -0
- package/src/hooks/useKeyboardShortcuts.ts +63 -0
- package/src/hooks/useLayoutMode.ts +44 -0
- package/src/hooks/useReanchorMode.ts +33 -0
- package/src/hooks/useScrollMetrics.ts +56 -0
- package/src/hooks/useScrollSpy.ts +81 -0
- package/src/hooks/useTextSelection.ts +123 -0
- package/src/hooks/useThemePreference.ts +66 -0
- package/src/index.css +823 -0
- package/src/lib/__fixtures__/bench-data.ts +167 -0
- package/src/lib/anchor.bench.ts +112 -0
- package/src/lib/anchor.test.ts +531 -0
- package/src/lib/anchor.ts +465 -0
- package/src/lib/comment-storage.bench.ts +63 -0
- package/src/lib/comment-storage.test.ts +624 -0
- package/src/lib/comment-storage.ts +263 -0
- package/src/lib/context.bench.ts +41 -0
- package/src/lib/context.test.ts +224 -0
- package/src/lib/context.ts +193 -0
- package/src/lib/export.bench.ts +35 -0
- package/src/lib/export.ts +43 -0
- package/src/lib/highlight/colors.ts +37 -0
- package/src/lib/highlight/core.test.ts +98 -0
- package/src/lib/highlight/core.ts +54 -0
- package/src/lib/highlight/dom.ts +342 -0
- package/src/lib/highlight/highlighter.ts +427 -0
- package/src/lib/highlight/index.ts +23 -0
- package/src/lib/highlight/script-builder.ts +485 -0
- package/src/lib/highlight/types.ts +57 -0
- package/src/lib/html-processor.test.tsx +170 -0
- package/src/lib/html-processor.tsx +95 -0
- package/src/lib/layout-constants.ts +12 -0
- package/src/lib/margin-layout.bench.ts +28 -0
- package/src/lib/margin-layout.ts +100 -0
- package/src/lib/scroll.test.ts +118 -0
- package/src/lib/scroll.ts +47 -0
- package/src/lib/shortcut-registry.test.ts +173 -0
- package/src/lib/shortcut-registry.ts +209 -0
- package/src/lib/utils.test.ts +110 -0
- package/src/lib/utils.ts +61 -0
- package/src/main.tsx +10 -0
- package/src/server/index.ts +883 -0
- package/src/store/index.test.ts +220 -0
- package/src/store/index.ts +234 -0
- package/src/test-setup.ts +1 -0
- package/src/types/index.ts +115 -0
- package/test.md +74 -0
- package/tsconfig.cli.json +12 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +19 -0
- package/vitest.config.ts +15 -0
- package/dist/assets/_basePickBy-hOr-yGsE.js +0 -1
- package/dist/assets/_baseUniq-b7bzdUSn.js +0 -1
- package/dist/assets/arc-D65wG9gm.js +0 -1
- package/dist/assets/architecture-PBZL5I3N-DBa6CAv_.js +0 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-Djwpsh98.js +0 -36
- package/dist/assets/array-DOVTz2Mq.js +0 -1
- package/dist/assets/blockDiagram-WCTKOSBZ-BdW5TTxj.js +0 -132
- package/dist/assets/c4Diagram-IC4MRINW-DTmkHEXu.js +0 -10
- package/dist/assets/channel-B3MUFipN.js +0 -1
- package/dist/assets/chunk-4BX2VUAB-DEqzsvDc.js +0 -1
- package/dist/assets/chunk-55IACEB6-BzVuSUV8.js +0 -1
- package/dist/assets/chunk-7E7YKBS2-CZ8IcA4c.js +0 -1
- package/dist/assets/chunk-7R4GIKGN-CWVVC8HX.js +0 -79
- package/dist/assets/chunk-C72U2L5F-B1Tso5TH.js +0 -1
- package/dist/assets/chunk-EGIJ26TM-Cx_7CFik.js +0 -1
- package/dist/assets/chunk-FMBD7UC4-Cfk_iGhv.js +0 -15
- package/dist/assets/chunk-GEFDOKGD-C_5hRbJt.js +0 -2
- package/dist/assets/chunk-GLR3WWYH-CkY7IyBj.js +0 -2
- package/dist/assets/chunk-HHEYEP7N-B0I4X5cr.js +0 -1
- package/dist/assets/chunk-JSJVCQXG-CAjwlVLg.js +0 -1
- package/dist/assets/chunk-KX2RTZJC-DWqnZZ02.js +0 -1
- package/dist/assets/chunk-KYZI473N-gjRVhJgJ.js +0 -53
- package/dist/assets/chunk-L3YUKLVL-D7C9GuxL.js +0 -1
- package/dist/assets/chunk-MX3YWQON-i-77iuVj.js +0 -1
- package/dist/assets/chunk-NQ4KR5QH-B22Pvemm.js +0 -220
- package/dist/assets/chunk-O4XLMI2P-ZQd5L6ZD.js +0 -7
- package/dist/assets/chunk-OZEHJAEY-BaPKTELw.js +0 -1
- package/dist/assets/chunk-PQ6SQG4A-DqE1eupT.js +0 -1
- package/dist/assets/chunk-PU5JKC2W-BTqWqedh.js +0 -70
- package/dist/assets/chunk-QZHKN3VN-Nm9TvMss.js +0 -1
- package/dist/assets/chunk-R5LLSJPH-DkiNs1dN.js +0 -1
- package/dist/assets/chunk-WL4C6EOR-CioD2fv2.js +0 -189
- package/dist/assets/chunk-XIRO2GV7-B4GGQONY.js +0 -1
- package/dist/assets/chunk-XPW4576I-C0IbbQos.js +0 -32
- package/dist/assets/chunk-XZSTWKYB-DMOqFWmT.js +0 -94
- package/dist/assets/chunk-YBOYWFTD-CoeQgeVY.js +0 -1
- package/dist/assets/classDiagram-VBA2DB6C-DV9ltQ7h.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-C6nD9wmM.js +0 -1
- package/dist/assets/clone-DuY6BQEm.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-B6FexK6p.js +0 -1
- package/dist/assets/cytoscape.esm-DoTFyJaN.js +0 -321
- package/dist/assets/dagre-CCcocoCU.js +0 -1
- package/dist/assets/dagre-KLK3FWXG-DIELowj9.js +0 -4
- package/dist/assets/defaultLocale-Ck2Xxk-C.js +0 -1
- package/dist/assets/diagram-E7M64L7V-D1mm0PoO.js +0 -24
- package/dist/assets/diagram-IFDJBPK2-7DVjly8y.js +0 -43
- package/dist/assets/diagram-P4PSJMXO-jO7pfyMb.js +0 -24
- package/dist/assets/dist-BywRdrPx.js +0 -1
- package/dist/assets/erDiagram-INFDFZHY-DSRxlRFy.js +0 -70
- package/dist/assets/flowDiagram-PKNHOUZH-CgKzzNdR.js +0 -162
- package/dist/assets/ganttDiagram-A5KZAMGK-CtsE7Y4E.js +0 -292
- package/dist/assets/gitGraph-HDMCJU4V-BU9uhwtz.js +0 -1
- package/dist/assets/gitGraphDiagram-K3NZZRJ6-DOU8RGdw.js +0 -65
- package/dist/assets/graphlib-WkJoBgka.js +0 -1
- package/dist/assets/index-CKVArt9D.js +0 -562
- package/dist/assets/index-DzRKJazf.css +0 -2
- package/dist/assets/info-3K5VOQVL-CPpvM-SG.js +0 -1
- package/dist/assets/infoDiagram-LFFYTUFH-VKLs5DsF.js +0 -2
- package/dist/assets/init-Bft5Ffpj.js +0 -1
- package/dist/assets/isArrayLikeObject-icl0H0jo.js +0 -1
- package/dist/assets/isEmpty-Du8sNmkE.js +0 -1
- package/dist/assets/ishikawaDiagram-PHBUUO56-CsWvEjux.js +0 -70
- package/dist/assets/journeyDiagram-4ABVD52K-BzJGTdIT.js +0 -139
- package/dist/assets/kanban-definition-K7BYSVSG-B_9ClJ1A.js +0 -89
- package/dist/assets/katex-BJrMXEjr.js +0 -261
- package/dist/assets/line-CC_tDGId.js +0 -1
- package/dist/assets/linear-Cts_d04Y.js +0 -1
- package/dist/assets/math-CNhlSIO3.js +0 -1
- package/dist/assets/mermaid-parser.core-Vb9KKv1R.js +0 -4
- package/dist/assets/mermaid.core-C_7xsp3d.js +0 -11
- package/dist/assets/mindmap-definition-YRQLILUH-BWmfy5wB.js +0 -68
- package/dist/assets/ordinal-DIg8h6NI.js +0 -1
- package/dist/assets/packet-RMMSAZCW-Q-WG6o3b.js +0 -1
- package/dist/assets/path-DfRbCp9y.js +0 -1
- package/dist/assets/pie-UPGHQEXC-Cwi2tLlt.js +0 -1
- package/dist/assets/pieDiagram-SKSYHLDU-Dyf3X_in.js +0 -30
- package/dist/assets/quadrantDiagram-337W2JSQ-B5_5m61Q.js +0 -7
- package/dist/assets/radar-KQ55EAFF-Dtw2VzxY.js +0 -1
- package/dist/assets/requirementDiagram-Z7DCOOCP-BSERBnlW.js +0 -73
- package/dist/assets/rough.esm-KjoEK0it.js +0 -1
- package/dist/assets/sankeyDiagram-WA2Y5GQK-CMcEY8Cz.js +0 -10
- package/dist/assets/sequenceDiagram-2WXFIKYE-D28qcXwC.js +0 -145
- package/dist/assets/src-C8kkzlHX.js +0 -1
- package/dist/assets/stateDiagram-RAJIS63D-7oVrCmRl.js +0 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-DtFptQAd.js +0 -1
- package/dist/assets/timeline-definition-YZTLITO2-rbCfBEvG.js +0 -61
- package/dist/assets/treemap-KZPCXAKY-BlRvF0um.js +0 -1
- package/dist/assets/vennDiagram-LZ73GAT5-DBit3zWa.js +0 -34
- package/dist/assets/xychartDiagram-JWTSCODW-BVYXv51y.js +0 -7
- package/dist/index.js +0 -1040
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { toast } from "sonner";
|
|
3
|
+
import { extractContext, formatForLLM } from "../lib/context";
|
|
4
|
+
import {
|
|
5
|
+
exportCommentsAsJson,
|
|
6
|
+
generatePrompt,
|
|
7
|
+
generateRawText,
|
|
8
|
+
} from "../lib/export";
|
|
9
|
+
import { truncate } from "../lib/utils";
|
|
10
|
+
import type { Comment, Document, Selection } from "../types";
|
|
11
|
+
|
|
12
|
+
interface UseClipboardParams {
|
|
13
|
+
comments: Comment[];
|
|
14
|
+
document: Document | undefined;
|
|
15
|
+
selection: Selection | undefined;
|
|
16
|
+
clearSelection: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useClipboard({
|
|
20
|
+
comments,
|
|
21
|
+
document,
|
|
22
|
+
selection,
|
|
23
|
+
clearSelection,
|
|
24
|
+
}: UseClipboardParams) {
|
|
25
|
+
// Export handlers
|
|
26
|
+
const copyAll = useCallback(() => {
|
|
27
|
+
if (!document) return;
|
|
28
|
+
const prompt = generatePrompt(comments, document.fileName);
|
|
29
|
+
navigator.clipboard.writeText(prompt);
|
|
30
|
+
toast.success("Copied all comments");
|
|
31
|
+
}, [comments, document]);
|
|
32
|
+
|
|
33
|
+
const copyAllRaw = useCallback(() => {
|
|
34
|
+
if (!document) return;
|
|
35
|
+
const raw = generateRawText(comments);
|
|
36
|
+
navigator.clipboard.writeText(raw);
|
|
37
|
+
toast.success("Copied all comments as raw text");
|
|
38
|
+
}, [comments, document]);
|
|
39
|
+
|
|
40
|
+
const exportJson = useCallback(() => {
|
|
41
|
+
if (!document) return;
|
|
42
|
+
exportCommentsAsJson(comments, document);
|
|
43
|
+
}, [comments, document]);
|
|
44
|
+
|
|
45
|
+
// Selection copy handlers
|
|
46
|
+
const copySelectionRaw = useCallback(() => {
|
|
47
|
+
if (!selection) return;
|
|
48
|
+
|
|
49
|
+
navigator.clipboard.writeText(selection.text);
|
|
50
|
+
toast.success(`Copied: "${truncate(selection.text)}"`);
|
|
51
|
+
clearSelection();
|
|
52
|
+
}, [selection, clearSelection]);
|
|
53
|
+
|
|
54
|
+
const copySelectionForLLM = useCallback(() => {
|
|
55
|
+
if (!selection || !document) return;
|
|
56
|
+
|
|
57
|
+
const context = extractContext({
|
|
58
|
+
content: document.content,
|
|
59
|
+
startOffset: selection.startOffset,
|
|
60
|
+
endOffset: selection.endOffset,
|
|
61
|
+
});
|
|
62
|
+
const formatted = formatForLLM({
|
|
63
|
+
context,
|
|
64
|
+
fileName: document.fileName,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
navigator.clipboard.writeText(formatted);
|
|
68
|
+
toast.success(`Copied for LLM: "${truncate(selection.text)}"`);
|
|
69
|
+
clearSelection();
|
|
70
|
+
}, [selection, document, clearSelection]);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
copyAll,
|
|
74
|
+
copyAllRaw,
|
|
75
|
+
exportJson,
|
|
76
|
+
copySelectionRaw,
|
|
77
|
+
copySelectionForLLM,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { appStore, useAppStore } from "../store";
|
|
3
|
+
import type { Comment } from "../types";
|
|
4
|
+
|
|
5
|
+
interface UseCommentNavigationResult {
|
|
6
|
+
currentIndex: number;
|
|
7
|
+
hoveredCommentId: string | undefined;
|
|
8
|
+
setHoveredCommentId: (id: string | undefined) => void;
|
|
9
|
+
navigateToComment: (commentId: string) => void;
|
|
10
|
+
navigatePrevious: () => void;
|
|
11
|
+
navigateNext: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Manage comment navigation with cycling, keyboard shortcuts, and scroll-to-comment.
|
|
16
|
+
* Handles Alt+↑/↓ keyboard navigation.
|
|
17
|
+
*/
|
|
18
|
+
export function useCommentNavigation(
|
|
19
|
+
sortedComments: Comment[],
|
|
20
|
+
): UseCommentNavigationResult {
|
|
21
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
22
|
+
const hoveredCommentId = useAppStore(
|
|
23
|
+
(s) => s.getActiveDocumentState()?.hoveredCommentId,
|
|
24
|
+
);
|
|
25
|
+
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
26
|
+
undefined,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Keep a ref to sortedComments so navigation callbacks stay stable
|
|
30
|
+
const sortedRef = useRef(sortedComments);
|
|
31
|
+
sortedRef.current = sortedComments;
|
|
32
|
+
|
|
33
|
+
// Cleanup hover timeout on unmount
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
return () => clearTimeout(hoverTimeoutRef.current);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
// Clamp index when comments are removed (derived during render, no effect needed)
|
|
39
|
+
const clampedIndex =
|
|
40
|
+
sortedComments.length === 0
|
|
41
|
+
? 0
|
|
42
|
+
: Math.min(currentIndex, sortedComments.length - 1);
|
|
43
|
+
if (clampedIndex !== currentIndex) {
|
|
44
|
+
setCurrentIndex(clampedIndex);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Update DOM data-focused attributes imperatively
|
|
48
|
+
const updateFocusedMarks = useCallback((commentId: string | undefined) => {
|
|
49
|
+
const marks = window.document.querySelectorAll("mark[data-comment-id]");
|
|
50
|
+
for (const mark of marks) {
|
|
51
|
+
const id = mark.getAttribute("data-comment-id");
|
|
52
|
+
if (id === commentId) {
|
|
53
|
+
mark.setAttribute("data-focused", "true");
|
|
54
|
+
} else {
|
|
55
|
+
mark.removeAttribute("data-focused");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const setHoveredCommentId = useCallback(
|
|
61
|
+
(id: string | undefined) => {
|
|
62
|
+
appStore.getState().setHoveredCommentId(id);
|
|
63
|
+
updateFocusedMarks(id);
|
|
64
|
+
},
|
|
65
|
+
[updateFocusedMarks],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Navigate to a comment by scrolling its highlight into view
|
|
69
|
+
const navigateToComment = useCallback(
|
|
70
|
+
(commentId: string) => {
|
|
71
|
+
const selector = `mark[data-comment-id="${commentId}"]`;
|
|
72
|
+
|
|
73
|
+
const scrollAndHighlight = (element: Element) => {
|
|
74
|
+
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
75
|
+
setHoveredCommentId(commentId);
|
|
76
|
+
clearTimeout(hoverTimeoutRef.current);
|
|
77
|
+
hoverTimeoutRef.current = setTimeout(
|
|
78
|
+
() => setHoveredCommentId(undefined),
|
|
79
|
+
1500,
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Try main document first (for markdown)
|
|
84
|
+
const mainHighlight = document.querySelector(selector);
|
|
85
|
+
if (mainHighlight) {
|
|
86
|
+
scrollAndHighlight(mainHighlight);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Try inside iframe (for HTML content)
|
|
91
|
+
const iframe = document.querySelector("iframe");
|
|
92
|
+
const iframeHighlight = iframe?.contentDocument?.querySelector(selector);
|
|
93
|
+
if (iframeHighlight) {
|
|
94
|
+
scrollAndHighlight(iframeHighlight);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[setHoveredCommentId],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Navigate to previous comment (cycles to last when at first)
|
|
101
|
+
const navigatePrevious = useCallback(() => {
|
|
102
|
+
const sc = sortedRef.current;
|
|
103
|
+
if (sc.length === 0) return;
|
|
104
|
+
setCurrentIndex((prev) => {
|
|
105
|
+
const newIndex = prev === 0 ? sc.length - 1 : prev - 1;
|
|
106
|
+
navigateToComment(sc[newIndex].id);
|
|
107
|
+
return newIndex;
|
|
108
|
+
});
|
|
109
|
+
}, [navigateToComment]);
|
|
110
|
+
|
|
111
|
+
// Navigate to next comment (cycles to first when at last)
|
|
112
|
+
const navigateNext = useCallback(() => {
|
|
113
|
+
const sc = sortedRef.current;
|
|
114
|
+
if (sc.length === 0) return;
|
|
115
|
+
setCurrentIndex((prev) => {
|
|
116
|
+
const newIndex = prev === sc.length - 1 ? 0 : prev + 1;
|
|
117
|
+
navigateToComment(sc[newIndex].id);
|
|
118
|
+
return newIndex;
|
|
119
|
+
});
|
|
120
|
+
}, [navigateToComment]);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
currentIndex: clampedIndex,
|
|
124
|
+
hoveredCommentId,
|
|
125
|
+
setHoveredCommentId,
|
|
126
|
+
navigateToComment,
|
|
127
|
+
navigatePrevious,
|
|
128
|
+
navigateNext,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { appStore, useAppStore } from "../store";
|
|
3
|
+
import { AnchorConfidences, type Comment } from "../types";
|
|
4
|
+
|
|
5
|
+
interface UseCommentsOptions {
|
|
6
|
+
clean?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface UseCommentsResult {
|
|
10
|
+
comments: Comment[];
|
|
11
|
+
error?: string;
|
|
12
|
+
addComment: (
|
|
13
|
+
selectedText: string,
|
|
14
|
+
comment: string,
|
|
15
|
+
startOffset: number,
|
|
16
|
+
endOffset: number,
|
|
17
|
+
) => void;
|
|
18
|
+
deleteComment: (id: string) => void;
|
|
19
|
+
deleteAll: () => void;
|
|
20
|
+
editComment: (id: string, newText: string) => void;
|
|
21
|
+
reanchorComment: (
|
|
22
|
+
id: string,
|
|
23
|
+
selectedText: string,
|
|
24
|
+
startOffset: number,
|
|
25
|
+
endOffset: number,
|
|
26
|
+
) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Hook for managing comments with optimistic updates.
|
|
31
|
+
* State lives in the Zustand store; this hook coordinates API mutations.
|
|
32
|
+
*/
|
|
33
|
+
export function useComments(
|
|
34
|
+
filePath: string | null,
|
|
35
|
+
options: UseCommentsOptions = {},
|
|
36
|
+
): UseCommentsResult {
|
|
37
|
+
const { clean = false } = options;
|
|
38
|
+
|
|
39
|
+
// Read comments and error from the store
|
|
40
|
+
const comments = useAppStore(
|
|
41
|
+
(s) => s.documents.get(filePath ?? "")?.comments ?? [],
|
|
42
|
+
);
|
|
43
|
+
const error = useAppStore(
|
|
44
|
+
(s) => s.documents.get(filePath ?? "")?.commentsError ?? undefined,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Track pending operations for rollback on error
|
|
48
|
+
const pendingOperations = useRef<Map<string, Comment[]>>(new Map());
|
|
49
|
+
|
|
50
|
+
// Capture filePath at call time for stable closures
|
|
51
|
+
const filePathRef = useRef(filePath);
|
|
52
|
+
filePathRef.current = filePath;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Execute an optimistic mutation with automatic rollback on error.
|
|
56
|
+
*/
|
|
57
|
+
const executeMutation = useCallback(
|
|
58
|
+
async <T>({
|
|
59
|
+
operationId,
|
|
60
|
+
optimisticUpdate,
|
|
61
|
+
apiCall,
|
|
62
|
+
onSuccess,
|
|
63
|
+
errorMessage,
|
|
64
|
+
}: {
|
|
65
|
+
operationId: string;
|
|
66
|
+
optimisticUpdate: (prev: Comment[]) => Comment[];
|
|
67
|
+
apiCall: () => Promise<T>;
|
|
68
|
+
onSuccess?: (result: T, prev: Comment[]) => Comment[];
|
|
69
|
+
errorMessage: string;
|
|
70
|
+
}) => {
|
|
71
|
+
const fp = filePathRef.current;
|
|
72
|
+
if (!fp) return;
|
|
73
|
+
|
|
74
|
+
// Read current comments from store for rollback
|
|
75
|
+
const currentDocState = appStore.getState().documents.get(fp);
|
|
76
|
+
const previousComments = [...(currentDocState?.comments ?? [])];
|
|
77
|
+
pendingOperations.current.set(operationId, previousComments);
|
|
78
|
+
|
|
79
|
+
// Apply optimistic update
|
|
80
|
+
appStore.getState().setComments(optimisticUpdate(previousComments), fp);
|
|
81
|
+
appStore.getState().setCommentsError(null, fp);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const result = await apiCall();
|
|
85
|
+
|
|
86
|
+
// Apply server response transformation if provided
|
|
87
|
+
if (onSuccess) {
|
|
88
|
+
const current = appStore.getState().documents.get(fp)?.comments ?? [];
|
|
89
|
+
appStore.getState().setComments(onSuccess(result, current), fp);
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(`${errorMessage}:`, err);
|
|
93
|
+
appStore
|
|
94
|
+
.getState()
|
|
95
|
+
.setCommentsError(
|
|
96
|
+
err instanceof Error ? err.message : errorMessage,
|
|
97
|
+
fp,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Rollback on error
|
|
101
|
+
const rollback = pendingOperations.current.get(operationId);
|
|
102
|
+
if (rollback) {
|
|
103
|
+
appStore.getState().setComments(rollback, fp);
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
pendingOperations.current.delete(operationId);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
[],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Build path-scoped API URL
|
|
113
|
+
const pathQuery = useCallback((base: string) => {
|
|
114
|
+
const fp = filePathRef.current;
|
|
115
|
+
if (!fp) return base;
|
|
116
|
+
return `${base}?path=${encodeURIComponent(fp)}`;
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
// Load comments from API
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!filePath) return;
|
|
122
|
+
|
|
123
|
+
const loadComments = async () => {
|
|
124
|
+
appStore.getState().setCommentsError(null, filePath);
|
|
125
|
+
const query = `?path=${encodeURIComponent(filePath)}`;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
// If clean flag is set, clear comments first
|
|
129
|
+
if (clean) {
|
|
130
|
+
await fetch(`/api/comments${query}`, { method: "DELETE" });
|
|
131
|
+
appStore.getState().setComments([], filePath);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const response = await fetch(`/api/comments${query}`);
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
throw new Error(`Failed to load comments: ${response.statusText}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const data = await response.json();
|
|
141
|
+
appStore.getState().setComments(data.comments || [], filePath);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error("Failed to load comments:", err);
|
|
144
|
+
appStore
|
|
145
|
+
.getState()
|
|
146
|
+
.setCommentsError(
|
|
147
|
+
err instanceof Error ? err.message : "Failed to load comments",
|
|
148
|
+
filePath,
|
|
149
|
+
);
|
|
150
|
+
appStore.getState().setComments([], filePath);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
loadComments();
|
|
155
|
+
}, [filePath, clean]);
|
|
156
|
+
|
|
157
|
+
const addComment = useCallback(
|
|
158
|
+
(
|
|
159
|
+
selectedText: string,
|
|
160
|
+
commentText: string,
|
|
161
|
+
startOffset: number,
|
|
162
|
+
endOffset: number,
|
|
163
|
+
) => {
|
|
164
|
+
const tempId = `temp-${crypto.randomUUID()}`;
|
|
165
|
+
const optimisticComment: Comment = {
|
|
166
|
+
id: tempId,
|
|
167
|
+
selectedText,
|
|
168
|
+
comment: commentText.trim(),
|
|
169
|
+
createdAt: new Date().toISOString(),
|
|
170
|
+
startOffset,
|
|
171
|
+
endOffset,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
executeMutation({
|
|
175
|
+
operationId: tempId,
|
|
176
|
+
optimisticUpdate: (prev) => [...prev, optimisticComment],
|
|
177
|
+
apiCall: async () => {
|
|
178
|
+
const response = await fetch(pathQuery("/api/comments"), {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "Content-Type": "application/json" },
|
|
181
|
+
body: JSON.stringify({
|
|
182
|
+
selectedText,
|
|
183
|
+
comment: commentText.trim(),
|
|
184
|
+
startOffset,
|
|
185
|
+
endOffset,
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new Error(`Failed to add comment: ${response.statusText}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return response.json();
|
|
194
|
+
},
|
|
195
|
+
onSuccess: (data, prev) =>
|
|
196
|
+
prev.map((c) => (c.id === tempId ? data.comment : c)),
|
|
197
|
+
errorMessage: "Failed to add comment",
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
[executeMutation, pathQuery],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const deleteComment = useCallback(
|
|
204
|
+
(id: string) => {
|
|
205
|
+
executeMutation({
|
|
206
|
+
operationId: `delete-${id}`,
|
|
207
|
+
optimisticUpdate: (prev) => prev.filter((c) => c.id !== id),
|
|
208
|
+
apiCall: async () => {
|
|
209
|
+
const response = await fetch(pathQuery(`/api/comments/${id}`), {
|
|
210
|
+
method: "DELETE",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
throw new Error(`Failed to delete comment: ${response.statusText}`);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
errorMessage: "Failed to delete comment",
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
[executeMutation, pathQuery],
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const deleteAll = useCallback(() => {
|
|
224
|
+
executeMutation({
|
|
225
|
+
operationId: "delete-all",
|
|
226
|
+
optimisticUpdate: () => [],
|
|
227
|
+
apiCall: async () => {
|
|
228
|
+
const response = await fetch(pathQuery("/api/comments"), {
|
|
229
|
+
method: "DELETE",
|
|
230
|
+
});
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Failed to delete all comments: ${response.statusText}`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
errorMessage: "Failed to delete all comments",
|
|
238
|
+
});
|
|
239
|
+
}, [executeMutation, pathQuery]);
|
|
240
|
+
|
|
241
|
+
const editComment = useCallback(
|
|
242
|
+
(id: string, newText: string) => {
|
|
243
|
+
const trimmed = newText.trim();
|
|
244
|
+
if (!trimmed) return;
|
|
245
|
+
|
|
246
|
+
executeMutation({
|
|
247
|
+
operationId: `edit-${id}`,
|
|
248
|
+
optimisticUpdate: (prev) =>
|
|
249
|
+
prev.map((c) => (c.id === id ? { ...c, comment: trimmed } : c)),
|
|
250
|
+
apiCall: async () => {
|
|
251
|
+
const response = await fetch(pathQuery(`/api/comments/${id}`), {
|
|
252
|
+
method: "PUT",
|
|
253
|
+
headers: { "Content-Type": "application/json" },
|
|
254
|
+
body: JSON.stringify({ comment: trimmed }),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
throw new Error(`Failed to update comment: ${response.statusText}`);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
errorMessage: "Failed to edit comment",
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
[executeMutation, pathQuery],
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const reanchorComment = useCallback(
|
|
268
|
+
(
|
|
269
|
+
id: string,
|
|
270
|
+
selectedText: string,
|
|
271
|
+
startOffset: number,
|
|
272
|
+
endOffset: number,
|
|
273
|
+
) => {
|
|
274
|
+
executeMutation({
|
|
275
|
+
operationId: `reanchor-${id}`,
|
|
276
|
+
optimisticUpdate: (prev) =>
|
|
277
|
+
prev.map((c) =>
|
|
278
|
+
c.id === id
|
|
279
|
+
? {
|
|
280
|
+
...c,
|
|
281
|
+
selectedText,
|
|
282
|
+
startOffset,
|
|
283
|
+
endOffset,
|
|
284
|
+
anchorConfidence: AnchorConfidences.EXACT,
|
|
285
|
+
}
|
|
286
|
+
: c,
|
|
287
|
+
),
|
|
288
|
+
apiCall: async () => {
|
|
289
|
+
const response = await fetch(
|
|
290
|
+
pathQuery(`/api/comments/${id}/reanchor`),
|
|
291
|
+
{
|
|
292
|
+
method: "PUT",
|
|
293
|
+
headers: { "Content-Type": "application/json" },
|
|
294
|
+
body: JSON.stringify({ selectedText, startOffset, endOffset }),
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
if (!response.ok) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Failed to re-anchor comment: ${response.statusText}`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return response.json();
|
|
305
|
+
},
|
|
306
|
+
onSuccess: (data, prev) =>
|
|
307
|
+
prev.map((c) => (c.id === id ? data.comment : c)),
|
|
308
|
+
errorMessage: "Failed to re-anchor comment",
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
[executeMutation, pathQuery],
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
comments,
|
|
316
|
+
error,
|
|
317
|
+
addComment,
|
|
318
|
+
deleteComment,
|
|
319
|
+
deleteAll,
|
|
320
|
+
editComment,
|
|
321
|
+
reanchorComment,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { toast } from "sonner";
|
|
3
|
+
import { appStore, useAppStore } from "../store";
|
|
4
|
+
import type { Document } from "../types";
|
|
5
|
+
|
|
6
|
+
interface UseDocumentResult {
|
|
7
|
+
document: Document | null;
|
|
8
|
+
error: string | null;
|
|
9
|
+
isInitialized: boolean;
|
|
10
|
+
reload: () => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Manage multi-document loading, lazy content fetching, and live reloading.
|
|
15
|
+
*
|
|
16
|
+
* On mount: fetches the document list from `/api/documents` and opens all
|
|
17
|
+
* files in the store. Content is loaded lazily when a tab becomes active.
|
|
18
|
+
* SSE events trigger content updates for already-loaded documents.
|
|
19
|
+
*/
|
|
20
|
+
export function useDocument(): UseDocumentResult {
|
|
21
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
23
|
+
|
|
24
|
+
const activeDocumentPath = useAppStore((s) => s.activeDocumentPath);
|
|
25
|
+
|
|
26
|
+
// Active document — null until content is loaded
|
|
27
|
+
const document = useAppStore((s) => {
|
|
28
|
+
const ds = s.getActiveDocumentState();
|
|
29
|
+
if (!ds || !ds.document.content) return null;
|
|
30
|
+
return ds.document;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Fetch document list on mount, populate store
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
async function init() {
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch("/api/documents");
|
|
38
|
+
if (!res.ok) throw new Error(`Server error: ${res.status}`);
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
|
|
41
|
+
const clean = data.clean || false;
|
|
42
|
+
for (const file of data.files) {
|
|
43
|
+
appStore.getState().openDocument({
|
|
44
|
+
content: "", // Content loaded lazily on tab activation
|
|
45
|
+
type: file.type,
|
|
46
|
+
filePath: file.path,
|
|
47
|
+
fileName: file.fileName,
|
|
48
|
+
clean,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
setError(
|
|
53
|
+
err instanceof Error ? err.message : "Failed to load documents",
|
|
54
|
+
);
|
|
55
|
+
} finally {
|
|
56
|
+
setIsInitialized(true);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
init();
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
// Load content when active document changes and has no content yet
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!activeDocumentPath) return;
|
|
65
|
+
const state = appStore.getState().documents.get(activeDocumentPath);
|
|
66
|
+
if (!state || state.document.content) return;
|
|
67
|
+
|
|
68
|
+
async function loadContent() {
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(
|
|
71
|
+
`/api/document?path=${encodeURIComponent(activeDocumentPath!)}`,
|
|
72
|
+
);
|
|
73
|
+
if (!res.ok) throw new Error(`Server error: ${res.status}`);
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
appStore
|
|
76
|
+
.getState()
|
|
77
|
+
.updateDocumentContent(data.content, activeDocumentPath!);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
setError(
|
|
80
|
+
err instanceof Error ? err.message : "Failed to load document",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
loadContent();
|
|
85
|
+
}, [activeDocumentPath]);
|
|
86
|
+
|
|
87
|
+
// SSE: listen for file updates, reload content for already-loaded documents
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const eventSource = new EventSource("/api/document/stream");
|
|
90
|
+
eventSource.onmessage = async (e) => {
|
|
91
|
+
try {
|
|
92
|
+
const data = JSON.parse(e.data);
|
|
93
|
+
if (data.type === "file-added" && data.path) {
|
|
94
|
+
appStore.getState().openDocument({
|
|
95
|
+
content: "", // Lazy-loaded when tab activated
|
|
96
|
+
type: data.fileType,
|
|
97
|
+
filePath: data.path,
|
|
98
|
+
fileName: data.fileName,
|
|
99
|
+
clean: false,
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (data.type === "update" && data.path) {
|
|
104
|
+
// Only reload if content was previously loaded
|
|
105
|
+
const state = appStore.getState().documents.get(data.path);
|
|
106
|
+
if (!state || !state.document.content) return;
|
|
107
|
+
|
|
108
|
+
const res = await fetch(
|
|
109
|
+
`/api/document?path=${encodeURIComponent(data.path)}`,
|
|
110
|
+
);
|
|
111
|
+
if (res.ok) {
|
|
112
|
+
const doc = await res.json();
|
|
113
|
+
appStore.getState().updateDocumentContent(doc.content, data.path);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// Ignore non-JSON messages ("connected", "ping")
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
return () => eventSource.close();
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
const reload = useCallback(async () => {
|
|
124
|
+
if (!activeDocumentPath) return;
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch(
|
|
127
|
+
`/api/document?path=${encodeURIComponent(activeDocumentPath)}`,
|
|
128
|
+
);
|
|
129
|
+
if (!res.ok) throw new Error(`Server error: ${res.status}`);
|
|
130
|
+
const data = await res.json();
|
|
131
|
+
appStore
|
|
132
|
+
.getState()
|
|
133
|
+
.updateDocumentContent(data.content, activeDocumentPath);
|
|
134
|
+
toast.success("Document reloaded");
|
|
135
|
+
} catch (err) {
|
|
136
|
+
toast.error(err instanceof Error ? err.message : "Failed to reload");
|
|
137
|
+
}
|
|
138
|
+
}, [activeDocumentPath]);
|
|
139
|
+
|
|
140
|
+
return { document, error, isInitialized, reload };
|
|
141
|
+
}
|