From 846da09a277ad2010a2dde83d24941420e8bed6b Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 18:03:04 +1000 Subject: [PATCH 01/17] chore: update dependencies in package.json and pnpm-lock.yaml --- package.json | 4 + pnpm-lock.yaml | 399 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 401 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d734e08..4354f4e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@ai-sdk/openai": "^2.0.32", + "@assistant-ui/react": "^0.11.10", + "@assistant-ui/react-ai-sdk": "^1.1.0", "@giscus/react": "^3.1.0", "@orama/orama": "^3.1.13", "@orama/tokenizers": "^3.1.13", @@ -21,6 +24,7 @@ "@radix-ui/react-slot": "^1.2.3", "@types/mdx": "^2.0.13", "@vercel/speed-insights": "^1.2.0", + "ai": "^5.0.45", "antd": "^5.27.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75c5e84..6e92100 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,15 @@ settings: importers: .: dependencies: + "@ai-sdk/openai": + specifier: ^2.0.32 + version: 2.0.32(zod@4.1.8) + "@assistant-ui/react": + specifier: ^0.11.10 + version: 0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + "@assistant-ui/react-ai-sdk": + specifier: ^1.1.0 + version: 1.1.0(@assistant-ui/react@0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)))(@types/react@19.1.12)(assistant-cloud@0.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) "@giscus/react": specifier: ^3.1.0 version: 3.1.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -28,6 +37,9 @@ importers: "@vercel/speed-insights": specifier: ^1.2.0 version: 1.2.0(next@15.5.3(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + ai: + specifier: ^5.0.45 + version: 5.0.45(zod@4.1.8) antd: specifier: ^5.27.3 version: 5.27.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -121,6 +133,53 @@ importers: version: 5.9.2 packages: + "@ai-sdk/gateway@1.0.23": + resolution: + { + integrity: sha512-ynV7WxpRK2zWLGkdOtrU2hW22mBVkEYVS3iMg1+ZGmAYSgzCqzC74bfOJZ2GU1UdcrFWUsFI9qAYjsPkd+AebA==, + } + engines: { node: ">=18" } + peerDependencies: + zod: ^3.25.76 || ^4 + + "@ai-sdk/openai@2.0.32": + resolution: + { + integrity: sha512-p7giSkCs66Q1qYO/NPYI41CrSg65mcm8R2uAdF86+Y1D1/q4mUrWMyf5UTOJ0bx/z4jIPiNgGDCg2Kabi5zrKQ==, + } + engines: { node: ">=18" } + peerDependencies: + zod: ^3.25.76 || ^4 + + "@ai-sdk/provider-utils@3.0.9": + resolution: + { + integrity: sha512-Pm571x5efqaI4hf9yW4KsVlDBDme8++UepZRnq+kqVBWWjgvGhQlzU8glaFq0YJEB9kkxZHbRRyVeHoV2sRYaQ==, + } + engines: { node: ">=18" } + peerDependencies: + zod: ^3.25.76 || ^4 + + "@ai-sdk/provider@2.0.0": + resolution: + { + integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==, + } + engines: { node: ">=18" } + + "@ai-sdk/react@2.0.45": + resolution: + { + integrity: sha512-jrTeBQpIsueV6EB/L6KNdH/yadK/Ehx1qCus+9RC29kRikVhjgj8xNvHfH3qHCwsfGqLX9ljj69dCRLrmzpvnw==, + } + engines: { node: ">=18" } + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.25.76 || ^4 + peerDependenciesMeta: + zod: + optional: true + "@alloc/quick-lru@5.2.0": resolution: { @@ -183,6 +242,49 @@ packages: peerDependencies: react: ">=16.9.0" + "@assistant-ui/react-ai-sdk@1.1.0": + resolution: + { + integrity: sha512-1eGXHH8HBeBX0vu0nvjNamrHkqovee8MirSgndprbijFsL0dbb7c1OA2yg76lnja3vmmPLN/GUiqEY9NrPac1g==, + } + peerDependencies: + "@assistant-ui/react": ^0.11.0 + "@types/react": "*" + assistant-cloud: "*" + react: ^18 || ^19 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + assistant-cloud: + optional: true + + "@assistant-ui/react@0.11.10": + resolution: + { + integrity: sha512-8g8O8e3imLQd1GYSyQWfCvi7Tjh0m5h5rVHyge4js+LW+6y7+UnpSmZ/eyIajOkLmdHiwkkpiNkce2cUC9r/5Q==, + } + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + + "@assistant-ui/tap@0.1.1": + resolution: + { + integrity: sha512-CG+u/h9yOzy7OcHwcti+GlnmyD6RoTd1pq7n1Rm3rqqVyFpdsfL25X6hwqir1NKQYflFVOyhR957X6/OFUbyDg==, + } + peerDependencies: + react: "*" + peerDependenciesMeta: + react: + optional: true + "@babel/runtime@7.28.4": resolution: { @@ -2146,6 +2248,15 @@ packages: engines: { node: ">=0.4.0" } hasBin: true + ai@5.0.45: + resolution: + { + integrity: sha512-go6J78B1oTXZMN2XLlNJnrFxwcqXQtpPqUVyk1wvzvpb2dk5nP9yNuxqqOX9HrrKuf5U9M6rSezEJWr1eEG9RA==, + } + engines: { node: ">=18" } + peerDependencies: + zod: ^3.25.76 || ^4 + ajv@6.12.6: resolution: { @@ -2265,6 +2376,18 @@ packages: } engines: { node: ">= 0.4" } + assistant-cloud@0.1.1: + resolution: + { + integrity: sha512-lTlNjBQGICdx08SgmKBcyuQkay6vBEhoasSQenz2ecvyQ25O0527H75v5OG+QMkNKthru3p5zOiOti90fJ0LCw==, + } + + assistant-stream@0.2.26: + resolution: + { + integrity: sha512-mTfTkaf9PIFE1x7/5PVAue4F/7DOmxZNPv9yVDGy5UMjLKZeIQu0nsmNUjw5BsbgXQJL0Gdb9plucSr40T3Xwg==, + } + ast-types-flow@0.0.8: resolution: { @@ -3063,6 +3186,13 @@ packages: integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==, } + eventsource-parser@3.0.6: + resolution: + { + integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==, + } + engines: { node: ">=18.0.0" } + extend@3.0.2: resolution: { @@ -3806,6 +3936,12 @@ packages: integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, } + json-schema@0.4.0: + resolution: + { + integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==, + } + json-stable-stringify-without-jsonify@1.0.1: resolution: { @@ -4458,6 +4594,14 @@ packages: engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true + nanoid@5.1.5: + resolution: + { + integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==, + } + engines: { node: ^18 || >=20 } + hasBin: true + napi-postinstall@0.3.3: resolution: { @@ -5181,6 +5325,15 @@ packages: "@types/react": optional: true + react-textarea-autosize@8.5.9: + resolution: + { + integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==, + } + engines: { node: ">=10" } + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react@19.1.1: resolution: { @@ -5400,6 +5553,12 @@ packages: integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==, } + secure-json-parse@4.0.0: + resolution: + { + integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==, + } + semver@6.3.1: resolution: { @@ -5687,6 +5846,14 @@ packages: } engines: { node: ">= 0.4" } + swr@2.3.6: + resolution: + { + integrity: sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==, + } + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + tailwind-merge@3.3.1: resolution: { @@ -5720,6 +5887,13 @@ packages: } engines: { node: ">=12.22" } + throttleit@2.1.0: + resolution: + { + integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==, + } + engines: { node: ">=18" } + tinyexec@1.0.1: resolution: { @@ -5917,6 +6091,18 @@ packages: "@types/react": optional: true + use-composed-ref@1.4.0: + resolution: + { + integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + use-intl@4.3.8: resolution: { @@ -5925,6 +6111,30 @@ packages: peerDependencies: react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + use-isomorphic-layout-effect@1.2.1: + resolution: + { + integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + + use-latest@1.3.0: + resolution: + { + integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + use-sidecar@1.1.3: resolution: { @@ -5938,6 +6148,14 @@ packages: "@types/react": optional: true + use-sync-external-store@1.5.0: + resolution: + { + integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==, + } + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: { @@ -6034,6 +6252,27 @@ packages: integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==, } + zustand@5.0.8: + resolution: + { + integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==, + } + engines: { node: ">=12.20.0" } + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + zwitch@2.0.4: resolution: { @@ -6041,6 +6280,39 @@ packages: } snapshots: + "@ai-sdk/gateway@1.0.23(zod@4.1.8)": + dependencies: + "@ai-sdk/provider": 2.0.0 + "@ai-sdk/provider-utils": 3.0.9(zod@4.1.8) + zod: 4.1.8 + + "@ai-sdk/openai@2.0.32(zod@4.1.8)": + dependencies: + "@ai-sdk/provider": 2.0.0 + "@ai-sdk/provider-utils": 3.0.9(zod@4.1.8) + zod: 4.1.8 + + "@ai-sdk/provider-utils@3.0.9(zod@4.1.8)": + dependencies: + "@ai-sdk/provider": 2.0.0 + "@standard-schema/spec": 1.0.0 + eventsource-parser: 3.0.6 + zod: 4.1.8 + + "@ai-sdk/provider@2.0.0": + dependencies: + json-schema: 0.4.0 + + "@ai-sdk/react@2.0.45(react@19.1.1)(zod@4.1.8)": + dependencies: + "@ai-sdk/provider-utils": 3.0.9(zod@4.1.8) + ai: 5.0.45(zod@4.1.8) + react: 19.1.1 + swr: 2.3.6(react@19.1.1) + throttleit: 2.1.0 + optionalDependencies: + zod: 4.1.8 + "@alloc/quick-lru@5.2.0": {} "@ant-design/colors@7.2.1": @@ -6092,6 +6364,58 @@ snapshots: resize-observer-polyfill: 1.5.1 throttle-debounce: 5.0.2 + "@assistant-ui/react-ai-sdk@1.1.0(@assistant-ui/react@0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)))(@types/react@19.1.12)(assistant-cloud@0.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1))": + dependencies: + "@ai-sdk/provider": 2.0.0 + "@ai-sdk/react": 2.0.45(react@19.1.1)(zod@4.1.8) + "@assistant-ui/react": 0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + "@radix-ui/react-use-callback-ref": 1.1.1(@types/react@19.1.12)(react@19.1.1) + "@types/json-schema": 7.0.15 + ai: 5.0.45(zod@4.1.8) + assistant-stream: 0.2.26 + json-schema: 0.4.0 + react: 19.1.1 + zod: 4.1.8 + zustand: 5.0.8(@types/react@19.1.12)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + optionalDependencies: + "@types/react": 19.1.12 + assistant-cloud: 0.1.1 + transitivePeerDependencies: + - immer + - use-sync-external-store + + "@assistant-ui/react@0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1))": + dependencies: + "@assistant-ui/tap": 0.1.1(react@19.1.1) + "@radix-ui/primitive": 1.1.3 + "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-context": 1.1.2(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-popover": 1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-slot": 1.2.3(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-use-callback-ref": 1.1.1(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-use-escape-keydown": 1.1.1(@types/react@19.1.12)(react@19.1.1) + "@standard-schema/spec": 1.0.0 + assistant-cloud: 0.1.1 + assistant-stream: 0.2.26 + json-schema: 0.4.0 + nanoid: 5.1.5 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-textarea-autosize: 8.5.9(@types/react@19.1.12)(react@19.1.1) + zod: 4.1.8 + zustand: 5.0.8(@types/react@19.1.12)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + optionalDependencies: + "@types/react": 19.1.12 + "@types/react-dom": 19.1.9(@types/react@19.1.12) + transitivePeerDependencies: + - immer + - use-sync-external-store + + "@assistant-ui/tap@0.1.1(react@19.1.1)": + optionalDependencies: + react: 19.1.1 + "@babel/runtime@7.28.4": {} "@emnapi/core@1.5.0": @@ -6496,8 +6820,7 @@ snapshots: "@nolyfill/is-core-module@1.0.39": {} - "@opentelemetry/api@1.9.0": - optional: true + "@opentelemetry/api@1.9.0": {} "@orama/orama@3.1.13": {} @@ -7268,6 +7591,14 @@ snapshots: acorn@8.15.0: {} + ai@5.0.45(zod@4.1.8): + dependencies: + "@ai-sdk/gateway": 1.0.23(zod@4.1.8) + "@ai-sdk/provider": 2.0.0 + "@ai-sdk/provider-utils": 3.0.9(zod@4.1.8) + "@opentelemetry/api": 1.9.0 + zod: 4.1.8 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -7420,6 +7751,16 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assistant-cloud@0.1.1: + dependencies: + assistant-stream: 0.2.26 + + assistant-stream@0.2.26: + dependencies: + "@types/json-schema": 7.0.15 + nanoid: 5.1.5 + secure-json-parse: 4.0.0 + ast-types-flow@0.0.8: {} astring@1.9.0: {} @@ -8056,6 +8397,8 @@ snapshots: eventemitter3@5.0.1: {} + eventsource-parser@3.0.6: {} + extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -8556,6 +8899,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json2mq@0.2.0: @@ -9171,6 +9516,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@5.1.5: {} + napi-postinstall@0.3.3: {} natural-compare@1.4.0: {} @@ -9724,6 +10071,15 @@ snapshots: optionalDependencies: "@types/react": 19.1.12 + react-textarea-autosize@8.5.9(@types/react@19.1.12)(react@19.1.1): + dependencies: + "@babel/runtime": 7.28.4 + react: 19.1.1 + use-composed-ref: 1.4.0(@types/react@19.1.12)(react@19.1.1) + use-latest: 1.3.0(@types/react@19.1.12)(react@19.1.1) + transitivePeerDependencies: + - "@types/react" + react@19.1.1: {} readdirp@4.1.2: {} @@ -9918,6 +10274,8 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 + secure-json-parse@4.0.0: {} + semver@6.3.1: {} semver@7.7.2: {} @@ -10143,6 +10501,12 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swr@2.3.6(react@19.1.1): + dependencies: + dequal: 2.0.3 + react: 19.1.1 + use-sync-external-store: 1.5.0(react@19.1.1) + tailwind-merge@3.3.1: {} tailwindcss@4.1.13: {} @@ -10160,6 +10524,8 @@ snapshots: throttle-debounce@5.0.2: {} + throttleit@2.1.0: {} + tinyexec@1.0.1: {} tinyglobby@0.2.15: @@ -10318,6 +10684,12 @@ snapshots: optionalDependencies: "@types/react": 19.1.12 + use-composed-ref@1.4.0(@types/react@19.1.12)(react@19.1.1): + dependencies: + react: 19.1.1 + optionalDependencies: + "@types/react": 19.1.12 + use-intl@4.3.8(react@19.1.1): dependencies: "@formatjs/fast-memoize": 2.2.7 @@ -10325,6 +10697,19 @@ snapshots: intl-messageformat: 10.7.16 react: 19.1.1 + use-isomorphic-layout-effect@1.2.1(@types/react@19.1.12)(react@19.1.1): + dependencies: + react: 19.1.1 + optionalDependencies: + "@types/react": 19.1.12 + + use-latest@1.3.0(@types/react@19.1.12)(react@19.1.1): + dependencies: + react: 19.1.1 + use-isomorphic-layout-effect: 1.2.1(@types/react@19.1.12)(react@19.1.1) + optionalDependencies: + "@types/react": 19.1.12 + use-sidecar@1.1.3(@types/react@19.1.12)(react@19.1.1): dependencies: detect-node-es: 1.1.0 @@ -10333,6 +10718,10 @@ snapshots: optionalDependencies: "@types/react": 19.1.12 + use-sync-external-store@1.5.0(react@19.1.1): + dependencies: + react: 19.1.1 + util-deprecate@1.0.2: {} vfile-message@4.0.3: @@ -10406,4 +10795,10 @@ snapshots: zod@4.1.8: {} + zustand@5.0.8(@types/react@19.1.12)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): + optionalDependencies: + "@types/react": 19.1.12 + react: 19.1.1 + use-sync-external-store: 1.5.0(react@19.1.1) + zwitch@2.0.4: {} From ca7b154f9b0d9d2153036308b21e16fc18641cb9 Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 18:22:07 +1000 Subject: [PATCH 02/17] chore: update component paths in components.json and add new dependencies in package.json --- .../assistant-ui/assistant-modal.tsx | 60 +++ app/components/assistant-ui/attachment.tsx | 239 +++++++++++ app/components/assistant-ui/markdown-text.tsx | 228 +++++++++++ app/components/assistant-ui/thread.tsx | 385 ++++++++++++++++++ app/components/assistant-ui/tool-fallback.tsx | 46 +++ .../assistant-ui/tooltip-icon-button.tsx | 42 ++ app/components/ui/avatar.tsx | 53 +++ app/components/ui/button.tsx | 59 +++ app/components/ui/dialog.tsx | 143 +++++++ app/components/ui/tooltip.tsx | 61 +++ app/docs/[...slug]/page.tsx | 1 + components.json | 4 +- package.json | 7 +- pnpm-lock.yaml | 232 +++++++++++ 14 files changed, 1557 insertions(+), 3 deletions(-) create mode 100644 app/components/assistant-ui/assistant-modal.tsx create mode 100644 app/components/assistant-ui/attachment.tsx create mode 100644 app/components/assistant-ui/markdown-text.tsx create mode 100644 app/components/assistant-ui/thread.tsx create mode 100644 app/components/assistant-ui/tool-fallback.tsx create mode 100644 app/components/assistant-ui/tooltip-icon-button.tsx create mode 100644 app/components/ui/avatar.tsx create mode 100644 app/components/ui/button.tsx create mode 100644 app/components/ui/dialog.tsx create mode 100644 app/components/ui/tooltip.tsx diff --git a/app/components/assistant-ui/assistant-modal.tsx b/app/components/assistant-ui/assistant-modal.tsx new file mode 100644 index 0000000..34601d0 --- /dev/null +++ b/app/components/assistant-ui/assistant-modal.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { BotIcon, ChevronDownIcon } from "lucide-react"; + +import { type FC, forwardRef } from "react"; +import { AssistantModalPrimitive } from "@assistant-ui/react"; + +import { Thread } from "@/app/components/assistant-ui/thread"; +import { TooltipIconButton } from "@/app/components/assistant-ui/tooltip-icon-button"; + +export const AssistantModal: FC = () => { + return ( + + + + + + + + + + + ); +}; + +type AssistantModalButtonProps = { "data-state"?: "open" | "closed" }; + +const AssistantModalButton = forwardRef< + HTMLButtonElement, + AssistantModalButtonProps +>(({ "data-state": state, ...rest }, ref) => { + const tooltip = state === "open" ? "Close Assistant" : "Open Assistant"; + + return ( + + + + + {tooltip} + + ); +}); + +AssistantModalButton.displayName = "AssistantModalButton"; diff --git a/app/components/assistant-ui/attachment.tsx b/app/components/assistant-ui/attachment.tsx new file mode 100644 index 0000000..e8e3c69 --- /dev/null +++ b/app/components/assistant-ui/attachment.tsx @@ -0,0 +1,239 @@ +"use client"; + +import { PropsWithChildren, useEffect, useState, type FC } from "react"; +import Image from "next/image"; +import { XIcon, PlusIcon, FileText } from "lucide-react"; +import { + AttachmentPrimitive, + ComposerPrimitive, + MessagePrimitive, + useAssistantState, + useAssistantApi, +} from "@assistant-ui/react"; +import { useShallow } from "zustand/shallow"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/app/components/ui/tooltip"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Avatar, + AvatarImage, + AvatarFallback, +} from "@/app/components/ui/avatar"; +import { TooltipIconButton } from "@/app/components/assistant-ui/tooltip-icon-button"; +import { cn } from "@/lib/utils"; + +const useFileSrc = (file: File | undefined) => { + const [src, setSrc] = useState(undefined); + + useEffect(() => { + if (!file) { + setSrc(undefined); + return; + } + + const objectUrl = URL.createObjectURL(file); + setSrc(objectUrl); + + return () => { + URL.revokeObjectURL(objectUrl); + }; + }, [file]); + + return src; +}; + +const useAttachmentSrc = () => { + const { file, src } = useAssistantState( + useShallow(({ attachment }): { file?: File; src?: string } => { + if (attachment.type !== "image") return {}; + if (attachment.file) return { file: attachment.file }; + const src = attachment.content?.filter((c) => c.type === "image")[0] + ?.image; + if (!src) return {}; + return { src }; + }), + ); + + return useFileSrc(file) ?? src; +}; + +type AttachmentPreviewProps = { + src: string; +}; + +const AttachmentPreview: FC = ({ src }) => { + const [isLoaded, setIsLoaded] = useState(false); + return ( + Image Preview setIsLoaded(true)} + priority={false} + /> + ); +}; + +const AttachmentPreviewDialog: FC = ({ children }) => { + const src = useAttachmentSrc(); + + if (!src) return children; + + return ( + + + {children} + + + + Image Attachment Preview + +
+ +
+
+
+ ); +}; + +const AttachmentThumb: FC = () => { + const isImage = useAssistantState( + ({ attachment }) => attachment.type === "image", + ); + const src = useAttachmentSrc(); + + return ( + + + + + + + ); +}; + +const AttachmentUI: FC = () => { + const api = useAssistantApi(); + const isComposer = api.attachment.source === "composer"; + + const isImage = useAssistantState( + ({ attachment }) => attachment.type === "image", + ); + const typeLabel = useAssistantState(({ attachment }) => { + const type = attachment.type; + switch (type) { + case "image": + return "Image"; + case "document": + return "Document"; + case "file": + return "File"; + default: + const _exhaustiveCheck: never = type; + throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`); + } + }); + + return ( + + #attachment-tile]:size-24", + )} + > + + +
+ +
+
+
+ {isComposer && } +
+ + + +
+ ); +}; + +const AttachmentRemove: FC = () => { + return ( + + + + + + ); +}; + +export const UserMessageAttachments: FC = () => { + return ( +
+ +
+ ); +}; + +export const ComposerAttachments: FC = () => { + return ( +
+ +
+ ); +}; + +export const ComposerAddAttachment: FC = () => { + return ( + + + + + + ); +}; diff --git a/app/components/assistant-ui/markdown-text.tsx b/app/components/assistant-ui/markdown-text.tsx new file mode 100644 index 0000000..89275b9 --- /dev/null +++ b/app/components/assistant-ui/markdown-text.tsx @@ -0,0 +1,228 @@ +"use client"; + +import "@assistant-ui/react-markdown/styles/dot.css"; + +import { + type CodeHeaderProps, + MarkdownTextPrimitive, + unstable_memoizeMarkdownComponents as memoizeMarkdownComponents, + useIsMarkdownCodeBlock, +} from "@assistant-ui/react-markdown"; +import remarkGfm from "remark-gfm"; +import { type FC, memo, useState } from "react"; +import { CheckIcon, CopyIcon } from "lucide-react"; + +import { TooltipIconButton } from "@/app/components/assistant-ui/tooltip-icon-button"; +import { cn } from "@/lib/utils"; + +const MarkdownTextImpl = () => { + return ( + + ); +}; + +export const MarkdownText = memo(MarkdownTextImpl); + +const CodeHeader: FC = ({ language, code }) => { + const { isCopied, copyToClipboard } = useCopyToClipboard(); + const onCopy = () => { + if (!code || isCopied) return; + copyToClipboard(code); + }; + + return ( +
+ + {language} + + + {!isCopied && } + {isCopied && } + +
+ ); +}; + +const useCopyToClipboard = ({ + copiedDuration = 3000, +}: { + copiedDuration?: number; +} = {}) => { + const [isCopied, setIsCopied] = useState(false); + + const copyToClipboard = (value: string) => { + if (!value) return; + + navigator.clipboard.writeText(value).then(() => { + setIsCopied(true); + setTimeout(() => setIsCopied(false), copiedDuration); + }); + }; + + return { isCopied, copyToClipboard }; +}; + +const defaultComponents = memoizeMarkdownComponents({ + h1: ({ className, ...props }) => ( +

+ ), + h2: ({ className, ...props }) => ( +

+ ), + h3: ({ className, ...props }) => ( +

+ ), + h4: ({ className, ...props }) => ( +

+ ), + h5: ({ className, ...props }) => ( +

+ ), + h6: ({ className, ...props }) => ( +
+ ), + p: ({ className, ...props }) => ( +

+ ), + a: ({ className, ...props }) => ( + + ), + blockquote: ({ className, ...props }) => ( +

+ ), + ul: ({ className, ...props }) => ( +
    li]:mt-2", className)} + {...props} + /> + ), + ol: ({ className, ...props }) => ( +
      li]:mt-2", className)} + {...props} + /> + ), + hr: ({ className, ...props }) => ( +
      + ), + table: ({ className, ...props }) => ( + + ), + th: ({ className, ...props }) => ( + td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg", + className, + )} + {...props} + /> + ), + sup: ({ className, ...props }) => ( + a]:text-xs [&>a]:no-underline", className)} + {...props} + /> + ), + pre: ({ className, ...props }) => ( +
      +  ),
      +  code: function Code({ className, ...props }) {
      +    const isCodeBlock = useIsMarkdownCodeBlock();
      +    return (
      +      
      +    );
      +  },
      +  CodeHeader,
      +});
      diff --git a/app/components/assistant-ui/thread.tsx b/app/components/assistant-ui/thread.tsx
      new file mode 100644
      index 0000000..82416bc
      --- /dev/null
      +++ b/app/components/assistant-ui/thread.tsx
      @@ -0,0 +1,385 @@
      +import {
      +  ActionBarPrimitive,
      +  BranchPickerPrimitive,
      +  ComposerPrimitive,
      +  ErrorPrimitive,
      +  MessagePrimitive,
      +  ThreadPrimitive,
      +} from "@assistant-ui/react";
      +import {
      +  ArrowDownIcon,
      +  ArrowUpIcon,
      +  CheckIcon,
      +  ChevronLeftIcon,
      +  ChevronRightIcon,
      +  CopyIcon,
      +  PencilIcon,
      +  RefreshCwIcon,
      +  Square,
      +} from "lucide-react";
      +import type { FC } from "react";
      +
      +import {
      +  ComposerAddAttachment,
      +  ComposerAttachments,
      +  UserMessageAttachments,
      +} from "@/app/components/assistant-ui/attachment";
      +import { MarkdownText } from "@/app/components/assistant-ui/markdown-text";
      +import { ToolFallback } from "@/app/components/assistant-ui/tool-fallback";
      +import { TooltipIconButton } from "@/app/components/assistant-ui/tooltip-icon-button";
      +import { Button } from "@/components/ui/button";
      +import { cn } from "@/lib/utils";
      +import { LazyMotion, MotionConfig, domAnimation } from "motion/react";
      +import * as m from "motion/react-m";
      +
      +export const Thread: FC = () => {
      +  return (
      +    
      +      
      +        
      +          
      +            
      +
      +            
      +            
      +              
      + + + + + + + ); +}; + +const ThreadScrollToBottom: FC = () => { + return ( + + + + + + ); +}; + +const ThreadWelcome: FC = () => { + return ( + +
      +
      +
      + + Hello there! + + + How can I help you today? + +
      +
      +
      +
      + ); +}; + +const ThreadWelcomeSuggestions: FC = () => { + return ( +
      + {[ + { + title: "What's the weather", + label: "in San Francisco?", + action: "What's the weather in San Francisco?", + }, + { + title: "Explain React hooks", + label: "like useState and useEffect", + action: "Explain React hooks like useState and useEffect", + }, + { + title: "Write a SQL query", + label: "to find top customers", + action: "Write a SQL query to find top customers", + }, + { + title: "Create a meal plan", + label: "for healthy weight loss", + action: "Create a meal plan for healthy weight loss", + }, + ].map((suggestedAction, index) => ( + + + + + + ))} +
      + ); +}; + +const Composer: FC = () => { + return ( +
      + + + + + + + + + +
      + ); +}; + +const ComposerAction: FC = () => { + return ( +
      + + + + + + + + + + + + + + + +
      + ); +}; + +const MessageError: FC = () => { + return ( + + + + + + ); +}; + +const AssistantMessage: FC = () => { + return ( + +
      +
      + + +
      + +
      + + +
      +
      +
      + ); +}; + +const AssistantActionBar: FC = () => { + return ( + + + + + + + + + + + + + + + + + + ); +}; + +const UserMessage: FC = () => { + return ( + +
      + + +
      +
      + +
      +
      + +
      +
      + + +
      +
      + ); +}; + +const UserActionBar: FC = () => { + return ( + + + + + + + + ); +}; + +const EditComposer: FC = () => { + return ( +
      + + + +
      + + + + + + +
      +
      +
      + ); +}; + +const BranchPicker: FC = ({ + className, + ...rest +}) => { + return ( + + + + + + + + / + + + + + + + + ); +}; diff --git a/app/components/assistant-ui/tool-fallback.tsx b/app/components/assistant-ui/tool-fallback.tsx new file mode 100644 index 0000000..aca4030 --- /dev/null +++ b/app/components/assistant-ui/tool-fallback.tsx @@ -0,0 +1,46 @@ +import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; + +export const ToolFallback: ToolCallMessagePartComponent = ({ + toolName, + argsText, + result, +}) => { + const [isCollapsed, setIsCollapsed] = useState(true); + return ( +
      +
      + +

      + Used tool: {toolName} +

      + +
      + {!isCollapsed && ( +
      +
      +
      +              {argsText}
      +            
      +
      + {result !== undefined && ( +
      +

      + Result: +

      +
      +                {typeof result === "string"
      +                  ? result
      +                  : JSON.stringify(result, null, 2)}
      +              
      +
      + )} +
      + )} +
      + ); +}; diff --git a/app/components/assistant-ui/tooltip-icon-button.tsx b/app/components/assistant-ui/tooltip-icon-button.tsx new file mode 100644 index 0000000..3028635 --- /dev/null +++ b/app/components/assistant-ui/tooltip-icon-button.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { ComponentPropsWithRef, forwardRef } from "react"; +import { Slottable } from "@radix-ui/react-slot"; + +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/app/components/ui/tooltip"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +export type TooltipIconButtonProps = ComponentPropsWithRef & { + tooltip: string; + side?: "top" | "bottom" | "left" | "right"; +}; + +export const TooltipIconButton = forwardRef< + HTMLButtonElement, + TooltipIconButtonProps +>(({ children, tooltip, side = "bottom", className, ...rest }, ref) => { + return ( + + + + + {tooltip} + + ); +}); + +TooltipIconButton.displayName = "TooltipIconButton"; diff --git a/app/components/ui/avatar.tsx b/app/components/ui/avatar.tsx new file mode 100644 index 0000000..c4475c2 --- /dev/null +++ b/app/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client"; + +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "@/lib/utils"; + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/app/components/ui/button.tsx b/app/components/ui/button.tsx new file mode 100644 index 0000000..2adaf00 --- /dev/null +++ b/app/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : "button"; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/app/components/ui/dialog.tsx b/app/components/ui/dialog.tsx new file mode 100644 index 0000000..7d60dd3 --- /dev/null +++ b/app/components/ui/dialog.tsx @@ -0,0 +1,143 @@ +"use client"; + +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { XIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function Dialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean; +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
      + ); +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
      + ); +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/app/components/ui/tooltip.tsx b/app/components/ui/tooltip.tsx new file mode 100644 index 0000000..bf4a342 --- /dev/null +++ b/app/components/ui/tooltip.tsx @@ -0,0 +1,61 @@ +"use client"; + +import * as React from "react"; +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; + +import { cn } from "@/lib/utils"; + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ); +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx index a71654f..f6e2c1a 100644 --- a/app/docs/[...slug]/page.tsx +++ b/app/docs/[...slug]/page.tsx @@ -6,6 +6,7 @@ import { getMDXComponents } from "@/mdx-components"; import { GiscusComments } from "@/app/components/GiscusComments"; import { getContributors } from "@/lib/github"; import { Contributors } from "@/app/components/Contributors"; +import { AssistantModal } from "@/app/components/assistant-ui/assistant-modal"; interface Param { params: Promise<{ diff --git a/components.json b/components.json index b7b9791..d1b5548 100644 --- a/components.json +++ b/components.json @@ -12,9 +12,9 @@ }, "iconLibrary": "lucide", "aliases": { - "components": "@/components", + "components": "@/app/components", "utils": "@/lib/utils", - "ui": "@/components/ui", + "ui": "@/app/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, diff --git a/package.json b/package.json index 4354f4e..d270ac6 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,14 @@ "@ai-sdk/openai": "^2.0.32", "@assistant-ui/react": "^0.11.10", "@assistant-ui/react-ai-sdk": "^1.1.0", + "@assistant-ui/react-markdown": "^0.11.0", "@giscus/react": "^3.1.0", "@orama/orama": "^3.1.13", "@orama/tokenizers": "^3.1.13", + "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tooltip": "^1.2.8", "@types/mdx": "^2.0.13", "@vercel/speed-insights": "^1.2.0", "ai": "^5.0.45", @@ -33,6 +36,7 @@ "fumadocs-mdx": "^11.9.1", "fumadocs-ui": "^15.7.11", "lucide-react": "^0.544.0", + "motion": "^12.23.14", "next": "^15.5.3", "next-intl": "^4.3.8", "react": "^19.1.1", @@ -40,7 +44,8 @@ "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", "remark-gfm": "^4.0.1", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "zustand": "^5.0.8" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e92100..9ddf906 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,9 @@ importers: "@assistant-ui/react-ai-sdk": specifier: ^1.1.0 version: 1.1.0(@assistant-ui/react@0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)))(@types/react@19.1.12)(assistant-cloud@0.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + "@assistant-ui/react-markdown": + specifier: ^0.11.0 + version: 0.11.0(@assistant-ui/react@0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)))(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) "@giscus/react": specifier: ^3.1.0 version: 3.1.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -25,12 +28,18 @@ importers: "@orama/tokenizers": specifier: ^3.1.13 version: 3.1.13 + "@radix-ui/react-avatar": + specifier: ^1.1.10 + version: 1.1.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) "@radix-ui/react-dialog": specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) "@radix-ui/react-slot": specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-tooltip": + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) "@types/mdx": specifier: ^2.0.13 version: 2.0.13 @@ -64,6 +73,9 @@ importers: lucide-react: specifier: ^0.544.0 version: 0.544.0(react@19.1.1) + motion: + specifier: ^12.23.14 + version: 12.23.14(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next: specifier: ^15.5.3 version: 15.5.3(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -88,6 +100,9 @@ importers: tailwind-merge: specifier: ^3.3.1 version: 3.3.1 + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@19.1.12)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) devDependencies: "@tailwindcss/postcss": specifier: ^4.1.13 @@ -258,6 +273,19 @@ packages: assistant-cloud: optional: true + "@assistant-ui/react-markdown@0.11.0": + resolution: + { + integrity: sha512-w5zKMKnAvJVyeDvHL1Afy3Pm+Bb5NBYpQ8FQRsYbaZMZIhT3/zCN6lMYID+XaB1nkvhztHucVk9u+G7Uk5DJIw==, + } + peerDependencies: + "@assistant-ui/react": ^0.11.0 + "@types/react": "*" + react: ^18 || ^19 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@assistant-ui/react@0.11.10": resolution: { @@ -1148,6 +1176,22 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-avatar@1.1.10": + resolution: + { + integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==, + } + 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 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-collapsible@1.1.12": resolution: { @@ -1444,6 +1488,22 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-tooltip@1.2.8": + resolution: + { + integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==, + } + 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 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-use-callback-ref@1.1.1": resolution: { @@ -1492,6 +1552,18 @@ packages: "@types/react": optional: true + "@radix-ui/react-use-is-hydrated@0.1.0": + resolution: + { + integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==, + } + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@radix-ui/react-use-layout-effect@1.1.1": resolution: { @@ -3296,6 +3368,23 @@ packages: integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==, } + framer-motion@12.23.14: + resolution: + { + integrity: sha512-8BQ6dvqOht2w8P1CwIEvAA0gypDR3fNG/M6/f5lT0QgNIKnJf7J43Bpv++NnCWU8YfmL47UEm2hbI0GRvdVhsQ==, + } + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + fumadocs-core@15.7.11: resolution: { @@ -3584,6 +3673,12 @@ packages: integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==, } + html-url-attributes@3.0.1: + resolution: + { + integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==, + } + html-void-elements@3.0.0: resolution: { @@ -4573,6 +4668,35 @@ packages: engines: { node: ">=10" } hasBin: true + motion-dom@12.23.12: + resolution: + { + integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==, + } + + motion-utils@12.23.6: + resolution: + { + integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==, + } + + motion@12.23.14: + resolution: + { + integrity: sha512-kcJde+A4AeUD2ujAhpvhCOjzt6NtXjqL9m0LsLdyPO5SPVQFsCpxVyLsqtS1o9Z+CEJ7U8kSIhsRSJF1oDZXfg==, + } + peerDependencies: + "@emotion/is-prop-valid": "*" + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/is-prop-valid": + optional: true + react: + optional: true + react-dom: + optional: true + ms@2.1.3: resolution: { @@ -5277,6 +5401,15 @@ packages: integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==, } + react-markdown@10.1.0: + resolution: + { + integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==, + } + peerDependencies: + "@types/react": ">=18" + react: ">=18" + react-medium-image-zoom@5.3.0: resolution: { @@ -6384,6 +6517,22 @@ snapshots: - immer - use-sync-external-store + "@assistant-ui/react-markdown@0.11.0(@assistant-ui/react@0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)))(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)": + dependencies: + "@assistant-ui/react": 0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-use-callback-ref": 1.1.1(@types/react@19.1.12)(react@19.1.1) + "@types/hast": 3.0.4 + classnames: 2.5.1 + react: 19.1.1 + react-markdown: 10.1.0(@types/react@19.1.12)(react@19.1.1) + optionalDependencies: + "@types/react": 19.1.12 + transitivePeerDependencies: + - "@types/react-dom" + - react-dom + - supports-color + "@assistant-ui/react@0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1))": dependencies: "@assistant-ui/tap": 0.1.1(react@19.1.1) @@ -6858,6 +7007,19 @@ snapshots: "@types/react": 19.1.12 "@types/react-dom": 19.1.9(@types/react@19.1.12) + "@radix-ui/react-avatar@1.1.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)": + dependencies: + "@radix-ui/react-context": 1.1.2(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-use-callback-ref": 1.1.1(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-use-is-hydrated": 0.1.0(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-use-layout-effect": 1.1.1(@types/react@19.1.12)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + "@types/react": 19.1.12 + "@types/react-dom": 19.1.9(@types/react@19.1.12) + "@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)": dependencies: "@radix-ui/primitive": 1.1.3 @@ -7112,6 +7274,26 @@ snapshots: "@types/react": 19.1.12 "@types/react-dom": 19.1.9(@types/react@19.1.12) + "@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)": + dependencies: + "@radix-ui/primitive": 1.1.3 + "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-context": 1.1.2(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-dismissable-layer": 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-id": 1.1.1(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-popper": 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-portal": 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-presence": 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-slot": 1.2.3(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-use-controllable-state": 1.2.2(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-visually-hidden": 1.2.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + "@types/react": 19.1.12 + "@types/react-dom": 19.1.9(@types/react@19.1.12) + "@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.12)(react@19.1.1)": dependencies: react: 19.1.1 @@ -7140,6 +7322,13 @@ snapshots: optionalDependencies: "@types/react": 19.1.12 + "@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.1.12)(react@19.1.1)": + dependencies: + react: 19.1.1 + use-sync-external-store: 1.5.0(react@19.1.1) + optionalDependencies: + "@types/react": 19.1.12 + "@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.12)(react@19.1.1)": dependencies: react: 19.1.1 @@ -8457,6 +8646,15 @@ snapshots: fraction.js@4.3.7: {} + framer-motion@12.23.14(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + motion-dom: 12.23.12 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + fumadocs-core@15.7.11(@types/react@19.1.12)(next@15.5.3(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: "@formatjs/intl-localematcher": 0.6.1 @@ -8709,6 +8907,8 @@ snapshots: dependencies: "@types/hast": 3.0.4 + html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} husky@9.1.7: {} @@ -9510,6 +9710,20 @@ snapshots: mkdirp@3.0.1: {} + motion-dom@12.23.12: + dependencies: + motion-utils: 12.23.6 + + motion-utils@12.23.6: {} + + motion@12.23.14(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + framer-motion: 12.23.14(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + tslib: 2.8.1 + optionalDependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + ms@2.1.3: {} nano-spawn@1.0.3: {} @@ -10039,6 +10253,24 @@ snapshots: react-is@18.3.1: {} + react-markdown@10.1.0(@types/react@19.1.12)(react@19.1.1): + dependencies: + "@types/hast": 3.0.4 + "@types/mdast": 4.0.4 + "@types/react": 19.1.12 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 19.1.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-medium-image-zoom@5.3.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: react: 19.1.1 From b578f62c12fabe7f2ca6a82e4eac7382e4f2081e Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 18:31:54 +1000 Subject: [PATCH 03/17] chore: add zod dependency and update pnpm-lock.yaml --- app/api/chat/route.ts | 65 ++++++++++++++++++++++++++++++++ app/components/DocsAssistant.tsx | 17 +++++++++ app/docs/[...slug]/page.tsx | 29 +++++++------- package.json | 1 + pnpm-lock.yaml | 55 ++++++++++++++------------- 5 files changed, 128 insertions(+), 39 deletions(-) create mode 100644 app/api/chat/route.ts create mode 100644 app/components/DocsAssistant.tsx diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts new file mode 100644 index 0000000..e1503ca --- /dev/null +++ b/app/api/chat/route.ts @@ -0,0 +1,65 @@ +import { openai } from "@ai-sdk/openai"; +import { streamText, UIMessage, convertToModelMessages, tool } from "ai"; +import { frontendTools } from "@assistant-ui/react-ai-sdk"; +import { z } from "zod"; + +// Allow streaming responses up to 30 seconds +export const maxDuration = 30; + +export async function POST(req: Request) { + const { + messages, + system, + tools, + }: { + messages: UIMessage[]; + system?: string; // System message forwarded from AssistantChatTransport + tools?: any; // Frontend tools forwarded from AssistantChatTransport + } = await req.json(); + + try { + const result = streamText({ + model: openai("gpt-4o-mini"), // Using more cost-effective model + system: + system || + `You are a helpful AI assistant for a documentation website. + You can help users understand the documentation, answer questions about the content, + and provide guidance on the topics covered in the docs. Be concise and helpful.`, + messages: convertToModelMessages(messages), + tools: { + // Wrap frontend tools with frontendTools helper + ...frontendTools(tools), + // Backend tools + get_documentation_info: tool({ + description: + "Get information about the current documentation page or topic", + inputSchema: z.object({ + topic: z + .string() + .describe("The topic or page to get information about"), + }), + execute: async ({ topic }) => { + return `This is a documentation website covering topics like AI, computer science, and development guides. The current topic "${topic}" is part of our comprehensive documentation collection.`; + }, + }), + search_docs: tool({ + description: "Search through the documentation for specific topics", + inputSchema: z.object({ + query: z.string().describe("The search query"), + }), + execute: async ({ query }) => { + return `Searching for "${query}" in the documentation. This site covers AI foundations, computer science fundamentals, and development guides.`; + }, + }), + }, + }); + + return result.toUIMessageStreamResponse(); + } catch (error) { + console.error("Chat API error:", error); + return Response.json( + { error: "Failed to process chat request" }, + { status: 500 }, + ); + } +} diff --git a/app/components/DocsAssistant.tsx b/app/components/DocsAssistant.tsx new file mode 100644 index 0000000..2ac3861 --- /dev/null +++ b/app/components/DocsAssistant.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { AssistantRuntimeProvider } from "@assistant-ui/react"; +import { useChatRuntime } from "@assistant-ui/react-ai-sdk"; +import { AssistantModal } from "@/app/components/assistant-ui/assistant-modal"; + +export function DocsAssistant() { + const runtime = useChatRuntime({ + api: "/api/chat", + }); + + return ( + + + + ); +} diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx index f6e2c1a..839b1be 100644 --- a/app/docs/[...slug]/page.tsx +++ b/app/docs/[...slug]/page.tsx @@ -6,7 +6,7 @@ import { getMDXComponents } from "@/mdx-components"; import { GiscusComments } from "@/app/components/GiscusComments"; import { getContributors } from "@/lib/github"; import { Contributors } from "@/app/components/Contributors"; -import { AssistantModal } from "@/app/components/assistant-ui/assistant-modal"; +import { DocsAssistant } from "@/app/components/DocsAssistant"; interface Param { params: Promise<{ @@ -31,18 +31,21 @@ export default async function DocPage({ params }: Param) { const Mdx = page.data.body; return ( - - -

      - {page.data.title} -

      - - -
      - -
      -
      -
      + <> + + +

      + {page.data.title} +

      + + +
      + +
      +
      +
      + + ); } diff --git a/package.json b/package.json index d270ac6..d5c9898 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "rehype-slug": "^6.0.0", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.3.1", + "zod": "^4.1.9", "zustand": "^5.0.8" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ddf906..14b1707 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ importers: dependencies: "@ai-sdk/openai": specifier: ^2.0.32 - version: 2.0.32(zod@4.1.8) + version: 2.0.32(zod@4.1.9) "@assistant-ui/react": specifier: ^0.11.10 version: 0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) @@ -48,7 +48,7 @@ importers: version: 1.2.0(next@15.5.3(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) ai: specifier: ^5.0.45 - version: 5.0.45(zod@4.1.8) + version: 5.0.45(zod@4.1.9) antd: specifier: ^5.27.3 version: 5.27.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -100,6 +100,9 @@ importers: tailwind-merge: specifier: ^3.3.1 version: 3.3.1 + zod: + specifier: ^4.1.9 + version: 4.1.9 zustand: specifier: ^5.0.8 version: 5.0.8(@types/react@19.1.12)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) @@ -6379,10 +6382,10 @@ packages: } engines: { node: ">=10" } - zod@4.1.8: + zod@4.1.9: resolution: { - integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==, + integrity: sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ==, } zustand@5.0.8: @@ -6413,38 +6416,38 @@ packages: } snapshots: - "@ai-sdk/gateway@1.0.23(zod@4.1.8)": + "@ai-sdk/gateway@1.0.23(zod@4.1.9)": dependencies: "@ai-sdk/provider": 2.0.0 - "@ai-sdk/provider-utils": 3.0.9(zod@4.1.8) - zod: 4.1.8 + "@ai-sdk/provider-utils": 3.0.9(zod@4.1.9) + zod: 4.1.9 - "@ai-sdk/openai@2.0.32(zod@4.1.8)": + "@ai-sdk/openai@2.0.32(zod@4.1.9)": dependencies: "@ai-sdk/provider": 2.0.0 - "@ai-sdk/provider-utils": 3.0.9(zod@4.1.8) - zod: 4.1.8 + "@ai-sdk/provider-utils": 3.0.9(zod@4.1.9) + zod: 4.1.9 - "@ai-sdk/provider-utils@3.0.9(zod@4.1.8)": + "@ai-sdk/provider-utils@3.0.9(zod@4.1.9)": dependencies: "@ai-sdk/provider": 2.0.0 "@standard-schema/spec": 1.0.0 eventsource-parser: 3.0.6 - zod: 4.1.8 + zod: 4.1.9 "@ai-sdk/provider@2.0.0": dependencies: json-schema: 0.4.0 - "@ai-sdk/react@2.0.45(react@19.1.1)(zod@4.1.8)": + "@ai-sdk/react@2.0.45(react@19.1.1)(zod@4.1.9)": dependencies: - "@ai-sdk/provider-utils": 3.0.9(zod@4.1.8) - ai: 5.0.45(zod@4.1.8) + "@ai-sdk/provider-utils": 3.0.9(zod@4.1.9) + ai: 5.0.45(zod@4.1.9) react: 19.1.1 swr: 2.3.6(react@19.1.1) throttleit: 2.1.0 optionalDependencies: - zod: 4.1.8 + zod: 4.1.9 "@alloc/quick-lru@5.2.0": {} @@ -6500,15 +6503,15 @@ snapshots: "@assistant-ui/react-ai-sdk@1.1.0(@assistant-ui/react@0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)))(@types/react@19.1.12)(assistant-cloud@0.1.1)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1))": dependencies: "@ai-sdk/provider": 2.0.0 - "@ai-sdk/react": 2.0.45(react@19.1.1)(zod@4.1.8) + "@ai-sdk/react": 2.0.45(react@19.1.1)(zod@4.1.9) "@assistant-ui/react": 0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) "@radix-ui/react-use-callback-ref": 1.1.1(@types/react@19.1.12)(react@19.1.1) "@types/json-schema": 7.0.15 - ai: 5.0.45(zod@4.1.8) + ai: 5.0.45(zod@4.1.9) assistant-stream: 0.2.26 json-schema: 0.4.0 react: 19.1.1 - zod: 4.1.8 + zod: 4.1.9 zustand: 5.0.8(@types/react@19.1.12)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) optionalDependencies: "@types/react": 19.1.12 @@ -6552,7 +6555,7 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) react-textarea-autosize: 8.5.9(@types/react@19.1.12)(react@19.1.1) - zod: 4.1.8 + zod: 4.1.9 zustand: 5.0.8(@types/react@19.1.12)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) optionalDependencies: "@types/react": 19.1.12 @@ -7780,13 +7783,13 @@ snapshots: acorn@8.15.0: {} - ai@5.0.45(zod@4.1.8): + ai@5.0.45(zod@4.1.9): dependencies: - "@ai-sdk/gateway": 1.0.23(zod@4.1.8) + "@ai-sdk/gateway": 1.0.23(zod@4.1.9) "@ai-sdk/provider": 2.0.0 - "@ai-sdk/provider-utils": 3.0.9(zod@4.1.8) + "@ai-sdk/provider-utils": 3.0.9(zod@4.1.9) "@opentelemetry/api": 1.9.0 - zod: 4.1.8 + zod: 4.1.9 ajv@6.12.6: dependencies: @@ -8699,7 +8702,7 @@ snapshots: tinyglobby: 0.2.15 unified: 11.0.5 unist-util-visit: 5.0.0 - zod: 4.1.8 + zod: 4.1.9 optionalDependencies: next: 15.5.3(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: 19.1.1 @@ -11025,7 +11028,7 @@ snapshots: yocto-queue@0.1.0: {} - zod@4.1.8: {} + zod@4.1.9: {} zustand@5.0.8(@types/react@19.1.12)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): optionalDependencies: From 98c1cccf90a29f99451670ac7d7e6c6a0a7675b1 Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 19:51:29 +1000 Subject: [PATCH 04/17] refactor: remove unnecessary background class from Composer component --- app/components/assistant-ui/thread.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/assistant-ui/thread.tsx b/app/components/assistant-ui/thread.tsx index 82416bc..e1175c9 100644 --- a/app/components/assistant-ui/thread.tsx +++ b/app/components/assistant-ui/thread.tsx @@ -167,7 +167,7 @@ const ThreadWelcomeSuggestions: FC = () => { const Composer: FC = () => { return ( -
      +
      From a4f6d44093ab6a68db7daeb769cab98449bf7d0f Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 21:04:07 +1000 Subject: [PATCH 05/17] feat: enhance chat API with page context and update DocsAssistant to utilize AI SDK --- app/api/chat/route.ts | 75 +++++++++++++++++--------------- app/components/DocsAssistant.tsx | 36 +++++++++++++-- app/docs/[...slug]/page.tsx | 53 +++++++++++++++++++++- package.json | 1 + pnpm-lock.yaml | 3 ++ 5 files changed, 127 insertions(+), 41 deletions(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index e1503ca..092cb8b 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,7 +1,5 @@ import { openai } from "@ai-sdk/openai"; -import { streamText, UIMessage, convertToModelMessages, tool } from "ai"; -import { frontendTools } from "@assistant-ui/react-ai-sdk"; -import { z } from "zod"; +import { streamText, UIMessage, convertToModelMessages } from "ai"; // Allow streaming responses up to 30 seconds export const maxDuration = 30; @@ -10,48 +8,53 @@ export async function POST(req: Request) { const { messages, system, - tools, + pageContext, }: { messages: UIMessage[]; system?: string; // System message forwarded from AssistantChatTransport tools?: any; // Frontend tools forwarded from AssistantChatTransport + pageContext?: { + title?: string; + description?: string; + content?: string; + slug?: string; + }; } = await req.json(); + console.log("Chat API received request. pageContext (API):", { + title: pageContext?.title, + contentPreview: pageContext?.content?.substring(0, 100) + "...", + slug: pageContext?.slug, + }); + try { + // Build system message with page context + let systemMessage = + system || + `You are a helpful AI assistant for a documentation website. + You can help users understand the documentation, answer questions about the content, + and provide guidance on the topics covered in the docs. Be concise and helpful.`; + + // Add current page context if available + if (pageContext?.content) { + systemMessage += `\n\n--- CURRENT PAGE CONTEXT ---\n`; + if (pageContext.title) { + systemMessage += `Page Title: ${pageContext.title}\n`; + } + if (pageContext.description) { + systemMessage += `Page Description: ${pageContext.description}\n`; + } + if (pageContext.slug) { + systemMessage += `Page URL: /docs/${pageContext.slug}\n`; + } + systemMessage += `Page Content:\n${pageContext.content}`; + systemMessage += `\n--- END OF CONTEXT ---\n\nWhen users ask about "this page", "current page", or refer to the content they're reading, use the above context to provide accurate answers. You can summarize, explain, or answer specific questions about the current page content.`; + } + const result = streamText({ - model: openai("gpt-4o-mini"), // Using more cost-effective model - system: - system || - `You are a helpful AI assistant for a documentation website. - You can help users understand the documentation, answer questions about the content, - and provide guidance on the topics covered in the docs. Be concise and helpful.`, + model: openai("gpt-4.1-nano"), // Using more cost-effective model + system: systemMessage, messages: convertToModelMessages(messages), - tools: { - // Wrap frontend tools with frontendTools helper - ...frontendTools(tools), - // Backend tools - get_documentation_info: tool({ - description: - "Get information about the current documentation page or topic", - inputSchema: z.object({ - topic: z - .string() - .describe("The topic or page to get information about"), - }), - execute: async ({ topic }) => { - return `This is a documentation website covering topics like AI, computer science, and development guides. The current topic "${topic}" is part of our comprehensive documentation collection.`; - }, - }), - search_docs: tool({ - description: "Search through the documentation for specific topics", - inputSchema: z.object({ - query: z.string().describe("The search query"), - }), - execute: async ({ query }) => { - return `Searching for "${query}" in the documentation. This site covers AI foundations, computer science fundamentals, and development guides.`; - }, - }), - }, }); return result.toUIMessageStreamResponse(); diff --git a/app/components/DocsAssistant.tsx b/app/components/DocsAssistant.tsx index 2ac3861..d27f04f 100644 --- a/app/components/DocsAssistant.tsx +++ b/app/components/DocsAssistant.tsx @@ -1,14 +1,42 @@ "use client"; import { AssistantRuntimeProvider } from "@assistant-ui/react"; -import { useChatRuntime } from "@assistant-ui/react-ai-sdk"; +import { useAISDKRuntime } from "@assistant-ui/react-ai-sdk"; +import { useChat } from "@ai-sdk/react"; +import { DefaultChatTransport } from "ai"; import { AssistantModal } from "@/app/components/assistant-ui/assistant-modal"; -export function DocsAssistant() { - const runtime = useChatRuntime({ - api: "/api/chat", +interface PageContext { + title?: string; + description?: string; + content?: string; + slug?: string; +} + +interface DocsAssistantProps { + pageContext: PageContext; +} + +export function DocsAssistant({ pageContext }: DocsAssistantProps) { + console.log("DocsAssistant received pageContext (Client):", { + title: pageContext.title, + contentPreview: pageContext.content?.substring(0, 100) + "...", + slug: pageContext.slug, }); + // Use DefaultChatTransport with request-level body configuration + const chat = useChat({ + transport: new DefaultChatTransport({ + api: "/api/chat", + // Use function to ensure dynamic values are captured at request time + body: () => ({ + pageContext: pageContext, + }), + }), + }); + + const runtime = useAISDKRuntime(chat); + return ( diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx index 839b1be..fbc938c 100644 --- a/app/docs/[...slug]/page.tsx +++ b/app/docs/[...slug]/page.tsx @@ -7,6 +7,25 @@ import { GiscusComments } from "@/app/components/GiscusComments"; import { getContributors } from "@/lib/github"; import { Contributors } from "@/app/components/Contributors"; import { DocsAssistant } from "@/app/components/DocsAssistant"; +import fs from "fs/promises"; +import path from "path"; + +// Extract clean text content from MDX +function extractTextFromMDX(content: string): string { + return content + .replace(/^---[\s\S]*?---/m, "") // Remove frontmatter + .replace(/```[\s\S]*?```/g, "") // Remove code blocks + .replace(/`([^`]+)`/g, "$1") // Remove inline code + .replace(/<[^>]+>/g, "") // Remove HTML/MDX tags + .replace(/\*\*([^*]+)\*\*/g, "$1") // Remove bold + .replace(/\*([^*]+)\*/g, "$1") // Remove italic + .replace(/#{1,6}\s+/g, "") // Remove headers + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // Remove links, keep text + .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1") // Remove images, keep alt text + .replace(/[#*`\[\]()!]/g, "") // Remove common markdown symbols + .replace(/\n{2,}/g, "\n") // Normalize line breaks + .trim(); +} interface Param { params: Promise<{ @@ -30,6 +49,31 @@ export default async function DocPage({ params }: Param) { const Mdx = page.data.body; + // Prepare page content for AI assistant + let pageContentForAI = ""; + try { + const fullFilePath = path.join(process.cwd(), "app/docs", page.file.path); + const rawContent = await fs.readFile(fullFilePath, "utf-8"); + const extractedText = extractTextFromMDX(rawContent); + // Truncate content to avoid token limits (4000 chars ≈ 1000 tokens) + pageContentForAI = extractedText.substring(0, 4000); + console.log( + "Page Content for AI (Server):", + pageContentForAI.substring(0, 200) + "...", + ); // Log first 200 chars + } catch (error) { + console.warn("Failed to read file content for AI assistant:", error); + // Fallback to using page metadata + pageContentForAI = `${page.data.title}\n${page.data.description || ""}`; + } + + console.log("Passing pageContext to DocsAssistant (Server):", { + title: page.data.title, + description: page.data.description, + contentPreview: pageContentForAI.substring(0, 100) + "...", // Log first 100 chars of content + slug: slug?.join("/"), + }); + return ( <> @@ -44,7 +88,14 @@ export default async function DocPage({ params }: Param) { - + ); } diff --git a/package.json b/package.json index d5c9898..63dbb31 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@ai-sdk/openai": "^2.0.32", + "@ai-sdk/react": "^2.0.45", "@assistant-ui/react": "^0.11.10", "@assistant-ui/react-ai-sdk": "^1.1.0", "@assistant-ui/react-markdown": "^0.11.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14b1707..0261cba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,9 @@ importers: "@ai-sdk/openai": specifier: ^2.0.32 version: 2.0.32(zod@4.1.9) + "@ai-sdk/react": + specifier: ^2.0.45 + version: 2.0.45(react@19.1.1)(zod@4.1.9) "@assistant-ui/react": specifier: ^0.11.10 version: 0.11.10(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) From cbb258bf2a2b83ecaf6cafe8a1437fee39ea9d83 Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 21:08:31 +1000 Subject: [PATCH 06/17] refactor: remove console logs from chat API and DocsAssistant components --- app/api/chat/route.ts | 6 ------ app/components/DocsAssistant.tsx | 6 ------ app/docs/[...slug]/page.tsx | 15 ++------------- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 092cb8b..c017500 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -21,12 +21,6 @@ export async function POST(req: Request) { }; } = await req.json(); - console.log("Chat API received request. pageContext (API):", { - title: pageContext?.title, - contentPreview: pageContext?.content?.substring(0, 100) + "...", - slug: pageContext?.slug, - }); - try { // Build system message with page context let systemMessage = diff --git a/app/components/DocsAssistant.tsx b/app/components/DocsAssistant.tsx index d27f04f..6eab900 100644 --- a/app/components/DocsAssistant.tsx +++ b/app/components/DocsAssistant.tsx @@ -18,12 +18,6 @@ interface DocsAssistantProps { } export function DocsAssistant({ pageContext }: DocsAssistantProps) { - console.log("DocsAssistant received pageContext (Client):", { - title: pageContext.title, - contentPreview: pageContext.content?.substring(0, 100) + "...", - slug: pageContext.slug, - }); - // Use DefaultChatTransport with request-level body configuration const chat = useChat({ transport: new DefaultChatTransport({ diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx index fbc938c..53721e6 100644 --- a/app/docs/[...slug]/page.tsx +++ b/app/docs/[...slug]/page.tsx @@ -55,25 +55,14 @@ export default async function DocPage({ params }: Param) { const fullFilePath = path.join(process.cwd(), "app/docs", page.file.path); const rawContent = await fs.readFile(fullFilePath, "utf-8"); const extractedText = extractTextFromMDX(rawContent); - // Truncate content to avoid token limits (4000 chars ≈ 1000 tokens) - pageContentForAI = extractedText.substring(0, 4000); - console.log( - "Page Content for AI (Server):", - pageContentForAI.substring(0, 200) + "...", - ); // Log first 200 chars + // Use full extracted content without truncation + pageContentForAI = extractedText; } catch (error) { console.warn("Failed to read file content for AI assistant:", error); // Fallback to using page metadata pageContentForAI = `${page.data.title}\n${page.data.description || ""}`; } - console.log("Passing pageContext to DocsAssistant (Server):", { - title: page.data.title, - description: page.data.description, - contentPreview: pageContentForAI.substring(0, 100) + "...", // Log first 100 chars of content - slug: slug?.join("/"), - }); - return ( <> From 11dc1800e9b1f18614abd1f326d4b77bc33e27a9 Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 21:28:19 +1000 Subject: [PATCH 07/17] feat: integrate Google AI SDK and enhance chat API with provider support --- app/api/chat/route.ts | 35 ++++++- app/components/DocsAssistant.tsx | 14 ++- .../assistant-ui/SettingsButton.tsx | 14 +++ .../assistant-ui/SettingsDialog.tsx | 94 +++++++++++++++++++ app/components/assistant-ui/thread.tsx | 73 ++++++++------ app/components/ui/label.tsx | 24 +++++ app/components/ui/radio-group.tsx | 45 +++++++++ app/hooks/useAssistantSettings.ts | 65 +++++++++++++ package.json | 3 + pnpm-lock.yaml | 83 ++++++++++++++++ 10 files changed, 414 insertions(+), 36 deletions(-) create mode 100644 app/components/assistant-ui/SettingsButton.tsx create mode 100644 app/components/assistant-ui/SettingsDialog.tsx create mode 100644 app/components/ui/label.tsx create mode 100644 app/components/ui/radio-group.tsx create mode 100644 app/hooks/useAssistantSettings.ts diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index c017500..35f1992 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,4 +1,5 @@ -import { openai } from "@ai-sdk/openai"; +import { openai, createOpenAI } from "@ai-sdk/openai"; +import { google, createGoogleGenerativeAI } from "@ai-sdk/google"; import { streamText, UIMessage, convertToModelMessages } from "ai"; // Allow streaming responses up to 30 seconds @@ -9,6 +10,8 @@ export async function POST(req: Request) { messages, system, pageContext, + provider, + apiKey, }: { messages: UIMessage[]; system?: string; // System message forwarded from AssistantChatTransport @@ -19,8 +22,21 @@ export async function POST(req: Request) { content?: string; slug?: string; }; + provider?: "openai" | "gemini"; + apiKey?: string; } = await req.json(); + // Check if API key is provided + if (!apiKey || apiKey.trim() === "") { + return Response.json( + { + error: + "API key is required. Please configure your API key in the settings.", + }, + { status: 400 }, + ); + } + try { // Build system message with page context let systemMessage = @@ -45,8 +61,23 @@ export async function POST(req: Request) { systemMessage += `\n--- END OF CONTEXT ---\n\nWhen users ask about "this page", "current page", or refer to the content they're reading, use the above context to provide accurate answers. You can summarize, explain, or answer specific questions about the current page content.`; } + // Select model based on provider + let model; + if (provider === "gemini") { + const customGoogle = createGoogleGenerativeAI({ + apiKey: apiKey, + }); + model = customGoogle("models/gemini-1.5-pro-latest"); + } else { + // Default to OpenAI + const customOpenAI = createOpenAI({ + apiKey: apiKey, + }); + model = customOpenAI("gpt-4o-mini"); + } + const result = streamText({ - model: openai("gpt-4.1-nano"), // Using more cost-effective model + model: model, system: systemMessage, messages: convertToModelMessages(messages), }); diff --git a/app/components/DocsAssistant.tsx b/app/components/DocsAssistant.tsx index 6eab900..873ed24 100644 --- a/app/components/DocsAssistant.tsx +++ b/app/components/DocsAssistant.tsx @@ -5,6 +5,7 @@ import { useAISDKRuntime } from "@assistant-ui/react-ai-sdk"; import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; import { AssistantModal } from "@/app/components/assistant-ui/assistant-modal"; +import { useAssistantSettings } from "@/app/hooks/useAssistantSettings"; interface PageContext { title?: string; @@ -18,14 +19,21 @@ interface DocsAssistantProps { } export function DocsAssistant({ pageContext }: DocsAssistantProps) { + const { provider, openaiApiKey, geminiApiKey } = useAssistantSettings(); + // Use DefaultChatTransport with request-level body configuration const chat = useChat({ transport: new DefaultChatTransport({ api: "/api/chat", // Use function to ensure dynamic values are captured at request time - body: () => ({ - pageContext: pageContext, - }), + body: () => { + const apiKey = provider === "openai" ? openaiApiKey : geminiApiKey; + return { + pageContext: pageContext, + provider: provider, + apiKey: apiKey, + }; + }, }), }); diff --git a/app/components/assistant-ui/SettingsButton.tsx b/app/components/assistant-ui/SettingsButton.tsx new file mode 100644 index 0000000..6abc588 --- /dev/null +++ b/app/components/assistant-ui/SettingsButton.tsx @@ -0,0 +1,14 @@ +import { SettingsIcon } from "lucide-react"; +import { TooltipIconButton } from "./tooltip-icon-button"; + +interface SettingsButtonProps { + onClick: () => void; +} + +export const SettingsButton = ({ onClick }: SettingsButtonProps) => { + return ( + + + + ); +}; diff --git a/app/components/assistant-ui/SettingsDialog.tsx b/app/components/assistant-ui/SettingsDialog.tsx new file mode 100644 index 0000000..ae016e1 --- /dev/null +++ b/app/components/assistant-ui/SettingsDialog.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { useAssistantSettings } from "@/app/hooks/useAssistantSettings"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Button } from "@/components/ui/button"; + +interface SettingsDialogProps { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +} + +export const SettingsDialog = ({ + isOpen, + onOpenChange, +}: SettingsDialogProps) => { + const { + provider, + setProvider, + openaiApiKey, + setOpenaiApiKey, + geminiApiKey, + setGeminiApiKey, + } = useAssistantSettings(); + + return ( + + + + AI Assistant Settings + + +
      +
      + + + setProvider(value as "openai" | "gemini") + } + > +
      + + +
      +
      + + +
      +
      +
      + + {provider === "openai" && ( +
      + + setOpenaiApiKey(e.target.value)} + /> +
      + )} + + {provider === "gemini" && ( +
      + + setGeminiApiKey(e.target.value)} + /> +
      + )} +
      + + + + +
      +
      + ); +}; diff --git a/app/components/assistant-ui/thread.tsx b/app/components/assistant-ui/thread.tsx index e1175c9..267bdbf 100644 --- a/app/components/assistant-ui/thread.tsx +++ b/app/components/assistant-ui/thread.tsx @@ -18,12 +18,15 @@ import { Square, } from "lucide-react"; import type { FC } from "react"; +import { useState } from "react"; import { ComposerAddAttachment, ComposerAttachments, UserMessageAttachments, } from "@/app/components/assistant-ui/attachment"; +import { SettingsButton } from "@/app/components/assistant-ui/SettingsButton"; +import { SettingsDialog } from "@/app/components/assistant-ui/SettingsDialog"; import { MarkdownText } from "@/app/components/assistant-ui/markdown-text"; import { ToolFallback } from "@/app/components/assistant-ui/tool-fallback"; import { TooltipIconButton } from "@/app/components/assistant-ui/tooltip-icon-button"; @@ -188,40 +191,48 @@ const Composer: FC = () => { }; const ComposerAction: FC = () => { + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + return ( -
      - + <> +
      + setIsSettingsOpen(true)} /> - - - - - - - + + + + + + + - - - - - -
      + + + + + +
      + + ); }; diff --git a/app/components/ui/label.tsx b/app/components/ui/label.tsx new file mode 100644 index 0000000..79d77b4 --- /dev/null +++ b/app/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; + +import { cn } from "@/lib/utils"; + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Label }; diff --git a/app/components/ui/radio-group.tsx b/app/components/ui/radio-group.tsx new file mode 100644 index 0000000..bc5495a --- /dev/null +++ b/app/components/ui/radio-group.tsx @@ -0,0 +1,45 @@ +"use client"; + +import * as React from "react"; +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; +import { CircleIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function RadioGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ); +} + +export { RadioGroup, RadioGroupItem }; diff --git a/app/hooks/useAssistantSettings.ts b/app/hooks/useAssistantSettings.ts new file mode 100644 index 0000000..e5e52e4 --- /dev/null +++ b/app/hooks/useAssistantSettings.ts @@ -0,0 +1,65 @@ +"use client"; + +import { useState, useEffect } from "react"; + +type Provider = "openai" | "gemini"; + +interface AssistantSettings { + provider: Provider; + openaiApiKey: string; + geminiApiKey: string; +} + +const SETTINGS_KEY = "assistant-settings-storage"; + +export const useAssistantSettings = () => { + const [settings, setSettings] = useState({ + provider: "openai", + openaiApiKey: "", + geminiApiKey: "", + }); + + // Load initial settings from localStorage on mount + useEffect(() => { + try { + const storedSettings = localStorage.getItem(SETTINGS_KEY); + if (storedSettings) { + setSettings(JSON.parse(storedSettings)); + } + } catch (error) { + console.error( + "Failed to parse assistant settings from localStorage", + error, + ); + } + }, []); + + // Sync settings to localStorage whenever they change + useEffect(() => { + try { + localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); + } catch (error) { + console.error("Failed to save assistant settings to localStorage", error); + } + }, [settings]); + + // Provide convenient methods for updating state + const setProvider = (provider: Provider) => { + setSettings((prev) => ({ ...prev, provider })); + }; + + const setOpenaiApiKey = (key: string) => { + setSettings((prev) => ({ ...prev, openaiApiKey: key })); + }; + + const setGeminiApiKey = (key: string) => { + setSettings((prev) => ({ ...prev, geminiApiKey: key })); + }; + + return { + ...settings, + setProvider, + setOpenaiApiKey, + setGeminiApiKey, + }; +}; diff --git a/package.json b/package.json index 63dbb31..8d3f082 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@ai-sdk/google": "^2.0.14", "@ai-sdk/openai": "^2.0.32", "@ai-sdk/react": "^2.0.45", "@assistant-ui/react": "^0.11.10", @@ -24,6 +25,8 @@ "@orama/tokenizers": "^3.1.13", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@types/mdx": "^2.0.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0261cba..9fe9b16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,9 @@ settings: importers: .: dependencies: + "@ai-sdk/google": + specifier: ^2.0.14 + version: 2.0.14(zod@4.1.9) "@ai-sdk/openai": specifier: ^2.0.32 version: 2.0.32(zod@4.1.9) @@ -37,6 +40,12 @@ importers: "@radix-ui/react-dialog": specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-label": + specifier: ^2.1.7 + version: 2.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-radio-group": + specifier: ^1.3.8 + version: 1.3.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) "@radix-ui/react-slot": specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.12)(react@19.1.1) @@ -163,6 +172,15 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + "@ai-sdk/google@2.0.14": + resolution: + { + integrity: sha512-OCBBkEUq1RNLkbJuD+ejqGsWDD0M5nRyuFWDchwylxy0J4HSsAiGNhutNYVTdnqmNw+r9LyZlkyZ1P4YfAfLdg==, + } + engines: { node: ">=18" } + peerDependencies: + zod: ^3.25.76 || ^4 + "@ai-sdk/openai@2.0.32": resolution: { @@ -1338,6 +1356,22 @@ packages: "@types/react": optional: true + "@radix-ui/react-label@2.1.7": + resolution: + { + integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==, + } + 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 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-navigation-menu@1.2.14": resolution: { @@ -1434,6 +1468,22 @@ packages: "@types/react-dom": optional: true + "@radix-ui/react-radio-group@1.3.8": + resolution: + { + integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==, + } + 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 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + "@radix-ui/react-roving-focus@1.1.11": resolution: { @@ -6425,6 +6475,12 @@ snapshots: "@ai-sdk/provider-utils": 3.0.9(zod@4.1.9) zod: 4.1.9 + "@ai-sdk/google@2.0.14(zod@4.1.9)": + dependencies: + "@ai-sdk/provider": 2.0.0 + "@ai-sdk/provider-utils": 3.0.9(zod@4.1.9) + zod: 4.1.9 + "@ai-sdk/openai@2.0.32(zod@4.1.9)": dependencies: "@ai-sdk/provider": 2.0.0 @@ -7131,6 +7187,15 @@ snapshots: optionalDependencies: "@types/react": 19.1.12 + "@radix-ui/react-label@2.1.7(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)": + dependencies: + "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + "@types/react": 19.1.12 + "@types/react-dom": 19.1.9(@types/react@19.1.12) + "@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)": dependencies: "@radix-ui/primitive": 1.1.3 @@ -7223,6 +7288,24 @@ snapshots: "@types/react": 19.1.12 "@types/react-dom": 19.1.9(@types/react@19.1.12) + "@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)": + dependencies: + "@radix-ui/primitive": 1.1.3 + "@radix-ui/react-compose-refs": 1.1.2(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-context": 1.1.2(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-direction": 1.1.1(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-presence": 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-primitive": 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-roving-focus": 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + "@radix-ui/react-use-controllable-state": 1.2.2(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-use-previous": 1.1.1(@types/react@19.1.12)(react@19.1.1) + "@radix-ui/react-use-size": 1.1.1(@types/react@19.1.12)(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + "@types/react": 19.1.12 + "@types/react-dom": 19.1.9(@types/react@19.1.12) + "@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)": dependencies: "@radix-ui/primitive": 1.1.3 From 77d5085ef19bec195e3eb280d31e33d9ecc08f1d Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 21:36:50 +1000 Subject: [PATCH 08/17] fix: update import path for RadioGroup in SettingsDialog component --- .../assistant-ui/SettingsDialog.tsx | 2 +- components/ui/label.tsx | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 components/ui/label.tsx diff --git a/app/components/assistant-ui/SettingsDialog.tsx b/app/components/assistant-ui/SettingsDialog.tsx index ae016e1..50d98df 100644 --- a/app/components/assistant-ui/SettingsDialog.tsx +++ b/app/components/assistant-ui/SettingsDialog.tsx @@ -10,7 +10,7 @@ import { } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { RadioGroup, RadioGroupItem } from "@/app/components/ui/radio-group"; import { Button } from "@/components/ui/button"; interface SettingsDialogProps { diff --git a/components/ui/label.tsx b/components/ui/label.tsx new file mode 100644 index 0000000..79d77b4 --- /dev/null +++ b/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; + +import { cn } from "@/lib/utils"; + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Label }; From 23d3c86ae44c5d2a778a9bfed2c2ed77b660d13b Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 22:02:40 +1000 Subject: [PATCH 09/17] refactor: streamline chat API imports and enhance DocsAssistant with settings provider --- app/api/chat/route.ts | 10 +-- app/components/DocsAssistant.tsx | 15 +++- app/hooks/useAssistantSettings.ts | 65 -------------- app/hooks/useAssistantSettings.tsx | 135 +++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 73 deletions(-) delete mode 100644 app/hooks/useAssistantSettings.ts create mode 100644 app/hooks/useAssistantSettings.tsx diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 35f1992..820c88f 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,5 +1,5 @@ -import { openai, createOpenAI } from "@ai-sdk/openai"; -import { google, createGoogleGenerativeAI } from "@ai-sdk/google"; +import { createOpenAI } from "@ai-sdk/openai"; +import { createGoogleGenerativeAI } from "@ai-sdk/google"; import { streamText, UIMessage, convertToModelMessages } from "ai"; // Allow streaming responses up to 30 seconds @@ -15,7 +15,7 @@ export async function POST(req: Request) { }: { messages: UIMessage[]; system?: string; // System message forwarded from AssistantChatTransport - tools?: any; // Frontend tools forwarded from AssistantChatTransport + tools?: unknown; // Frontend tools forwarded from AssistantChatTransport pageContext?: { title?: string; description?: string; @@ -67,13 +67,13 @@ export async function POST(req: Request) { const customGoogle = createGoogleGenerativeAI({ apiKey: apiKey, }); - model = customGoogle("models/gemini-1.5-pro-latest"); + model = customGoogle("models/gemini-2.0-flash"); } else { // Default to OpenAI const customOpenAI = createOpenAI({ apiKey: apiKey, }); - model = customOpenAI("gpt-4o-mini"); + model = customOpenAI("gpt-4.1-nano"); } const result = streamText({ diff --git a/app/components/DocsAssistant.tsx b/app/components/DocsAssistant.tsx index 873ed24..0b2a244 100644 --- a/app/components/DocsAssistant.tsx +++ b/app/components/DocsAssistant.tsx @@ -5,7 +5,10 @@ import { useAISDKRuntime } from "@assistant-ui/react-ai-sdk"; import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; import { AssistantModal } from "@/app/components/assistant-ui/assistant-modal"; -import { useAssistantSettings } from "@/app/hooks/useAssistantSettings"; +import { + AssistantSettingsProvider, + useAssistantSettings, +} from "@/app/hooks/useAssistantSettings"; interface PageContext { title?: string; @@ -19,13 +22,19 @@ interface DocsAssistantProps { } export function DocsAssistant({ pageContext }: DocsAssistantProps) { + return ( + + + + ); +} + +function DocsAssistantInner({ pageContext }: DocsAssistantProps) { const { provider, openaiApiKey, geminiApiKey } = useAssistantSettings(); - // Use DefaultChatTransport with request-level body configuration const chat = useChat({ transport: new DefaultChatTransport({ api: "/api/chat", - // Use function to ensure dynamic values are captured at request time body: () => { const apiKey = provider === "openai" ? openaiApiKey : geminiApiKey; return { diff --git a/app/hooks/useAssistantSettings.ts b/app/hooks/useAssistantSettings.ts deleted file mode 100644 index e5e52e4..0000000 --- a/app/hooks/useAssistantSettings.ts +++ /dev/null @@ -1,65 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; - -type Provider = "openai" | "gemini"; - -interface AssistantSettings { - provider: Provider; - openaiApiKey: string; - geminiApiKey: string; -} - -const SETTINGS_KEY = "assistant-settings-storage"; - -export const useAssistantSettings = () => { - const [settings, setSettings] = useState({ - provider: "openai", - openaiApiKey: "", - geminiApiKey: "", - }); - - // Load initial settings from localStorage on mount - useEffect(() => { - try { - const storedSettings = localStorage.getItem(SETTINGS_KEY); - if (storedSettings) { - setSettings(JSON.parse(storedSettings)); - } - } catch (error) { - console.error( - "Failed to parse assistant settings from localStorage", - error, - ); - } - }, []); - - // Sync settings to localStorage whenever they change - useEffect(() => { - try { - localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); - } catch (error) { - console.error("Failed to save assistant settings to localStorage", error); - } - }, [settings]); - - // Provide convenient methods for updating state - const setProvider = (provider: Provider) => { - setSettings((prev) => ({ ...prev, provider })); - }; - - const setOpenaiApiKey = (key: string) => { - setSettings((prev) => ({ ...prev, openaiApiKey: key })); - }; - - const setGeminiApiKey = (key: string) => { - setSettings((prev) => ({ ...prev, geminiApiKey: key })); - }; - - return { - ...settings, - setProvider, - setOpenaiApiKey, - setGeminiApiKey, - }; -}; diff --git a/app/hooks/useAssistantSettings.tsx b/app/hooks/useAssistantSettings.tsx new file mode 100644 index 0000000..33779c7 --- /dev/null +++ b/app/hooks/useAssistantSettings.tsx @@ -0,0 +1,135 @@ +"use client"; + +import { createContext, useContext, useEffect, useMemo, useState } from "react"; +import type { ReactNode } from "react"; + +type Provider = "openai" | "gemini"; + +interface AssistantSettingsState { + provider: Provider; + openaiApiKey: string; + geminiApiKey: string; +} + +interface AssistantSettingsContextValue extends AssistantSettingsState { + setProvider: (provider: Provider) => void; + setOpenaiApiKey: (key: string) => void; + setGeminiApiKey: (key: string) => void; +} + +const SETTINGS_KEY = "assistant-settings-storage"; + +const defaultSettings: AssistantSettingsState = { + provider: "openai", + openaiApiKey: "", + geminiApiKey: "", +}; + +const AssistantSettingsContext = createContext< + AssistantSettingsContextValue | undefined +>(undefined); + +const parseStoredSettings = (raw: string | null): AssistantSettingsState => { + if (!raw) { + return { ...defaultSettings }; + } + + try { + const parsed = JSON.parse(raw) as Partial; + return { + provider: parsed.provider === "gemini" ? "gemini" : "openai", + openaiApiKey: + typeof parsed.openaiApiKey === "string" ? parsed.openaiApiKey : "", + geminiApiKey: + typeof parsed.geminiApiKey === "string" ? parsed.geminiApiKey : "", + }; + } catch (error) { + console.error( + "Failed to parse assistant settings from localStorage", + error, + ); + return { ...defaultSettings }; + } +}; + +const readStoredSettings = (): AssistantSettingsState => { + if (typeof window === "undefined") { + return { ...defaultSettings }; + } + + const raw = window.localStorage.getItem(SETTINGS_KEY); + return parseStoredSettings(raw); +}; + +export const AssistantSettingsProvider = ({ + children, +}: { + children: ReactNode; +}) => { + const [settings, setSettings] = useState(() => + readStoredSettings(), + ); + + useEffect(() => { + if (typeof window === "undefined") { + return; + } + + try { + window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); + } catch (error) { + console.error("Failed to save assistant settings to localStorage", error); + } + }, [settings]); + + useEffect(() => { + if (typeof window === "undefined") { + return; + } + + const handleStorage = (event: StorageEvent) => { + if (event.key !== SETTINGS_KEY) { + return; + } + + setSettings(parseStoredSettings(event.newValue)); + }; + + window.addEventListener("storage", handleStorage); + return () => window.removeEventListener("storage", handleStorage); + }, []); + + const value = useMemo( + (): AssistantSettingsContextValue => ({ + ...settings, + setProvider: (provider: Provider) => { + setSettings((prev) => ({ ...prev, provider })); + }, + setOpenaiApiKey: (key: string) => { + setSettings((prev) => ({ ...prev, openaiApiKey: key })); + }, + setGeminiApiKey: (key: string) => { + setSettings((prev) => ({ ...prev, geminiApiKey: key })); + }, + }), + [settings], + ); + + return ( + + {children} + + ); +}; + +export const useAssistantSettings = () => { + const context = useContext(AssistantSettingsContext); + + if (!context) { + throw new Error( + "useAssistantSettings must be used within an AssistantSettingsProvider", + ); + } + + return context; +}; From a6950907825782f6c8a2c88d4d9aa1dd2f57e2cf Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 22:05:04 +1000 Subject: [PATCH 10/17] refactor: remove unused ComposerAddAttachment import from thread component --- app/components/assistant-ui/thread.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/assistant-ui/thread.tsx b/app/components/assistant-ui/thread.tsx index 267bdbf..924752c 100644 --- a/app/components/assistant-ui/thread.tsx +++ b/app/components/assistant-ui/thread.tsx @@ -21,7 +21,6 @@ import type { FC } from "react"; import { useState } from "react"; import { - ComposerAddAttachment, ComposerAttachments, UserMessageAttachments, } from "@/app/components/assistant-ui/attachment"; From ababd4d04182fcdacd1aa5c707a863fdc3386319 Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 22:18:55 +1000 Subject: [PATCH 11/17] refactor: enhance Composer component with API key validation and settings integration --- app/components/assistant-ui/thread.tsx | 81 +++++++++++++++++++++----- 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/app/components/assistant-ui/thread.tsx b/app/components/assistant-ui/thread.tsx index 924752c..27379b8 100644 --- a/app/components/assistant-ui/thread.tsx +++ b/app/components/assistant-ui/thread.tsx @@ -13,6 +13,7 @@ import { ChevronLeftIcon, ChevronRightIcon, CopyIcon, + LockIcon, PencilIcon, RefreshCwIcon, Square, @@ -29,6 +30,7 @@ import { SettingsDialog } from "@/app/components/assistant-ui/SettingsDialog"; import { MarkdownText } from "@/app/components/assistant-ui/markdown-text"; import { ToolFallback } from "@/app/components/assistant-ui/tool-fallback"; import { TooltipIconButton } from "@/app/components/assistant-ui/tooltip-icon-button"; +import { useAssistantSettings } from "@/app/hooks/useAssistantSettings"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { LazyMotion, MotionConfig, domAnimation } from "motion/react"; @@ -168,13 +170,37 @@ const ThreadWelcomeSuggestions: FC = () => { }; const Composer: FC = () => { + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const { provider, openaiApiKey, geminiApiKey } = useAssistantSettings(); + const activeKey = provider === "openai" ? openaiApiKey : geminiApiKey; + const hasActiveKey = activeKey.trim().length > 0; + const providerLabel = provider === "gemini" ? "Google Gemini" : "OpenAI"; + return (
      - + + {!hasActiveKey && ( +
      +

      + Add your {providerLabel} API key in Settings to start chatting. +

      + +
      + )} { rows={1} autoFocus aria-label="Message input" + disabled={!hasActiveKey} + /> + -
      ); }; -const ComposerAction: FC = () => { - const [isSettingsOpen, setIsSettingsOpen] = useState(false); +interface ComposerActionProps { + canSend: boolean; + isSettingsOpen: boolean; + onOpenChange: (open: boolean) => void; +} +const ComposerAction: FC = ({ + canSend, + isSettingsOpen, + onOpenChange, +}) => { return ( <>
      - setIsSettingsOpen(true)} /> + onOpenChange(true)} /> - + {canSend ? ( + + + + + + ) : ( onOpenChange(true)} > - + - + )} @@ -227,10 +281,7 @@ const ComposerAction: FC = () => {
      - + ); }; From 966141b243a88795b9cf08681feaa3a1b39646db Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 23:01:08 +1000 Subject: [PATCH 12/17] feat: enhance DocsAssistant and AssistantModal with error handling and settings integration --- app/components/DocsAssistant.tsx | 165 +++++++++++++++++- .../assistant-ui/assistant-modal.tsx | 18 +- app/components/assistant-ui/thread.tsx | 130 ++++++++++++-- 3 files changed, 294 insertions(+), 19 deletions(-) diff --git a/app/components/DocsAssistant.tsx b/app/components/DocsAssistant.tsx index 0b2a244..34cddf3 100644 --- a/app/components/DocsAssistant.tsx +++ b/app/components/DocsAssistant.tsx @@ -1,5 +1,7 @@ "use client"; +import { useCallback, useEffect, useState } from "react"; + import { AssistantRuntimeProvider } from "@assistant-ui/react"; import { useAISDKRuntime } from "@assistant-ui/react-ai-sdk"; import { useChat } from "@ai-sdk/react"; @@ -38,19 +40,174 @@ function DocsAssistantInner({ pageContext }: DocsAssistantProps) { body: () => { const apiKey = provider === "openai" ? openaiApiKey : geminiApiKey; return { - pageContext: pageContext, - provider: provider, - apiKey: apiKey, + pageContext, + provider, + apiKey, }; }, }), }); + const { + error: chatError, + status: chatStatus, + clearError: clearChatError, + } = chat; + const [assistantError, setAssistantError] = + useState(null); + + useEffect(() => { + if (!chatError) { + return; + } + + setAssistantError(deriveAssistantError(chatError, provider)); + clearChatError(); + }, [chatError, clearChatError, provider]); + + useEffect(() => { + if (chatStatus === "submitted" || chatStatus === "streaming") { + setAssistantError(null); + } + }, [chatStatus]); + + const handleClearError = useCallback(() => { + setAssistantError(null); + clearChatError(); + }, [clearChatError]); + const runtime = useAISDKRuntime(chat); return ( - + ); } + +interface AssistantErrorState { + message: string; + showSettingsCTA: boolean; +} + +function deriveAssistantError( + err: unknown, + provider: "openai" | "gemini", +): AssistantErrorState { + const providerLabel = provider === "gemini" ? "Google Gemini" : "OpenAI"; + const fallback: AssistantErrorState = { + message: + "The assistant couldn't complete that request. Please try again later.", + showSettingsCTA: false, + }; + + if (!err) { + return fallback; + } + + const maybeError = err as Partial<{ + message?: string; + statusCode?: number; + responseBody?: string; + data?: unknown; + }>; + + let message = ""; + + if ( + typeof maybeError.message === "string" && + maybeError.message.trim().length > 0 + ) { + message = maybeError.message.trim(); + } + + if ( + typeof maybeError.responseBody === "string" && + maybeError.responseBody.trim().length > 0 + ) { + const extracted = extractErrorFromResponseBody(maybeError.responseBody); + if (extracted) { + message = extracted; + } + } + + if (!message && err instanceof Error && typeof err.message === "string") { + message = err.message.trim(); + } + + if (!message && maybeError.data && typeof maybeError.data === "object") { + const dataError = (maybeError.data as { error?: unknown }).error; + if (typeof dataError === "string" && dataError.trim().length > 0) { + message = dataError.trim(); + } + } + + const statusCode = + typeof maybeError.statusCode === "number" + ? maybeError.statusCode + : undefined; + const normalized = message.toLowerCase(); + + let showSettingsCTA = false; + + if ( + statusCode === 400 || + statusCode === 401 || + statusCode === 403 || + normalized.includes("api key") || + normalized.includes("apikey") || + normalized.includes("missing key") || + normalized.includes("unauthorized") + ) { + showSettingsCTA = true; + } + + let friendlyMessage = message || fallback.message; + + if (showSettingsCTA) { + friendlyMessage = + message && message.length > 0 + ? message + : `The ${providerLabel} API key looks incorrect. Update it in settings and try again.`; + } else if (statusCode === 429) { + friendlyMessage = + "The provider is rate limiting requests. Please wait and try again."; + } else if (statusCode && statusCode >= 500) { + friendlyMessage = + "The AI provider is currently unavailable. Please try again soon."; + } + + return { + message: friendlyMessage, + showSettingsCTA, + }; +} + +function extractErrorFromResponseBody(body: string): string | undefined { + const trimmed = body.trim(); + if (!trimmed) { + return undefined; + } + + try { + const parsed = JSON.parse(trimmed); + if (typeof parsed === "string") { + return parsed.trim(); + } + if ( + parsed && + typeof parsed === "object" && + typeof (parsed as { error?: unknown }).error === "string" + ) { + return (parsed as { error: string }).error.trim(); + } + } catch { + // Ignore JSON parsing issues and fall back to the raw body text. + } + + return trimmed; +} diff --git a/app/components/assistant-ui/assistant-modal.tsx b/app/components/assistant-ui/assistant-modal.tsx index 34601d0..ee46937 100644 --- a/app/components/assistant-ui/assistant-modal.tsx +++ b/app/components/assistant-ui/assistant-modal.tsx @@ -8,7 +8,17 @@ import { AssistantModalPrimitive } from "@assistant-ui/react"; import { Thread } from "@/app/components/assistant-ui/thread"; import { TooltipIconButton } from "@/app/components/assistant-ui/tooltip-icon-button"; -export const AssistantModal: FC = () => { +interface AssistantModalProps { + errorMessage?: string; + showSettingsAction?: boolean; + onClearError?: () => void; +} + +export const AssistantModal: FC = ({ + errorMessage, + showSettingsAction = false, + onClearError, +}) => { return ( @@ -20,7 +30,11 @@ export const AssistantModal: FC = () => { sideOffset={16} className="aui-root aui-modal-content z-50 h-[500px] w-[400px] overflow-clip rounded-xl border bg-popover p-0 text-popover-foreground shadow-md outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-bottom-1/2 data-[state=closed]:slide-out-to-right-1/2 data-[state=closed]:zoom-out data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-bottom-1/2 data-[state=open]:slide-in-from-right-1/2 data-[state=open]:zoom-in [&>.aui-thread-root]:bg-inherit" > - + ); diff --git a/app/components/assistant-ui/thread.tsx b/app/components/assistant-ui/thread.tsx index 27379b8..d384854 100644 --- a/app/components/assistant-ui/thread.tsx +++ b/app/components/assistant-ui/thread.tsx @@ -7,6 +7,7 @@ import { ThreadPrimitive, } from "@assistant-ui/react"; import { + AlertCircleIcon, ArrowDownIcon, ArrowUpIcon, CheckIcon, @@ -19,7 +20,7 @@ import { Square, } from "lucide-react"; import type { FC } from "react"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { ComposerAttachments, @@ -36,7 +37,33 @@ import { cn } from "@/lib/utils"; import { LazyMotion, MotionConfig, domAnimation } from "motion/react"; import * as m from "motion/react-m"; -export const Thread: FC = () => { +interface ThreadProps { + errorMessage?: string; + showSettingsAction?: boolean; + onClearError?: () => void; +} + +export const Thread: FC = ({ + errorMessage, + showSettingsAction = false, + onClearError, +}) => { + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + + const handleSettingsChange = useCallback( + (open: boolean) => { + if (open) { + onClearError?.(); + } + setIsSettingsOpen(open); + }, + [onClearError], + ); + + const handleOpenSettings = useCallback(() => { + handleSettingsChange(true); + }, [handleSettingsChange]); + return ( @@ -56,10 +83,23 @@ export const Thread: FC = () => { AssistantMessage, }} /> + {errorMessage ? ( + + ) : null}
      - + @@ -67,6 +107,53 @@ export const Thread: FC = () => { ); }; +interface ThreadErrorNoticeProps { + message: string; + onDismiss?: () => void; + onOpenSettings?: () => void; +} + +const ThreadErrorNotice: FC = ({ + message, + onDismiss, + onOpenSettings, +}) => { + return ( +
      +
      + +
      + + {message} + +
      + {onOpenSettings ? ( + + ) : null} + {onDismiss ? ( + + ) : null} +
      +
      +
      +
      + ); +}; + const ThreadScrollToBottom: FC = () => { return ( @@ -169,13 +256,27 @@ const ThreadWelcomeSuggestions: FC = () => { ); }; -const Composer: FC = () => { - const [isSettingsOpen, setIsSettingsOpen] = useState(false); +interface ComposerProps { + isSettingsOpen: boolean; + onOpenChange: (open: boolean) => void; + onClearError?: () => void; +} + +const Composer: FC = ({ + isSettingsOpen, + onOpenChange, + onClearError, +}) => { const { provider, openaiApiKey, geminiApiKey } = useAssistantSettings(); const activeKey = provider === "openai" ? openaiApiKey : geminiApiKey; const hasActiveKey = activeKey.trim().length > 0; const providerLabel = provider === "gemini" ? "Google Gemini" : "OpenAI"; + const handleOpenSettings = useCallback(() => { + onClearError?.(); + onOpenChange(true); + }, [onClearError, onOpenChange]); + return (
      @@ -192,11 +293,7 @@ const Composer: FC = () => {

      Add your {providerLabel} API key in Settings to start chatting.

      -
      @@ -213,7 +310,9 @@ const Composer: FC = () => {
      @@ -224,17 +323,21 @@ interface ComposerActionProps { canSend: boolean; isSettingsOpen: boolean; onOpenChange: (open: boolean) => void; + onOpenSettings: () => void; + onClearError?: () => void; } const ComposerAction: FC = ({ canSend, isSettingsOpen, onOpenChange, + onOpenSettings, + onClearError, }) => { return ( <>
      - onOpenChange(true)} /> + {canSend ? ( @@ -247,6 +350,7 @@ const ComposerAction: FC = ({ size="icon" className="aui-composer-send size-[34px] rounded-full p-1" aria-label="Send message" + onClick={onClearError} > @@ -260,7 +364,7 @@ const ComposerAction: FC = ({ size="icon" className="aui-composer-send size-[34px] rounded-full p-1" aria-label="Open assistant settings" - onClick={() => onOpenChange(true)} + onClick={onOpenSettings} > From 96e998dda2f8572f2c9736b8b58e7389c1d036bf Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 23:34:52 +1000 Subject: [PATCH 13/17] feat: add refresh functionality to settings and improve DocsAssistant with refs for API keys --- app/components/DocsAssistant.tsx | 30 ++++++++++++++++--- .../assistant-ui/SettingsDialog.tsx | 9 +++++- app/hooks/useAssistantSettings.tsx | 18 +++++++++-- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/app/components/DocsAssistant.tsx b/app/components/DocsAssistant.tsx index 34cddf3..8e25a62 100644 --- a/app/components/DocsAssistant.tsx +++ b/app/components/DocsAssistant.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState, useRef } from "react"; import { AssistantRuntimeProvider } from "@assistant-ui/react"; import { useAISDKRuntime } from "@assistant-ui/react-ai-sdk"; @@ -34,15 +34,37 @@ export function DocsAssistant({ pageContext }: DocsAssistantProps) { function DocsAssistantInner({ pageContext }: DocsAssistantProps) { const { provider, openaiApiKey, geminiApiKey } = useAssistantSettings(); + // Use refs to ensure we always get the latest values + const providerRef = useRef(provider); + const openaiApiKeyRef = useRef(openaiApiKey); + const geminiApiKeyRef = useRef(geminiApiKey); + + // Update refs whenever the values change + providerRef.current = provider; + openaiApiKeyRef.current = openaiApiKey; + geminiApiKeyRef.current = geminiApiKey; + const chat = useChat({ transport: new DefaultChatTransport({ api: "/api/chat", body: () => { - const apiKey = provider === "openai" ? openaiApiKey : geminiApiKey; + // Use refs to get the current values at request time + const currentProvider = providerRef.current; + const currentApiKey = + currentProvider === "openai" + ? openaiApiKeyRef.current + : geminiApiKeyRef.current; + + console.log("[DocsAssistant] useChat body function called with:", { + provider: currentProvider, + apiKeyLength: currentApiKey.length, + hasApiKey: currentApiKey.trim().length > 0, + }); + return { pageContext, - provider, - apiKey, + provider: currentProvider, + apiKey: currentApiKey, }; }, }), diff --git a/app/components/assistant-ui/SettingsDialog.tsx b/app/components/assistant-ui/SettingsDialog.tsx index 50d98df..a8520cf 100644 --- a/app/components/assistant-ui/SettingsDialog.tsx +++ b/app/components/assistant-ui/SettingsDialog.tsx @@ -29,8 +29,15 @@ export const SettingsDialog = ({ setOpenaiApiKey, geminiApiKey, setGeminiApiKey, + refreshFromStorage, } = useAssistantSettings(); + const handleSave = () => { + // Force refresh from localStorage to ensure all components get the latest values + refreshFromStorage(); + onOpenChange(false); + }; + return ( @@ -86,7 +93,7 @@ export const SettingsDialog = ({
      - + diff --git a/app/hooks/useAssistantSettings.tsx b/app/hooks/useAssistantSettings.tsx index 33779c7..878d697 100644 --- a/app/hooks/useAssistantSettings.tsx +++ b/app/hooks/useAssistantSettings.tsx @@ -1,6 +1,13 @@ "use client"; -import { createContext, useContext, useEffect, useMemo, useState } from "react"; +import { + createContext, + useContext, + useEffect, + useMemo, + useState, + useCallback, +} from "react"; import type { ReactNode } from "react"; type Provider = "openai" | "gemini"; @@ -15,6 +22,7 @@ interface AssistantSettingsContextValue extends AssistantSettingsState { setProvider: (provider: Provider) => void; setOpenaiApiKey: (key: string) => void; setGeminiApiKey: (key: string) => void; + refreshFromStorage: () => void; } const SETTINGS_KEY = "assistant-settings-storage"; @@ -99,6 +107,11 @@ export const AssistantSettingsProvider = ({ return () => window.removeEventListener("storage", handleStorage); }, []); + const refreshFromStorage = useCallback(() => { + const latestSettings = readStoredSettings(); + setSettings(latestSettings); + }, []); + const value = useMemo( (): AssistantSettingsContextValue => ({ ...settings, @@ -111,8 +124,9 @@ export const AssistantSettingsProvider = ({ setGeminiApiKey: (key: string) => { setSettings((prev) => ({ ...prev, geminiApiKey: key })); }, + refreshFromStorage, }), - [settings], + [settings, refreshFromStorage], ); return ( From e1702dc8fd9ba1cba29bd05cff17242aad8844c7 Mon Sep 17 00:00:00 2001 From: Crokily Date: Thu, 18 Sep 2025 23:36:42 +1000 Subject: [PATCH 14/17] feat: add default tag to new article creation in Contribute component --- app/components/Contribute.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/Contribute.tsx b/app/components/Contribute.tsx index 02254b4..8d40c64 100644 --- a/app/components/Contribute.tsx +++ b/app/components/Contribute.tsx @@ -34,7 +34,8 @@ function buildGithubNewUrl(dirPath: string, filename: string, title: string) { title: ${title || "New Article"} description: date: ${new Date().toISOString().slice(0, 10)} -tags: [] +tags: + - tag-one --- # ${title || "New Article"} From 9a44b99270cdae1fca71ba3b98238ee455c43f78 Mon Sep 17 00:00:00 2001 From: Crokily Date: Fri, 19 Sep 2025 00:05:36 +1000 Subject: [PATCH 15/17] feat: update welcome suggestions in Thread component with Chinese prompts --- app/components/assistant-ui/thread.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/components/assistant-ui/thread.tsx b/app/components/assistant-ui/thread.tsx index d384854..c71b6cc 100644 --- a/app/components/assistant-ui/thread.tsx +++ b/app/components/assistant-ui/thread.tsx @@ -203,24 +203,24 @@ const ThreadWelcomeSuggestions: FC = () => {
      {[ { - title: "What's the weather", - label: "in San Francisco?", - action: "What's the weather in San Francisco?", + title: "总结本文", + label: "内容要点", + action: "请帮我总结一下当前页面的主要内容和要点", }, { - title: "Explain React hooks", - label: "like useState and useEffect", - action: "Explain React hooks like useState and useEffect", + title: "什么是基座大模型", + label: "概念解释", + action: "什么是基座大模型?请详细解释一下", }, { - title: "Write a SQL query", - label: "to find top customers", - action: "Write a SQL query to find top customers", + title: "解释技术概念", + label: "深入理解", + action: "请解释一下这个页面中提到的核心技术概念", }, { - title: "Create a meal plan", - label: "for healthy weight loss", - action: "Create a meal plan for healthy weight loss", + title: "学习建议", + label: "如何入门", + action: "基于当前内容,你能给出一些学习建议和入门路径吗?", }, ].map((suggestedAction, index) => ( Date: Fri, 19 Sep 2025 09:40:47 +1000 Subject: [PATCH 16/17] fix: update front matter formatting in Contribute component and wrap DocPage return with fragment --- app/components/Contribute.tsx | 6 +++--- app/docs/[...slug]/page.tsx | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/components/Contribute.tsx b/app/components/Contribute.tsx index e73d2d0..c863f76 100644 --- a/app/components/Contribute.tsx +++ b/app/components/Contribute.tsx @@ -27,9 +27,9 @@ type DirNode = { name: string; path: string; children?: DirNode[] }; function buildGithubNewUrl(dirPath: string, filename: string, title: string) { const file = filename.endsWith(".mdx") ? filename : `${filename}.mdx`; const frontMatter = `--- -title: ${title || "New Article"} -description: -date: ${new Date().toISOString().slice(0, 10)} +title: '${title || "New Article"}' +description: "" +date: "${new Date().toISOString().slice(0, 10)}" tags: - tag-one --- diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx index b729964..73ce5ab 100644 --- a/app/docs/[...slug]/page.tsx +++ b/app/docs/[...slug]/page.tsx @@ -65,6 +65,7 @@ export default async function DocPage({ params }: Param) { } return ( + <>
      @@ -88,6 +89,7 @@ export default async function DocPage({ params }: Param) { slug: slug?.join("/"), }} /> + ); } From 1d52fa2af1921faeddb5c2c6a84ffc904fac4b1b Mon Sep 17 00:00:00 2001 From: Crokily Date: Fri, 19 Sep 2025 10:39:37 +1000 Subject: [PATCH 17/17] chore: update README (trigger re-deploy to Vercel testing environment) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c5f97b..c56e790 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ pnpm dev ├── 📂 app/ # Next.js App Router │ ├── 📂 components/ # React 组件 │ ├── 📂 docs/ # 文档内容 -│ │ └── 📂 computer-science/ # 计算机科学知识库 +│ │ └── 📂 ai/ # ai知识库 │ ├── 📄 layout.tsx # 根布局 │ └── 📄 page.tsx # 主页 ├── 📂 source.config.ts # Fumadocs 配置
      + ), + td: ({ className, ...props }) => ( + + ), + tr: ({ className, ...props }) => ( +