diff --git a/bun.lock b/bun.lock index 0c132b750..b34b53cfb 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index d22d33a83..548936a88 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index 914f9be5b..74b14b7af 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -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[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; /** diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts index 2573dbcb0..178a0ba77 100644 --- a/packages/gitbook-v2/src/lib/data/types.ts +++ b/packages/gitbook-v2/src/lib/data/types.ts @@ -179,4 +179,15 @@ export interface GitBookDataFetcher { integrationName: string; request: api.RenderIntegrationUI; }): Promise>; + + /** + * Stream an AI response. + */ + streamAIResponse(params: { + organizationId: string; + siteId: string; + input: api.AIMessageInput[]; + output: api.AIOutputFormat; + model: api.AIModel; + }): AsyncGenerator; } diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 84d2fa83c..4fa8a5b4e 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -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", diff --git a/packages/gitbook/src/app/middleware/(site)/error.tsx b/packages/gitbook/src/app/middleware/(site)/error.tsx index 4f96f4e65..dc89d8547 100644 --- a/packages/gitbook/src/app/middleware/(site)/error.tsx +++ b/packages/gitbook/src/app/middleware/(site)/error.tsx @@ -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')} - + label={tString(language, 'unexpected_error_retry')} + /> diff --git a/packages/gitbook/src/components/Adaptive/AIPageLinkSummary.tsx b/packages/gitbook/src/components/Adaptive/AIPageLinkSummary.tsx new file mode 100644 index 000000000..fed93e56c --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/AIPageLinkSummary.tsx @@ -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 ( +
+
+ {showTrademark ? ( + + ) : ( + + )} +
{t(language, 'link_tooltip_ai_summary')}
+
+ {highlight.length > 0 ? ( +

{highlight}

+ ) : ( +
+ {shimmerBlocks.map((block, index) => ( +
+ ))} +
+ )} + {highlight.length > 0 ? ( +
+ {t(language, 'link_tooltip_ai_summary_description')} +
+ ) : null} +
+ ); +} diff --git a/packages/gitbook/src/components/Adaptive/index.ts b/packages/gitbook/src/components/Adaptive/index.ts new file mode 100644 index 000000000..2d93029d7 --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/index.ts @@ -0,0 +1 @@ +export * from './AIPageLinkSummary'; diff --git a/packages/gitbook/src/components/Adaptive/server-actions/api.ts b/packages/gitbook/src/components/Adaptive/server-actions/api.ts new file mode 100644 index 000000000..a1396987d --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/server-actions/api.ts @@ -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( + promise: MaybePromise<{ + stream: EventIterator; + 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( + context: GitBookBaseContext, + { + organizationId, + siteId, + }: { + organizationId: string; + siteId: string; + }, + { + schema, + messages, + model = AIModel.Fast, + }: { + schema: z.ZodSchema; + 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>(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( + responseStream: EventIterator, + parse: (response: AIStreamResponse) => T | undefined +): { + stream: EventIterator; + response: Promise<{ responseId: string }>; +} { + let resolveResponse: (value: { responseId: string }) => void; + const response = new Promise<{ responseId: string }>((resolve) => { + resolveResponse = resolve; + }); + + const stream = new EventIterator((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 }; +} diff --git a/packages/gitbook/src/components/Adaptive/server-actions/index.ts b/packages/gitbook/src/components/Adaptive/server-actions/index.ts new file mode 100644 index 000000000..664e869e2 --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/server-actions/index.ts @@ -0,0 +1 @@ +export * from './streamLinkPageSummary'; diff --git a/packages/gitbook/src/components/Adaptive/server-actions/streamLinkPageSummary.ts b/packages/gitbook/src/components/Adaptive/server-actions/streamLinkPageSummary.ts new file mode 100644 index 000000000..88abfe019 --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/server-actions/streamLinkPageSummary.ts @@ -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; + } +} diff --git a/packages/gitbook/src/components/Cookies/CookiesToast.tsx b/packages/gitbook/src/components/Cookies/CookiesToast.tsx index d386669aa..13dc7605d 100644 --- a/packages/gitbook/src/components/Cookies/CookiesToast.tsx +++ b/packages/gitbook/src/components/Cookies/CookiesToast.tsx @@ -97,9 +97,8 @@ export function CookiesToast(props: { privacyPolicy?: string }) { onClick={() => { onUpdateState(true); }} - > - {t(language, 'cookies_accept')} - + label={tString(language, 'cookies_accept')} + /> + label={tString(language, 'cookies_reject')} + />
); diff --git a/packages/gitbook/src/components/DocumentView/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink.tsx index a12c59e08..37a812bb3 100644 --- a/packages/gitbook/src/components/DocumentView/InlineLink.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineLink.tsx @@ -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) { 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 ( ) { ); } + const isExternal = inline.data.ref.kind === 'url'; return ( - - - + + + + {isExternal ? ( + + ) : null} + + ); } diff --git a/packages/gitbook/src/components/DocumentView/InlineLinkTooltip.tsx b/packages/gitbook/src/components/DocumentView/InlineLinkTooltip.tsx new file mode 100644 index 000000000..1a5cefa1e --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineLinkTooltip.tsx @@ -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: , + }, + ]; + 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 ( + + + {children} + + +
+
+
+
+ {breadcrumbs && breadcrumbs.length > 0 ? ( +
+ {breadcrumbs.map((crumb, index) => { + const Tag = crumb.href ? StyledLink : 'div'; + + return ( + + {index !== 0 ? ( + + ) : null} + + {crumb.icon ? ( + + {crumb.icon} + + ) : null} + {crumb.label} + + + ); + })} +
+ ) : null} +
+ {resolved.icon ? ( +
+ {resolved.icon} +
+ ) : null} +
{resolved.text}
+
+
+ {!isSamePage && resolved.href ? ( +
+ {resolved.subText ? ( +

{resolved.subText}

+ ) : null} +
+ + {hasAISummary && 'page' in context && 'page' in inline.data.ref ? ( +
+ +
+ ) : null} +
+ +
+
+
+
+ ); +} diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx index 7c1ecb995..d5ddcdc49 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx @@ -168,7 +168,7 @@ export async function RecordColumnValue( 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 ? { diff --git a/packages/gitbook/src/components/Header/HeaderLink.tsx b/packages/gitbook/src/components/Header/HeaderLink.tsx index 1a66e13b3..184537c15 100644 --- a/packages/gitbook/src/components/Header/HeaderLink.tsx +++ b/packages/gitbook/src/components/Header/HeaderLink.tsx @@ -142,10 +142,9 @@ function HeaderItemButton( position: SiteInsightsLinkPosition.Header, }, }} + label={title} {...rest} - > - {title} - + /> ); } diff --git a/packages/gitbook/src/components/Insights/InsightsProvider.tsx b/packages/gitbook/src/components/Insights/InsightsProvider.tsx index 2f76a301c..7ea3fe5a3 100644 --- a/packages/gitbook/src/components/Insights/InsightsProvider.tsx +++ b/packages/gitbook/src/components/Insights/InsightsProvider.tsx @@ -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(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) { diff --git a/packages/gitbook/src/components/Insights/index.ts b/packages/gitbook/src/components/Insights/index.ts index ff922086e..792440a5c 100644 --- a/packages/gitbook/src/components/Insights/index.ts +++ b/packages/gitbook/src/components/Insights/index.ts @@ -2,3 +2,4 @@ export * from './InsightsProvider'; export * from './visitorId'; export * from './cookies'; export * from './TrackPageViewEvent'; +export * from './useVisitedPages'; diff --git a/packages/gitbook/src/components/Insights/useVisitedPages.tsx b/packages/gitbook/src/components/Insights/useVisitedPages.tsx new file mode 100644 index 000000000..f5450b432 --- /dev/null +++ b/packages/gitbook/src/components/Insights/useVisitedPages.tsx @@ -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] }; + }), +})); diff --git a/packages/gitbook/src/components/PDF/PageControlButtons.tsx b/packages/gitbook/src/components/PDF/PageControlButtons.tsx index 456508422..3e5e41e5f 100644 --- a/packages/gitbook/src/components/PDF/PageControlButtons.tsx +++ b/packages/gitbook/src/components/PDF/PageControlButtons.tsx @@ -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')} - + label={tString(language, 'pdf_mode_only_page')} + /> )} + label={tString(language, 'pdf_mode_all')} + /> {trademark ?
{trademark}
: null} diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 54623ad66..6d548dec9 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -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'; diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx index 2239512f4..d434d5eba 100644 --- a/packages/gitbook/src/components/PageBody/PageCover.tsx +++ b/packages/gitbook/src/components/PageBody/PageCover.tsx @@ -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 }; /** diff --git a/packages/gitbook/src/components/PageBody/PageHeader.tsx b/packages/gitbook/src/components/PageBody/PageHeader.tsx index a246b0475..b8b62f945 100644 --- a/packages/gitbook/src/components/PageBody/PageHeader.tsx +++ b/packages/gitbook/src/components/PageBody/PageHeader.tsx @@ -35,7 +35,7 @@ export async function PageHeader(props: {
  • (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 {children}; +} + +/** + * 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; +} diff --git a/packages/gitbook/src/components/PageContext/index.ts b/packages/gitbook/src/components/PageContext/index.ts new file mode 100644 index 000000000..26654b4f4 --- /dev/null +++ b/packages/gitbook/src/components/PageContext/index.ts @@ -0,0 +1 @@ +export * from './PageContext'; diff --git a/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx b/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx index 3be83b96e..551a8b83f 100644 --- a/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx +++ b/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx @@ -106,9 +106,8 @@ export function PageFeedbackForm(props: { + label={tString(languages, 'submit')} + /> {comment.length > MAX_COMMENT_LENGTH * 0.8 ? ( + {withFullPageCover && page.cover ? ( ) : null} @@ -92,7 +90,7 @@ export async function SitePage(props: SitePageProps) { - + ); } diff --git a/packages/gitbook/src/components/primitives/Button.tsx b/packages/gitbook/src/components/primitives/Button.tsx index 01c9e1e18..01d888007 100644 --- a/packages/gitbook/src/components/primitives/Button.tsx +++ b/packages/gitbook/src/components/primitives/Button.tsx @@ -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; +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 ( - - {children} + + {icon ? : null} + {iconOnly ? null : label} ); } return ( - ); } diff --git a/packages/gitbook/src/components/primitives/Loading.tsx b/packages/gitbook/src/components/primitives/Loading.tsx index 280782594..2935d7ca8 100644 --- a/packages/gitbook/src/components/primitives/Loading.tsx +++ b/packages/gitbook/src/components/primitives/Loading.tsx @@ -2,7 +2,10 @@ import type { SVGProps } from 'react'; import { tcls } from '@/lib/tailwind'; -export const Loading = (props: Partial>) => { +export const Loading = ({ + busy = true, + ...props +}: { busy?: boolean } & SVGProps) => { return ( >) => { {...props} > >) => { 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" diff --git a/packages/gitbook/src/components/primitives/StyledLink.tsx b/packages/gitbook/src/components/primitives/StyledLink.tsx index 13b68f0e0..3fb346f5f 100644 --- a/packages/gitbook/src/components/primitives/StyledLink.tsx +++ b/packages/gitbook/src/components/primitives/StyledLink.tsx @@ -25,11 +25,11 @@ export const linkStyles = [ /** * Styled version of Link component. */ -export function StyledLink(props: Omit & { style?: ClassValue }) { - const { style, ...rest } = props; +export function StyledLink(props: Omit & { className?: ClassValue }) { + const { className, ...rest } = props; return ( - + {props.children} ); diff --git a/packages/gitbook/src/intl/translations/de.ts b/packages/gitbook/src/intl/translations/de.ts index 72f2563a6..b42da6c55 100644 --- a/packages/gitbook/src/intl/translations/de.ts +++ b/packages/gitbook/src/intl/translations/de.ts @@ -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', }; diff --git a/packages/gitbook/src/intl/translations/en.ts b/packages/gitbook/src/intl/translations/en.ts index c6e976784..3850f2632 100644 --- a/packages/gitbook/src/intl/translations/en.ts +++ b/packages/gitbook/src/intl/translations/en.ts @@ -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', }; diff --git a/packages/gitbook/src/intl/translations/es.ts b/packages/gitbook/src/intl/translations/es.ts index 11fb4dbb0..2d0d46565 100644 --- a/packages/gitbook/src/intl/translations/es.ts +++ b/packages/gitbook/src/intl/translations/es.ts @@ -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', }; diff --git a/packages/gitbook/src/intl/translations/fr.ts b/packages/gitbook/src/intl/translations/fr.ts index c5edac9ad..71d340332 100644 --- a/packages/gitbook/src/intl/translations/fr.ts +++ b/packages/gitbook/src/intl/translations/fr.ts @@ -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', }; diff --git a/packages/gitbook/src/intl/translations/ja.ts b/packages/gitbook/src/intl/translations/ja.ts index 08a11ae23..f3f548068 100644 --- a/packages/gitbook/src/intl/translations/ja.ts +++ b/packages/gitbook/src/intl/translations/ja.ts @@ -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: '新しいタブで開く', }; diff --git a/packages/gitbook/src/intl/translations/nl.ts b/packages/gitbook/src/intl/translations/nl.ts index 2304b5fd9..6bfb45e9a 100644 --- a/packages/gitbook/src/intl/translations/nl.ts +++ b/packages/gitbook/src/intl/translations/nl.ts @@ -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 pagina’s.', 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', }; diff --git a/packages/gitbook/src/intl/translations/no.ts b/packages/gitbook/src/intl/translations/no.ts index 21d08ca4f..6be6b413e 100644 --- a/packages/gitbook/src/intl/translations/no.ts +++ b/packages/gitbook/src/intl/translations/no.ts @@ -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', }; diff --git a/packages/gitbook/src/intl/translations/pt-br.ts b/packages/gitbook/src/intl/translations/pt-br.ts index f49f3f805..35a40cd45 100644 --- a/packages/gitbook/src/intl/translations/pt-br.ts +++ b/packages/gitbook/src/intl/translations/pt-br.ts @@ -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', }; diff --git a/packages/gitbook/src/intl/translations/zh.ts b/packages/gitbook/src/intl/translations/zh.ts index 34a08fde3..08efdddde 100644 --- a/packages/gitbook/src/intl/translations/zh.ts +++ b/packages/gitbook/src/intl/translations/zh.ts @@ -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: '在新标签页中打开', }; diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index 83b5a6f09..9b1e469b6 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -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: , + 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: , + href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href, + }); if (resolveAnchorText) { const document = await getPageDocument(dataFetcher, space, page); @@ -151,13 +171,14 @@ export async function resolveContentRef( icon = ; } - 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), }; } diff --git a/packages/gitbook/src/lib/v1.ts b/packages/gitbook/src/lib/v1.ts index 7c4c723e6..625eef0a4 100644 --- a/packages/gitbook/src/lib/v1.ts +++ b/packages/gitbook/src/lib/v1.ts @@ -270,6 +270,10 @@ async function getDataFetcherV1(): Promise { return result; }); }, + + streamAIResponse() { + throw new Error('Not implemented in v1'); + }, }; return dataFetcher; diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts index 692487f09..7a05bd15f 100644 --- a/packages/gitbook/tailwind.config.ts +++ b/packages/gitbook/tailwind.config.ts @@ -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',