From bc1e91f580c90d68ec9a72f846cc175ade69968d Mon Sep 17 00:00:00 2001 From: Panthevm Date: Wed, 20 May 2026 19:31:43 +0300 Subject: [PATCH 1/3] [#7624] Notebooks: view route, list page polish, REST/SQL cell views - Sidebar: Notebooks link with Notebook icon - /notebooks (index): list of personal + community notebooks via aidbox.notebooks/notebooks RPC, fuzzy search, tag chips for cell types (rest/sql/markdown), keyboard navigation - /notebooks/$id (view): renders cells read-only. - Markdown cells via react-markdown + remark-gfm - REST cells: raw editor + Send + response with Body/Headers tabs, status/duration; saved cell.result auto-populates response - SQL cells: SQL editor + Send + results rendered via shared ResultContent (db-console table); saved result auto-populates - /notebooks/$id/edit placeholder - /notebooks/new placeholder - /notebooks parent layout for breadcrumb - Search/tag UX consistency fix in Analytics and ResourceBrowser: trailing-token isn't promoted to a chip until whitespace is typed --- package.json | 2 + pnpm-lock.yaml | 849 +++++++++++++++++++++ src/components/ResourceBrowser/browser.tsx | 17 +- src/layout/sidebar.tsx | 11 + src/routeTree.gen.ts | 123 +++ src/routes/analytics.index.tsx | 17 +- src/routes/notebooks.$id.edit.tsx | 11 + src/routes/notebooks.$id.tsx | 739 ++++++++++++++++++ src/routes/notebooks.index.tsx | 458 +++++++++++ src/routes/notebooks.new.tsx | 11 + src/routes/notebooks.tsx | 7 + 11 files changed, 2241 insertions(+), 4 deletions(-) create mode 100644 src/routes/notebooks.$id.edit.tsx create mode 100644 src/routes/notebooks.$id.tsx create mode 100644 src/routes/notebooks.index.tsx create mode 100644 src/routes/notebooks.new.tsx create mode 100644 src/routes/notebooks.tsx diff --git a/package.json b/package.json index f4561f6..059bc24 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,8 @@ "lucide-react": "^0.577.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", "sql-formatter": "^15.7.2", "zod-to-json-schema": "^3.25.1" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d6ffb4..20342cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,12 @@ importers: react-dom: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.0)(react@19.2.4) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 sql-formatter: specifier: ^15.7.2 version: 15.7.2 @@ -1946,6 +1952,12 @@ packages: '@types/d3-zoom@3.0.8': resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1958,6 +1970,12 @@ packages: '@types/json-patch@0.0.33': resolution: {integrity: sha512-XQ9hIoJCtnvTCnIV+p+SWQbY8Bt1pe+RuTkPG7lQeRCa9sPwYbhb0aYzM2paVPk0SGvV70R33rxjPjBcJ4Kdxw==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@25.5.0': resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} @@ -1969,6 +1987,9 @@ packages: '@types/react@19.2.0': resolution: {integrity: sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1978,6 +1999,9 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2041,6 +2065,9 @@ packages: babel-plugin-react-compiler@19.1.0-rc.3: resolution: {integrity: sha512-mjRn69WuTz4adL0bXGx8Rsyk1086zFJeKmes6aK0xPuK3aaXmDJdLHqwKKMrpm6KAI1MCoUK72d2VeqQbu8YIA==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + baseline-browser-mapping@2.10.8: resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==} engines: {node: '>=6.0.0'} @@ -2062,6 +2089,21 @@ packages: caniuse-lite@1.0.30001779: resolution: {integrity: sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2089,6 +2131,9 @@ packages: codemirror@6.0.2: resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -2188,6 +2233,9 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2244,14 +2292,24 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} @@ -2310,16 +2368,28 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + highlight.js@11.11.1: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + immer@10.2.0: resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} immer@11.1.4: resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + input-otp@1.4.2: resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} peerDependencies: @@ -2330,10 +2400,19 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2342,10 +2421,17 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + isbot@5.1.36: resolution: {integrity: sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ==} engines: {node: '>=18'} @@ -2445,6 +2531,9 @@ packages: resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} engines: {node: '>= 12.0.0'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lowlight@3.3.0: resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} @@ -2459,11 +2548,143 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@15.0.12: resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} engines: {node: '>= 18'} hasBin: true + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + moo@0.5.3: resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==} @@ -2499,6 +2720,9 @@ packages: oauth4webapi@3.8.5: resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + path-expression-matcher@1.5.0: resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} engines: {node: '>=14.0.0'} @@ -2526,6 +2750,9 @@ packages: engines: {node: '>=14'} hasBin: true + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + radix-ui@1.4.3: resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} peerDependencies: @@ -2566,6 +2793,12 @@ packages: react-is@19.2.4: resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-redux@9.2.0: resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} peerDependencies: @@ -2651,6 +2884,18 @@ packages: redux@5.0.1: resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -2701,6 +2946,9 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + sql-formatter@15.7.2: resolution: {integrity: sha512-b0BGoM81KFRVSpZFwPpIPU5gng4YD8DI/taLD96NXCFRf5af3FzSE4aSwjKmxcyTmf/MfPu91j75883nRrWDBw==} hasBin: true @@ -2709,6 +2957,9 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@7.2.0: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} @@ -2719,6 +2970,12 @@ packages: style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} @@ -2743,6 +3000,12 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2762,6 +3025,24 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + unplugin@2.3.11: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} @@ -2803,6 +3084,12 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} @@ -2935,6 +3222,9 @@ packages: react: optional: true + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@agentclientprotocol/sdk@0.14.1(zod@4.3.6)': @@ -4797,6 +5087,14 @@ snapshots: '@types/d3-interpolate': 3.0.4 '@types/d3-selection': 3.0.11 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} '@types/hast@3.0.4': @@ -4807,6 +5105,12 @@ snapshots: '@types/json-patch@0.0.33': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@25.5.0': dependencies: undici-types: 7.18.2 @@ -4819,6 +5123,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/unist@2.0.11': {} + '@types/unist@3.0.3': {} '@types/use-sync-external-store@0.0.6': {} @@ -4827,6 +5133,8 @@ snapshots: dependencies: '@types/node': 25.5.0 + '@ungap/structured-clone@1.3.1': {} + '@vitejs/plugin-react@4.7.0(vite@7.3.2(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 @@ -4910,6 +5218,8 @@ snapshots: dependencies: '@babel/types': 7.29.0 + bail@2.0.2: {} + baseline-browser-mapping@2.10.8: {} binary-extensions@2.3.0: {} @@ -4928,6 +5238,16 @@ snapshots: caniuse-lite@1.0.30001779: {} + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -4976,6 +5296,8 @@ snapshots: '@codemirror/state': 6.6.0 '@codemirror/view': 6.42.1 + comma-separated-tokens@2.0.3: {} + commander@2.20.3: {} convert-source-map@2.0.0: {} @@ -5060,6 +5382,10 @@ snapshots: decimal.js-light@2.5.1: {} + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -5128,10 +5454,16 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@5.0.0: {} + esprima@4.0.1: {} + estree-util-is-identifier-name@3.0.0: {} + eventemitter3@5.0.4: {} + extend@3.0.2: {} + fast-diff@1.3.0: {} fast-xml-builder@1.2.0: @@ -5177,12 +5509,40 @@ snapshots: graceful-fs@4.2.11: {} + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + highlight.js@11.11.1: {} + html-url-attributes@3.0.1: {} + immer@10.2.0: {} immer@11.1.4: {} + inline-style-parser@0.2.7: {} + input-otp@1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 @@ -5190,18 +5550,31 @@ snapshots: internmap@2.0.3: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-decimal@2.0.1: {} + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + isbot@5.1.36: {} jiti@2.6.1: {} @@ -5265,6 +5638,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.31.1 lightningcss-win32-x64-msvc: 1.31.1 + longest-streak@3.1.0: {} + lowlight@3.3.0: dependencies: '@types/hast': 3.0.4 @@ -5283,8 +5658,354 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-table@3.0.4: {} + marked@15.0.12: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + moo@0.5.3: {} ms@2.1.3: {} @@ -5311,6 +6032,16 @@ snapshots: oauth4webapi@3.8.5: {} + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + path-expression-matcher@1.5.0: {} pathe@2.0.3: {} @@ -5329,6 +6060,8 @@ snapshots: prettier@3.8.1: {} + property-information@7.1.0: {} + radix-ui@1.4.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@radix-ui/primitive': 1.1.3 @@ -5418,6 +6151,24 @@ snapshots: react-is@19.2.4: {} + react-markdown@10.1.0(@types/react@19.2.0)(react@19.2.4): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.0 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.4 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-redux@9.2.0(@types/react@19.2.0)(react@19.2.4)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 @@ -5508,6 +6259,40 @@ snapshots: redux@5.0.1: {} + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + reselect@5.1.1: {} resolve-pkg-maps@1.0.0: {} @@ -5566,6 +6351,8 @@ snapshots: source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} + sql-formatter@15.7.2: dependencies: argparse: 2.0.1 @@ -5577,6 +6364,11 @@ snapshots: get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@7.2.0: dependencies: ansi-regex: 6.2.2 @@ -5585,6 +6377,14 @@ snapshots: style-mod@4.1.3: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + tailwind-merge@3.5.0: {} tailwindcss@4.2.1: {} @@ -5604,6 +6404,10 @@ snapshots: dependencies: is-number: 7.0.0 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + tslib@2.8.1: {} tsx@4.21.0: @@ -5619,6 +6423,39 @@ snapshots: undici-types@7.18.2: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 @@ -5660,6 +6497,16 @@ snapshots: - '@types/react' - '@types/react-dom' + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + victory-vendor@37.3.6: dependencies: '@types/d3-array': 3.2.2 @@ -5754,3 +6601,5 @@ snapshots: '@types/react': 19.2.0 immer: 11.1.4 react: 19.2.4 + + zwitch@2.0.4: {} diff --git a/src/components/ResourceBrowser/browser.tsx b/src/components/ResourceBrowser/browser.tsx index 4c5de45..cf9b01b 100644 --- a/src/components/ResourceBrowser/browser.tsx +++ b/src/components/ResourceBrowser/browser.tsx @@ -408,7 +408,20 @@ export function Browser() { setTags(chips.filter((c) => c !== tag)); }; const updateTextPart = (next: string) => { - const parsed = parseQuery(next); + // last token without trailing whitespace is "in progress" — don't parse yet + let toParse = next; + let tail = ""; + if (!/\s$/.test(next)) { + const m = next.match(/^(.*\s)(\S*)$/); + if (m) { + toParse = m[1] ?? ""; + tail = m[2] ?? ""; + } else { + toParse = ""; + tail = next; + } + } + const parsed = parseQuery(toParse); if (parsed.chips.length > 0) { const seen = new Set(chips.map(tagSlug)); const extra: string[] = []; @@ -420,7 +433,7 @@ export function Browser() { } } if (extra.length > 0) setTags([...chips, ...extra]); - setText(parsed.text); + setText([parsed.text, tail].filter(Boolean).join(" ")); } else { setText(next); } diff --git a/src/layout/sidebar.tsx b/src/layout/sidebar.tsx index 5169997..7ab4db0 100644 --- a/src/layout/sidebar.tsx +++ b/src/layout/sidebar.tsx @@ -15,6 +15,7 @@ import { ClipboardList, Columns3Cog, Database, + Notebook, Package, PanelLeftClose, PanelLeftOpen, @@ -76,6 +77,16 @@ const mainMenuItems: { ), }, + { + url: "/notebooks", + title: "Notebooks", + link: ( + + + Notebooks + + ), + }, { url: "/ig", title: "FHIR packages", diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 86fca70..0d72407 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -12,15 +12,19 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as SettingsRouteImport } from './routes/settings' import { Route as RestRouteImport } from './routes/rest' import { Route as ResourceRouteImport } from './routes/resource' +import { Route as NotebooksRouteImport } from './routes/notebooks' import { Route as IgRouteImport } from './routes/ig' import { Route as DbConsoleRouteImport } from './routes/db-console' import { Route as AuditEventsRouteImport } from './routes/audit-events' import { Route as AnalyticsRouteImport } from './routes/analytics' import { Route as IndexRouteImport } from './routes/index' import { Route as ResourceIndexRouteImport } from './routes/resource.index' +import { Route as NotebooksIndexRouteImport } from './routes/notebooks.index' import { Route as IgIndexRouteImport } from './routes/ig.index' import { Route as AnalyticsIndexRouteImport } from './routes/analytics.index' import { Route as ResourceResourceTypeRouteImport } from './routes/resource.$resourceType' +import { Route as NotebooksNewRouteImport } from './routes/notebooks.new' +import { Route as NotebooksIdRouteImport } from './routes/notebooks.$id' import { Route as IgAddRouteImport } from './routes/ig.add' import { Route as IgPackageIdRouteImport } from './routes/ig.$packageId' import { Route as AnalyticsViewsRouteImport } from './routes/analytics.views' @@ -30,6 +34,7 @@ import { Route as IgPackageIdIndexRouteImport } from './routes/ig.$packageId.ind import { Route as AnalyticsViewsIndexRouteImport } from './routes/analytics.views.index' import { Route as AnalyticsQueriesIndexRouteImport } from './routes/analytics.queries.index' import { Route as ResourceResourceTypeCreateRouteImport } from './routes/resource.$resourceType.create' +import { Route as NotebooksIdEditRouteImport } from './routes/notebooks.$id.edit' import { Route as AnalyticsViewsCreateRouteImport } from './routes/analytics.views.create' import { Route as AnalyticsQueriesCreateRouteImport } from './routes/analytics.queries.create' import { Route as ResourceResourceTypeEditIdRouteImport } from './routes/resource.$resourceType.edit.$id' @@ -52,6 +57,11 @@ const ResourceRoute = ResourceRouteImport.update({ path: '/resource', getParentRoute: () => rootRouteImport, } as any) +const NotebooksRoute = NotebooksRouteImport.update({ + id: '/notebooks', + path: '/notebooks', + getParentRoute: () => rootRouteImport, +} as any) const IgRoute = IgRouteImport.update({ id: '/ig', path: '/ig', @@ -82,6 +92,11 @@ const ResourceIndexRoute = ResourceIndexRouteImport.update({ path: '/', getParentRoute: () => ResourceRoute, } as any) +const NotebooksIndexRoute = NotebooksIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => NotebooksRoute, +} as any) const IgIndexRoute = IgIndexRouteImport.update({ id: '/', path: '/', @@ -97,6 +112,16 @@ const ResourceResourceTypeRoute = ResourceResourceTypeRouteImport.update({ path: '/$resourceType', getParentRoute: () => ResourceRoute, } as any) +const NotebooksNewRoute = NotebooksNewRouteImport.update({ + id: '/new', + path: '/new', + getParentRoute: () => NotebooksRoute, +} as any) +const NotebooksIdRoute = NotebooksIdRouteImport.update({ + id: '/$id', + path: '/$id', + getParentRoute: () => NotebooksRoute, +} as any) const IgAddRoute = IgAddRouteImport.update({ id: '/add', path: '/add', @@ -144,6 +169,11 @@ const ResourceResourceTypeCreateRoute = path: '/create', getParentRoute: () => ResourceResourceTypeRoute, } as any) +const NotebooksIdEditRoute = NotebooksIdEditRouteImport.update({ + id: '/edit', + path: '/edit', + getParentRoute: () => NotebooksIdRoute, +} as any) const AnalyticsViewsCreateRoute = AnalyticsViewsCreateRouteImport.update({ id: '/create', path: '/create', @@ -183,6 +213,7 @@ export interface FileRoutesByFullPath { '/audit-events': typeof AuditEventsRoute '/db-console': typeof DbConsoleRoute '/ig': typeof IgRouteWithChildren + '/notebooks': typeof NotebooksRouteWithChildren '/resource': typeof ResourceRouteWithChildren '/rest': typeof RestRoute '/settings': typeof SettingsRoute @@ -190,12 +221,16 @@ export interface FileRoutesByFullPath { '/analytics/views': typeof AnalyticsViewsRouteWithChildren '/ig/$packageId': typeof IgPackageIdRouteWithChildren '/ig/add': typeof IgAddRoute + '/notebooks/$id': typeof NotebooksIdRouteWithChildren + '/notebooks/new': typeof NotebooksNewRoute '/resource/$resourceType': typeof ResourceResourceTypeRouteWithChildren '/analytics/': typeof AnalyticsIndexRoute '/ig/': typeof IgIndexRoute + '/notebooks/': typeof NotebooksIndexRoute '/resource/': typeof ResourceIndexRoute '/analytics/queries/create': typeof AnalyticsQueriesCreateRoute '/analytics/views/create': typeof AnalyticsViewsCreateRoute + '/notebooks/$id/edit': typeof NotebooksIdEditRoute '/resource/$resourceType/create': typeof ResourceResourceTypeCreateRoute '/analytics/queries/': typeof AnalyticsQueriesIndexRoute '/analytics/views/': typeof AnalyticsViewsIndexRoute @@ -213,11 +248,15 @@ export interface FileRoutesByTo { '/rest': typeof RestRoute '/settings': typeof SettingsRoute '/ig/add': typeof IgAddRoute + '/notebooks/$id': typeof NotebooksIdRouteWithChildren + '/notebooks/new': typeof NotebooksNewRoute '/analytics': typeof AnalyticsIndexRoute '/ig': typeof IgIndexRoute + '/notebooks': typeof NotebooksIndexRoute '/resource': typeof ResourceIndexRoute '/analytics/queries/create': typeof AnalyticsQueriesCreateRoute '/analytics/views/create': typeof AnalyticsViewsCreateRoute + '/notebooks/$id/edit': typeof NotebooksIdEditRoute '/resource/$resourceType/create': typeof ResourceResourceTypeCreateRoute '/analytics/queries': typeof AnalyticsQueriesIndexRoute '/analytics/views': typeof AnalyticsViewsIndexRoute @@ -235,6 +274,7 @@ export interface FileRoutesById { '/audit-events': typeof AuditEventsRoute '/db-console': typeof DbConsoleRoute '/ig': typeof IgRouteWithChildren + '/notebooks': typeof NotebooksRouteWithChildren '/resource': typeof ResourceRouteWithChildren '/rest': typeof RestRoute '/settings': typeof SettingsRoute @@ -242,12 +282,16 @@ export interface FileRoutesById { '/analytics/views': typeof AnalyticsViewsRouteWithChildren '/ig/$packageId': typeof IgPackageIdRouteWithChildren '/ig/add': typeof IgAddRoute + '/notebooks/$id': typeof NotebooksIdRouteWithChildren + '/notebooks/new': typeof NotebooksNewRoute '/resource/$resourceType': typeof ResourceResourceTypeRouteWithChildren '/analytics/': typeof AnalyticsIndexRoute '/ig/': typeof IgIndexRoute + '/notebooks/': typeof NotebooksIndexRoute '/resource/': typeof ResourceIndexRoute '/analytics/queries/create': typeof AnalyticsQueriesCreateRoute '/analytics/views/create': typeof AnalyticsViewsCreateRoute + '/notebooks/$id/edit': typeof NotebooksIdEditRoute '/resource/$resourceType/create': typeof ResourceResourceTypeCreateRoute '/analytics/queries/': typeof AnalyticsQueriesIndexRoute '/analytics/views/': typeof AnalyticsViewsIndexRoute @@ -266,6 +310,7 @@ export interface FileRouteTypes { | '/audit-events' | '/db-console' | '/ig' + | '/notebooks' | '/resource' | '/rest' | '/settings' @@ -273,12 +318,16 @@ export interface FileRouteTypes { | '/analytics/views' | '/ig/$packageId' | '/ig/add' + | '/notebooks/$id' + | '/notebooks/new' | '/resource/$resourceType' | '/analytics/' | '/ig/' + | '/notebooks/' | '/resource/' | '/analytics/queries/create' | '/analytics/views/create' + | '/notebooks/$id/edit' | '/resource/$resourceType/create' | '/analytics/queries/' | '/analytics/views/' @@ -296,11 +345,15 @@ export interface FileRouteTypes { | '/rest' | '/settings' | '/ig/add' + | '/notebooks/$id' + | '/notebooks/new' | '/analytics' | '/ig' + | '/notebooks' | '/resource' | '/analytics/queries/create' | '/analytics/views/create' + | '/notebooks/$id/edit' | '/resource/$resourceType/create' | '/analytics/queries' | '/analytics/views' @@ -317,6 +370,7 @@ export interface FileRouteTypes { | '/audit-events' | '/db-console' | '/ig' + | '/notebooks' | '/resource' | '/rest' | '/settings' @@ -324,12 +378,16 @@ export interface FileRouteTypes { | '/analytics/views' | '/ig/$packageId' | '/ig/add' + | '/notebooks/$id' + | '/notebooks/new' | '/resource/$resourceType' | '/analytics/' | '/ig/' + | '/notebooks/' | '/resource/' | '/analytics/queries/create' | '/analytics/views/create' + | '/notebooks/$id/edit' | '/resource/$resourceType/create' | '/analytics/queries/' | '/analytics/views/' @@ -347,6 +405,7 @@ export interface RootRouteChildren { AuditEventsRoute: typeof AuditEventsRoute DbConsoleRoute: typeof DbConsoleRoute IgRoute: typeof IgRouteWithChildren + NotebooksRoute: typeof NotebooksRouteWithChildren ResourceRoute: typeof ResourceRouteWithChildren RestRoute: typeof RestRoute SettingsRoute: typeof SettingsRoute @@ -375,6 +434,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ResourceRouteImport parentRoute: typeof rootRouteImport } + '/notebooks': { + id: '/notebooks' + path: '/notebooks' + fullPath: '/notebooks' + preLoaderRoute: typeof NotebooksRouteImport + parentRoute: typeof rootRouteImport + } '/ig': { id: '/ig' path: '/ig' @@ -417,6 +483,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ResourceIndexRouteImport parentRoute: typeof ResourceRoute } + '/notebooks/': { + id: '/notebooks/' + path: '/' + fullPath: '/notebooks/' + preLoaderRoute: typeof NotebooksIndexRouteImport + parentRoute: typeof NotebooksRoute + } '/ig/': { id: '/ig/' path: '/' @@ -438,6 +511,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ResourceResourceTypeRouteImport parentRoute: typeof ResourceRoute } + '/notebooks/new': { + id: '/notebooks/new' + path: '/new' + fullPath: '/notebooks/new' + preLoaderRoute: typeof NotebooksNewRouteImport + parentRoute: typeof NotebooksRoute + } + '/notebooks/$id': { + id: '/notebooks/$id' + path: '/$id' + fullPath: '/notebooks/$id' + preLoaderRoute: typeof NotebooksIdRouteImport + parentRoute: typeof NotebooksRoute + } '/ig/add': { id: '/ig/add' path: '/add' @@ -501,6 +588,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ResourceResourceTypeCreateRouteImport parentRoute: typeof ResourceResourceTypeRoute } + '/notebooks/$id/edit': { + id: '/notebooks/$id/edit' + path: '/edit' + fullPath: '/notebooks/$id/edit' + preLoaderRoute: typeof NotebooksIdEditRouteImport + parentRoute: typeof NotebooksIdRoute + } '/analytics/views/create': { id: '/analytics/views/create' path: '/create' @@ -622,6 +716,34 @@ const IgRouteChildren: IgRouteChildren = { const IgRouteWithChildren = IgRoute._addFileChildren(IgRouteChildren) +interface NotebooksIdRouteChildren { + NotebooksIdEditRoute: typeof NotebooksIdEditRoute +} + +const NotebooksIdRouteChildren: NotebooksIdRouteChildren = { + NotebooksIdEditRoute: NotebooksIdEditRoute, +} + +const NotebooksIdRouteWithChildren = NotebooksIdRoute._addFileChildren( + NotebooksIdRouteChildren, +) + +interface NotebooksRouteChildren { + NotebooksIdRoute: typeof NotebooksIdRouteWithChildren + NotebooksNewRoute: typeof NotebooksNewRoute + NotebooksIndexRoute: typeof NotebooksIndexRoute +} + +const NotebooksRouteChildren: NotebooksRouteChildren = { + NotebooksIdRoute: NotebooksIdRouteWithChildren, + NotebooksNewRoute: NotebooksNewRoute, + NotebooksIndexRoute: NotebooksIndexRoute, +} + +const NotebooksRouteWithChildren = NotebooksRoute._addFileChildren( + NotebooksRouteChildren, +) + interface ResourceResourceTypeRouteChildren { ResourceResourceTypeCreateRoute: typeof ResourceResourceTypeCreateRoute ResourceResourceTypeIndexRoute: typeof ResourceResourceTypeIndexRoute @@ -657,6 +779,7 @@ const rootRouteChildren: RootRouteChildren = { AuditEventsRoute: AuditEventsRoute, DbConsoleRoute: DbConsoleRoute, IgRoute: IgRouteWithChildren, + NotebooksRoute: NotebooksRouteWithChildren, ResourceRoute: ResourceRouteWithChildren, RestRoute: RestRoute, SettingsRoute: SettingsRoute, diff --git a/src/routes/analytics.index.tsx b/src/routes/analytics.index.tsx index 8c20f85..5d1033e 100644 --- a/src/routes/analytics.index.tsx +++ b/src/routes/analytics.index.tsx @@ -532,7 +532,20 @@ export function AnalyticsListPage({ setTags(tags.filter((t) => t !== tag)); }; const updateTextPart = (next: string) => { - const parsed = parseQuery(next); + // last token without trailing whitespace is "in progress" — don't parse yet + let toParse = next; + let tail = ""; + if (!/\s$/.test(next)) { + const m = next.match(/^(.*\s)(\S*)$/); + if (m) { + toParse = m[1] ?? ""; + tail = m[2] ?? ""; + } else { + toParse = ""; + tail = next; + } + } + const parsed = parseQuery(toParse); if (parsed.chips.length > 0) { const seen = new Set(tags.map(tagSlug)); const extra: string[] = []; @@ -544,7 +557,7 @@ export function AnalyticsListPage({ } } if (extra.length > 0) setTags([...tags, ...extra]); - setText(parsed.text); + setText([parsed.text, tail].filter(Boolean).join(" ")); } else { setText(next); } diff --git a/src/routes/notebooks.$id.edit.tsx b/src/routes/notebooks.$id.edit.tsx new file mode 100644 index 0000000..ab5a7e2 --- /dev/null +++ b/src/routes/notebooks.$id.edit.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from "@tanstack/react-router"; + +function NotebookEditPage() { + return
; +} + +export const Route = createFileRoute("/notebooks/$id/edit")({ + staticData: { title: "Edit notebook" }, + loader: ({ params }) => ({ breadCrumb: params.id }), + component: NotebookEditPage, +}); diff --git a/src/routes/notebooks.$id.tsx b/src/routes/notebooks.$id.tsx new file mode 100644 index 0000000..391dea5 --- /dev/null +++ b/src/routes/notebooks.$id.tsx @@ -0,0 +1,739 @@ +import * as HSComp from "@health-samurai/react-components"; +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute, Link } from "@tanstack/react-router"; +import * as yaml from "js-yaml"; +import { + Check, + ChevronLeft, + Copy, + Globe, + Loader2, + Pencil, + Play, + Timer, + User, +} from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { useAidboxClient } from "../AidboxClient"; +import { ResultContent } from "../components/db-console/result-content"; +import { transformToQueryResultItems } from "../components/db-console/tables-view"; +import { HTTP_STATUS_CODES } from "../shared/const"; +import type { QueryResultItem } from "../webmcp/db-console-context"; + +type SavedRestResult = { + status?: number; + statusText?: string; + headers?: Record; + body?: string; +}; + +type Cell = { + id?: string; + type?: "rest" | "sql" | "markdown" | "rpc" | string; + value?: string; + result?: SavedRestResult | unknown; +}; + +type Notebook = { + id: string; + resourceType?: string; + name?: string; + description?: string; + cells?: Cell[]; + origin?: string; + "publication-id"?: string; + "edit-secret"?: string; +}; + +function useNotebook(id: string, path?: string) { + const client = useAidboxClient(); + return useQuery({ + queryKey: ["notebook", id, path ?? null], + queryFn: async () => { + const body = path + ? { + method: "aidbox.notebooks/open-repo-notebook", + params: { "notebook-url": path }, + } + : { + method: "aidbox.notebooks/get-notebook-by-id", + params: { notebook: { id } }, + }; + const resp = await client.rawRequest({ + method: "POST", + url: `/rpc?_m=${body.method}`, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + const json = (await resp.response.json()) as { + result?: { notebook?: Notebook }; + error?: unknown; + }; + return json.result?.notebook ?? null; + }, + }); +} + +const ACRONYM_TAGS = new Set(["rest", "sql", "rpc"]); +function formatTag(s: string): string { + const lower = s.toLowerCase(); + if (ACRONYM_TAGS.has(lower)) return lower.toUpperCase(); + return lower.charAt(0).toUpperCase() + lower.slice(1); +} + +const MD_COMPONENTS = { + h1: ({ children }: { children?: React.ReactNode }) => ( +

{children}

+ ), + h2: ({ children }: { children?: React.ReactNode }) => ( +

+ {children} +

+ ), + h3: ({ children }: { children?: React.ReactNode }) => ( +

+ {children} +

+ ), + p: ({ children }: { children?: React.ReactNode }) => ( +

{children}

+ ), + ul: ({ children }: { children?: React.ReactNode }) => ( +
    {children}
+ ), + ol: ({ children }: { children?: React.ReactNode }) => ( +
    + {children} +
+ ), + li: ({ children }: { children?: React.ReactNode }) => ( +
  • {children}
  • + ), + a: ({ href, children }: { href?: string; children?: React.ReactNode }) => ( + + {children} + + ), + blockquote: ({ children }: { children?: React.ReactNode }) => ( +
    + {children} +
    + ), + code: ({ + className, + children, + }: { + className?: string; + children?: React.ReactNode; + }) => { + const isBlock = className?.startsWith("language-"); + return ( + + {children} + + ); + }, + pre: ({ children }: { children?: React.ReactNode }) => ( +
    +			{children}
    +		
    + ), + hr: () =>
    , + strong: ({ children }: { children?: React.ReactNode }) => ( + {children} + ), + em: ({ children }: { children?: React.ReactNode }) => ( + {children} + ), + table: ({ children }: { children?: React.ReactNode }) => ( + + {children} +
    + ), + th: ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ), + td: ({ children }: { children?: React.ReactNode }) => ( + {children} + ), +}; + +function parseRawRequest(raw: string): { + method: string; + path: string; + headers: Record; + body: string; +} { + const [head = "", ...rest] = raw.split(/\r?\n\r?\n/); + const body = rest.join("\n\n"); + const lines = head.split(/\r?\n/); + const firstLine = lines[0]?.trim() ?? ""; + const m = firstLine.match(/^(\S+)\s+(.+)$/); + const method = m ? (m[1] ?? "GET").toUpperCase() : "GET"; + const path = m ? (m[2] ?? "") : firstLine; + const headers: Record = {}; + for (let i = 1; i < lines.length; i++) { + const line = (lines[i] ?? "").trim(); + if (!line) continue; + const idx = line.indexOf(":"); + if (idx > 0) { + const name = line.slice(0, idx).trim(); + const value = line.slice(idx + 1).trim(); + if (name) headers[name] = value; + } + } + return { method, path, headers, body }; +} + +function parsePathQuery(path: string): { name: string; value: string }[] { + const qIdx = path.indexOf("?"); + if (qIdx < 0) return []; + const qs = path.slice(qIdx + 1); + return qs + .split("&") + .filter(Boolean) + .map((pair) => { + const eq = pair.indexOf("="); + const name = eq < 0 ? pair : pair.slice(0, eq); + const value = eq < 0 ? "" : pair.slice(eq + 1); + try { + return { + name: decodeURIComponent(name), + value: decodeURIComponent(value), + }; + } catch { + return { name, value }; + } + }); +} + +function KeyValueTable({ + rows, + empty, +}: { + rows: { name: string; value: string }[]; + empty: string; +}) { + if (rows.length === 0) { + return ( +
    + {empty} +
    + ); + } + return ( + + + {rows.map((r, i) => ( + + + + + ))} + +
    + {r.name} + + {r.value} +
    + ); +} + +type CellResponse = { + status: number; + statusText: string; + headers: Record; + body: string; + duration?: number; +}; + +function savedRestToResponse(saved: unknown): CellResponse | null { + if (!saved || typeof saved !== "object") return null; + const s = saved as SavedRestResult; + if (typeof s.status !== "number") return null; + return { + status: s.status, + statusText: s.statusText ?? "", + headers: s.headers ?? {}, + body: typeof s.body === "string" ? s.body : JSON.stringify(s.body ?? ""), + }; +} + +function detectBodyMode(headers: Record): "json" | "yaml" { + const ct = headers["content-type"] ?? headers["Content-Type"] ?? ""; + return /yaml/i.test(ct) ? "yaml" : "json"; +} + +function formatBodyContent(body: string, mode: "json" | "yaml"): string { + if (mode === "yaml") { + try { + return yaml.dump(yaml.load(body), { + indent: 2, + lineWidth: -1, + noRefs: true, + }); + } catch { + return body; + } + } + try { + return JSON.stringify(JSON.parse(body), null, 2); + } catch { + return body; + } +} + +function ResponseStatus({ response }: { response: CellResponse }) { + const messageColor = + response.status >= 400 ? "text-critical-default" : "text-green-500"; + return ( + + Status: + + {response.status}{" "} + + {response.statusText || HTTP_STATUS_CODES[response.status]} + + + + ); +} + +function RestCellView({ cell }: { cell: Cell }) { + const [raw, setRaw] = useState(cell.value ?? ""); + const [response, setResponse] = useState( + savedRestToResponse(cell.result), + ); + const [loading, setLoading] = useState(false); + const [hasFreshResponse, setHasFreshResponse] = useState(false); + const [tab, setTab] = useState<"body" | "headers">("body"); + const [reqTab, setReqTab] = useState<"params" | "headers" | "raw">("raw"); + const responseRef = useRef(null); + const client = useAidboxClient(); + + useEffect(() => { + if (response && hasFreshResponse) { + responseRef.current?.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + } + }, [response, hasFreshResponse]); + + const send = async () => { + setLoading(true); + try { + const { method, path, headers, body } = parseRawRequest(raw); + const t0 = performance.now(); + const resp = await client.rawRequest({ + method: method as "GET", + url: path, + ...(Object.keys(headers).length > 0 ? { headers } : {}), + ...(body ? { body } : {}), + }); + const text = await resp.response.text(); + const duration = performance.now() - t0; + const respHeaders: Record = {}; + resp.response.headers.forEach((value, key) => { + respHeaders[key] = value; + }); + setResponse({ + status: resp.response.status, + statusText: resp.response.statusText, + headers: respHeaders, + body: text, + duration, + }); + setHasFreshResponse(true); + } finally { + setLoading(false); + } + }; + + const bodyMode = response ? detectBodyMode(response.headers) : "json"; + const content = response + ? tab === "headers" + ? JSON.stringify(response.headers, null, 2) + : formatBodyContent(response.body, bodyMode) + : ""; + const mode = tab === "headers" ? "json" : bodyMode; + + return ( +
    + setReqTab(v as "params" | "headers" | "raw")} + className="flex flex-col" + > +
    +
    + + Request + + + Raw + Params + Headers + +
    + +
    + {reqTab === "raw" && ( +
    + +
    + )} + {reqTab === "params" && ( + + )} + {reqTab === "headers" && ( + ({ name, value }), + )} + empty="No headers." + /> + )} +
    + {response && ( + setTab(v as "body" | "headers")} + className="flex flex-col" + > +
    +
    + + Response + + + Body + Headers + +
    +
    + + {response.duration !== undefined && ( + + + + {Math.round(response.duration)} + + ms + + )} +
    +
    +
    + )} + {response && ( +
    + +
    + )} +
    + ); +} + +function normalizeMarkdown(s: string): string { + // CommonMark requires a space after #/##/etc., but old notebooks often have + // "##Heading" written without it. Insert a space so headings render. + return s.replace(/^(#{1,6})(?=[^\s#])/gm, "$1 "); +} + +function savedSqlToResults(saved: unknown): QueryResultItem[] | null { + if (!saved) return null; + if (Array.isArray(saved)) { + const rows = saved as Record[]; + return [ + { + query: "", + duration: 0, + status: "success", + result: rows, + rows: rows.length, + }, + ]; + } + if (typeof saved === "object") { + const s = saved as { + query?: string; + duration?: number; + status?: "success" | "error"; + result?: Array< + | { type: "rset"; data: Record[] } + | { type: "count"; data: number } + >; + error?: string; + position?: number; + }; + if (s.status === "success" || s.status === "error") { + return transformToQueryResultItems({ + query: s.query ?? "", + duration: s.duration ?? 0, + status: s.status, + result: s.result, + error: s.error, + position: s.position, + }); + } + } + return null; +} + +function SqlCellView({ cell }: { cell: Cell }) { + const [query, setQuery] = useState(cell.value ?? ""); + const [results, setResults] = useState(() => + savedSqlToResults(cell.result), + ); + const [error, setError] = useState(null); + const [duration, setDuration] = useState(); + const [loading, setLoading] = useState(false); + const client = useAidboxClient(); + + const send = async () => { + setLoading(true); + setError(null); + try { + const t0 = performance.now(); + const resp = await fetch(`${client.getBaseUrl()}/$psql`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ query }), + }); + const took = performance.now() - t0; + setDuration(took); + if (!resp.ok) { + setError(`HTTP ${resp.status}: ${await resp.text()}`); + setResults(null); + return; + } + const items = transformToQueryResultItems(await resp.json()); + setResults(items); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + setResults(null); + } finally { + setLoading(false); + } + }; + + const hasResponse = results !== null || error !== null; + + return ( +
    +
    + + SQL + + +
    +
    + +
    + {hasResponse && ( + <> +
    + + Result + {results + ? ` (${results.reduce((s, r) => s + (r.result?.length ?? 0), 0)})` + : ""} + + {duration !== undefined && ( + + + {Math.round(duration)} + ms + + )} +
    +
    + void send()} + onCancel={() => undefined} + /> +
    + + )} +
    + ); +} + +function CellView({ cell }: { cell: Cell }) { + const type = cell.type ?? "rest"; + if (type === "rest") { + return ; + } + if (type === "sql") { + return ; + } + if (type === "markdown") { + return ( +
    + + {normalizeMarkdown(cell.value ?? "")} + +
    + ); + } + return ( +
    +
    + + #{formatTag(type)} + +
    +
    +				{cell.value ?? ""}
    +			
    +
    + ); +} + +function NotebookViewPage() { + const { id } = Route.useParams(); + const { path } = Route.useSearch(); + const { data: notebook, isLoading } = useNotebook(id, path); + + const isCommunity = !!path || !!notebook?.origin; + const canEdit = !isCommunity; + + return ( +
    +
    +
    + {isLoading ? null : !notebook ? ( +
    + Notebook not found. +
    + ) : ( +
    +
    +
    + {isCommunity ? ( + + ) : ( + + )} + {isCommunity ? "Community" : "Personal"} +
    +
    +

    + {notebook.name ?? "(unnamed)"} +

    + {canEdit && ( + + + + Edit + + + )} +
    + {notebook.description && ( +

    + {notebook.description} +

    + )} +
    + {(notebook.cells ?? []).length > 0 && ( +
    + {(notebook.cells ?? []).map((cell, i) => ( + + ))} +
    + )} +
    + )} +
    +
    +
    + ); +} + +const validateSearch = (search: { path?: unknown }): { path?: string } => { + if (typeof search.path === "string" && search.path.length > 0) + return { path: search.path }; + return {}; +}; + +export const Route = createFileRoute("/notebooks/$id")({ + staticData: { title: "Notebook" }, + loader: ({ params }) => ({ breadCrumb: params.id }), + component: NotebookViewPage, + validateSearch, +}); diff --git a/src/routes/notebooks.index.tsx b/src/routes/notebooks.index.tsx new file mode 100644 index 0000000..2a2a420 --- /dev/null +++ b/src/routes/notebooks.index.tsx @@ -0,0 +1,458 @@ +import * as HSComp from "@health-samurai/react-components"; +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; +import Fuse from "fuse.js"; +import { + FileUp, + Globe, + Link as LinkIcon, + Plus, + Search, + Upload, + User, + X, +} from "lucide-react"; +import * as React from "react"; +import { useAidboxClient } from "../AidboxClient"; +import { EmptyState } from "../components/empty-state"; +import { + filterHighlightRanges, + highlight, + type MatchRange, +} from "../utils/highlight"; +import { parseQuery, tagSlug } from "../utils/tag-search"; + +const ACRONYM_TAGS = new Set(["rest", "sql", "rpc"]); +function formatTag(s: string): string { + const lower = s.toLowerCase(); + if (ACRONYM_TAGS.has(lower)) return lower.toUpperCase(); + return lower.charAt(0).toUpperCase() + lower.slice(1); +} + +type RpcNotebook = { + id: string; + name?: string; + description?: string; + source?: { type: "rpc" | "uri"; id?: string; path?: string }; + tags?: { value?: string[] }; + "cell-types"?: string[] | null; +}; + +type NotebookItem = { + id: string; + name: string; + description?: string; + isCommunity: boolean; + path?: string; + cellTypes: string[]; + nameMatches?: readonly MatchRange[]; + descriptionMatches?: readonly MatchRange[]; +}; + +function useNotebooks() { + const client = useAidboxClient(); + return useQuery({ + queryKey: ["notebooks-list"], + queryFn: async () => { + const resp = await client.rawRequest({ + method: "POST", + url: "/rpc?_m=aidbox.notebooks/notebooks", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + method: "aidbox.notebooks/notebooks", + params: { query: "" }, + }), + }); + const json = (await resp.response.json()) as { + result?: { notebooks?: RpcNotebook[] }; + error?: unknown; + }; + const list = json.result?.notebooks ?? []; + return list.flatMap((nb) => { + if (!nb.id) return []; + const cellTypes = Array.from(new Set(nb["cell-types"] ?? [])).sort(); + return [ + { + id: nb.id, + name: nb.name ?? "(unnamed)", + description: nb.description, + isCommunity: nb.source?.type === "uri", + path: nb.source?.path, + cellTypes, + } satisfies NotebookItem, + ]; + }); + }, + }); +} + +function SearchBar({ + chips, + textPart, + inputRef, + onTextChange, + onRemoveChip, + onClear, + onInputKeyDown, +}: { + chips: string[]; + textPart: string; + inputRef: (el: HTMLInputElement | null) => void; + onTextChange: (next: string) => void; + onRemoveChip: (tag: string) => void; + onClear: () => void; + onInputKeyDown?: (e: React.KeyboardEvent) => void; +}) { + return ( +
    + + {chips.map((chip) => ( + + ))} + onTextChange(e.target.value)} + onKeyDown={(e) => { + const last = chips[chips.length - 1]; + if (e.key === "Backspace" && textPart === "" && last) { + e.preventDefault(); + onRemoveChip(last); + return; + } + onInputKeyDown?.(e); + }} + placeholder={ + chips.length === 0 ? "Search notebooks by name or description…" : "" + } + className="flex-1 min-w-[80px] bg-transparent outline-none typo-body text-text-primary placeholder:text-text-tertiary" + /> + {(chips.length > 0 || textPart.length > 0) && ( + } + /> + )} +
    + ); +} + +function NotebookRow({ + nb, + focused, + focusedRef, + onTagClick, +}: { + nb: NotebookItem; + focused: boolean; + focusedRef: React.RefObject | undefined; + onTagClick: (t: string) => void; +}) { + const KindIcon = nb.isCommunity ? Globe : User; + const kindLabel = nb.isCommunity ? "Community" : "Personal"; + const accentClass = nb.isCommunity + ? "text-text-success-primary" + : "text-text-warning-primary"; + return ( +
  • + +
    + + {kindLabel} +
    +
    + {highlight(nb.name, nb.nameMatches)} +
    + {nb.description && ( +
    + {highlight(nb.description, nb.descriptionMatches)} +
    + )} + {nb.cellTypes.length > 0 && ( +
    + {nb.cellTypes.map((t) => ( + + ))} +
    + )} + +
  • + ); +} + +function NotebooksPage() { + const search = Route.useSearch(); + const navigate = useNavigate({ from: "/notebooks/" }); + const text = search.q ?? ""; + const tags = search.tags ?? []; + const { data: rawItems = [], isLoading } = useNotebooks(); + + const setText = (next: string) => + navigate({ + search: (prev) => ({ ...prev, q: next || undefined }), + replace: true, + }); + const setTags = (next: string[]) => + navigate({ + search: (prev) => ({ ...prev, tags: next.length > 0 ? next : undefined }), + replace: true, + }); + + const tagTokens = tags.map(tagSlug); + + const tagFiltered = React.useMemo(() => { + if (tagTokens.length === 0) return rawItems; + return rawItems.filter((nb) => { + const slugs = nb.cellTypes.map(tagSlug); + return tagTokens.every((t) => slugs.includes(t)); + }); + }, [rawItems, tagTokens]); + + const fuse = React.useMemo( + () => + new Fuse(tagFiltered, { + keys: ["name", "description"], + includeMatches: true, + threshold: 0.3, + ignoreLocation: true, + minMatchCharLength: 1, + }), + [tagFiltered], + ); + + const items: NotebookItem[] = text + ? fuse.search(text).map((r) => { + const nameMatch = r.matches?.find((m) => m.key === "name"); + const descriptionMatch = r.matches?.find( + (m) => m.key === "description", + ); + return { + ...r.item, + nameMatches: filterHighlightRanges( + text, + nameMatch?.indices as readonly MatchRange[] | undefined, + ), + descriptionMatches: filterHighlightRanges( + text, + descriptionMatch?.indices as readonly MatchRange[] | undefined, + ), + }; + }) + : [...tagFiltered].sort( + (a, b) => Number(a.isCommunity) - Number(b.isCommunity), + ); + + const [focusedIndex, setFocusedIndex] = React.useState(-1); + const focusedRowRef = React.useRef(null); + const didFocus = React.useRef(false); + const setSearchInputRef = React.useCallback((el: HTMLInputElement | null) => { + if (el && !didFocus.current) { + el.focus(); + didFocus.current = true; + } + }, []); + + // biome-ignore lint/correctness/useExhaustiveDependencies: focusedIndex triggers scroll + React.useEffect(() => { + focusedRowRef.current?.scrollIntoView({ block: "nearest" }); + }, [focusedIndex]); + + React.useEffect(() => { + if (text && items.length > 0) setFocusedIndex(0); + else setFocusedIndex(-1); + }, [text, items.length]); + + const openItem = (it: NotebookItem) => { + navigate({ + to: "/notebooks/$id", + params: { id: it.id }, + search: it.path ? { path: it.path } : {}, + }); + }; + + const addTag = (tagText: string) => { + const slug = tagSlug(tagText); + if (tags.some((t) => tagSlug(t) === slug)) return; + setTags([...tags, tagText]); + }; + const removeChip = (tag: string) => { + setTags(tags.filter((t) => t !== tag)); + }; + const updateTextPart = (next: string) => { + // last token without trailing whitespace is "in progress" — don't parse yet + let toParse = next; + let tail = ""; + if (!/\s$/.test(next)) { + const m = next.match(/^(.*\s)(\S*)$/); + if (m) { + toParse = m[1] ?? ""; + tail = m[2] ?? ""; + } else { + toParse = ""; + tail = next; + } + } + const parsed = parseQuery(toParse); + if (parsed.chips.length > 0) { + const seen = new Set(tags.map(tagSlug)); + const extra: string[] = []; + for (const c of parsed.chips) { + const s = tagSlug(c); + if (!seen.has(s)) { + extra.push(c); + seen.add(s); + } + } + if (extra.length > 0) setTags([...tags, ...extra]); + setText([parsed.text, tail].filter(Boolean).join(" ")); + } else { + setText(next); + } + }; + const onClear = () => { + setTags([]); + setText(""); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) { + e.preventDefault(); + setFocusedIndex((p) => Math.min(p + 1, items.length - 1)); + } else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) { + e.preventDefault(); + setFocusedIndex((p) => Math.max(p - 1, -1)); + } else if (e.key === "Enter") { + if (focusedIndex < 0) return; + const it = items[focusedIndex]; + if (!it) return; + e.preventDefault(); + openItem(it); + } + }; + + const isEmpty = rawItems.length === 0 && tags.length === 0 && !text; + + return ( +
    +
    +
    + + + + + + Upload + + + + + + as link + + + + as file + + + + navigate({ to: "/notebooks/new" })} + > + + New + +
    +
    +
    + {isLoading ? null : isEmpty ? ( + + ) : items.length === 0 ? ( +
    + Nothing matches “ + {[...tags.map((t) => `#${t}`), text].filter(Boolean).join(" ")}”. +
    + ) : ( +
      + {items.map((nb, index) => ( + + ))} +
    + )} +
    +
    + ); +} + +const validateSearch = (search: { + q?: unknown; + tags?: unknown; +}): { q?: string; tags?: string[] } => { + const out: { q?: string; tags?: string[] } = {}; + if (typeof search.q === "string" && search.q.length > 0) out.q = search.q; + if (Array.isArray(search.tags)) { + const tags = search.tags.filter( + (t): t is string => typeof t === "string" && t.length > 0, + ); + if (tags.length > 0) out.tags = tags; + } else if (typeof search.tags === "string" && search.tags.length > 0) { + out.tags = [search.tags]; + } + return out; +}; + +export const Route = createFileRoute("/notebooks/")({ + staticData: { title: "Notebooks" }, + loader: () => ({ breadCrumb: "Notebooks" }), + component: NotebooksPage, + validateSearch, +}); diff --git a/src/routes/notebooks.new.tsx b/src/routes/notebooks.new.tsx new file mode 100644 index 0000000..c947944 --- /dev/null +++ b/src/routes/notebooks.new.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from "@tanstack/react-router"; + +function NotebookCreatePage() { + return
    ; +} + +export const Route = createFileRoute("/notebooks/new")({ + staticData: { title: "New notebook" }, + loader: () => ({ breadCrumb: "New notebook" }), + component: NotebookCreatePage, +}); diff --git a/src/routes/notebooks.tsx b/src/routes/notebooks.tsx new file mode 100644 index 0000000..a88a7b1 --- /dev/null +++ b/src/routes/notebooks.tsx @@ -0,0 +1,7 @@ +import { createFileRoute, Outlet } from "@tanstack/react-router"; + +export const Route = createFileRoute("/notebooks")({ + staticData: { title: "Notebooks" }, + loader: () => ({ breadCrumb: "Notebooks" }), + component: () => , +}); From 16ee6ca72d0719a9dd1cd0b40b97ad3a251f1403 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Thu, 21 May 2026 12:24:51 +0300 Subject: [PATCH 2/3] Notebooks: code-block style, layout spacing, breadcrumb name - Markdown code: treat fenced code without a language as a block (was rendered as inline pill); switch block style to typo-code so it matches the rest of the app. - Pre blocks: bleed -mx-3 like REST/SQL cells, add mb-4. - Page layout: drop gap-6 in favor of mt-7 between header and cells, bump container py to 8. - REST/SQL cell wrappers: replace mb-2 with mt-4 mb-4 for breathing room. - Breadcrumb: show notebook name (with id fallback) by reusing the same React Query key as the page; works for both personal and community notebooks via the optional search.path. --- src/layout/navbar.tsx | 91 +++++++++++++++++++++++++++++++----- src/routes/notebooks.$id.tsx | 21 +++++---- 2 files changed, 92 insertions(+), 20 deletions(-) diff --git a/src/layout/navbar.tsx b/src/layout/navbar.tsx index cc06816..bf28ed8 100644 --- a/src/layout/navbar.tsx +++ b/src/layout/navbar.tsx @@ -16,6 +16,7 @@ import { TooltipContent, TooltipTrigger, } from "@health-samurai/react-components"; +import { useQuery } from "@tanstack/react-query"; import { Link, useMatches } from "@tanstack/react-router"; import { BookOpenText, @@ -26,6 +27,7 @@ import { UserRound, } from "lucide-react"; import React, { lazy, Suspense, useEffect } from "react"; +import { useAidboxClient } from "../AidboxClient"; import { useInstanceName, useLogout, useUserInfo } from "../api/auth"; import { useCanonicalDisplay } from "../api/canonical-resources"; import AidboxLogo from "../assets/aidbox-logo.svg"; @@ -46,18 +48,73 @@ function inferResourceTypeFromPath(path: string): string | null { return null; } -function CurrentCrumb({ title, path }: { title: string; path: string }) { +type NotebookLite = { id?: string; name?: string } | null; + +function useNotebookDisplay( + id: string | null, + path: string | null, +): { display: string | null; isLoading: boolean } { + const client = useAidboxClient(); + const enabled = !!id; + const { data, isLoading, isFetching } = useQuery({ + queryKey: enabled + ? ["notebook", id, path ?? null] + : ["__notebook-display-skip__"], + enabled, + queryFn: async () => { + const body = path + ? { + method: "aidbox.notebooks/open-repo-notebook", + params: { "notebook-url": path }, + } + : { + method: "aidbox.notebooks/get-notebook-by-id", + params: { notebook: { id } }, + }; + const resp = await client.rawRequest({ + method: "POST", + url: `/rpc?_m=${body.method}`, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + const json = (await resp.response.json()) as { + result?: { notebook?: NotebookLite }; + }; + return json.result?.notebook ?? null; + }, + staleTime: 60_000, + }); + if (!enabled) return { display: null, isLoading: false }; + if (data) return { display: data.name ?? null, isLoading: false }; + return { display: null, isLoading: isLoading || isFetching }; +} + +function CurrentCrumb({ + title, + path, + search, +}: { + title: string; + path: string; + search?: Record; +}) { const isUuid = /^[0-9a-f-]{36}$/i.test(title); - const resourceType: string | null = isUuid - ? (inferResourceTypeFromPath(path) ?? null) - : null; - const { display, isLoading } = useCanonicalDisplay( - resourceType, - isUuid ? title : null, - ); + const isNotebook = /^\/notebooks\/[^/]+/.test(path); + const resourceType: string | null = + isUuid && !isNotebook ? (inferResourceTypeFromPath(path) ?? null) : null; + const { display: canonicalDisplay, isLoading: canonicalLoading } = + useCanonicalDisplay(resourceType, isUuid ? title : null); + const notebookPath = + typeof search?.path === "string" ? (search.path as string) : null; + const { display: notebookDisplay, isLoading: notebookLoading } = + useNotebookDisplay(isUuid && isNotebook ? title : null, notebookPath); + const display = canonicalDisplay ?? notebookDisplay ?? null; + const showSkeleton = + (!!resourceType && canonicalLoading) || + (isUuid && isNotebook && notebookLoading); return ( <> - {resourceType && isLoading ? ( + {showSkeleton ? ( @@ -82,7 +139,15 @@ function Breadcrumbs() { ...(instanceName ? [{ title: instanceName, path: "/" }] : []), ...matches.flatMap((match) => { const breadCrumb = match.loaderData?.breadCrumb; - return breadCrumb ? [{ title: breadCrumb, path: match.pathname }] : []; + return breadCrumb + ? [ + { + title: breadCrumb, + path: match.pathname, + search: (match.search ?? {}) as Record, + }, + ] + : []; }), ]; @@ -103,7 +168,11 @@ function Breadcrumbs() { } > {index === breadcrumbs.length - 1 ? ( - + ) : ( {crumb.title} diff --git a/src/routes/notebooks.$id.tsx b/src/routes/notebooks.$id.tsx index 391dea5..8c4e9f6 100644 --- a/src/routes/notebooks.$id.tsx +++ b/src/routes/notebooks.$id.tsx @@ -13,7 +13,7 @@ import { Timer, User, } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { Children, useEffect, useRef, useState } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { useAidboxClient } from "../AidboxClient"; @@ -133,12 +133,15 @@ const MD_COMPONENTS = { className?: string; children?: React.ReactNode; }) => { - const isBlock = className?.startsWith("language-"); + const text = Children.toArray(children) + .filter((c): c is string => typeof c === "string") + .join(""); + const isBlock = className?.startsWith("language-") || text.includes("\n"); return ( @@ -147,7 +150,7 @@ const MD_COMPONENTS = { ); }, pre: ({ children }: { children?: React.ReactNode }) => ( -
    +		
     			{children}
     		
    ), @@ -377,7 +380,7 @@ function RestCellView({ cell }: { cell: Cell }) { const mode = tab === "headers" ? "json" : bodyMode; return ( -
    +
    setReqTab(v as "params" | "headers" | "raw")} @@ -571,7 +574,7 @@ function SqlCellView({ cell }: { cell: Cell }) { const hasResponse = results !== null || error !== null; return ( -
    +
    SQL @@ -673,13 +676,13 @@ function NotebookViewPage() { return (
    -
    +
    {isLoading ? null : !notebook ? (
    Notebook not found.
    ) : ( -
    +
    {(notebook.cells ?? []).length > 0 && ( -
    +
    {(notebook.cells ?? []).map((cell, i) => ( ))} From 35656e28f399c89291d38f08360dfd6677a52b12 Mon Sep 17 00:00:00 2001 From: Panthevm Date: Thu, 21 May 2026 17:58:31 +0300 Subject: [PATCH 3/3] Notebooks: editor, publish/upload, run results, dialogs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New editor at /notebooks/new and /notebooks/$id/edit reusing the view-mode REST/SQL/RPC cells with editable values plus a markdown cell with Edit/Preview tabs. Insert dividers ("+") around every cell, delete cell action overflows to the left. - Toolbars (view + edit) match the resource editor: sticky strip with ghost-style buttons (Edit/Save in link color, others default, Share dropdown last). - Run inside cells now saves cell.result back into the notebook and triggers a background save when the notebook has an id. - Publish: view-mode action triggers aidbox.notebooks/publish-notebook on a fresh copy; updates happen automatically on Save when a notebook is already published. - Unpublish: extra cleanup save afterwards so publication-id and edit-secret are cleared on the local notebook; auto-unpublish when deleting a published notebook. - Upload: dropdown with "as link" (dialog + import-notebook RPC) and "as file" (parses the blob produced by Share→As file and posts import-notebook-as-json). - Share-as-file now wraps the page section with mx-auto max-w-[990px] so the exported HTML matches the in-app layout. - Breadcrumbs: edit route shows Notebooks / / Edit by injecting a middle crumb that resolves the notebook name. - Markdown rendering: tighten code-block detection (block when fenced with language OR multi-line), switch to typo-code, fix layout spacing between view and edit. - EDN pretty printer in src/utils/edn.ts (clojure.pprint style) used for RPC responses with application/edn content type. - ConfirmDialog primitive for Delete / Publish / Unpublish. - List dedup: hide the community copy when its origin matches a local personal notebook. --- src/components/confirm-dialog.tsx | 59 +++ src/components/notebook-editor.tsx | 454 ++++++++++++++++++++ src/layout/navbar.tsx | 38 +- src/routeTree.gen.ts | 44 +- src/routes/notebooks.$id.edit.tsx | 11 - src/routes/notebooks.$id.tsx | 654 +++++++++++++++++++++++++---- src/routes/notebooks.$id_.edit.tsx | 76 ++++ src/routes/notebooks.index.tsx | 195 ++++++++- src/routes/notebooks.new.tsx | 3 +- src/utils/edn.ts | 172 ++++++++ 10 files changed, 1554 insertions(+), 152 deletions(-) create mode 100644 src/components/confirm-dialog.tsx create mode 100644 src/components/notebook-editor.tsx delete mode 100644 src/routes/notebooks.$id.edit.tsx create mode 100644 src/routes/notebooks.$id_.edit.tsx create mode 100644 src/utils/edn.ts diff --git a/src/components/confirm-dialog.tsx b/src/components/confirm-dialog.tsx new file mode 100644 index 0000000..6581e22 --- /dev/null +++ b/src/components/confirm-dialog.tsx @@ -0,0 +1,59 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@health-samurai/react-components"; +import type { ReactNode } from "react"; + +export function ConfirmDialog({ + open, + onOpenChange, + title, + description, + confirmLabel = "Confirm", + cancelLabel = "Cancel", + danger = false, + onConfirm, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description?: ReactNode; + confirmLabel?: string; + cancelLabel?: string; + danger?: boolean; + onConfirm: () => void; +}) { + return ( + + + + {title} + + {description && ( + {description} + )} + + onOpenChange(false)}> + {cancelLabel} + + { + onConfirm(); + onOpenChange(false); + }} + > + {confirmLabel} + + + + + ); +} diff --git a/src/components/notebook-editor.tsx b/src/components/notebook-editor.tsx new file mode 100644 index 0000000..c77901f --- /dev/null +++ b/src/components/notebook-editor.tsx @@ -0,0 +1,454 @@ +import * as HSComp from "@health-samurai/react-components"; +import { useMutation } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; +import { Loader2, Plus, Save, Trash2, User } from "lucide-react"; +import { useEffect, useState } from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { useAidboxClient } from "../AidboxClient"; +import { + type Cell, + MD_COMPONENTS, + normalizeMarkdown, + RestCellView, + SqlCellView, +} from "../routes/notebooks.$id"; +import { ConfirmDialog } from "./confirm-dialog"; + +export type CellType = "rest" | "sql" | "markdown" | "rpc"; + +export type EditableCell = { + id: string; + type: CellType; + value: string; + result?: unknown; +}; + +export type EditableNotebook = { + id?: string; + name?: string; + description?: string; + cells: EditableCell[]; + "publication-id"?: string; + "edit-secret"?: string; + origin?: string; +}; + +const CELL_TYPES: { value: CellType; label: string }[] = [ + { value: "rest", label: "REST" }, + { value: "rpc", label: "RPC" }, + { value: "sql", label: "SQL" }, + { value: "markdown", label: "Markdown" }, +]; + +const DEFAULT_CELL_VALUE: Record = { + rest: "GET /fhir/Patient", + rpc: "POST /rpc\ncontent-type: application/json\n\n{}", + sql: "SELECT 1;", + markdown: "", +}; + +function genCellId(): string { + if (typeof crypto !== "undefined" && "randomUUID" in crypto) + return crypto.randomUUID(); + return Math.random().toString(36).slice(2) + Date.now().toString(36); +} + +export function emptyNotebook(): EditableNotebook { + return { + name: "", + description: "", + cells: [], + }; +} + +function AddCellDivider({ onAdd }: { onAdd: (t: CellType) => void }) { + return ( +
    +
    + + + + + + {CELL_TYPES.map((t) => ( + onAdd(t.value)} + > + {t.label} + + ))} + + +
    + ); +} + +function MarkdownEditCell({ + cell, + onChange, +}: { + cell: EditableCell; + onChange: (value: string) => void; +}) { + const [value, setValue] = useState(cell.value); + const [mode, setMode] = useState<"edit" | "preview">("edit"); + const update = (v: string) => { + setValue(v); + onChange(v); + }; + return ( +
    + setMode(v as "edit" | "preview")} + className="flex flex-col" + > +
    +
    + + Markdown + + + Edit + Preview + +
    +
    + {mode === "edit" ? ( +