feat: introduce a new markdown editor component and enhance UI panel state management, while banning the standard library logger.
This commit is contained in:
28
.claude/hookify.ban-stdlib-logger.local.md
Normal file
28
.claude/hookify.ban-stdlib-logger.local.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: ban-stdlib-logger
|
||||
enabled: true
|
||||
event: file
|
||||
action: block
|
||||
conditions:
|
||||
- field: file_path
|
||||
operator: regex_match
|
||||
pattern: \.py$
|
||||
- field: new_text
|
||||
operator: regex_match
|
||||
pattern: import logging|from logging import|logging\.getLogger
|
||||
---
|
||||
|
||||
**Stdlib logger is banned in this project.**
|
||||
|
||||
Use the project's custom logger instead:
|
||||
|
||||
```python
|
||||
from noteflow.infrastructure.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
```
|
||||
|
||||
**Why:**
|
||||
- Project uses structured logging with `get_logger()`
|
||||
- Custom logger provides consistent formatting, context injection, and observability features
|
||||
- Stdlib logger bypasses project logging configuration
|
||||
@@ -17,5 +17,8 @@
|
||||
"enabledMcpjsonServers": [
|
||||
"lightrag-mcp"
|
||||
],
|
||||
"outputStyle": "YAML Structured"
|
||||
"outputStyle": "YAML Structured",
|
||||
"enabledPlugins": {
|
||||
"frontend-design@claude-code-plugins": true
|
||||
}
|
||||
}
|
||||
|
||||
854
client/package-lock.json
generated
854
client/package-lock.json
generated
@@ -43,6 +43,11 @@
|
||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@tiptap/extension-placeholder": "^3.15.3",
|
||||
"@tiptap/extension-task-item": "^3.15.3",
|
||||
"@tiptap/extension-task-list": "^3.15.3",
|
||||
"@tiptap/react": "^3.15.3",
|
||||
"@tiptap/starter-kit": "^3.15.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -63,6 +68,7 @@
|
||||
"sonner": "^1.7.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"vaul": "^0.9.9",
|
||||
"vitest": "^4.0.16",
|
||||
"zod": "^3.25.76"
|
||||
@@ -3535,6 +3541,12 @@
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@remirror/core-constants": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
|
||||
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||
@@ -4518,6 +4530,479 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.15.3.tgz",
|
||||
"integrity": "sha512-bmXydIHfm2rEtGju39FiQNfzkFx9CDvJe+xem1dgEZ2P6Dj7nQX9LnA1ZscW7TuzbBRkL5p3dwuBIi3f62A66A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/pm": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-blockquote": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.15.3.tgz",
|
||||
"integrity": "sha512-13x5UsQXtttFpoS/n1q173OeurNxppsdWgP3JfsshzyxIghhC141uL3H6SGYQLPU31AizgDs2OEzt6cSUevaZg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bold": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.15.3.tgz",
|
||||
"integrity": "sha512-I8JYbkkUTNUXbHd/wCse2bR0QhQtJD7+0/lgrKOmGfv5ioLxcki079Nzuqqay3PjgYoJLIJQvm3RAGxT+4X91w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bubble-menu": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.15.3.tgz",
|
||||
"integrity": "sha512-e88DG1bTy6hKxrt7iPVQhJnH5/EOrnKpIyp09dfRDgWrrW88fE0Qjys7a/eT8W+sXyXM3z10Ye7zpERWsrLZDg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3",
|
||||
"@tiptap/pm": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bullet-list": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.15.3.tgz",
|
||||
"integrity": "sha512-MGwEkNT7ltst6XaWf0ObNgpKQ4PvuuV3igkBrdYnQS+qaAx9IF4isygVPqUc9DvjYC306jpyKsNqNrENIXcosA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.15.3.tgz",
|
||||
"integrity": "sha512-x6LFt3Og6MFINYpsMzrJnz7vaT9Yk1t4oXkbJsJRSavdIWBEBcoRudKZ4sSe/AnsYlRJs8FY2uR76mt9e+7xAQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code-block": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.15.3.tgz",
|
||||
"integrity": "sha512-q1UB9icNfdJppTqMIUWfoRKkx5SSdWIpwZoL2NeOI5Ah3E20/dQKVttIgLhsE521chyvxCYCRaHD5tMNGKfhyw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3",
|
||||
"@tiptap/pm": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-document": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.15.3.tgz",
|
||||
"integrity": "sha512-AC72nI2gnogBuETCKbZekn+h6t5FGGcZG2abPGKbz/x9rwpb6qV2hcbAQ30t6M7H6cTOh2/Ut8bEV2MtMB15sw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-dropcursor": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.15.3.tgz",
|
||||
"integrity": "sha512-jGI5XZpdo8GSYQFj7HY15/oEwC2m2TqZz0/Fln5qIhY32XlZhWrsMuMI6WbUJrTH16es7xO6jmRlDsc6g+vJWg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-floating-menu": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.15.3.tgz",
|
||||
"integrity": "sha512-+3DVBleKKffadEJEdLYxmYAJOjHjLSqtiSFUE3RABT4V2ka1ODy2NIpyKX0o1SvQ5N1jViYT9Q+yUbNa6zCcDw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@floating-ui/dom": "^1.0.0",
|
||||
"@tiptap/core": "^3.15.3",
|
||||
"@tiptap/pm": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-gapcursor": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.15.3.tgz",
|
||||
"integrity": "sha512-Kaw0sNzP0bQI/xEAMSfIpja6xhsu9WqqAK/puzOIS1RKWO47Wps/tzqdSJ9gfslPIb5uY5mKCfy8UR8Xgiia8w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-hard-break": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.15.3.tgz",
|
||||
"integrity": "sha512-8HjxmeRbBiXW+7JKemAJtZtHlmXQ9iji398CPQ0yYde68WbIvUhHXjmbJE5pxFvvQTJ/zJv1aISeEOZN2bKBaw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-heading": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.15.3.tgz",
|
||||
"integrity": "sha512-G1GG6iN1YXPS+75arDpo+bYRzhr3dNDw99c7D7na3aDawa9Qp7sZ/bVrzFUUcVEce0cD6h83yY7AooBxEc67hA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.15.3.tgz",
|
||||
"integrity": "sha512-FYkN7L6JsfwwNEntmLklCVKvgL0B0N47OXMacRk6kYKQmVQ4Nvc7q/VJLpD9sk4wh4KT1aiCBfhKEBTu5pv1fg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3",
|
||||
"@tiptap/pm": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-italic": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.15.3.tgz",
|
||||
"integrity": "sha512-6XeuPjcWy7OBxpkgOV7bD6PATO5jhIxc8SEK4m8xn8nelGTBIbHGqK37evRv+QkC7E0MUryLtzwnmmiaxcKL0Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-link": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.15.3.tgz",
|
||||
"integrity": "sha512-PdDXyBF9Wco9U1x6e+b7tKBWG+kqBDXDmaYXHkFm/gYuQCQafVJ5mdrDdKgkHDWVnJzMWZXBcZjT9r57qtlLWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"linkifyjs": "^4.3.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3",
|
||||
"@tiptap/pm": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.15.3.tgz",
|
||||
"integrity": "sha512-n7y/MF9lAM5qlpuH5IR4/uq+kJPEJpe9NrEiH+NmkO/5KJ6cXzpJ6F4U17sMLf2SNCq+TWN9QK8QzoKxIn50VQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3",
|
||||
"@tiptap/pm": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-item": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.15.3.tgz",
|
||||
"integrity": "sha512-CCxL5ek1p0lO5e8aqhnPzIySldXRSigBFk2fP9OLgdl5qKFLs2MGc19jFlx5+/kjXnEsdQTFbGY1Sizzt0TVDw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-keymap": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.15.3.tgz",
|
||||
"integrity": "sha512-UxqnTEEAKrL+wFQeSyC9z0mgyUUVRS2WTcVFoLZCE6/Xus9F53S4bl7VKFadjmqI4GpDk5Oe2IOUc72o129jWg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-ordered-list": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.15.3.tgz",
|
||||
"integrity": "sha512-/8uhw528Iy0c9wF6tHCiIn0ToM0Ml6Ll2c/3iPRnKr4IjXwx2Lr994stUFihb+oqGZwV1J8CPcZJ4Ufpdqi4Dw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-paragraph": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.15.3.tgz",
|
||||
"integrity": "sha512-lc0Qu/1AgzcEfS67NJMj5tSHHhH6NtA6uUpvppEKGsvJwgE2wKG1onE4isrVXmcGRdxSMiCtyTDemPNMu6/ozQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-placeholder": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.15.3.tgz",
|
||||
"integrity": "sha512-XcHHnojT186hKIoOgcPBesXk89+caNGVUdMtc171Vcr/5s0dpnr4q5LfE+YRC+S85CpCxCRRnh84Ou+XRtOqrw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-strike": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.15.3.tgz",
|
||||
"integrity": "sha512-Y1P3eGNY7RxQs2BcR6NfLo9VfEOplXXHAqkOM88oowWWOE7dMNeFFZM9H8HNxoQgXJ7H0aWW9B7ZTWM9hWli2Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-task-item": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-3.15.3.tgz",
|
||||
"integrity": "sha512-bkrmouc1rE5n9ONw2G7+zCGfBRoF2HJWq8REThPMzg/6+L5GJJ5YTN4UmncaP48U9jHX8xeihjgg9Ypenjl4lw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-task-list": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-3.15.3.tgz",
|
||||
"integrity": "sha512-nh8iBk1LHVIoqxphLoqZlLAN9fF2i9ZeK+2TjGSS35lfh7sYzRoSjNW0E81Uy48YuCzM1NQYghYR5Qfc7vm4jA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.15.3.tgz",
|
||||
"integrity": "sha512-MhkBz8ZvrqOKtKNp+ZWISKkLUlTrDR7tbKZc2OnNcUTttL9dz0HwT+cg91GGz19fuo7ttDcfsPV6eVmflvGToA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-underline": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.15.3.tgz",
|
||||
"integrity": "sha512-r/IwcNN0W366jGu4Y0n2MiFq9jGa4aopOwtfWO4d+J0DyeS2m7Go3+KwoUqi0wQTiVU74yfi4DF6eRsMQ9/iHQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extensions": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.15.3.tgz",
|
||||
"integrity": "sha512-ycx/BgxR4rc9tf3ZyTdI98Z19yKLFfqM3UN+v42ChuIwkzyr9zyp7kG8dB9xN2lNqrD+5y/HyJobz/VJ7T90gA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3",
|
||||
"@tiptap/pm": "^3.15.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/pm": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.15.3.tgz",
|
||||
"integrity": "sha512-Zm1BaU1TwFi3CQiisxjgnzzIus+q40bBKWLqXf6WEaus8Z6+vo1MT2pU52dBCMIRaW9XNDq3E5cmGtMc1AlveA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.3.0",
|
||||
"prosemirror-collab": "^1.3.1",
|
||||
"prosemirror-commands": "^1.6.2",
|
||||
"prosemirror-dropcursor": "^1.8.1",
|
||||
"prosemirror-gapcursor": "^1.3.2",
|
||||
"prosemirror-history": "^1.4.1",
|
||||
"prosemirror-inputrules": "^1.4.0",
|
||||
"prosemirror-keymap": "^1.2.2",
|
||||
"prosemirror-markdown": "^1.13.1",
|
||||
"prosemirror-menu": "^1.2.4",
|
||||
"prosemirror-model": "^1.24.1",
|
||||
"prosemirror-schema-basic": "^1.2.3",
|
||||
"prosemirror-schema-list": "^1.5.0",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-tables": "^1.6.4",
|
||||
"prosemirror-trailing-node": "^3.0.0",
|
||||
"prosemirror-transform": "^1.10.2",
|
||||
"prosemirror-view": "^1.38.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/react": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.15.3.tgz",
|
||||
"integrity": "sha512-XvouB+Hrqw8yFmZLPEh+HWlMeRSjZfHSfWfWuw5d8LSwnxnPeu3Bg/rjHrRrdwb+7FumtzOnNWMorpb/PSOttQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"fast-equals": "^5.3.3",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tiptap/extension-bubble-menu": "^3.15.3",
|
||||
"@tiptap/extension-floating-menu": "^3.15.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.15.3",
|
||||
"@tiptap/pm": "^3.15.3",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/starter-kit": {
|
||||
"version": "3.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.15.3.tgz",
|
||||
"integrity": "sha512-ia+eQr9Mt1ln2UO+kK4kFTJOrZK4GhvZXFjpCCYuHtco3rhr2fZAIxEEY4cl/vo5VO5WWyPqxhkFeLcoWmNjSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^3.15.3",
|
||||
"@tiptap/extension-blockquote": "^3.15.3",
|
||||
"@tiptap/extension-bold": "^3.15.3",
|
||||
"@tiptap/extension-bullet-list": "^3.15.3",
|
||||
"@tiptap/extension-code": "^3.15.3",
|
||||
"@tiptap/extension-code-block": "^3.15.3",
|
||||
"@tiptap/extension-document": "^3.15.3",
|
||||
"@tiptap/extension-dropcursor": "^3.15.3",
|
||||
"@tiptap/extension-gapcursor": "^3.15.3",
|
||||
"@tiptap/extension-hard-break": "^3.15.3",
|
||||
"@tiptap/extension-heading": "^3.15.3",
|
||||
"@tiptap/extension-horizontal-rule": "^3.15.3",
|
||||
"@tiptap/extension-italic": "^3.15.3",
|
||||
"@tiptap/extension-link": "^3.15.3",
|
||||
"@tiptap/extension-list": "^3.15.3",
|
||||
"@tiptap/extension-list-item": "^3.15.3",
|
||||
"@tiptap/extension-list-keymap": "^3.15.3",
|
||||
"@tiptap/extension-ordered-list": "^3.15.3",
|
||||
"@tiptap/extension-paragraph": "^3.15.3",
|
||||
"@tiptap/extension-strike": "^3.15.3",
|
||||
"@tiptap/extension-text": "^3.15.3",
|
||||
"@tiptap/extension-underline": "^3.15.3",
|
||||
"@tiptap/extensions": "^3.15.3",
|
||||
"@tiptap/pm": "^3.15.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/quickjs-emscripten": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
|
||||
@@ -4651,6 +5136,28 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "^5",
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mocha": {
|
||||
"version": "10.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
|
||||
@@ -4679,14 +5186,12 @@
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.27",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -4697,7 +5202,6 @@
|
||||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
@@ -4717,6 +5221,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz",
|
||||
@@ -5741,7 +6251,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
@@ -6165,9 +6674,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001764",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
|
||||
"integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==",
|
||||
"version": "1.0.30001765",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz",
|
||||
"integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -6584,6 +7093,12 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -7351,7 +7866,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -9362,6 +9876,21 @@
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/linkifyjs": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
|
||||
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-app": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz",
|
||||
@@ -9636,12 +10165,53 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
|
||||
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it-task-lists": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
|
||||
"integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/markdown-it/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
|
||||
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -10204,6 +10774,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/orderedmap": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@@ -10795,6 +11371,201 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prosemirror-changeset": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz",
|
||||
"integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-collab": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
|
||||
"integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-commands": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||||
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-dropcursor": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
|
||||
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0",
|
||||
"prosemirror-view": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-gapcursor": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz",
|
||||
"integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-view": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-history": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.31.0",
|
||||
"rope-sequence": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-inputrules": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||||
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-keymap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"w3c-keyname": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz",
|
||||
"integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/markdown-it": "^14.0.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
"prosemirror-model": "^1.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-menu": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz",
|
||||
"integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"crelt": "^1.0.0",
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-model": {
|
||||
"version": "1.25.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-basic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
|
||||
"integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-list": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-state": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.27.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-tables": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.2.3",
|
||||
"prosemirror-model": "^1.25.4",
|
||||
"prosemirror-state": "^1.4.4",
|
||||
"prosemirror-transform": "^1.10.5",
|
||||
"prosemirror-view": "^1.41.4"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-trailing-node": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
|
||||
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remirror/core-constants": "3.0.0",
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prosemirror-model": "^1.22.1",
|
||||
"prosemirror-state": "^1.4.2",
|
||||
"prosemirror-view": "^1.33.8"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-transform": {
|
||||
"version": "1.10.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.5.tgz",
|
||||
"integrity": "sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-view": {
|
||||
"version": "1.41.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.5.tgz",
|
||||
"integrity": "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-agent": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
|
||||
@@ -10852,6 +11623,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/query-selector-shadow-dom": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
|
||||
@@ -11537,6 +12317,12 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rope-sequence": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/run-async": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz",
|
||||
@@ -12437,6 +13223,46 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tiptap-markdown": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.9.0.tgz",
|
||||
"integrity": "sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"example"
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/markdown-it": "^13.0.7",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"prosemirror-markdown": "^1.11.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tiptap-markdown/node_modules/@types/linkify-it": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz",
|
||||
"integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiptap-markdown/node_modules/@types/markdown-it": {
|
||||
"version": "13.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.9.tgz",
|
||||
"integrity": "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "^3",
|
||||
"@types/mdurl": "^1"
|
||||
}
|
||||
},
|
||||
"node_modules/tiptap-markdown/node_modules/@types/mdurl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz",
|
||||
"integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.0.19",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz",
|
||||
@@ -13072,6 +13898,12 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
@@ -13906,6 +14738,12 @@
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
|
||||
@@ -68,6 +68,11 @@
|
||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@tiptap/extension-placeholder": "^3.15.3",
|
||||
"@tiptap/extension-task-item": "^3.15.3",
|
||||
"@tiptap/extension-task-list": "^3.15.3",
|
||||
"@tiptap/react": "^3.15.3",
|
||||
"@tiptap/starter-kit": "^3.15.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -88,6 +93,7 @@
|
||||
"sonner": "^1.7.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"vaul": "^0.9.9",
|
||||
"vitest": "^4.0.16",
|
||||
"zod": "^3.25.76"
|
||||
@@ -108,8 +114,8 @@
|
||||
"@wdio/mocha-framework": "^9.22.0",
|
||||
"@wdio/spec-reporter": "^9.20.0",
|
||||
"@wdio/types": "^9.20.0",
|
||||
"edgedriver": "^6.1.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"edgedriver": "^6.1.0",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||
|
||||
import { type NoteEdit, TimestampedNotesEditor } from '@/components/timestamped-notes-editor';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { buttonSize } from '@/lib/styles';
|
||||
|
||||
export interface NotesPanelProps {
|
||||
panelRef: RefObject<ImperativePanelHandle | null>;
|
||||
@@ -16,6 +17,7 @@ export interface NotesPanelProps {
|
||||
isRecording: boolean;
|
||||
notes: NoteEdit[];
|
||||
onNotesChange: (notes: NoteEdit[]) => void;
|
||||
onTogglePanel?: () => void;
|
||||
}
|
||||
|
||||
export function NotesPanel({
|
||||
@@ -25,7 +27,23 @@ export function NotesPanel({
|
||||
isRecording,
|
||||
notes,
|
||||
onNotesChange,
|
||||
onTogglePanel,
|
||||
}: NotesPanelProps) {
|
||||
const handleCollapse = () => {
|
||||
if (onTogglePanel) {
|
||||
onTogglePanel();
|
||||
} else {
|
||||
panelRef.current?.collapse();
|
||||
}
|
||||
};
|
||||
|
||||
const handleExpand = () => {
|
||||
if (onTogglePanel) {
|
||||
onTogglePanel();
|
||||
} else {
|
||||
panelRef.current?.expand();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="h-full flex flex-col border-l border-border bg-card/50">
|
||||
{showPanel ? (
|
||||
@@ -35,8 +53,8 @@ export function NotesPanel({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => panelRef.current?.collapse()}
|
||||
className="h-7 w-7 p-0"
|
||||
onClick={handleCollapse}
|
||||
className={buttonSize.iconSm}
|
||||
title="Collapse notes panel"
|
||||
>
|
||||
<PanelRightClose className="h-4 w-4" />
|
||||
@@ -52,19 +70,23 @@ export function NotesPanel({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center pt-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => panelRef.current?.expand()}
|
||||
className="h-8 w-8 p-0"
|
||||
title="Expand notes panel"
|
||||
>
|
||||
<PanelRightOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-[10px] text-muted-foreground mt-2 [writing-mode:vertical-rl] rotate-180">
|
||||
Notes
|
||||
</span>
|
||||
<div className="h-full flex flex-col border-l border-border bg-card/50">
|
||||
<div className="p-2 flex justify-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleExpand}
|
||||
className={buttonSize.iconSm}
|
||||
title="Expand notes panel"
|
||||
>
|
||||
<PanelRightOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<span className="text-xs font-medium text-muted-foreground [writing-mode:vertical-lr]">
|
||||
Notes
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
* Stats panel component for the recording page.
|
||||
*/
|
||||
|
||||
import { BarChart3, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
|
||||
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
|
||||
import type { RefObject } from 'react';
|
||||
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||
|
||||
import type { FinalSegment } from '@/api/types';
|
||||
import { StatsContent } from '@/components/recording/stats-content';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { buttonSize } from '@/lib/styles';
|
||||
|
||||
export interface StatsPanelProps {
|
||||
panelRef: RefObject<ImperativePanelHandle | null>;
|
||||
@@ -20,6 +21,7 @@ export interface StatsPanelProps {
|
||||
isVadActive: boolean;
|
||||
audioLevel: number | null;
|
||||
speakerNameMap: Map<string, string>;
|
||||
onTogglePanel?: () => void;
|
||||
}
|
||||
|
||||
export function StatsPanel({
|
||||
@@ -32,7 +34,24 @@ export function StatsPanel({
|
||||
isVadActive,
|
||||
audioLevel,
|
||||
speakerNameMap,
|
||||
onTogglePanel,
|
||||
}: StatsPanelProps) {
|
||||
const handleCollapse = () => {
|
||||
if (onTogglePanel) {
|
||||
onTogglePanel();
|
||||
} else {
|
||||
panelRef.current?.collapse();
|
||||
}
|
||||
};
|
||||
|
||||
const handleExpand = () => {
|
||||
if (onTogglePanel) {
|
||||
onTogglePanel();
|
||||
} else {
|
||||
panelRef.current?.expand();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col border-l border-border bg-card/50 overflow-auto">
|
||||
{showPanel ? (
|
||||
@@ -42,8 +61,8 @@ export function StatsPanel({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => panelRef.current?.collapse()}
|
||||
className="h-7 w-7 p-0"
|
||||
onClick={handleCollapse}
|
||||
className={buttonSize.iconSm}
|
||||
title="Collapse stats panel"
|
||||
>
|
||||
<PanelLeftClose className="h-4 w-4" />
|
||||
@@ -60,20 +79,23 @@ export function StatsPanel({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center pt-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => panelRef.current?.expand()}
|
||||
className="h-8 w-8 p-0"
|
||||
title="Expand stats panel"
|
||||
>
|
||||
<PanelLeftOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-[10px] text-muted-foreground mt-2 [writing-mode:vertical-rl] rotate-180">
|
||||
<BarChart3 className="h-3 w-3 mb-1" />
|
||||
Stats
|
||||
</span>
|
||||
<div className="h-full flex flex-col border-l border-border bg-card/50">
|
||||
<div className="p-2 flex justify-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleExpand}
|
||||
className={buttonSize.iconSm}
|
||||
title="Expand stats panel"
|
||||
>
|
||||
<PanelLeftOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<span className="text-xs font-medium text-muted-foreground [writing-mode:vertical-lr]">
|
||||
Stats
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { History, PenLine, Save, Trash2 } from 'lucide-react';
|
||||
import { Code, History, PenLine, Save, Trash2 } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { Editor } from '@tiptap/react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { MarkdownEditor, getEditorMarkdown, useMarkdownEditor } from '@/components/ui/markdown-editor';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { formatElapsedTime } from '@/lib/format';
|
||||
import { generateUuid } from '@/lib/id-utils';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -39,9 +42,47 @@ export function TimestampedNotesEditor({
|
||||
const [currentNote, setCurrentNote] = useState('');
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
|
||||
const [isRawMode, setIsRawMode] = useState(false);
|
||||
const lastSavedContent = useRef('');
|
||||
const lastAutoSaveTime = useRef(0);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const editorRef = useRef<Editor | null>(null);
|
||||
|
||||
// WYSIWYG editor instance
|
||||
const editor = useMarkdownEditor({
|
||||
content: currentNote,
|
||||
placeholder: isRecording
|
||||
? "Take notes during your meeting...\n\nTip: Use the toolbar for formatting."
|
||||
: 'Start recording to take timestamped notes...',
|
||||
editable: isRecording,
|
||||
});
|
||||
|
||||
// Keep editor ref updated
|
||||
useEffect(() => {
|
||||
editorRef.current = editor;
|
||||
}, [editor]);
|
||||
|
||||
// Sync editor content when currentNote changes externally (e.g., from getCombinedContent)
|
||||
useEffect(() => {
|
||||
if (editor && !isRawMode) {
|
||||
const editorContent = getEditorMarkdown(editor);
|
||||
if (currentNote !== editorContent) {
|
||||
editor.commands.setContent(currentNote);
|
||||
}
|
||||
}
|
||||
}, [editor, currentNote, isRawMode]);
|
||||
|
||||
// Handle mode toggle - sync content between modes
|
||||
const handleModeToggle = useCallback((rawMode: boolean) => {
|
||||
if (rawMode && editor) {
|
||||
// Switching to raw: get markdown from editor
|
||||
setCurrentNote(getEditorMarkdown(editor));
|
||||
} else if (!rawMode && editor) {
|
||||
// Switching to WYSIWYG: set editor content from currentNote
|
||||
editor.commands.setContent(currentNote);
|
||||
}
|
||||
setIsRawMode(rawMode);
|
||||
}, [editor, currentNote]);
|
||||
|
||||
// Get combined note content from all edits
|
||||
const getCombinedContent = useCallback(() => {
|
||||
@@ -65,7 +106,9 @@ export function TimestampedNotesEditor({
|
||||
// Save note with timestamp
|
||||
const saveNote = useCallback(
|
||||
(isAutoSave = false) => {
|
||||
const trimmed = currentNote.trim();
|
||||
// Get content from appropriate source based on mode
|
||||
const content = isRawMode ? currentNote : getEditorMarkdown(editorRef.current);
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed || trimmed === lastSavedContent.current) {
|
||||
return false;
|
||||
}
|
||||
@@ -85,7 +128,7 @@ export function TimestampedNotesEditor({
|
||||
lastAutoSaveTime.current = elapsedTime;
|
||||
return true;
|
||||
},
|
||||
[currentNote, elapsedTime, notes, onNotesChange]
|
||||
[currentNote, elapsedTime, notes, onNotesChange, isRawMode]
|
||||
);
|
||||
|
||||
// Auto-save at periodic intervals
|
||||
@@ -116,32 +159,44 @@ export function TimestampedNotesEditor({
|
||||
saveNote(false);
|
||||
};
|
||||
|
||||
// Generic text insertion
|
||||
// Generic text insertion - handles both raw and WYSIWYG modes
|
||||
const insertText = useCallback((text: string, cursorOffset: number = 0) => {
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const newContent = currentNote.substring(0, start) + text + currentNote.substring(end);
|
||||
setCurrentNote(newContent);
|
||||
// Focus and move cursor
|
||||
setTimeout(() => {
|
||||
textarea.focus();
|
||||
const newCursor = start + text.length + cursorOffset;
|
||||
textarea.setSelectionRange(newCursor, newCursor);
|
||||
}, 0);
|
||||
} else {
|
||||
setCurrentNote((prev) => prev + text);
|
||||
if (isRawMode) {
|
||||
// Raw mode: insert into textarea
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const newContent = currentNote.substring(0, start) + text + currentNote.substring(end);
|
||||
setCurrentNote(newContent);
|
||||
// Focus and move cursor
|
||||
setTimeout(() => {
|
||||
textarea.focus();
|
||||
const newCursor = start + text.length + cursorOffset;
|
||||
textarea.setSelectionRange(newCursor, newCursor);
|
||||
}, 0);
|
||||
} else {
|
||||
setCurrentNote((prev) => prev + text);
|
||||
}
|
||||
} else if (editor) {
|
||||
// WYSIWYG mode: insert into editor
|
||||
editor.chain().focus().insertContent(text).run();
|
||||
}
|
||||
}, [currentNote]);
|
||||
}, [currentNote, isRawMode, editor]);
|
||||
|
||||
const handleInsertTimestamp = useCallback(() => {
|
||||
insertText(`\n\n---\n**[${formatElapsedTime(elapsedTime)}]** `);
|
||||
}, [insertText, elapsedTime]);
|
||||
|
||||
const handleAddActionItem = useCallback(() => {
|
||||
insertText('\n- [ ] ');
|
||||
}, [insertText]);
|
||||
if (!isRawMode && editor) {
|
||||
// WYSIWYG mode: toggle task list
|
||||
editor.chain().focus().toggleTaskList().run();
|
||||
} else {
|
||||
// Raw mode: insert markdown checkbox
|
||||
insertText('\n- [ ] ');
|
||||
}
|
||||
}, [insertText, isRawMode, editor]);
|
||||
|
||||
const handleAddDecision = useCallback(() => {
|
||||
insertText('\n> **DECISION:** ');
|
||||
@@ -294,50 +349,81 @@ export function TimestampedNotesEditor({
|
||||
|
||||
{/* Quick Actions Toolbar */}
|
||||
{isRecording && (
|
||||
<div className="px-3 pt-2">
|
||||
<div className="flex items-center justify-between px-3 pt-2">
|
||||
<NotesQuickActions
|
||||
elapsedTime={elapsedTime}
|
||||
onInsertTimestamp={handleInsertTimestamp}
|
||||
onAddActionItem={handleAddActionItem}
|
||||
onAddDecision={handleAddDecision}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleModeToggle(!isRawMode)}
|
||||
className="h-6 px-2 text-xs gap-1"
|
||||
>
|
||||
<Code className="h-3 w-3" />
|
||||
{isRawMode ? 'Preview' : 'Raw'}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{isRawMode ? 'Switch to WYSIWYG editor' : 'Switch to raw markdown'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Editor */}
|
||||
<div className="flex-1 px-3 pt-1 flex flex-col min-h-0">
|
||||
<div className="flex-1 relative">
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
value={currentNote}
|
||||
onChange={(e) => setCurrentNote(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
placeholder={
|
||||
isRecording
|
||||
? "Take notes during your meeting...\n\nTip: Use **bold** and *italic* for markdown formatting.\nClick 'Insert Time' to add a timestamp marker."
|
||||
: 'Start recording to take timestamped notes...'
|
||||
}
|
||||
disabled={!isRecording}
|
||||
className={cn(
|
||||
'h-full resize-none font-mono text-sm',
|
||||
'bg-background border-border',
|
||||
!isRecording && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
/>
|
||||
{isRawMode ? (
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
value={currentNote}
|
||||
onChange={(e) => setCurrentNote(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
placeholder={
|
||||
isRecording
|
||||
? "Take notes during your meeting...\n\nTip: Use **bold** and *italic* for markdown formatting.\nClick 'Insert Time' to add a timestamp marker."
|
||||
: 'Start recording to take timestamped notes...'
|
||||
}
|
||||
disabled={!isRecording}
|
||||
className={cn(
|
||||
'h-full resize-none font-mono text-sm',
|
||||
'bg-background border-border',
|
||||
!isRecording && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<MarkdownEditor
|
||||
editor={editor}
|
||||
disabled={!isRecording}
|
||||
showToolbar={isRecording}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status bar */}
|
||||
{isRecording && (
|
||||
<div className="flex items-center justify-between pt-2 text-[10px] text-muted-foreground">
|
||||
<span>
|
||||
{currentNote.length} chars • {currentNote.split(/\s+/).filter(Boolean).length} words
|
||||
{(() => {
|
||||
const content = isRawMode ? currentNote : getEditorMarkdown(editor);
|
||||
return `${content.length} chars • ${content.split(/\s+/).filter(Boolean).length} words`;
|
||||
})()}
|
||||
</span>
|
||||
<span>
|
||||
{lastSavedContent.current !== currentNote.trim() ? (
|
||||
<span className="text-warning">Unsaved changes</span>
|
||||
) : (
|
||||
<span className="text-success">Saved</span>
|
||||
)}
|
||||
{(() => {
|
||||
const content = isRawMode ? currentNote : getEditorMarkdown(editor);
|
||||
return lastSavedContent.current !== content.trim() ? (
|
||||
<span className="text-warning">Unsaved changes</span>
|
||||
) : (
|
||||
<span className="text-success">Saved</span>
|
||||
);
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
365
client/src/components/ui/markdown-editor.tsx
Normal file
365
client/src/components/ui/markdown-editor.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* WYSIWYG Markdown Editor component built on TipTap.
|
||||
*
|
||||
* Provides rich text editing with markdown import/export.
|
||||
* Uses existing Toggle, Tooltip, and Separator UI components for consistency.
|
||||
*/
|
||||
|
||||
import { Bold, Code, Italic, List, ListOrdered, Quote, Redo, Undo } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { EditorContent, useEditor, type Editor } from '@tiptap/react';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import TaskItem from '@tiptap/extension-task-item';
|
||||
import TaskList from '@tiptap/extension-task-list';
|
||||
import { Markdown } from 'tiptap-markdown';
|
||||
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Toggle } from '@/components/ui/toggle';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type-safe markdown storage access
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface MarkdownStorage {
|
||||
getMarkdown: () => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe accessor for tiptap-markdown storage.
|
||||
* The Markdown extension adds this storage at runtime.
|
||||
*
|
||||
* TipTap's Storage type is `{}` but extensions add properties dynamically.
|
||||
* We use bracket notation and runtime validation for safe access.
|
||||
*/
|
||||
function getMarkdownStorage(editor: Editor): MarkdownStorage {
|
||||
const storage = editor.storage;
|
||||
// Access via bracket notation to avoid TS structural typing issues
|
||||
const markdown = (storage as Record<string, unknown>)['markdown'];
|
||||
if (markdown && typeof markdown === 'object' && 'getMarkdown' in markdown) {
|
||||
return markdown as MarkdownStorage;
|
||||
}
|
||||
// Fallback (should never happen when Markdown extension is installed)
|
||||
return { getMarkdown: () => '' };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface MarkdownEditorProps {
|
||||
/** Initial content as markdown string */
|
||||
content?: string;
|
||||
/** Placeholder text when editor is empty */
|
||||
placeholder?: string;
|
||||
/** Whether the editor is disabled */
|
||||
disabled?: boolean;
|
||||
/** Called when content changes (debounced) */
|
||||
onChange?: (markdown: string) => void;
|
||||
/** Additional class names for the editor container */
|
||||
className?: string;
|
||||
/** Whether to show the formatting toolbar */
|
||||
showToolbar?: boolean;
|
||||
/** External editor instance (if using useMarkdownEditor hook) */
|
||||
editor?: Editor | null;
|
||||
}
|
||||
|
||||
export interface UseMarkdownEditorOptions {
|
||||
/** Initial content as markdown string */
|
||||
content?: string;
|
||||
/** Placeholder text */
|
||||
placeholder?: string;
|
||||
/** Called when content changes */
|
||||
onChange?: (markdown: string) => void;
|
||||
/** Whether editor is editable */
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook: useMarkdownEditor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hook to create and manage a TipTap editor instance with markdown support.
|
||||
* Use this when you need external access to the editor (e.g., for custom toolbars).
|
||||
*/
|
||||
export function useMarkdownEditor({
|
||||
content = '',
|
||||
placeholder = '',
|
||||
onChange,
|
||||
editable = true,
|
||||
}: UseMarkdownEditorOptions = {}): Editor | null {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
// Disable default task list since we're using the dedicated extension
|
||||
bulletList: { keepMarks: true },
|
||||
orderedList: { keepMarks: true },
|
||||
}),
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder,
|
||||
emptyEditorClass: 'is-editor-empty',
|
||||
}),
|
||||
Markdown.configure({
|
||||
html: false, // Don't allow raw HTML
|
||||
transformPastedText: true, // Parse markdown on paste
|
||||
transformCopiedText: true, // Copy as markdown
|
||||
}),
|
||||
],
|
||||
content,
|
||||
editable,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: cn(
|
||||
'prose prose-sm dark:prose-invert max-w-none',
|
||||
'focus:outline-none min-h-[100px] px-3 py-2',
|
||||
'[&_ul[data-type="taskList"]]:list-none [&_ul[data-type="taskList"]]:pl-0',
|
||||
'[&_ul[data-type="taskList"]_li]:flex [&_ul[data-type="taskList"]_li]:gap-2 [&_ul[data-type="taskList"]_li]:items-start',
|
||||
'[&_ul[data-type="taskList"]_li_label]:mt-0.5',
|
||||
'[&_ul[data-type="taskList"]_li_div]:flex-1'
|
||||
),
|
||||
},
|
||||
},
|
||||
onUpdate: ({ editor: e }) => {
|
||||
if (onChange) {
|
||||
const markdown = getMarkdownStorage(e).getMarkdown();
|
||||
onChange(markdown);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Update content when prop changes
|
||||
useEffect(() => {
|
||||
if (editor && content !== getMarkdownStorage(editor).getMarkdown()) {
|
||||
editor.commands.setContent(content);
|
||||
}
|
||||
}, [editor, content]);
|
||||
|
||||
// Update editable state
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
editor.setEditable(editable);
|
||||
}
|
||||
}, [editor, editable]);
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Toolbar Button Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ToolbarButtonProps {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
shortcut?: string;
|
||||
isActive?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function ToolbarButton({ icon: Icon, label, shortcut, isActive, disabled, onClick }: ToolbarButtonProps) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={isActive}
|
||||
onPressedChange={onClick}
|
||||
disabled={disabled}
|
||||
className="h-7 w-7 p-0"
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</Toggle>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="flex items-center gap-1.5">
|
||||
<span>{label}</span>
|
||||
{shortcut && <kbd className="text-[10px] text-muted-foreground">{shortcut}</kbd>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Editor Toolbar Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface EditorToolbarProps {
|
||||
editor: Editor | null;
|
||||
}
|
||||
|
||||
function EditorToolbar({ editor }: EditorToolbarProps) {
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5 p-1 border-b border-border bg-muted/30 rounded-t-md">
|
||||
{/* Text formatting */}
|
||||
<ToolbarButton
|
||||
icon={Bold}
|
||||
label="Bold"
|
||||
shortcut="⌘B"
|
||||
isActive={editor.isActive('bold')}
|
||||
disabled={!editor.can().toggleBold()}
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={Italic}
|
||||
label="Italic"
|
||||
shortcut="⌘I"
|
||||
isActive={editor.isActive('italic')}
|
||||
disabled={!editor.can().toggleItalic()}
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={Code}
|
||||
label="Code"
|
||||
shortcut="⌘E"
|
||||
isActive={editor.isActive('code')}
|
||||
disabled={!editor.can().toggleCode()}
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
/>
|
||||
|
||||
<Separator orientation="vertical" className="h-5 mx-1" />
|
||||
|
||||
{/* Lists */}
|
||||
<ToolbarButton
|
||||
icon={List}
|
||||
label="Bullet List"
|
||||
isActive={editor.isActive('bulletList')}
|
||||
disabled={!editor.can().toggleBulletList()}
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={ListOrdered}
|
||||
label="Numbered List"
|
||||
isActive={editor.isActive('orderedList')}
|
||||
disabled={!editor.can().toggleOrderedList()}
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={Quote}
|
||||
label="Blockquote"
|
||||
isActive={editor.isActive('blockquote')}
|
||||
disabled={!editor.can().toggleBlockquote()}
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
/>
|
||||
|
||||
<Separator orientation="vertical" className="h-5 mx-1" />
|
||||
|
||||
{/* Undo/Redo */}
|
||||
<ToolbarButton
|
||||
icon={Undo}
|
||||
label="Undo"
|
||||
shortcut="⌘Z"
|
||||
disabled={!editor.can().undo()}
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
/>
|
||||
<ToolbarButton
|
||||
icon={Redo}
|
||||
label="Redo"
|
||||
shortcut="⌘⇧Z"
|
||||
disabled={!editor.can().redo()}
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main MarkdownEditor Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* WYSIWYG Markdown Editor with optional toolbar.
|
||||
*
|
||||
* Can be used standalone or with an external editor instance from useMarkdownEditor.
|
||||
*
|
||||
* @example
|
||||
* // Standalone usage
|
||||
* <MarkdownEditor
|
||||
* content="**Hello** world"
|
||||
* onChange={(md) => console.log(md)}
|
||||
* placeholder="Start typing..."
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // With external editor control
|
||||
* const editor = useMarkdownEditor({ content: initialMarkdown });
|
||||
* <MarkdownEditor editor={editor} showToolbar />
|
||||
* // Access markdown: getEditorMarkdown(editor)
|
||||
*/
|
||||
export function MarkdownEditor({
|
||||
content = '',
|
||||
placeholder = '',
|
||||
disabled = false,
|
||||
onChange,
|
||||
className,
|
||||
showToolbar = true,
|
||||
editor: externalEditor,
|
||||
}: MarkdownEditorProps) {
|
||||
// Use external editor if provided, otherwise create internal one
|
||||
const internalEditor = useMarkdownEditor({
|
||||
content,
|
||||
placeholder,
|
||||
onChange,
|
||||
editable: !disabled,
|
||||
});
|
||||
|
||||
const editor = externalEditor ?? internalEditor;
|
||||
|
||||
// Cleanup internal editor on unmount (external editor is managed externally)
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (!externalEditor && internalEditor) {
|
||||
internalEditor.destroy();
|
||||
}
|
||||
};
|
||||
}, [externalEditor, internalEditor]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background',
|
||||
disabled && 'opacity-50 cursor-not-allowed',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{showToolbar && <EditorToolbar editor={editor} />}
|
||||
<EditorContent
|
||||
editor={editor}
|
||||
className={cn(
|
||||
'min-h-[100px] overflow-auto',
|
||||
'[&_.is-editor-empty:first-child::before]:content-[attr(data-placeholder)]',
|
||||
'[&_.is-editor-empty:first-child::before]:text-muted-foreground',
|
||||
'[&_.is-editor-empty:first-child::before]:float-left',
|
||||
'[&_.is-editor-empty:first-child::before]:h-0',
|
||||
'[&_.is-editor-empty:first-child::before]:pointer-events-none'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utility: Get markdown from editor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Helper to safely get markdown content from an editor instance.
|
||||
*/
|
||||
export function getEditorMarkdown(editor: Editor | null): string {
|
||||
if (!editor) {
|
||||
return '';
|
||||
}
|
||||
return getMarkdownStorage(editor).getMarkdown();
|
||||
}
|
||||
@@ -7,11 +7,11 @@ const COLLAPSED_SIZE_PERCENT = 3;
|
||||
/** Minimum transcript panel size to ensure readability */
|
||||
const MIN_TRANSCRIPT_SIZE_PERCENT = 30;
|
||||
/** Default notes panel size when expanded */
|
||||
const DEFAULT_NOTES_SIZE_PERCENT = 20;
|
||||
const DEFAULT_NOTES_SIZE_PERCENT = 25;
|
||||
/** Default stats panel size when expanded */
|
||||
const DEFAULT_STATS_SIZE_PERCENT = 20;
|
||||
const DEFAULT_STATS_SIZE_PERCENT = 22;
|
||||
/** Default transcript panel size (100 - notes - stats) */
|
||||
const DEFAULT_TRANSCRIPT_SIZE_PERCENT = 60;
|
||||
const DEFAULT_TRANSCRIPT_SIZE_PERCENT = 53;
|
||||
/** Total panel group size must equal this percentage */
|
||||
const TOTAL_SIZE_PERCENT = 100;
|
||||
/** Number of side panels (notes + stats) */
|
||||
@@ -22,7 +22,9 @@ const MAX_NOTES_SIZE_PERCENT = 40;
|
||||
const MAX_STATS_SIZE_PERCENT = 35;
|
||||
|
||||
/** Minimum size for notes panel when expanded */
|
||||
const MIN_NOTES_SIZE_PERCENT = 25;
|
||||
const MIN_NOTES_SIZE_PERCENT = 20;
|
||||
/** Minimum size for stats panel when expanded */
|
||||
const MIN_STATS_SIZE_PERCENT = 18;
|
||||
|
||||
interface PanelPreferences {
|
||||
showNotesPanel: boolean;
|
||||
@@ -40,16 +42,28 @@ const DEFAULT_PREFERENCES: PanelPreferences = {
|
||||
transcriptPanelSize: DEFAULT_TRANSCRIPT_SIZE_PERCENT,
|
||||
};
|
||||
|
||||
/**
|
||||
* Clamp a value between min and max bounds.
|
||||
*/
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize panel sizes to ensure they sum to 100% based on visibility.
|
||||
* Accounts for collapsed panels taking COLLAPSED_SIZE_PERCENT when hidden.
|
||||
* Clamps panel sizes to their min/max constraints to handle legacy stored values.
|
||||
*/
|
||||
function normalizeSizes(prefs: PanelPreferences): PanelPreferences {
|
||||
const { showNotesPanel, showStatsPanel } = prefs;
|
||||
|
||||
// Calculate effective sizes for side panels
|
||||
const notesSize = showNotesPanel ? prefs.notesPanelSize : COLLAPSED_SIZE_PERCENT;
|
||||
const statsSize = showStatsPanel ? prefs.statsPanelSize : COLLAPSED_SIZE_PERCENT;
|
||||
// Clamp stored sizes to valid ranges (handles legacy values that are now invalid)
|
||||
const clampedNotesSize = clamp(prefs.notesPanelSize, MIN_NOTES_SIZE_PERCENT, MAX_NOTES_SIZE_PERCENT);
|
||||
const clampedStatsSize = clamp(prefs.statsPanelSize, MIN_STATS_SIZE_PERCENT, MAX_STATS_SIZE_PERCENT);
|
||||
|
||||
// Calculate effective sizes for side panels (use collapsed size when hidden)
|
||||
const notesSize = showNotesPanel ? clampedNotesSize : COLLAPSED_SIZE_PERCENT;
|
||||
const statsSize = showStatsPanel ? clampedStatsSize : COLLAPSED_SIZE_PERCENT;
|
||||
|
||||
// Calculate transcript size to fill remaining space
|
||||
let transcriptSize = TOTAL_SIZE_PERCENT - notesSize - statsSize;
|
||||
@@ -65,8 +79,8 @@ function normalizeSizes(prefs: PanelPreferences): PanelPreferences {
|
||||
const sideSpace = (remaining - SIDE_PANEL_COUNT * COLLAPSED_SIZE_PERCENT) / SIDE_PANEL_COUNT;
|
||||
return {
|
||||
...prefs,
|
||||
notesPanelSize: sideSpace,
|
||||
statsPanelSize: sideSpace,
|
||||
notesPanelSize: Math.max(sideSpace, MIN_NOTES_SIZE_PERCENT),
|
||||
statsPanelSize: Math.max(sideSpace, MIN_STATS_SIZE_PERCENT),
|
||||
transcriptPanelSize: transcriptSize,
|
||||
};
|
||||
}
|
||||
@@ -74,6 +88,8 @@ function normalizeSizes(prefs: PanelPreferences): PanelPreferences {
|
||||
|
||||
return {
|
||||
...prefs,
|
||||
notesPanelSize: clampedNotesSize,
|
||||
statsPanelSize: clampedStatsSize,
|
||||
transcriptPanelSize: transcriptSize,
|
||||
};
|
||||
}
|
||||
@@ -107,6 +123,7 @@ export {
|
||||
MAX_NOTES_SIZE_PERCENT,
|
||||
MAX_STATS_SIZE_PERCENT,
|
||||
MIN_NOTES_SIZE_PERCENT,
|
||||
MIN_STATS_SIZE_PERCENT,
|
||||
};
|
||||
|
||||
export function usePanelPreferences() {
|
||||
|
||||
175
client/src/hooks/use-recording-panels.ts
Normal file
175
client/src/hooks/use-recording-panels.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Custom hook for managing recording page panel state and behavior.
|
||||
* Handles cascade prevention, programmatic expand/collapse, and size synchronization.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||
|
||||
import {
|
||||
COLLAPSED_SIZE_PERCENT,
|
||||
MAX_NOTES_SIZE_PERCENT,
|
||||
MAX_STATS_SIZE_PERCENT,
|
||||
MIN_NOTES_SIZE_PERCENT,
|
||||
MIN_STATS_SIZE_PERCENT,
|
||||
} from '@/hooks/use-panel-preferences';
|
||||
|
||||
export interface UseRecordingPanelsOptions {
|
||||
showNotesPanel: boolean;
|
||||
showStatsPanel: boolean;
|
||||
setShowNotesPanel: (value: boolean) => void;
|
||||
setShowStatsPanel: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export interface UseRecordingPanelsResult {
|
||||
notesPanelRef: React.RefObject<ImperativePanelHandle>;
|
||||
statsPanelRef: React.RefObject<ImperativePanelHandle>;
|
||||
handleToggleNotesPanel: () => void;
|
||||
handleToggleStatsPanel: () => void;
|
||||
handleNotesCollapse: () => void;
|
||||
handleStatsCollapse: () => void;
|
||||
handleNotesExpand: () => void;
|
||||
handleStatsExpand: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages recording page panel behavior including:
|
||||
* - Cascade prevention: toggling one panel doesn't affect the other
|
||||
* - Programmatic expand/collapse with proper visual sizing
|
||||
* - Syncing panel state with preferences
|
||||
*/
|
||||
export function useRecordingPanels({
|
||||
showNotesPanel,
|
||||
showStatsPanel,
|
||||
setShowNotesPanel,
|
||||
setShowStatsPanel,
|
||||
}: UseRecordingPanelsOptions): UseRecordingPanelsResult {
|
||||
// Panel refs for imperative collapse/expand
|
||||
// Cast needed because ResizablePanel ref prop expects non-null RefObject
|
||||
const notesPanelRef = useRef<ImperativePanelHandle>(null) as React.RefObject<ImperativePanelHandle>;
|
||||
const statsPanelRef = useRef<ImperativePanelHandle>(null) as React.RefObject<ImperativePanelHandle>;
|
||||
|
||||
// Track when ANY programmatic panel operation is in progress.
|
||||
// When toggling one panel, the library may auto-collapse the other panel to fit,
|
||||
// which would trigger its onCollapse callback. We need to ignore ALL collapse
|
||||
// callbacks during programmatic operations to prevent cascade effects.
|
||||
const operationInProgressRef = useRef(false);
|
||||
|
||||
// Toggle handlers - block all collapse callbacks during the operation
|
||||
const handleToggleNotesPanel = useCallback(() => {
|
||||
operationInProgressRef.current = true;
|
||||
setShowNotesPanel(!showNotesPanel);
|
||||
// Reset after panel animations complete (useEffect + library animations)
|
||||
setTimeout(() => {
|
||||
operationInProgressRef.current = false;
|
||||
}, 150);
|
||||
}, [showNotesPanel, setShowNotesPanel]);
|
||||
|
||||
const handleToggleStatsPanel = useCallback(() => {
|
||||
operationInProgressRef.current = true;
|
||||
setShowStatsPanel(!showStatsPanel);
|
||||
// Reset after panel animations complete (useEffect + library animations)
|
||||
setTimeout(() => {
|
||||
operationInProgressRef.current = false;
|
||||
}, 150);
|
||||
}, [showStatsPanel, setShowStatsPanel]);
|
||||
|
||||
// Collapse handlers - ignore ALL collapse events during programmatic operations.
|
||||
// This prevents cascade collapses when expanding one panel causes the library
|
||||
// to auto-collapse another panel for space.
|
||||
const handleNotesCollapse = useCallback(() => {
|
||||
if (!operationInProgressRef.current) {
|
||||
setShowNotesPanel(false);
|
||||
}
|
||||
}, [setShowNotesPanel]);
|
||||
|
||||
const handleStatsCollapse = useCallback(() => {
|
||||
if (!operationInProgressRef.current) {
|
||||
setShowStatsPanel(false);
|
||||
}
|
||||
}, [setShowStatsPanel]);
|
||||
|
||||
// Expand handlers - also ignore during programmatic operations
|
||||
const handleNotesExpand = useCallback(() => {
|
||||
if (!operationInProgressRef.current && !showNotesPanel) {
|
||||
setShowNotesPanel(true);
|
||||
}
|
||||
}, [showNotesPanel, setShowNotesPanel]);
|
||||
|
||||
const handleStatsExpand = useCallback(() => {
|
||||
if (!operationInProgressRef.current && !showStatsPanel) {
|
||||
setShowStatsPanel(true);
|
||||
}
|
||||
}, [showStatsPanel, setShowStatsPanel]);
|
||||
|
||||
// Sync notes panel collapsed state with preferences
|
||||
useEffect(() => {
|
||||
const notesPanel = notesPanelRef.current;
|
||||
if (!notesPanel) return;
|
||||
|
||||
if (showNotesPanel) {
|
||||
// Expand panel first, then resize to preferred size
|
||||
// Use requestAnimationFrame to ensure DOM has updated before resize
|
||||
notesPanel.expand(MIN_NOTES_SIZE_PERCENT);
|
||||
requestAnimationFrame(() => {
|
||||
// Double-check panel is expanded and resize to max
|
||||
if (!notesPanel.isCollapsed()) {
|
||||
notesPanel.resize(MAX_NOTES_SIZE_PERCENT);
|
||||
}
|
||||
// Verify size after another frame
|
||||
requestAnimationFrame(() => {
|
||||
const size = notesPanel.getSize();
|
||||
if (size < MIN_NOTES_SIZE_PERCENT) {
|
||||
notesPanel.resize(MAX_NOTES_SIZE_PERCENT);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Collapse panel
|
||||
notesPanel.resize(COLLAPSED_SIZE_PERCENT);
|
||||
notesPanel.collapse();
|
||||
requestAnimationFrame(() => {
|
||||
if (!notesPanel.isCollapsed() || notesPanel.getSize() > COLLAPSED_SIZE_PERCENT + 1) {
|
||||
notesPanel.collapse();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [showNotesPanel]);
|
||||
|
||||
// Sync stats panel collapsed state with preferences
|
||||
useEffect(() => {
|
||||
const statsPanel = statsPanelRef.current;
|
||||
if (!statsPanel) return;
|
||||
|
||||
if (showStatsPanel) {
|
||||
// Expand panel first, then resize to preferred size
|
||||
statsPanel.expand(MIN_STATS_SIZE_PERCENT);
|
||||
requestAnimationFrame(() => {
|
||||
if (!statsPanel.isCollapsed()) {
|
||||
statsPanel.resize(MAX_STATS_SIZE_PERCENT);
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
const size = statsPanel.getSize();
|
||||
if (size < MIN_STATS_SIZE_PERCENT) {
|
||||
statsPanel.resize(MAX_STATS_SIZE_PERCENT);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Collapse panel
|
||||
statsPanel.resize(COLLAPSED_SIZE_PERCENT);
|
||||
statsPanel.collapse();
|
||||
}
|
||||
}, [showStatsPanel]);
|
||||
|
||||
return {
|
||||
notesPanelRef,
|
||||
statsPanelRef,
|
||||
handleToggleNotesPanel,
|
||||
handleToggleStatsPanel,
|
||||
handleNotesCollapse,
|
||||
handleStatsCollapse,
|
||||
handleNotesExpand,
|
||||
handleStatsExpand,
|
||||
};
|
||||
}
|
||||
@@ -106,5 +106,11 @@ export const ButtonVariant = {
|
||||
SECONDARY: 'secondary',
|
||||
} as const;
|
||||
|
||||
// Small square icon button sizing (used in panel headers)
|
||||
export const buttonSize = {
|
||||
/** Small square icon button (28px) */
|
||||
iconSm: 'h-7 w-7 p-0',
|
||||
} as const;
|
||||
|
||||
// External link security attribute (prevents tab-nabbing)
|
||||
export const EXTERNAL_LINK_REL = 'noopener noreferrer' as const;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
||||
import { useNavigate, useOutletContext, useParams } from 'react-router-dom';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
|
||||
@@ -35,8 +34,10 @@ import {
|
||||
MAX_NOTES_SIZE_PERCENT,
|
||||
MAX_STATS_SIZE_PERCENT,
|
||||
MIN_NOTES_SIZE_PERCENT,
|
||||
MIN_STATS_SIZE_PERCENT,
|
||||
usePanelPreferences,
|
||||
} from '@/hooks/use-panel-preferences';
|
||||
import { useRecordingPanels } from '@/hooks/use-recording-panels';
|
||||
import { useRecordingSession } from '@/hooks/use-recording-session';
|
||||
import { preferences } from '@/lib/preferences';
|
||||
import { buildSpeakerNameMap } from '@/lib/speaker-utils';
|
||||
@@ -161,9 +162,22 @@ export default function RecordingPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Panel refs for imperative collapse/expand
|
||||
const notesPanelRef = useRef<ImperativePanelHandle>(null);
|
||||
const statsPanelRef = useRef<ImperativePanelHandle>(null);
|
||||
// Panel refs, toggle handlers, and collapse/expand callbacks
|
||||
const {
|
||||
notesPanelRef,
|
||||
statsPanelRef,
|
||||
handleToggleNotesPanel,
|
||||
handleToggleStatsPanel,
|
||||
handleNotesCollapse,
|
||||
handleStatsCollapse,
|
||||
handleNotesExpand,
|
||||
handleStatsExpand,
|
||||
} = useRecordingPanels({
|
||||
showNotesPanel,
|
||||
showStatsPanel,
|
||||
setShowNotesPanel,
|
||||
setShowStatsPanel,
|
||||
});
|
||||
|
||||
const shouldVirtualizeTranscript = filteredSegments.length > TRANSCRIPT_VIRTUALIZE_THRESHOLD;
|
||||
const transcriptVirtualizer = useVirtualizer({
|
||||
@@ -210,29 +224,6 @@ export default function RecordingPage() {
|
||||
scrollElement.scrollTop = scrollElement.scrollHeight;
|
||||
}, [segments.length, partialText, recordingState]);
|
||||
|
||||
// Sync panel collapsed state with preferences
|
||||
useEffect(() => {
|
||||
const notesPanel = notesPanelRef.current;
|
||||
if (notesPanel) {
|
||||
if (showNotesPanel && notesPanel.isCollapsed()) {
|
||||
notesPanel.expand();
|
||||
} else if (!showNotesPanel && !notesPanel.isCollapsed()) {
|
||||
notesPanel.collapse();
|
||||
}
|
||||
}
|
||||
}, [showNotesPanel]);
|
||||
|
||||
useEffect(() => {
|
||||
const statsPanel = statsPanelRef.current;
|
||||
if (statsPanel) {
|
||||
if (showStatsPanel && statsPanel.isCollapsed()) {
|
||||
statsPanel.expand();
|
||||
} else if (!showStatsPanel && !statsPanel.isCollapsed()) {
|
||||
statsPanel.collapse();
|
||||
}
|
||||
}
|
||||
}, [showStatsPanel]);
|
||||
|
||||
// Navigate after stop
|
||||
const handleStopRecording = async () => {
|
||||
await stopRecording();
|
||||
@@ -387,8 +378,8 @@ export default function RecordingPage() {
|
||||
maxSize={MAX_NOTES_SIZE_PERCENT}
|
||||
collapsible
|
||||
collapsedSize={COLLAPSED_SIZE_PERCENT}
|
||||
onCollapse={() => setShowNotesPanel(false)}
|
||||
onExpand={() => setShowNotesPanel(true)}
|
||||
onCollapse={handleNotesCollapse}
|
||||
onExpand={handleNotesExpand}
|
||||
onResize={setNotesPanelSize}
|
||||
>
|
||||
<NotesPanel
|
||||
@@ -398,6 +389,7 @@ export default function RecordingPage() {
|
||||
isRecording={recordingState === 'recording'}
|
||||
notes={notes}
|
||||
onNotesChange={setNotes}
|
||||
onTogglePanel={handleToggleNotesPanel}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
@@ -412,12 +404,12 @@ export default function RecordingPage() {
|
||||
id="stats"
|
||||
order={3}
|
||||
defaultSize={showStatsPanel ? statsPanelSize : COLLAPSED_SIZE_PERCENT}
|
||||
minSize={COLLAPSED_SIZE_PERCENT}
|
||||
minSize={MIN_STATS_SIZE_PERCENT}
|
||||
maxSize={MAX_STATS_SIZE_PERCENT}
|
||||
collapsible
|
||||
collapsedSize={COLLAPSED_SIZE_PERCENT}
|
||||
onCollapse={() => setShowStatsPanel(false)}
|
||||
onExpand={() => setShowStatsPanel(true)}
|
||||
onCollapse={handleStatsCollapse}
|
||||
onExpand={handleStatsExpand}
|
||||
onResize={setStatsPanelSize}
|
||||
>
|
||||
<StatsPanel
|
||||
@@ -430,6 +422,7 @@ export default function RecordingPage() {
|
||||
isVadActive={isVadActive}
|
||||
audioLevel={audioLevel}
|
||||
speakerNameMap={speakerNameMap}
|
||||
onTogglePanel={handleToggleStatsPanel}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
|
||||
@@ -27,7 +27,10 @@
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"client/src/components"
|
||||
"client/src",
|
||||
"src/",
|
||||
"clinet/src-tauri"
|
||||
|
||||
],
|
||||
"ignore": {
|
||||
"useGitignore": true,
|
||||
|
||||
@@ -67,8 +67,13 @@ def _load_watchfiles() -> tuple[type[_WatchfilesPythonFilter], Callable[..., obj
|
||||
|
||||
|
||||
def run_server() -> None:
|
||||
"""Start the gRPC server process."""
|
||||
subprocess.run([sys.executable, "-m", "noteflow.grpc.server"], check=False)
|
||||
"""Start the gRPC server process.
|
||||
|
||||
Uses os.execvp() to replace this process with the server, avoiding
|
||||
nested subprocess spawning that causes zombie processes when watchfiles
|
||||
restarts on file changes.
|
||||
"""
|
||||
os.execvp(sys.executable, [sys.executable, "-m", "noteflow.grpc.server"])
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -11,11 +11,16 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from opentelemetry.trace import Span
|
||||
|
||||
from noteflow.infrastructure.logging import get_logger
|
||||
|
||||
from ._refinement import NoDiarizationAudioError
|
||||
from ._types import DIARIZATION_TIMEOUT_SECONDS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._job_validation import DiarizationJobContext
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def _set_diarization_span_attributes(span: Span, ctx: DiarizationJobContext) -> None:
|
||||
"""Set initial span attributes for diarization job tracing."""
|
||||
@@ -74,6 +79,19 @@ async def _execute_diarization_with_span(
|
||||
span.set_attribute("diarization.cancelled", True)
|
||||
await ctx.host.handle_job_cancelled(ctx.job_id, ctx.job, ctx.meeting_id)
|
||||
raise # Re-raise to propagate cancellation
|
||||
except NoDiarizationAudioError as exc:
|
||||
# Expected for simulated transcripts or meetings without audio capture.
|
||||
# Mark as completed with 0 updates rather than failed.
|
||||
span.set_attribute("diarization.skipped", True)
|
||||
span.set_attribute("diarization.skip_reason", str(exc))
|
||||
logger.info(
|
||||
"Diarization skipped for meeting %s: %s",
|
||||
ctx.meeting_id,
|
||||
exc,
|
||||
)
|
||||
# Complete the job with 0 updates - this is a valid "nothing to do" state
|
||||
await ctx.host.update_job_completed(ctx.job_id, ctx.job, 0, [])
|
||||
span.add_event("job_completed_no_audio")
|
||||
# INTENTIONAL BROAD HANDLER: Job error boundary
|
||||
# - Diarization can fail in many ways (model errors, audio issues, etc.)
|
||||
# - Must capture any failure and update job status
|
||||
|
||||
Reference in New Issue
Block a user