Link tooltips + AI link summaries (#3088)

Co-authored-by: Samy Pessé <samypesse@gmail.com>
This commit is contained in:
Zeno Kapitein
2025-04-04 20:56:42 +02:00
committed by GitHub
parent 580f7ade15
commit 416bde70bc
42 changed files with 937 additions and 127 deletions
+60 -26
View File
@@ -64,15 +64,15 @@
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.1.8",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.16",
"@upstash/redis": "^1.27.1",
"ai": "^4.1.46",
"ajv": "^8.12.0",
"ai": "^4.2.2",
"assert-never": "^1.2.1",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
"event-iterator": "^2.0.0",
"framer-motion": "^10.16.14",
"js-cookie": "^3.0.5",
"jsontoxml": "^1.0.1",
@@ -81,13 +81,14 @@
"mathjax": "^3.2.2",
"mdast-util-to-markdown": "^2.1.2",
"memoizee": "^0.4.17",
"next": "14.2.25",
"next": "14.2.26",
"next-themes": "^0.2.1",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
"openapi-types": "^12.1.3",
"p-map": "^7.0.0",
"parse-cache-control": "^1.0.1",
"partial-json": "^0.1.7",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hotkeys-hook": "^4.4.1",
@@ -104,6 +105,9 @@
"unified": "^11.0.5",
"url-join": "^5.0.0",
"usehooks-ts": "^3.1.0",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.5",
"zustand": "^5.0.3",
},
"devDependencies": {
"@argos-ci/playwright": "^4.3.0",
@@ -267,13 +271,13 @@
"react-dom": "18.3.1",
},
"packages": {
"@ai-sdk/provider": ["@ai-sdk/provider@1.0.9", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA=="],
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.1.10", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "eventsource-parser": "^3.0.0", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-4GZ8GHjOFxePFzkl3q42AU0DQOtTQ5w09vmaWUf/pKFXJPizlnzKSUkF0f+VkapIUfDugyMqPMT1ge8XQzVI7Q=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.4", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-13sEGBxB6kgaMPGOgCLYibF6r8iv8mgjhuToFrOTU09bBxbFQd8ZoARarCfJN6VomCUbUvMKwjTBLb1vQnN+WA=="],
"@ai-sdk/react": ["@ai-sdk/react@1.1.19", "", { "dependencies": { "@ai-sdk/provider-utils": "2.1.10", "@ai-sdk/ui-utils": "1.1.16", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.0.0" }, "optionalPeers": ["react", "zod"] }, "sha512-zqSOWmJxpB45ZrwZ04+Q7Uo3xeGM0tva2eUYz2T4gv9Yk6MQAfOBA6+scsKg8CyIuUy4M4/C4pCY3eWQf7sfQg=="],
"@ai-sdk/react": ["@ai-sdk/react@1.2.6", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.4", "@ai-sdk/ui-utils": "1.2.5", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-5BFChNbcYtcY9MBStcDev7WZRHf0NpTrk8yfSoedWctB3jfWkFd1HECBvdc8w3mUQshF2MumLHtAhRO7IFtGGQ=="],
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.1.16", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "@ai-sdk/provider-utils": "2.1.10", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-jfblR2yZVISmNK2zyNzJZFtkgX57WDAUQXcmn3XUBJyo8LFsADu+/vYMn5AOyBi9qJT0RBk11PEtIxIqvByw3Q=="],
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.5", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "@ai-sdk/provider-utils": "2.2.4", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-XDgqnJcaCkDez7qolvk+PDbs/ceJvgkNkxkOlc9uDWqxfDJxtvCZ+14MP/1qr4IBwGIgKVHzMDYDXvqVhSWLzg=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
@@ -751,25 +755,25 @@
"@msgpack/msgpack": ["@msgpack/msgpack@3.0.0-beta2", "", {}, "sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw=="],
"@next/env": ["@next/env@14.2.25", "", {}, "sha512-JnzQ2cExDeG7FxJwqAksZ3aqVJrHjFwZQAEJ9gQZSoEhIow7SNoKZzju/AwQ+PLIR4NY8V0rhcVozx/2izDO0w=="],
"@next/env": ["@next/env@14.2.26", "", {}, "sha512-vO//GJ/YBco+H7xdQhzJxF7ub3SUwft76jwaeOyVVQFHCi5DCnkP16WHB+JBylo4vOKPoZBlR94Z8xBxNBdNJA=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.25", "", { "os": "darwin", "cpu": "arm64" }, "sha512-09clWInF1YRd6le00vt750s3m7SEYNehz9C4PUcSu3bAdCTpjIV4aTYQZ25Ehrr83VR1rZeqtKUPWSI7GfuKZQ=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.26", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zDJY8gsKEseGAxG+C2hTMT0w9Nk9N1Sk1qV7vXYz9MEiyRoF5ogQX2+vplyUMIfygnjn9/A04I6yrUTRTuRiyQ=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.25", "", { "os": "darwin", "cpu": "x64" }, "sha512-V+iYM/QR+aYeJl3/FWWU/7Ix4b07ovsQ5IbkwgUK29pTHmq+5UxeDr7/dphvtXEq5pLB/PucfcBNh9KZ8vWbug=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.26", "", { "os": "darwin", "cpu": "x64" }, "sha512-U0adH5ryLfmTDkahLwG9sUQG2L0a9rYux8crQeC92rPhi3jGQEY47nByQHrVrt3prZigadwj/2HZ1LUUimuSbg=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.25", "", { "os": "linux", "cpu": "arm64" }, "sha512-LFnV2899PJZAIEHQ4IMmZIgL0FBieh5keMnriMY1cK7ompR+JUd24xeTtKkcaw8QmxmEdhoE5Mu9dPSuDBgtTg=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-SINMl1I7UhfHGM7SoRiw0AbwnLEMUnJ/3XXVmhyptzriHbWvPPbbm0OEVG24uUKhuS1t0nvN/DBvm5kz6ZIqpg=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.25", "", { "os": "linux", "cpu": "arm64" }, "sha512-QC5y5PPTmtqFExcKWKYgUNkHeHE/z3lUsu83di488nyP0ZzQ3Yse2G6TCxz6nNsQwgAx1BehAJTZez+UQxzLfw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-s6JaezoyJK2DxrwHWxLWtJKlqKqTdi/zaYigDXUJ/gmx/72CrzdVZfMvUc6VqnZ7YEvRijvYo+0o4Z9DencduA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.25", "", { "os": "linux", "cpu": "x64" }, "sha512-y6/ML4b9eQ2D/56wqatTJN5/JR8/xdObU2Fb1RBidnrr450HLCKr6IJZbPqbv7NXmje61UyxjF5kvSajvjye5w=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.26", "", { "os": "linux", "cpu": "x64" }, "sha512-FEXeUQi8/pLr/XI0hKbe0tgbLmHFRhgXOUiPScz2hk0hSmbGiU8aUqVslj/6C6KA38RzXnWoJXo4FMo6aBxjzg=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.25", "", { "os": "linux", "cpu": "x64" }, "sha512-sPX0TSXHGUOZFvv96GoBXpB3w4emMqKeMgemrSxI7A6l55VBJp/RKYLwZIB9JxSqYPApqiREaIIap+wWq0RU8w=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.26", "", { "os": "linux", "cpu": "x64" }, "sha512-BUsomaO4d2DuXhXhgQCVt2jjX4B4/Thts8nDoIruEJkhE5ifeQFtvW5c9JkdOtYvE5p2G0hcwQ0UbRaQmQwaVg=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.25", "", { "os": "win32", "cpu": "arm64" }, "sha512-ReO9S5hkA1DU2cFCsGoOEp7WJkhFzNbU/3VUF6XxNGUCQChyug6hZdYL/istQgfT/GWE6PNIg9cm784OI4ddxQ=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.26", "", { "os": "win32", "cpu": "arm64" }, "sha512-5auwsMVzT7wbB2CZXQxDctpWbdEnEW/e66DyXO1DcgHxIyhP06awu+rHKshZE+lPLIGiwtjo7bsyeuubewwxMw=="],
"@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.25", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZ/gc0o9neuCDyD5IumyTGHVun2dCox5TfPQI/BJTYwpSNYM3CZDI4i6TOdjeq1JMo+Ug4kPSMuZdwsycwFbAw=="],
"@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.26", "", { "os": "win32", "cpu": "ia32" }, "sha512-GQWg/Vbz9zUGi9X80lOeGsz1rMH/MtFO/XqigDznhhhTfDlDoynCM6982mPCbSlxJ/aveZcKtTlwfAjwhyxDpg=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.25", "", { "os": "win32", "cpu": "x64" }, "sha512-KSznmS6eFjQ9RJ1nEc66kJvtGIL1iZMYmGEXsZPh2YtnLtqrgdVvKXJY2ScjjoFnG6nGLyPFR0UiEvDwVah4Tw=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.26", "", { "os": "win32", "cpu": "x64" }, "sha512-2rdB3T1/Gp7bv1eQTTm9d1Y1sv9UuJ2LAwOE0Pe2prHKe32UNscj7YS13fRB37d0GAiGNR+Y7ZcW8YjDI8Ns0w=="],
"@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="],
@@ -837,6 +841,8 @@
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="],
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="],
@@ -1315,8 +1321,6 @@
"@unhead/schema": ["@unhead/schema@1.11.14", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-V9W9u5tF1/+TiLqxu+Qvh1ShoMDkPEwHoEo4DKdDG6ko7YlbzFfDxV6el9JwCren45U/4Vy/4Xi7j8OH02wsiA=="],
"@upstash/redis": ["@upstash/redis@1.34.3", "", { "dependencies": { "crypto-js": "^4.2.0" } }, "sha512-VT25TyODGy/8ljl7GADnJoMmtmJ1F8d84UXfGonRRF8fWYJz7+2J6GzW+a6ETGtk4OyuRTt7FRSvFG5GvrfSdQ=="],
"@vercel/build-utils": ["@vercel/build-utils@9.0.1", "", {}, "sha512-pG/izEqA0AGyqQj6QBfGoTOKU9FPG18sYw9qpncEq00uA+J4Ly4e8ssNbENsXtnXqkMjeoS3c5ncR3jT0bOyiA=="],
"@vercel/error-utils": ["@vercel/error-utils@2.0.3", "", {}, "sha512-CqC01WZxbLUxoiVdh9B/poPbNpY9U+tO1N9oWHwTl5YAZxcqXmmWJ8KNMFItJCUUWdY3J3xv8LvAuQv2KZ5YdQ=="],
@@ -1393,7 +1397,7 @@
"agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
"ai": ["ai@4.1.47", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "@ai-sdk/provider-utils": "2.1.10", "@ai-sdk/react": "1.1.19", "@ai-sdk/ui-utils": "1.1.16", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.0.0" }, "optionalPeers": ["react", "zod"] }, "sha512-9UZ8Mkv1HlprCJfQ0Kq+rgKbfkrkDtJjslr1WOHBRzvC70jDJJYp8r0Qq4vlF7zs9VBkGyy8Rhm5zQJJIUjvgw=="],
"ai": ["ai@4.3.0", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "@ai-sdk/provider-utils": "2.2.4", "@ai-sdk/react": "1.2.6", "@ai-sdk/ui-utils": "1.2.5", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-PxyQYKhWaU3LiZEpeKRaekVonZIbWdKAwgnqm0CSAxy1MFufmYEC5SM5Mc9uiK2DoHcbAL3d1jyaQ2fSDAJL8w=="],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
@@ -1585,8 +1589,6 @@
"cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="],
"crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="],
"crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="],
"css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="],
@@ -2319,7 +2321,7 @@
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"next": ["next@14.2.25", "", { "dependencies": { "@next/env": "14.2.25", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.25", "@next/swc-darwin-x64": "14.2.25", "@next/swc-linux-arm64-gnu": "14.2.25", "@next/swc-linux-arm64-musl": "14.2.25", "@next/swc-linux-x64-gnu": "14.2.25", "@next/swc-linux-x64-musl": "14.2.25", "@next/swc-win32-arm64-msvc": "14.2.25", "@next/swc-win32-ia32-msvc": "14.2.25", "@next/swc-win32-x64-msvc": "14.2.25" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-N5M7xMc4wSb4IkPvEV5X2BRRXUmhVHNyaXwEM86+voXthSZz8ZiRyQW4p9mwAoAPIm6OzuVZtn7idgEJeAJN3Q=="],
"next": ["next@14.2.26", "", { "dependencies": { "@next/env": "14.2.26", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.26", "@next/swc-darwin-x64": "14.2.26", "@next/swc-linux-arm64-gnu": "14.2.26", "@next/swc-linux-arm64-musl": "14.2.26", "@next/swc-linux-x64-gnu": "14.2.26", "@next/swc-linux-x64-musl": "14.2.26", "@next/swc-win32-arm64-msvc": "14.2.26", "@next/swc-win32-ia32-msvc": "14.2.26", "@next/swc-win32-x64-msvc": "14.2.26" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-b81XSLihMwCfwiUVRRja3LphLo4uBBMZEzBBWMaISbKTwOmq3wPknIETy/8000tr7Gq4WmbuFYPS7jOYIf+ZJw=="],
"next-themes": ["next-themes@0.2.1", "", { "peerDependencies": { "next": "*", "react": "*", "react-dom": "*" } }, "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A=="],
@@ -2417,6 +2419,8 @@
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
@@ -2949,9 +2953,9 @@
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
"zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
"zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="],
@@ -3679,6 +3683,24 @@
"@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A=="],
"@radix-ui/react-tooltip/@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
"@radix-ui/react-tooltip/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
"@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
"@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.2", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA=="],
"@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
"@radix-ui/react-tooltip/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="],
"@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
"@radix-ui/react-tooltip/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q=="],
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="],
"@react-aria/focus/clsx": ["clsx@2.0.0", "", {}, "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="],
@@ -3689,12 +3711,20 @@
"@scalar/api-client/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="],
"@scalar/api-client/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
"@scalar/code-highlight/remark-gfm": ["remark-gfm@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA=="],
"@scalar/oas-utils/flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="],
"@scalar/oas-utils/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
"@scalar/object-utils/flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="],
"@scalar/types/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
"@scalar/use-hooks/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
"@shikijs/core/hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"@smithy/abort-controller/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -4623,6 +4653,8 @@
"@cloudflare/next-on-pages/miniflare/youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="],
"@cloudflare/next-on-pages/miniflare/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
"@codemirror/lang-json/@codemirror/language/@codemirror/view": ["@codemirror/view@6.34.1", "", { "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ=="],
"@codemirror/lang-json/@codemirror/language/@lezer/common": ["@lezer/common@1.2.2", "", {}, "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="],
@@ -4701,6 +4733,8 @@
"@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
"@scalar/api-client/pretty-ms/parse-ms": ["parse-ms@3.0.0", "", {}, "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw=="],
+1 -1
View File
@@ -7,7 +7,7 @@
"turbo": "^2.4.4",
"vercel": "^39.3.0"
},
"packageManager": "bun@1.2.5",
"packageManager": "bun@1.2.8",
"overrides": {
"@codemirror/state": "6.4.1",
"react": "18.3.1",
+20
View File
@@ -179,6 +179,10 @@ export function createDataFetcher(
getUserById(userId) {
return trace('getUserById', () => getUserById(input, { userId }));
},
streamAIResponse(params) {
return streamAIResponse(input, params);
},
};
}
@@ -643,6 +647,22 @@ const renderIntegrationUi = memoize(async function renderIntegrationUi(
});
});
async function* streamAIResponse(
input: DataFetcherInput,
params: Parameters<GitBookDataFetcher['streamAIResponse']>[0]
) {
const api = await apiClient(input);
const res = await api.orgs.streamAiResponseInSite(params.organizationId, params.siteId, {
input: params.input,
output: params.output,
model: params.model,
});
for await (const event of res) {
yield event;
}
}
let loggedServiceBinding = false;
/**
+11
View File
@@ -179,4 +179,15 @@ export interface GitBookDataFetcher {
integrationName: string;
request: api.RenderIntegrationUI;
}): Promise<DataFetcherResponse<api.ContentKitRenderOutput>>;
/**
* Stream an AI response.
*/
streamAIResponse(params: {
organizationId: string;
siteId: string;
input: api.AIMessageInput[];
output: api.AIOutputFormat;
model: api.AIModel;
}): AsyncGenerator<api.AIStreamResponse, void, unknown>;
}
+9 -5
View File
@@ -29,12 +29,11 @@
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.1.8",
"@sindresorhus/fnv1a": "^3.1.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.16",
"@upstash/redis": "^1.27.1",
"ai": "^4.1.46",
"ajv": "^8.12.0",
"ai": "^4.2.2",
"assert-never": "^1.2.1",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
@@ -46,7 +45,7 @@
"mathjax": "^3.2.2",
"mdast-util-to-markdown": "^2.1.2",
"memoizee": "^0.4.17",
"next": "14.2.25",
"next": "14.2.26",
"next-themes": "^0.2.1",
"nuqs": "^2.2.3",
"object-hash": "^3.0.0",
@@ -68,7 +67,12 @@
"tailwind-shades": "^1.1.2",
"unified": "^11.0.5",
"url-join": "^5.0.0",
"usehooks-ts": "^3.1.0"
"usehooks-ts": "^3.1.0",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.5",
"event-iterator": "^2.0.0",
"partial-json": "^0.1.7",
"zustand": "^5.0.3"
},
"devDependencies": {
"@argos-ci/playwright": "^4.3.0",
@@ -1,7 +1,7 @@
'use client';
import { Button } from '@/components/primitives/Button';
import { t, useLanguage } from '@/intl/client';
import { t, tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
export default function ErrorPage(props: {
@@ -35,9 +35,8 @@ export default function ErrorPage(props: {
}}
variant="secondary"
size="small"
>
{t(language, 'unexpected_error_retry')}
</Button>
label={tString(language, 'unexpected_error_retry')}
/>
</div>
</div>
</div>
@@ -0,0 +1,104 @@
'use client';
import { useLanguage } from '@/intl/client';
import { t } from '@/intl/translate';
import { Icon } from '@gitbook/icons';
import { useEffect, useState } from 'react';
import { useVisitedPages } from '../Insights';
import { usePageContext } from '../PageContext';
import { Loading } from '../primitives';
import { streamLinkPageSummary } from './server-actions/streamLinkPageSummary';
/**
* Summarise a page's content for use in a link preview
*/
export function AIPageLinkSummary(props: {
targetSpaceId: string;
targetPageId: string;
linkPreview?: string;
linkTitle?: string;
showTrademark: boolean;
}) {
const { targetSpaceId, targetPageId, linkPreview, linkTitle, showTrademark = true } = props;
const currentPage = usePageContext();
const language = useLanguage();
const visitedPages = useVisitedPages((state) => state.pages);
const [highlight, setHighlight] = useState('');
useEffect(() => {
let canceled = false;
setHighlight('');
(async () => {
const stream = await streamLinkPageSummary({
currentSpaceId: currentPage.spaceId,
currentPageId: currentPage.pageId,
currentPageTitle: currentPage.title,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
visitedPages,
});
for await (const highlight of stream) {
if (canceled) return;
setHighlight(highlight ?? '');
}
})();
return () => {
canceled = true;
};
}, [
currentPage.pageId,
currentPage.spaceId,
currentPage.title,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
visitedPages,
]);
const shimmerBlocks = [
'w-[20%] [animation-delay:-1s]',
'w-[35%] [animation-delay:-0.8s]',
'w-[25%] [animation-delay:-0.6s]',
'w-[10%] [animation-delay:-0.4s]',
'w-[40%] [animation-delay:-0.2s]',
'w-[30%] [animation-delay:0s]',
];
return (
<div className="flex flex-col gap-1">
<div className="flex w-screen items-center gap-1 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
{showTrademark ? (
<Loading className="size-4" busy={!highlight || highlight.length === 0} />
) : (
<Icon icon="sparkle" className="size-3" />
)}
<h6 className="text-tint">{t(language, 'link_tooltip_ai_summary')}</h6>
</div>
{highlight.length > 0 ? (
<p className="animate-fadeIn">{highlight}</p>
) : (
<div className="mt-2 flex flex-wrap gap-2">
{shimmerBlocks.map((block, index) => (
<div
key={`${index}-${block}`}
className={`${block} h-4 animate-pulse rounded straight-corners:rounded-none bg-tint-active`}
/>
))}
</div>
)}
{highlight.length > 0 ? (
<div className="animate-fadeIn text-tint-subtle text-xs">
{t(language, 'link_tooltip_ai_summary_description')}
</div>
) : null}
</div>
);
}
@@ -0,0 +1 @@
export * from './AIPageLinkSummary';
@@ -0,0 +1,124 @@
'use server';
import { type AIMessageInput, AIModel, type AIStreamResponse } from '@gitbook/api';
import type { GitBookBaseContext } from '@v2/lib/context';
import { EventIterator } from 'event-iterator';
import type { MaybePromise } from 'p-map';
import * as partialJson from 'partial-json';
import type { DeepPartial } from 'ts-essentials';
import type { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
/**
* Get the latest value from a stream and the response id.
*/
export async function generate<T>(
promise: MaybePromise<{
stream: EventIterator<T>;
response: Promise<{ responseId: string }>;
}>
) {
const input = await promise;
let value: T | undefined;
for await (const event of input.stream) {
value = event;
}
const { responseId } = await input.response;
return {
responseId,
value,
};
}
/**
* Stream the generation of an object using the AI.
*/
export async function streamGenerateObject<T>(
context: GitBookBaseContext,
{
organizationId,
siteId,
}: {
organizationId: string;
siteId: string;
},
{
schema,
messages,
model = AIModel.Fast,
}: {
schema: z.ZodSchema<T>;
messages: AIMessageInput[];
model?: AIModel;
previousResponseId?: string;
}
) {
const rawStream = context.dataFetcher.streamAIResponse({
organizationId,
siteId,
input: messages,
output: {
type: 'object',
schema: zodToJsonSchema(schema),
},
model,
});
let json = '';
return parseResponse<DeepPartial<T>>(rawStream, (event) => {
if (event.type === 'response_object') {
json += event.jsonChunk;
const parsed = partialJson.parse(json, partialJson.ALL);
return parsed;
}
});
}
/**
* Parse a stream from the API to extract the responseId.
*/
function parseResponse<T>(
responseStream: EventIterator<AIStreamResponse>,
parse: (response: AIStreamResponse) => T | undefined
): {
stream: EventIterator<T>;
response: Promise<{ responseId: string }>;
} {
let resolveResponse: (value: { responseId: string }) => void;
const response = new Promise<{ responseId: string }>((resolve) => {
resolveResponse = resolve;
});
const stream = new EventIterator<T>((queue) => {
(async () => {
let foundResponse = false;
for await (const event of responseStream) {
if (event.type === 'response_finish') {
foundResponse = true;
resolveResponse({ responseId: event.responseId });
} else {
const parsed = parse(event);
if (parsed !== undefined) {
queue.push(parsed);
}
}
}
if (!foundResponse) {
throw new Error('No response found');
}
})().then(
() => {
queue.stop();
},
(error) => {
queue.fail(error);
}
);
});
return { stream, response };
}
@@ -0,0 +1 @@
export * from './streamLinkPageSummary';
@@ -0,0 +1,166 @@
'use server';
import { filterOutNullable } from '@/lib/typescript';
import { getV1BaseContext } from '@/lib/v1';
import { isV2 } from '@/lib/v2';
import { AIMessageRole } from '@gitbook/api';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
import { getServerActionBaseContext } from '@v2/lib/server-actions';
import { z } from 'zod';
import { streamGenerateObject } from './api';
/**
* Get a summary of a page, in the context of another page
*/
export async function* streamLinkPageSummary({
currentSpaceId,
currentPageId,
targetSpaceId,
targetPageId,
linkPreview,
linkTitle,
visitedPages,
}: {
currentSpaceId: string;
currentPageId: string;
currentPageTitle: string;
targetSpaceId: string;
targetPageId: string;
linkPreview?: string;
linkTitle?: string;
visitedPages?: Array<{ spaceId: string; pageId: string }>;
}) {
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
const { stream } = await streamGenerateObject(
baseContext,
{
organizationId: siteURLData.organization,
siteId: siteURLData.site,
},
{
schema: z.object({
highlight: z
.string()
.describe('The reason why the user should read the target page.'),
// questions: z.array(z.string().describe('The questions to sea')).max(3),
}),
messages: [
{
role: AIMessageRole.Developer,
content: `# 1. Role
You are a contextual fact extractor. Your job is to find the exact fact from the linked page that directly answers the implied question in the current paragraph.
# 2. Task
Extract a contextually-relevant fact that:
- Directly answers the specific need or question implied by the link's placement
- States a capability, limitation, or specification from the target page
- Connects precisely to the user's current paragraph or sentence
- Completes the user's understanding based on what they're currently reading
# 3. Instructions
1. First, identify the exact need, question, or gap in the current paragraph where the link appears
2. Find the specific fact in the target page that addresses this exact contextual need
3. Ensure the fact relates directly to the context of the paragraph containing the link
4. Avoid ALL instructional language including words like "use", "click", "select", "create"
5. Keep it under 30 words, factual and declarative about what EXISTS or IS TRUE`,
},
{
role: AIMessageRole.Developer,
content: `# 4. Current page
The content of the current page is:`,
attachments: [
{
type: 'page' as const,
spaceId: currentSpaceId,
pageId: currentPageId,
},
],
},
...(visitedPages
? [
{
role: AIMessageRole.Developer,
content: '# 5. Previous pages',
},
...visitedPages.map(({ spaceId, pageId }) => ({
role: AIMessageRole.Developer,
content: `## Page ${pageId}`,
attachments: [
{
type: 'page' as const,
spaceId,
pageId,
},
],
})),
]
: []),
{
role: AIMessageRole.Developer,
content: `# 6. Target page
The content of the target page is:`,
attachments: [
{
type: 'page' as const,
spaceId: targetSpaceId,
pageId: targetPageId,
},
],
},
{
role: AIMessageRole.Developer,
content: `# 7. Link preview
The content of the link preview is:
> ${linkPreview}
> Page ID: ${targetPageId}`,
},
{
role: AIMessageRole.Developer,
content: `# 8. Guidelines & Examples
ALWAYS:
- ALWAYS choose facts that directly fulfill the contextual need where the link appears
- ALWAYS connect target page information specifically to the current paragraph context
- ALWAYS focus on the gap in knowledge that the link is meant to fill
- ALWAYS consider user's navigation history to ensure contextual continuity
- ALWAYS use action verbs like "click", "select", "use", "create", "enable"
NEVER:
- NEVER include ANY unspecifc language like "learn", "how to", "discover", etc. State the fact directly.
- NEVER select general facts unrelated to the specific link context
- NEVER ignore the specific context where the link appears
- NEVER repeat the same fact in different words
## Examples
Current paragraph: "When organizing content, headings are limited to 3 levels. For more advanced editing, you can use (multiple select)[/multiple-select] to move multiple blocks at once."
Preview: "Multiple Select: Select multiple content blocks at once."
✓ "Shift selects content between two points, useful for reorganizing your current heading structure."
✗ "Shift and Ctrl/Cmd keys are the modifiers for selecting multiple blocks."
Current paragraph: "Most changes can be published directly, but for major revisions, if you want others to review changes before publishing, create a (change request)[/change-requests]."
Preview: "Change Requests: Collaborative content editing workflow."
✓ "Each reviewer's approval is tracked separately, with specific change highlighting for your major revisions."
✗ "Each reviewer receives an email notification and can approve or request changes."
Current paragraph: "Your team mentioned issues with conflicting edits. Need to collaborate in real-time? You can use (live edit mode)[/live-edit]."
Preview: "Live Edit: Real-time collaborative editing."
✓ "Teams with GitHub repositories (like yours) cannot use this feature due to sync limitations."
✗ "Incompatible with GitHub/GitLab sync and requires specific visibility settings."`,
},
{
role: AIMessageRole.User,
content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
},
].filter(filterOutNullable),
}
);
for await (const value of stream) {
const highlight = value.highlight;
if (!highlight) {
continue;
}
yield highlight;
}
}
@@ -97,9 +97,8 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
onClick={() => {
onUpdateState(true);
}}
>
{t(language, 'cookies_accept')}
</Button>
label={tString(language, 'cookies_accept')}
/>
<Button
variant="secondary"
size="small"
@@ -107,9 +106,8 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
onClick={() => {
onUpdateState(false);
}}
>
{t(language, 'cookies_reject')}
</Button>
label={tString(language, 'cookies_reject')}
/>
</div>
</div>
);
@@ -1,19 +1,22 @@
import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api';
import { resolveContentRef } from '@/lib/references';
import { Icon } from '@gitbook/icons';
import { StyledLink } from '../primitives';
import type { InlineProps } from './Inline';
import { InlineLinkTooltip } from './InlineLinkTooltip';
import { Inlines } from './Inlines';
export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
const { inline, document, context, ancestorInlines } = props;
const resolved = context.contentContext
? await resolveContentRef(inline.data.ref, context.contentContext)
? await resolveContentRef(inline.data.ref, context.contentContext, {
resolveAnchorText: true,
})
: null;
if (!resolved) {
if (!context.contentContext || !resolved) {
return (
<span title="Broken link" className="underline">
<Inlines
@@ -25,24 +28,33 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
</span>
);
}
const isExternal = inline.data.ref.kind === 'url';
return (
<StyledLink
href={resolved.href}
insights={{
type: 'link_click',
link: {
target: inline.data.ref,
position: SiteInsightsLinkPosition.Content,
},
}}
>
<Inlines
context={context}
document={document}
nodes={inline.nodes}
ancestorInlines={[...ancestorInlines, inline]}
/>
</StyledLink>
<InlineLinkTooltip inline={inline} context={context.contentContext} resolved={resolved}>
<StyledLink
href={resolved.href}
insights={{
type: 'link_click',
link: {
target: inline.data.ref,
position: SiteInsightsLinkPosition.Content,
},
}}
>
<Inlines
context={context}
document={document}
nodes={inline.nodes}
ancestorInlines={[...ancestorInlines, inline]}
/>
{isExternal ? (
<Icon
icon="arrow-up-right"
className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
/>
) : null}
</StyledLink>
</InlineLinkTooltip>
);
}
@@ -0,0 +1,159 @@
import type { DocumentInlineLink } from '@gitbook/api';
import type { ResolvedContentRef } from '@/lib/references';
import { getSpaceLanguage } from '@/intl/server';
import { tString } from '@/intl/translate';
import { languages } from '@/intl/translations';
import { getNodeText } from '@/lib/document';
import { tcls } from '@/lib/tailwind';
import { Icon } from '@gitbook/icons';
import * as Tooltip from '@radix-ui/react-tooltip';
import type { GitBookAnyContext } from '@v2/lib/context';
import { Fragment } from 'react';
import { AIPageLinkSummary } from '../Adaptive/AIPageLinkSummary';
import { Button, StyledLink } from '../primitives';
export async function InlineLinkTooltip(props: {
inline: DocumentInlineLink;
context: GitBookAnyContext;
children: React.ReactNode;
resolved: ResolvedContentRef;
}) {
const { inline, context, resolved, children } = props;
let breadcrumbs = resolved.ancestors;
const language =
'customization' in context ? getSpaceLanguage(context.customization) : languages.en;
const isExternal = inline.data.ref.kind === 'url';
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
if (isExternal) {
breadcrumbs = [
{
label: tString(language, 'link_tooltip_external_link'),
},
];
}
if (isSamePage) {
breadcrumbs = [
{
label: tString(language, 'link_tooltip_page_anchor'),
icon: <Icon icon="arrow-down-short-wide" className="size-3" />,
},
];
resolved.subText = undefined;
}
const hasAISummary =
!isExternal &&
!isSamePage &&
'customization' in context &&
context.customization.ai?.pageLinkSummaries.enabled &&
(inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor');
return (
<Tooltip.Provider delayDuration={200}>
<Tooltip.Root>
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="z-40 w-screen max-w-md animate-present px-4 sm:w-auto">
<div className="overflow-hidden rounded-md straight-corners:rounded-none shadow-lg shadow-tint-12/4 ring-1 ring-tint-subtle dark:shadow-tint-1 ">
<div className="bg-tint-base p-4">
<div className="flex items-start gap-4">
<div className="flex flex-col">
{breadcrumbs && breadcrumbs.length > 0 ? (
<div className="mb-1 flex grow flex-wrap items-center gap-x-2 gap-y-0.5 font-semibold text-tint text-xs uppercase leading-tight tracking-wide">
{breadcrumbs.map((crumb, index) => {
const Tag = crumb.href ? StyledLink : 'div';
return (
<Fragment key={crumb.label}>
{index !== 0 ? (
<Icon
icon="chevron-right"
className="size-3 text-tint-subtle"
/>
) : null}
<Tag
className={tcls(
'flex gap-1',
crumb.href &&
'links-default:text-tint no-underline hover:underline contrast-more:underline contrast-more:decoration-current'
)}
href={crumb.href ?? '#'}
>
{crumb.icon ? (
<span className="mt-0.5 text-tint-subtle empty:hidden">
{crumb.icon}
</span>
) : null}
{crumb.label}
</Tag>
</Fragment>
);
})}
</div>
) : null}
<div
className={tcls(
'flex gap-2 leading-snug',
isExternal && 'text-sm [overflow-wrap:anywhere]'
)}
>
{resolved.icon ? (
<div className="mt-1 text-tint-subtle empty:hidden">
{resolved.icon}
</div>
) : null}
<h5 className="font-semibold">{resolved.text}</h5>
</div>
</div>
{!isSamePage && resolved.href ? (
<Button
className={tcls(
'-mx-2 -my-2 ml-auto',
breadcrumbs?.length === 0
? 'place-self-center'
: null
)}
variant="blank"
href={resolved.href}
target="_blank"
label={tString(language, 'open_in_new_tab')}
size="small"
icon="arrow-up-right-from-square"
iconOnly={true}
/>
) : null}
</div>
{resolved.subText ? (
<p className="mt-1 text-sm text-tint">{resolved.subText}</p>
) : null}
</div>
{hasAISummary && 'page' in context && 'page' in inline.data.ref ? (
<div className="border-tint-subtle border-t bg-tint p-4">
<AIPageLinkSummary
targetPageId={
resolved.page?.id ??
inline.data.ref.page ??
context.page.id
}
targetSpaceId={inline.data.ref.space ?? context.space.id}
linkTitle={getNodeText(inline)}
linkPreview={`**${resolved.text}**: ${resolved.subText}`}
showTrademark={
'customization' in context &&
context.customization.trademark.enabled
}
/>
</div>
) : null}
</div>
<Tooltip.Arrow className={hasAISummary ? 'fill-tint-3' : 'fill-tint-1'} />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
@@ -168,7 +168,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
key={index}
href={ref.href}
target="_blank"
style={['flex', 'flex-row', 'items-center', 'gap-2']}
className="flex flex-row items-center gap-2"
insights={
ref.file
? {
@@ -142,10 +142,9 @@ function HeaderItemButton(
position: SiteInsightsLinkPosition.Header,
},
}}
label={title}
{...rest}
>
{title}
</Button>
/>
);
}
@@ -8,6 +8,7 @@ import { useDebounceCallback, useEventCallback } from 'usehooks-ts';
import type { VisitorAuthClaims } from '@/lib/adaptive';
import { getAllBrowserCookiesMap } from '@/lib/browser-cookies';
import { getSession } from './sessions';
import { useVisitedPages } from './useVisitedPages';
import { getVisitorId } from './visitorId';
export type InsightsEventName = api.SiteInsightsEvent['type'];
@@ -85,6 +86,7 @@ interface InsightsProviderProps extends InsightsEventContext {
export function InsightsProvider(props: InsightsProviderProps) {
const { enabled, appURL, apiHost, children, visitorCookieTrackingEnabled, ...context } = props;
const addVisitedPage = useVisitedPages((state) => state.addPage);
const visitorIdRef = React.useRef<string | null>(null);
const eventsRef = React.useRef<{
[pathname: string]:
@@ -134,6 +136,14 @@ export function InsightsProvider(props: InsightsProviderProps) {
...eventsForPathname,
events: [],
};
// Mark the page as visited in our local state
if (eventsForPathname.pageContext.pageId) {
addVisitedPage({
spaceId: context.spaceId,
pageId: eventsForPathname.pageContext.pageId,
});
}
}
if (allEvents.length > 0) {
@@ -2,3 +2,4 @@ export * from './InsightsProvider';
export * from './visitorId';
export * from './cookies';
export * from './TrackPageViewEvent';
export * from './useVisitedPages';
@@ -0,0 +1,25 @@
import { create } from 'zustand';
type VisitedPage = {
spaceId: string;
pageId: string;
};
/**
* A store for the pages that have been visited in the current session.
*/
export const useVisitedPages = create<{
pages: VisitedPage[];
addPage: (page: VisitedPage) => void;
}>((set) => ({
pages: [],
addPage: (page) =>
set((state) => {
const lastPage = state.pages[state.pages.length - 1];
if (lastPage && lastPage.spaceId === page.spaceId && lastPage.pageId === page.pageId) {
return { pages: state.pages };
}
return { pages: [...state.pages, page] };
}),
}));
@@ -5,7 +5,7 @@ import React from 'react';
import { useScrollActiveId } from '@/components/hooks';
import { Button } from '@/components/primitives';
import { t, useLanguage } from '@/intl/client';
import { t, tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { type PDFSearchParams, getPDFURLSearchParams } from './urls';
@@ -58,9 +58,8 @@ export function PageControlButtons(props: {
only: true,
}).toString()}`}
variant="secondary"
>
{t(language, 'pdf_mode_only_page')}
</Button>
label={tString(language, 'pdf_mode_only_page')}
/>
)}
<Button
href={`?${getPDFURLSearchParams({
@@ -69,9 +68,8 @@ export function PageControlButtons(props: {
only: false,
}).toString()}`}
variant="secondary"
>
{t(language, 'pdf_mode_all')}
</Button>
label={tString(language, 'pdf_mode_all')}
/>
{trademark ? <div className={tcls('mt-5')}>{trademark}</div> : null}
</div>
@@ -7,7 +7,6 @@ import { t } from '@/intl/translate';
import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document';
import type { AncestorRevisionPage } from '@/lib/pages';
import { tcls } from '@/lib/tailwind';
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
import { TrackPageViewEvent } from '../Insights';
import { PageFeedbackForm } from '../PageFeedback';
@@ -1,12 +1,14 @@
import type { RevisionPageDocument, RevisionPageDocumentCover } from '@gitbook/api';
import type { GitBookSiteContext } from '@v2/lib/context';
import type { StaticImageData } from 'next/image';
import { Image, type ImageSize } from '@/components/utils';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import defaultPageCover from './default-page-cover.svg';
import defaultPageCoverSVG from './default-page-cover.svg';
const defaultPageCover = defaultPageCoverSVG as StaticImageData;
const PAGE_COVER_SIZE: ImageSize = { width: 1990, height: 480 };
/**
@@ -35,7 +35,7 @@ export async function PageHeader(props: {
<li key={breadcrumb.id}>
<StyledLink
href={href}
style={tcls(
className={tcls(
'no-underline',
'hover:underline',
'text-xs',
@@ -0,0 +1,33 @@
'use client';
import React from 'react';
export type PageContextType = {
pageId: string;
spaceId: string;
title: string;
};
export const PageContext = React.createContext<PageContextType | null>(null);
/**
* Client side context provider to pass information about the current page.
*/
export function PageContextProvider(props: PageContextType & { children: React.ReactNode }) {
const { pageId, spaceId, title, children } = props;
const value = React.useMemo(() => ({ pageId, spaceId, title }), [pageId, spaceId, title]);
return <PageContext.Provider value={value}>{children}</PageContext.Provider>;
}
/**
* Hook to use the page context.
*/
export function usePageContext() {
const context = React.useContext(PageContext);
if (!context) {
throw new Error('usePageContext must be used within a PageContextProvider');
}
return context;
}
@@ -0,0 +1 @@
export * from './PageContext';
@@ -106,9 +106,8 @@ export function PageFeedbackForm(props: {
<Button
size="small"
onClick={() => onSubmitComment(rating, comment)}
>
{t(languages, 'submit')}
</Button>
label={tString(languages, 'submit')}
/>
{comment.length > MAX_COMMENT_LENGTH * 0.8 ? (
<span
className={
@@ -11,12 +11,10 @@ import { getPagePath } from '@/lib/pages';
import { isPageIndexable, isSiteIndexable } from '@/lib/seo';
import { getResizedImageURL } from '@v2/lib/images';
import { PageContextProvider } from '../PageContext';
import { PageClientLayout } from './PageClientLayout';
import { type PagePathParams, fetchPageData, getPathnameParam } from './fetch';
export const runtime = 'edge';
export const dynamic = 'force-dynamic';
export type SitePageProps = {
context: GitBookSiteContext;
pageParams: PagePathParams;
@@ -67,7 +65,7 @@ export async function SitePage(props: SitePageProps) {
const document = await getPageDocument(context.dataFetcher, context.space, page);
return (
<>
<PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
{withFullPageCover && page.cover ? (
<PageCover as="full" page={page} cover={page.cover} context={context} />
) : null}
@@ -92,7 +90,7 @@ export async function SitePage(props: SitePageProps) {
<React.Suspense fallback={null}>
<PageClientLayout withSections={withSections} />
</React.Suspense>
</>
</PageContextProvider>
);
}
@@ -1,62 +1,80 @@
'use client';
import type { HTMLAttributes } from 'react';
import type { HTMLAttributeAnchorTarget, HTMLAttributes } from 'react';
import { type ClassValue, tcls } from '@/lib/tailwind';
import { Icon, type IconName } from '@gitbook/icons';
import { Link, type LinkInsightsProps } from './Link';
type ButtonProps = {
href?: string;
variant?: 'primary' | 'secondary';
variant?: 'primary' | 'secondary' | 'blank';
icon?: IconName;
iconOnly?: boolean;
size?: 'default' | 'medium' | 'small';
className?: ClassValue;
label?: string;
} & LinkInsightsProps &
HTMLAttributes<HTMLElement>;
const variantClasses = {
primary: [
'bg-primary-solid',
'text-contrast-primary-solid',
'hover:bg-primary-solid-hover',
'hover:text-contrast-primary-solid-hover',
'ring-0',
'contrast-more:ring-1',
],
blank: [
'bg-transparent',
'text-tint',
'ring-0',
'shadow-none',
'hover:bg-primary-hover',
'hover:text-primary',
'hover:scale-1',
'hover:shadow-none',
'contrast-more:bg-tint-subtle',
],
secondary: [
'bg-tint',
'text-tint',
'hover:bg-tint-hover',
'hover:text-primary',
'contrast-more:bg-tint-subtle',
],
};
export function Button({
href,
children,
variant = 'primary',
size = 'default',
className,
insights,
target,
label,
icon,
iconOnly = false,
...rest
}: ButtonProps) {
const variantClasses =
variant === 'primary'
? //PRIMARY
[
'bg-primary-solid',
'text-contrast-primary-solid',
'hover:bg-primary-solid-hover',
'hover:text-contrast-primary-solid-hover',
'ring-0',
'contrast-more:ring-1',
]
: // SECONDARY
[
'bg-tint',
'text-tint',
'hover:bg-tint-hover',
'hover:text-primary',
'contrast-more:bg-tint-subtle',
];
}: ButtonProps & { target?: HTMLAttributeAnchorTarget }) {
const sizes = {
default: ['text-base', 'px-4', 'py-2'],
medium: ['text-sm', 'px-3', 'py-1.5'],
small: ['text-xs', 'px-3 py-2'],
small: ['text-xs', 'py-2', iconOnly ? 'px-2' : 'px-3'],
};
const sizeClasses = sizes[size] || sizes.default;
const domClassName = tcls(
'button',
'inline-block',
'inline-flex',
'items-center',
'gap-2',
'rounded-md',
'straight-corners:rounded-none',
'place-self-start',
// 'place-self-start',
'ring-1',
'ring-tint',
@@ -79,22 +97,31 @@ export function Button({
'grow-0',
'shrink-0',
'truncate',
variantClasses,
variantClasses[variant],
sizeClasses,
className
);
if (href) {
return (
<Link href={href} className={domClassName} insights={insights} {...rest}>
{children}
<Link
href={href}
className={domClassName}
insights={insights}
aria-label={label}
target={target}
{...rest}
>
{icon ? <Icon icon={icon} className={tcls('size-[1em]')} /> : null}
{iconOnly ? null : label}
</Link>
);
}
return (
<button type="button" className={domClassName} {...rest}>
{children}
<button type="button" className={domClassName} aria-label={label} {...rest}>
{icon ? <Icon icon={icon} className={tcls('size-[1em]')} /> : null}
{iconOnly ? null : label}
</button>
);
}
@@ -2,7 +2,10 @@ import type { SVGProps } from 'react';
import { tcls } from '@/lib/tailwind';
export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
export const Loading = ({
busy = true,
...props
}: { busy?: boolean } & SVGProps<SVGSVGElement>) => {
return (
<svg
width="100%"
@@ -14,7 +17,11 @@ export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
{...props}
>
<path
className={tcls('animate-[pathLoading_2s_ease_infinite_forwards]')}
className={tcls(
busy
? 'animate-[pathLoading_2s_ease_infinite_forwards]'
: 'animate-[pathLoading_2s_ease_forwards]'
)}
d="M6 59.5V56.291C6 45.8865 11.5194 36.263 20.5 31.0091V31.0091L60.9857 7.32407C63.4452 5.88525 66.4843 5.86317 68.9643 7.26611L116 33.8734L70.4183 60.2148C67.9468 61.6431 64.9014 61.6462 62.4269 60.223L29.9772 41.5592C19.3106 35.4242 6 43.1236 6 55.4288V64.8776C6 73.4486 10.5708 81.3691 17.9918 85.6575L54.59 106.807C62.0198 111.1 71.1766 111.1 78.6064 106.807L116.364 84.9874C120.074 82.8432 122.36 78.883 122.36 74.5975V59.2647C122.36 57.7248 120.692 56.7626 119.359 57.5331L72.6023 84.5529C68.8874 86.6996 64.309 86.6996 60.5941 84.5529L26 64.5617"
stroke="currentColor"
pathLength="100"
@@ -27,7 +34,8 @@ export const Loading = (props: Partial<SVGProps<SVGSVGElement>>) => {
d="M6 59.5V56.291C6 45.8865 11.5194 36.263 20.5 31.0091V31.0091L60.9857 7.32407C63.4452 5.88525 66.4843 5.86317 68.9643 7.26611L116 33.8734L70.4183 60.2148C67.9468 61.6431 64.9014 61.6462 62.4269 60.223L29.9772 41.5592C19.3106 35.4242 6 43.1236 6 55.4288V64.8776C6 73.4486 10.5708 81.3691 17.9918 85.6575L54.59 106.807C62.0198 111.1 71.1766 111.1 78.6064 106.807L116.364 84.9874C120.074 82.8432 122.36 78.883 122.36 74.5975V59.2647C122.36 57.7248 120.692 56.7626 119.359 57.5331L72.6023 84.5529C68.8874 86.6996 64.309 86.6996 60.5941 84.5529L26 64.5617"
stroke="currentColor"
pathLength="100"
strokeOpacity="0.24"
strokeOpacity={busy ? 0.24 : 1}
className="transition-opacity duration-1000"
fill="none"
strokeWidth="11"
strokeLinecap="round"
@@ -25,11 +25,11 @@ export const linkStyles = [
/**
* Styled version of Link component.
*/
export function StyledLink(props: Omit<LinkProps, 'style'> & { style?: ClassValue }) {
const { style, ...rest } = props;
export function StyledLink(props: Omit<LinkProps, 'style'> & { className?: ClassValue }) {
const { className, ...rest } = props;
return (
<Link {...rest} className={tcls(linkStyles, style)}>
<Link {...rest} className={tcls(linkStyles, className)}>
{props.children}
</Link>
);
@@ -58,4 +58,9 @@ export const de = {
'Das PDF konnte für ${1} Seiten nicht generiert werden, Generierung wurde bei ${2} gestoppt.',
pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.',
more: 'Mehr',
link_tooltip_external_link: 'Externe Verlinkung zu',
link_tooltip_page_anchor: 'Zum Abschnitt springen',
link_tooltip_ai_summary: 'Seitenhighlight',
link_tooltip_ai_summary_description: 'Basierend auf Ihrem Kontext. Kann Fehler enthalten.',
open_in_new_tab: 'In neuem Tab öffnen',
};
@@ -56,4 +56,9 @@ export const en = {
pdf_limit_reached: "Couldn't generate the PDF for ${1} pages, generation stopped at ${2}.",
pdf_limit_reached_continue: 'Extend with ${1} more pages.',
more: 'More',
link_tooltip_external_link: 'External link to',
link_tooltip_page_anchor: 'Jump to section',
link_tooltip_ai_summary: 'Page highlight',
link_tooltip_ai_summary_description: 'Based on your context. May contain mistakes.',
open_in_new_tab: 'Open in new tab',
};
@@ -60,4 +60,9 @@ export const es: TranslationLanguage = {
'No se pudo generar el PDF para ${1} páginas, la generación se detuvo en ${2}.',
pdf_limit_reached_continue: 'Extender con ${1} páginas más.',
more: 'Más',
link_tooltip_external_link: 'Enlace externo a',
link_tooltip_page_anchor: 'Saltar a la sección',
link_tooltip_ai_summary: 'Resumen de la página',
link_tooltip_ai_summary_description: 'Basado en tu contexto. Puede contener errores.',
open_in_new_tab: 'Abrir en una nueva pestaña',
};
@@ -58,4 +58,9 @@ export const fr: TranslationLanguage = {
pdf_limit_reached: 'Impossible de générer le PDF pour ${1} pages, génération arrêtée à ${2}.',
pdf_limit_reached_continue: 'Étendre avec ${1} pages supplémentaires.',
more: 'Plus',
link_tooltip_external_link: 'Lien externe à',
link_tooltip_page_anchor: 'Sauter à la section',
link_tooltip_ai_summary: 'Résumé de la page',
link_tooltip_ai_summary_description: 'Basé sur votre contexte. Peut contenir des erreurs.',
open_in_new_tab: 'Ouvrir dans un nouvel onglet',
};
@@ -58,4 +58,10 @@ export const ja: TranslationLanguage = {
pdf_limit_reached: '${1}ページのPDFを生成できませんでした、${2}で生成が停止しました。',
pdf_limit_reached_continue: 'さらに${1}ページで拡張',
more: '詳細',
link_tooltip_external_link: '外部リンク先',
link_tooltip_page_anchor: 'ページ内リンク先',
link_tooltip_ai_summary: 'ページのハイライト',
link_tooltip_ai_summary_description:
'あなたのコンテキストに基づいています。間違いが含まれる可能性があります。',
open_in_new_tab: '新しいタブで開く',
};
@@ -58,4 +58,9 @@ export const nl: TranslationLanguage = {
pdf_limit_reached: "Kon de PDF niet genereren voor ${1} pagina's, generatie gestopt bij ${2}.",
pdf_limit_reached_continue: 'Verleng met ${1} extra paginas.',
more: 'Meer',
link_tooltip_external_link: 'Externe link naar',
link_tooltip_page_anchor: 'Spring naar sectie',
link_tooltip_ai_summary: 'Pagina-samenvatting',
link_tooltip_ai_summary_description: 'Gebaseerd op je context. Kan fouten bevatten.',
open_in_new_tab: 'Open in nieuw tabblad',
};
@@ -58,4 +58,9 @@ export const no: TranslationLanguage = {
pdf_limit_reached: 'Kunne ikke generere PDF for ${1} sider, generering stoppet ved ${2}.',
pdf_limit_reached_continue: 'Utvid med ${1} flere sider.',
more: 'Mer',
link_tooltip_external_link: 'Ekstern lenke til',
link_tooltip_page_anchor: 'Hopp til seksjon',
link_tooltip_ai_summary: 'Sidesammendrag',
link_tooltip_ai_summary_description: 'Basert på din kontekst. Kan inneholde feil.',
open_in_new_tab: 'Åpne i ny fane',
};
@@ -58,4 +58,9 @@ export const pt_br = {
'Não foi possível gerar o PDF para ${1} páginas, generation stopped at ${2}.',
pdf_limit_reached_continue: 'Extender com mais ${1} páginas.',
more: 'Mais',
link_tooltip_external_link: 'Link externo para',
link_tooltip_page_anchor: 'Pular para a seção',
link_tooltip_ai_summary: 'Resumo da página',
link_tooltip_ai_summary_description: 'Baseado no seu contexto. Pode conter erros.',
open_in_new_tab: 'Abrir em uma nova guia',
};
@@ -56,4 +56,9 @@ export const zh: TranslationLanguage = {
pdf_limit_reached: '无法为${1}页生成 PDF,生成在${2}页时停止。',
pdf_limit_reached_continue: '使用${1}页进行扩展。',
more: '更多',
link_tooltip_external_link: '外部链接到',
link_tooltip_page_anchor: '跳转到页面',
link_tooltip_ai_summary: '页面要点',
link_tooltip_ai_summary_description: '基于您的上下文。可能包含错误。',
open_in_new_tab: '在新标签页中打开',
};
+30 -3
View File
@@ -1,6 +1,7 @@
import type {
ContentRef,
RevisionFile,
RevisionPageDocument,
RevisionReusableContent,
SiteSpace,
Space,
@@ -19,6 +20,7 @@ import { getGitbookAppHref } from './links';
import { resolvePageId } from './pages';
import { findSiteSpaceById } from './sites';
import type { ClassValue } from './tailwind';
import { filterOutNullable } from './typescript';
export interface ResolvedContentRef {
/** Text to render in the content ref */
@@ -29,12 +31,16 @@ export interface ResolvedContentRef {
icon?: React.ReactNode;
/** Emoji associated with the reference */
emoji?: string;
/** The content ref's ancestors */
ancestors?: { icon?: React.ReactNode; label: string; href?: string }[];
/** URL to open for the content ref */
href: string;
/** True if the content ref is active */
active: boolean;
/** File, if the reference is a file */
file?: RevisionFile;
/** Page document resolved from the content ref */
page?: RevisionPageDocument;
/** Resolved reusable content, if the ref points to reusable content on a revision. */
reusableContent?: RevisionReusableContent;
/** Resolve OpenAPI spec filesystem. */
@@ -115,6 +121,14 @@ export async function resolveContentRef(
: resolvePageId(pages, contentRef.page);
const page = resolvePageResult?.page;
const ancestors =
resolvePageResult?.ancestors.map((ancestor) => ({
label: ancestor.title,
icon: <PageIcon page={ancestor} style={iconStyle} />,
href: resolveAsAbsoluteURL
? linker.toAbsoluteURL(linker.toPathForPage({ page: ancestor, pages }))
: linker.toPathForPage({ page: ancestor, pages }),
})) ?? [];
if (!page) {
return null;
}
@@ -125,10 +139,16 @@ export async function resolveContentRef(
let text = '';
let icon: React.ReactNode | undefined = undefined;
let emoji: string | undefined = undefined;
const href = linker.toPathForPage({ page, pages, anchor });
// Compute the text to display for the link
if (anchor) {
text = `#${anchor}`;
ancestors.push({
label: page.title,
icon: <PageIcon page={page} style={iconStyle} />,
href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href,
});
if (resolveAnchorText) {
const document = await getPageDocument(dataFetcher, space, page);
@@ -151,13 +171,14 @@ export async function resolveContentRef(
icon = <PageIcon page={page} style={iconStyle} />;
}
const href = linker.toPathForPage({ page, pages, anchor });
return {
href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href,
text,
subText: page.description,
ancestors: ancestors,
emoji,
icon,
page,
active: !anchor && page.id === activePage?.id,
};
}
@@ -346,6 +367,12 @@ async function resolveContentRefInSpace(
return {
...resolved,
subText: space.title,
ancestors: [
{
label: space.title,
href: baseURL.toString(),
},
...(resolved.ancestors ?? []),
].filter(filterOutNullable),
};
}
+4
View File
@@ -270,6 +270,10 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
return result;
});
},
streamAIResponse() {
throw new Error('Not implemented in v1');
},
};
return dataFetcher;
+2 -2
View File
@@ -295,7 +295,7 @@ const config: Config = {
),
},
animation: {
present: 'present .5s ease-out both',
present: 'present 200ms cubic-bezier(0.25, 1, 0.5, 1) both',
scaleIn: 'scaleIn 200ms ease',
scaleOut: 'scaleOut 200ms ease',
fadeIn: 'fadeIn 200ms ease forwards',
@@ -330,7 +330,7 @@ const config: Config = {
present: {
from: {
opacity: '0',
transform: 'translateY(2rem) scale(0.9)',
transform: 'translateY(1rem) scale(90%)',
},
to: {
opacity: '1',