mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-07 06:52:28 +00:00
feat: integrate @assistant-ui/react library with new chat components
This commit is contained in:
@@ -68,6 +68,8 @@
|
||||
"name": "hapi-web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@assistant-ui/react": "^0.11.51",
|
||||
"@assistant-ui/react-markdown": "^0.11.8",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -100,6 +102,12 @@
|
||||
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@assistant-ui/react": ["@assistant-ui/react@0.11.51", "", { "dependencies": { "@assistant-ui/tap": "^0.3.4", "@radix-ui/primitive": "^1.1.3", "@radix-ui/react-compose-refs": "^1.1.2", "@radix-ui/react-context": "^1.1.3", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-use-callback-ref": "^1.1.1", "@radix-ui/react-use-escape-keydown": "^1.1.1", "@standard-schema/spec": "^1.0.0", "assistant-cloud": "^0.1.11", "assistant-stream": "^0.2.45", "nanoid": "5.1.6", "react-textarea-autosize": "^8.5.9", "zod": "^4.1.13", "zustand": "^5.0.9" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-8u0tmQBIio+fERgiUGqXTf+cm1MHp6i9V879or7DeyRwRnlwBS0Ew0OCFiR8ZqoJHn9ma0NBH2bXjHNkILnSew=="],
|
||||
|
||||
"@assistant-ui/react-markdown": ["@assistant-ui/react-markdown@0.11.8", "", { "dependencies": { "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-use-callback-ref": "^1.1.1", "@types/hast": "^3.0.4", "classnames": "^2.5.1", "react-markdown": "^10.1.0" }, "peerDependencies": { "@assistant-ui/react": "^0.11.50", "@types/react": "*", "react": "^18 || ^19 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Us7yD9xUGozmmmDuWn+lCp4bylA2sINWyGE6RXr0onmAlwXcOUaw1ZgofKscJXkw6DmC0jrLj7Bdsw8O4IUJhw=="],
|
||||
|
||||
"@assistant-ui/tap": ["@assistant-ui/tap@0.3.4", "", { "peerDependencies": { "react": "*" }, "optionalPeers": ["react"] }, "sha512-62Fz1y4EJA7Hk+aStjNS9VTciZ/TUf9SKdv5RfGF65A1Y/REOBzxBDL4ZCeGHCX2dxmwESg5/Kehky+57og3EA=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||
@@ -228,6 +236,14 @@
|
||||
|
||||
"@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
|
||||
|
||||
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||
|
||||
"@grammyjs/types": ["@grammyjs/types@3.22.2", "", {}, "sha512-uu7DX2ezhnBPozL3bXHmwhLvaFsh59E4QyviNH4Cij7EdVekYrs6mCzeXsa2pDk30l3uXo7DBahlZLzTPtpYZg=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
|
||||
@@ -324,9 +340,11 @@
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||
|
||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
|
||||
|
||||
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
|
||||
|
||||
@@ -338,11 +356,15 @@
|
||||
|
||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
|
||||
|
||||
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
||||
|
||||
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
|
||||
|
||||
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
|
||||
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||
|
||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||
|
||||
@@ -356,6 +378,12 @@
|
||||
|
||||
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
|
||||
|
||||
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"@rollup/plugin-alias": ["@rollup/plugin-alias@6.0.0", "", { "peerDependencies": { "rollup": ">=4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g=="],
|
||||
@@ -420,6 +448,8 @@
|
||||
|
||||
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
|
||||
|
||||
"@tsconfig/node10": ["@tsconfig/node10@1.0.12", "", {}, "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ=="],
|
||||
@@ -534,6 +564,10 @@
|
||||
|
||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||
|
||||
"assistant-cloud": ["assistant-cloud@0.1.11", "", { "dependencies": { "assistant-stream": "^0.2.45" } }, "sha512-1ZS7ccwiuy1NxNzIbHKubdCEcqe914YXVYxAR9N3M4csiLdrtUC1/U7KgLXy5Sig2Jp951L/wnN2fLzl302+sA=="],
|
||||
|
||||
"assistant-stream": ["assistant-stream@0.2.45", "", { "dependencies": { "@types/json-schema": "^7.0.15", "nanoid": "5.1.6", "secure-json-parse": "^4.1.0" } }, "sha512-gfCwkPcGpfCpwWDzKrgzR5TvKP/ppc+MqotFHPCufbYK8nMft1+GyWGhQp6XXzQR2dC7T/IrqmtEGlGSs8uobA=="],
|
||||
|
||||
"ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
|
||||
|
||||
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
||||
@@ -624,6 +658,8 @@
|
||||
|
||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
|
||||
"classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
|
||||
@@ -1668,8 +1704,14 @@
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.0", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ=="],
|
||||
|
||||
"zustand": ["zustand@5.0.9", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@assistant-ui/react/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
||||
|
||||
"@assistant-ui/react/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
|
||||
|
||||
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
@@ -1700,9 +1742,29 @@
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
|
||||
"@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-popover/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||
|
||||
"@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||
|
||||
"@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
@@ -1712,6 +1774,8 @@
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"assistant-stream/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
||||
|
||||
"c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"c12/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
|
||||
@@ -1812,6 +1876,16 @@
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
|
||||
"@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@assistant-ui/react": "^0.11.51",
|
||||
"@assistant-ui/react-markdown": "^0.11.8",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { ComposerPrimitive } from '@assistant-ui/react'
|
||||
|
||||
function SettingsIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function AbortIcon(props: { spinning: boolean }) {
|
||||
if (props.spinning) {
|
||||
return (
|
||||
<svg
|
||||
className="animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
||||
<path d="M12 2a10 10 0 0 1 10 10" strokeOpacity="0.75" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4-2.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-4a.5.5 0 0 1-.5-.5v-4Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function SendIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="12" y1="19" x2="12" y2="5" />
|
||||
<polyline points="5 12 12 5 19 12" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ComposerButtons(props: {
|
||||
canSend: boolean
|
||||
controlsDisabled: boolean
|
||||
showSettingsButton: boolean
|
||||
onSettingsToggle: () => void
|
||||
showAbortButton: boolean
|
||||
abortDisabled: boolean
|
||||
isAborting: boolean
|
||||
onAbort: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-2 pb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{props.showSettingsButton ? (
|
||||
<button
|
||||
type="button"
|
||||
className="settings-button flex h-8 w-8 items-center justify-center rounded-full text-[var(--app-fg)]/60 transition-colors hover:bg-[var(--app-bg)] hover:text-[var(--app-fg)]"
|
||||
onClick={props.onSettingsToggle}
|
||||
disabled={props.controlsDisabled}
|
||||
>
|
||||
<SettingsIcon />
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{props.showAbortButton ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={props.abortDisabled}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full text-[var(--app-fg)]/60 transition-colors hover:bg-[var(--app-bg)] hover:text-red-500 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onClick={props.onAbort}
|
||||
>
|
||||
<AbortIcon spinning={props.isAborting} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<ComposerPrimitive.Send
|
||||
disabled={props.controlsDisabled || !props.canSend}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full transition-colors ${
|
||||
props.canSend && !props.controlsDisabled
|
||||
? 'bg-black text-white'
|
||||
: 'bg-[#C0C0C0] text-white'
|
||||
} disabled:cursor-not-allowed`}
|
||||
>
|
||||
<SendIcon />
|
||||
</ComposerPrimitive.Send>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
import { ComposerPrimitive, useAssistantApi, useAssistantState } from '@assistant-ui/react'
|
||||
import {
|
||||
type ChangeEvent as ReactChangeEvent,
|
||||
type KeyboardEvent as ReactKeyboardEvent,
|
||||
type SyntheticEvent as ReactSyntheticEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import type { AgentState, ModelMode, PermissionMode } from '@/types/api'
|
||||
import type { Suggestion } from '@/hooks/useActiveSuggestions'
|
||||
import { useActiveWord } from '@/hooks/useActiveWord'
|
||||
import { useActiveSuggestions } from '@/hooks/useActiveSuggestions'
|
||||
import { applySuggestion } from '@/utils/applySuggestion'
|
||||
import { getTelegramWebApp } from '@/hooks/useTelegram'
|
||||
import { FloatingOverlay } from '@/components/ChatInput/FloatingOverlay'
|
||||
import { Autocomplete } from '@/components/ChatInput/Autocomplete'
|
||||
import { StatusBar } from '@/components/AssistantChat/StatusBar'
|
||||
import { ComposerButtons } from '@/components/AssistantChat/ComposerButtons'
|
||||
|
||||
export interface TextInputState {
|
||||
text: string
|
||||
selection: { start: number; end: number }
|
||||
}
|
||||
|
||||
const PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'bypassPermissions'] as const
|
||||
const PERMISSION_MODE_LABELS: Record<string, string> = {
|
||||
default: 'Default',
|
||||
acceptEdits: 'Accept Edits',
|
||||
plan: 'Plan Mode',
|
||||
bypassPermissions: 'Bypass All'
|
||||
}
|
||||
|
||||
const MODEL_MODES = ['default', 'sonnet', 'opus'] as const
|
||||
const MODEL_MODE_LABELS: Record<string, string> = {
|
||||
default: 'Default',
|
||||
sonnet: 'Sonnet',
|
||||
opus: 'Opus'
|
||||
}
|
||||
|
||||
const defaultSuggestionHandler = async (): Promise<Suggestion[]> => []
|
||||
|
||||
export function HappyComposer(props: {
|
||||
disabled?: boolean
|
||||
permissionMode?: PermissionMode
|
||||
modelMode?: ModelMode
|
||||
active?: boolean
|
||||
thinking?: boolean
|
||||
agentState?: AgentState | null
|
||||
contextSize?: number
|
||||
onPermissionModeChange?: (mode: PermissionMode) => void
|
||||
onModelModeChange?: (mode: ModelMode) => void
|
||||
autocompletePrefixes?: string[]
|
||||
autocompleteSuggestions?: (query: string) => Promise<Suggestion[]>
|
||||
}) {
|
||||
const {
|
||||
disabled = false,
|
||||
permissionMode = 'default',
|
||||
modelMode = 'default',
|
||||
active = true,
|
||||
thinking = false,
|
||||
agentState,
|
||||
contextSize,
|
||||
onPermissionModeChange,
|
||||
onModelModeChange,
|
||||
autocompletePrefixes = ['@', '/'],
|
||||
autocompleteSuggestions = defaultSuggestionHandler
|
||||
} = props
|
||||
|
||||
const api = useAssistantApi()
|
||||
const composerText = useAssistantState(({ composer }) => composer.text)
|
||||
const threadIsRunning = useAssistantState(({ thread }) => thread.isRunning)
|
||||
const threadIsDisabled = useAssistantState(({ thread }) => thread.isDisabled)
|
||||
|
||||
const controlsDisabled = disabled || !active || threadIsDisabled
|
||||
const trimmed = composerText.trim()
|
||||
const hasText = trimmed.length > 0
|
||||
const canSend = hasText && !controlsDisabled && !threadIsRunning
|
||||
|
||||
const [inputState, setInputState] = useState<TextInputState>({
|
||||
text: '',
|
||||
selection: { start: 0, end: 0 }
|
||||
})
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [isAborting, setIsAborting] = useState(false)
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setInputState((prev) => {
|
||||
if (prev.text === composerText) return prev
|
||||
return { ...prev, text: composerText }
|
||||
})
|
||||
}, [composerText])
|
||||
|
||||
const activeWord = useActiveWord(inputState.text, inputState.selection, autocompletePrefixes)
|
||||
const [suggestions, selectedIndex, moveUp, moveDown, clearSuggestions] = useActiveSuggestions(
|
||||
activeWord,
|
||||
autocompleteSuggestions,
|
||||
{ clampSelection: true, wrapAround: true }
|
||||
)
|
||||
|
||||
const haptic = useCallback((type: 'light' | 'success' | 'error' = 'light') => {
|
||||
const tg = getTelegramWebApp()
|
||||
if (type === 'light') {
|
||||
tg?.HapticFeedback?.impactOccurred('light')
|
||||
} else if (type === 'success') {
|
||||
tg?.HapticFeedback?.notificationOccurred('success')
|
||||
} else {
|
||||
tg?.HapticFeedback?.notificationOccurred('error')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSuggestionSelect = useCallback((index: number) => {
|
||||
const suggestion = suggestions[index]
|
||||
if (!suggestion || !textareaRef.current) return
|
||||
|
||||
const result = applySuggestion(
|
||||
inputState.text,
|
||||
inputState.selection,
|
||||
suggestion.text,
|
||||
autocompletePrefixes,
|
||||
true
|
||||
)
|
||||
|
||||
api.composer().setText(result.text)
|
||||
setInputState({
|
||||
text: result.text,
|
||||
selection: { start: result.cursorPosition, end: result.cursorPosition }
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
const el = textareaRef.current
|
||||
if (!el) return
|
||||
el.setSelectionRange(result.cursorPosition, result.cursorPosition)
|
||||
try {
|
||||
el.focus({ preventScroll: true })
|
||||
} catch {
|
||||
el.focus()
|
||||
}
|
||||
}, 0)
|
||||
|
||||
haptic('light')
|
||||
}, [api, suggestions, inputState, autocompletePrefixes, haptic])
|
||||
|
||||
const abortDisabled = controlsDisabled || isAborting || !threadIsRunning
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAborting) return
|
||||
if (threadIsRunning) return
|
||||
setIsAborting(false)
|
||||
}, [isAborting, threadIsRunning])
|
||||
|
||||
const handleAbort = useCallback(() => {
|
||||
if (abortDisabled) return
|
||||
haptic('error')
|
||||
setIsAborting(true)
|
||||
api.thread().cancelRun()
|
||||
}, [abortDisabled, api, haptic])
|
||||
|
||||
const handleKeyDown = useCallback((e: ReactKeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const key = e.key
|
||||
|
||||
// Avoid intercepting IME composition keystrokes (Enter, arrows, etc.)
|
||||
if (e.nativeEvent.isComposing) {
|
||||
return
|
||||
}
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
if (key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
moveUp()
|
||||
return
|
||||
}
|
||||
if (key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
moveDown()
|
||||
return
|
||||
}
|
||||
if ((key === 'Enter' || key === 'Tab') && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
const indexToSelect = selectedIndex >= 0 ? selectedIndex : 0
|
||||
handleSuggestionSelect(indexToSelect)
|
||||
return
|
||||
}
|
||||
if (key === 'Escape') {
|
||||
e.preventDefault()
|
||||
clearSuggestions()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'Escape' && threadIsRunning) {
|
||||
e.preventDefault()
|
||||
handleAbort()
|
||||
return
|
||||
}
|
||||
|
||||
if (key === 'Tab' && e.shiftKey && onPermissionModeChange) {
|
||||
e.preventDefault()
|
||||
const currentIndex = PERMISSION_MODES.indexOf(permissionMode as typeof PERMISSION_MODES[number])
|
||||
const nextIndex = (currentIndex + 1) % PERMISSION_MODES.length
|
||||
onPermissionModeChange(PERMISSION_MODES[nextIndex])
|
||||
haptic('light')
|
||||
}
|
||||
}, [
|
||||
suggestions,
|
||||
selectedIndex,
|
||||
moveUp,
|
||||
moveDown,
|
||||
clearSuggestions,
|
||||
handleSuggestionSelect,
|
||||
threadIsRunning,
|
||||
handleAbort,
|
||||
onPermissionModeChange,
|
||||
permissionMode,
|
||||
haptic
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const handleGlobalKeyDown = (e: globalThis.KeyboardEvent) => {
|
||||
if (e.key === 'm' && (e.metaKey || e.ctrlKey) && onModelModeChange) {
|
||||
e.preventDefault()
|
||||
const currentIndex = MODEL_MODES.indexOf(modelMode as typeof MODEL_MODES[number])
|
||||
const nextIndex = (currentIndex + 1) % MODEL_MODES.length
|
||||
onModelModeChange(MODEL_MODES[nextIndex])
|
||||
haptic('light')
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleGlobalKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleGlobalKeyDown)
|
||||
}, [modelMode, onModelModeChange, haptic])
|
||||
|
||||
const handleChange = useCallback((e: ReactChangeEvent<HTMLTextAreaElement>) => {
|
||||
const selection = {
|
||||
start: e.target.selectionStart,
|
||||
end: e.target.selectionEnd
|
||||
}
|
||||
setInputState({ text: e.target.value, selection })
|
||||
}, [])
|
||||
|
||||
const handleSelect = useCallback((e: ReactSyntheticEvent<HTMLTextAreaElement>) => {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
setInputState(prev => ({
|
||||
...prev,
|
||||
selection: { start: target.selectionStart, end: target.selectionEnd }
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const handleSettingsToggle = useCallback(() => {
|
||||
haptic('light')
|
||||
setShowSettings(prev => !prev)
|
||||
}, [haptic])
|
||||
|
||||
const handlePermissionChange = useCallback((mode: PermissionMode) => {
|
||||
if (!onPermissionModeChange || controlsDisabled) return
|
||||
onPermissionModeChange(mode)
|
||||
setShowSettings(false)
|
||||
haptic('light')
|
||||
}, [onPermissionModeChange, controlsDisabled, haptic])
|
||||
|
||||
const handleModelChange = useCallback((mode: ModelMode) => {
|
||||
if (!onModelModeChange || controlsDisabled) return
|
||||
onModelModeChange(mode)
|
||||
setShowSettings(false)
|
||||
haptic('light')
|
||||
}, [onModelModeChange, controlsDisabled, haptic])
|
||||
|
||||
const showSettingsButton = Boolean(onPermissionModeChange || onModelModeChange)
|
||||
const showAbortButton = true
|
||||
|
||||
const overlays = useMemo(() => {
|
||||
if (showSettings) {
|
||||
return (
|
||||
<div className="absolute bottom-[100%] mb-2 w-full">
|
||||
<FloatingOverlay maxHeight={320}>
|
||||
<div className="py-2">
|
||||
<div className="px-3 pb-1 text-xs font-semibold text-[var(--app-hint)]">
|
||||
Permission Mode
|
||||
</div>
|
||||
{PERMISSION_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
disabled={controlsDisabled}
|
||||
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors ${
|
||||
controlsDisabled
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: 'cursor-pointer hover:bg-[var(--app-secondary-bg)]'
|
||||
}`}
|
||||
onClick={() => handlePermissionChange(mode)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
className={`flex h-4 w-4 items-center justify-center rounded-full border-2 ${
|
||||
permissionMode === mode
|
||||
? 'border-[var(--app-link)]'
|
||||
: 'border-[var(--app-hint)]'
|
||||
}`}
|
||||
>
|
||||
{permissionMode === mode && (
|
||||
<div className="h-2 w-2 rounded-full bg-[var(--app-link)]" />
|
||||
)}
|
||||
</div>
|
||||
<span className={permissionMode === mode ? 'text-[var(--app-link)]' : ''}>
|
||||
{PERMISSION_MODE_LABELS[mode]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mx-3 h-px bg-[var(--app-divider)]" />
|
||||
|
||||
<div className="py-2">
|
||||
<div className="px-3 pb-1 text-xs font-semibold text-[var(--app-hint)]">
|
||||
Model
|
||||
</div>
|
||||
{MODEL_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
disabled={controlsDisabled}
|
||||
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors ${
|
||||
controlsDisabled
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: 'cursor-pointer hover:bg-[var(--app-secondary-bg)]'
|
||||
}`}
|
||||
onClick={() => handleModelChange(mode)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
className={`flex h-4 w-4 items-center justify-center rounded-full border-2 ${
|
||||
modelMode === mode
|
||||
? 'border-[var(--app-link)]'
|
||||
: 'border-[var(--app-hint)]'
|
||||
}`}
|
||||
>
|
||||
{modelMode === mode && (
|
||||
<div className="h-2 w-2 rounded-full bg-[var(--app-link)]" />
|
||||
)}
|
||||
</div>
|
||||
<span className={modelMode === mode ? 'text-[var(--app-link)]' : ''}>
|
||||
{MODEL_MODE_LABELS[mode]}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</FloatingOverlay>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
return (
|
||||
<div className="absolute bottom-[100%] mb-2 w-full">
|
||||
<FloatingOverlay>
|
||||
<Autocomplete
|
||||
suggestions={suggestions}
|
||||
selectedIndex={selectedIndex}
|
||||
onSelect={(index) => handleSuggestionSelect(index)}
|
||||
/>
|
||||
</FloatingOverlay>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [
|
||||
showSettings,
|
||||
suggestions,
|
||||
selectedIndex,
|
||||
controlsDisabled,
|
||||
permissionMode,
|
||||
modelMode,
|
||||
handlePermissionChange,
|
||||
handleModelChange,
|
||||
handleSuggestionSelect
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="px-3 pb-3 pt-2 bg-[var(--app-bg)]">
|
||||
<div className="mx-auto w-full max-w-[720px]">
|
||||
<ComposerPrimitive.Root className="relative">
|
||||
{overlays}
|
||||
|
||||
<StatusBar
|
||||
active={active}
|
||||
thinking={thinking}
|
||||
agentState={agentState}
|
||||
contextSize={contextSize}
|
||||
permissionMode={permissionMode}
|
||||
/>
|
||||
|
||||
<div className="overflow-hidden rounded-[20px] bg-[var(--app-secondary-bg)]">
|
||||
<div className="flex items-center px-4 py-3">
|
||||
<ComposerPrimitive.Input
|
||||
ref={textareaRef}
|
||||
placeholder="Type a message..."
|
||||
disabled={controlsDisabled}
|
||||
maxRows={5}
|
||||
submitOnEnter
|
||||
cancelOnEscape={false}
|
||||
onChange={handleChange}
|
||||
onSelect={handleSelect}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="flex-1 resize-none bg-transparent text-sm leading-snug text-[var(--app-fg)] placeholder-[var(--app-hint)] focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ComposerButtons
|
||||
canSend={canSend}
|
||||
controlsDisabled={controlsDisabled}
|
||||
showSettingsButton={showSettingsButton}
|
||||
onSettingsToggle={handleSettingsToggle}
|
||||
showAbortButton={showAbortButton}
|
||||
abortDisabled={abortDisabled}
|
||||
isAborting={isAborting}
|
||||
onAbort={handleAbort}
|
||||
/>
|
||||
</div>
|
||||
</ComposerPrimitive.Root>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { ThreadPrimitive } from '@assistant-ui/react'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { SessionMetadataSummary } from '@/types/api'
|
||||
import { HappyChatProvider } from '@/components/AssistantChat/context'
|
||||
import { HappyAssistantMessage } from '@/components/AssistantChat/messages/AssistantMessage'
|
||||
import { HappyUserMessage } from '@/components/AssistantChat/messages/UserMessage'
|
||||
import { HappySystemMessage } from '@/components/AssistantChat/messages/SystemMessage'
|
||||
|
||||
const THREAD_MESSAGE_COMPONENTS = {
|
||||
UserMessage: HappyUserMessage,
|
||||
AssistantMessage: HappyAssistantMessage,
|
||||
SystemMessage: HappySystemMessage
|
||||
} as const
|
||||
|
||||
export function HappyThread(props: {
|
||||
api: ApiClient
|
||||
sessionId: string
|
||||
metadata: SessionMetadataSummary | null
|
||||
disabled: boolean
|
||||
onRefresh: () => void
|
||||
onRetryMessage?: (localId: string) => void
|
||||
header?: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<HappyChatProvider value={{
|
||||
api: props.api,
|
||||
sessionId: props.sessionId,
|
||||
metadata: props.metadata,
|
||||
disabled: props.disabled,
|
||||
onRefresh: props.onRefresh,
|
||||
onRetryMessage: props.onRetryMessage
|
||||
}}>
|
||||
<ThreadPrimitive.Root className="flex min-h-0 flex-1 flex-col">
|
||||
<ThreadPrimitive.Viewport className="min-h-0 flex-1 overflow-y-auto" autoScroll>
|
||||
<div className="mx-auto w-full max-w-[720px] p-3">
|
||||
{props.header}
|
||||
<div className="flex flex-col gap-3">
|
||||
<ThreadPrimitive.Messages components={THREAD_MESSAGE_COMPONENTS} />
|
||||
</div>
|
||||
</div>
|
||||
</ThreadPrimitive.Viewport>
|
||||
</ThreadPrimitive.Root>
|
||||
</HappyChatProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { AgentState, PermissionMode } from '@/types/api'
|
||||
|
||||
const PERMISSION_MODE_LABELS: Record<string, string> = {
|
||||
default: 'Default',
|
||||
acceptEdits: 'Accept Edits',
|
||||
plan: 'Plan Mode',
|
||||
bypassPermissions: 'Bypass All'
|
||||
}
|
||||
|
||||
// Max context size for percentage calculation
|
||||
const MAX_CONTEXT_SIZE = 190000
|
||||
|
||||
// Vibing messages for thinking state
|
||||
const VIBING_MESSAGES = [
|
||||
"Accomplishing", "Actioning", "Actualizing", "Baking", "Booping", "Brewing",
|
||||
"Calculating", "Cerebrating", "Channelling", "Churning", "Clauding", "Coalescing",
|
||||
"Cogitating", "Computing", "Combobulating", "Concocting", "Conjuring", "Considering",
|
||||
"Contemplating", "Cooking", "Crafting", "Creating", "Crunching", "Deciphering",
|
||||
"Deliberating", "Determining", "Discombobulating", "Divining", "Doing", "Effecting",
|
||||
"Elucidating", "Enchanting", "Envisioning", "Finagling", "Flibbertigibbeting",
|
||||
"Forging", "Forming", "Frolicking", "Generating", "Germinating", "Hatching",
|
||||
"Herding", "Honking", "Ideating", "Imagining", "Incubating", "Inferring",
|
||||
"Manifesting", "Marinating", "Meandering", "Moseying", "Mulling", "Mustering",
|
||||
"Musing", "Noodling", "Percolating", "Perusing", "Philosophising", "Pontificating",
|
||||
"Pondering", "Processing", "Puttering", "Puzzling", "Reticulating", "Ruminating",
|
||||
"Scheming", "Schlepping", "Shimmying", "Simmering", "Smooshing", "Spelunking",
|
||||
"Spinning", "Stewing", "Sussing", "Synthesizing", "Thinking", "Tinkering",
|
||||
"Transmuting", "Unfurling", "Unravelling", "Vibing", "Wandering", "Whirring",
|
||||
"Wibbling", "Wizarding", "Working", "Wrangling"
|
||||
]
|
||||
|
||||
function getConnectionStatus(
|
||||
active: boolean,
|
||||
thinking: boolean,
|
||||
agentState: AgentState | null | undefined
|
||||
): { text: string; color: string; dotColor: string; isPulsing: boolean } {
|
||||
const hasPermissions = agentState?.requests && Object.keys(agentState.requests).length > 0
|
||||
|
||||
if (!active) {
|
||||
return {
|
||||
text: 'offline',
|
||||
color: 'text-[#999]',
|
||||
dotColor: 'bg-[#999]',
|
||||
isPulsing: false
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPermissions) {
|
||||
return {
|
||||
text: 'permission required',
|
||||
color: 'text-[#FF9500]',
|
||||
dotColor: 'bg-[#FF9500]',
|
||||
isPulsing: true
|
||||
}
|
||||
}
|
||||
|
||||
if (thinking) {
|
||||
const vibingMessage = VIBING_MESSAGES[Math.floor(Math.random() * VIBING_MESSAGES.length)].toLowerCase() + '…'
|
||||
return {
|
||||
text: vibingMessage,
|
||||
color: 'text-[#007AFF]',
|
||||
dotColor: 'bg-[#007AFF]',
|
||||
isPulsing: true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
text: 'online',
|
||||
color: 'text-[#34C759]',
|
||||
dotColor: 'bg-[#34C759]',
|
||||
isPulsing: false
|
||||
}
|
||||
}
|
||||
|
||||
function getContextWarning(contextSize: number): { text: string; color: string } | null {
|
||||
const percentageUsed = (contextSize / MAX_CONTEXT_SIZE) * 100
|
||||
const percentageRemaining = 100 - percentageUsed
|
||||
|
||||
if (percentageRemaining <= 5) {
|
||||
return { text: `${Math.round(percentageRemaining)}% left`, color: 'text-red-500' }
|
||||
} else if (percentageRemaining <= 10) {
|
||||
return { text: `${Math.round(percentageRemaining)}% left`, color: 'text-amber-500' }
|
||||
} else {
|
||||
return { text: `${Math.round(percentageRemaining)}% left`, color: 'text-[var(--app-hint)]' }
|
||||
}
|
||||
}
|
||||
|
||||
export function StatusBar(props: {
|
||||
active: boolean
|
||||
thinking: boolean
|
||||
agentState: AgentState | null | undefined
|
||||
contextSize?: number
|
||||
permissionMode?: PermissionMode
|
||||
}) {
|
||||
const connectionStatus = useMemo(
|
||||
() => getConnectionStatus(props.active, props.thinking, props.agentState),
|
||||
[props.active, props.thinking, props.agentState]
|
||||
)
|
||||
|
||||
const contextWarning = useMemo(
|
||||
() => props.contextSize !== undefined ? getContextWarning(props.contextSize) : null,
|
||||
[props.contextSize]
|
||||
)
|
||||
|
||||
const permissionMode = props.permissionMode
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-2 pb-1">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={`h-2 w-2 rounded-full ${connectionStatus.dotColor} ${connectionStatus.isPulsing ? 'animate-pulse' : ''}`}
|
||||
/>
|
||||
<span className={`text-xs ${connectionStatus.color}`}>
|
||||
{connectionStatus.text}
|
||||
</span>
|
||||
</div>
|
||||
{contextWarning ? (
|
||||
<span className={`text-[10px] ${contextWarning.color}`}>
|
||||
{contextWarning.text}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{(permissionMode && permissionMode !== 'default') ? (
|
||||
<span className={`text-xs ${
|
||||
permissionMode === 'acceptEdits' ? 'text-amber-500' :
|
||||
permissionMode === 'bypassPermissions' ? 'text-red-500' :
|
||||
permissionMode === 'plan' ? 'text-blue-500' :
|
||||
'text-[var(--app-hint)]'
|
||||
}`}>
|
||||
{PERMISSION_MODE_LABELS[permissionMode]}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { createContext, useContext } from 'react'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { SessionMetadataSummary } from '@/types/api'
|
||||
|
||||
export type HappyChatContextValue = {
|
||||
api: ApiClient
|
||||
sessionId: string
|
||||
metadata: SessionMetadataSummary | null
|
||||
disabled: boolean
|
||||
onRefresh: () => void
|
||||
onRetryMessage?: (localId: string) => void
|
||||
}
|
||||
|
||||
const HappyChatContext = createContext<HappyChatContextValue | null>(null)
|
||||
|
||||
export function HappyChatProvider(props: { value: HappyChatContextValue; children: ReactNode }) {
|
||||
return (
|
||||
<HappyChatContext.Provider value={props.value}>
|
||||
{props.children}
|
||||
</HappyChatContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useHappyChatContext(): HappyChatContextValue {
|
||||
const ctx = useContext(HappyChatContext)
|
||||
if (!ctx) {
|
||||
throw new Error('HappyChatContext is missing')
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { SyntaxHighlighterProps } from '@assistant-ui/react-markdown'
|
||||
import { MarkdownTextPrimitive } from '@assistant-ui/react-markdown'
|
||||
import { MessagePrimitive, useAssistantState } from '@assistant-ui/react'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { CodeBlock } from '@/components/CodeBlock'
|
||||
import { HappyToolMessage } from '@/components/AssistantChat/messages/ToolMessage'
|
||||
|
||||
function MarkdownSyntaxHighlighter(props: SyntaxHighlighterProps) {
|
||||
return (
|
||||
<CodeBlock
|
||||
code={props.code}
|
||||
language={props.language}
|
||||
showCopyButton={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const MARKDOWN_PLUGINS = [remarkGfm]
|
||||
const MARKDOWN_COMPONENTS = {
|
||||
SyntaxHighlighter: MarkdownSyntaxHighlighter
|
||||
} as const
|
||||
|
||||
function MarkdownText() {
|
||||
return (
|
||||
<MarkdownTextPrimitive
|
||||
className="markdown-content text-sm"
|
||||
remarkPlugins={MARKDOWN_PLUGINS}
|
||||
components={MARKDOWN_COMPONENTS}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TOOL_COMPONENTS = {
|
||||
Fallback: HappyToolMessage
|
||||
} as const
|
||||
|
||||
const MESSAGE_PART_COMPONENTS = {
|
||||
Text: MarkdownText,
|
||||
tools: TOOL_COMPONENTS
|
||||
} as const
|
||||
|
||||
export function HappyAssistantMessage() {
|
||||
const toolOnly = useAssistantState(({ message }) => {
|
||||
if (message.role !== 'assistant') return false
|
||||
const parts = message.content
|
||||
return parts.length > 0 && parts.every((part) => part.type === 'tool-call')
|
||||
})
|
||||
const rootClass = toolOnly ? 'py-1' : 'px-1'
|
||||
|
||||
return (
|
||||
<MessagePrimitive.Root className={rootClass}>
|
||||
<MessagePrimitive.Content components={MESSAGE_PART_COMPONENTS} />
|
||||
</MessagePrimitive.Root>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MessagePrimitive, useAssistantState } from '@assistant-ui/react'
|
||||
|
||||
export function HappySystemMessage() {
|
||||
const message = useAssistantState(({ message }) => message)
|
||||
if (message.role !== 'system') return null
|
||||
|
||||
const text = message.content[0]?.type === 'text' ? message.content[0].text : ''
|
||||
|
||||
return (
|
||||
<MessagePrimitive.Root className="py-1">
|
||||
<div className="mx-auto w-fit max-w-[92%] px-2 text-center text-xs text-[var(--app-hint)] opacity-80">
|
||||
{text}
|
||||
</div>
|
||||
</MessagePrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import type { ToolCallMessagePartProps } from '@assistant-ui/react'
|
||||
import type { ChatBlock } from '@/chat/types'
|
||||
import type { AgentEvent, ToolCallBlock } from '@/chat/types'
|
||||
import type { MessageStatus } from '@/types/api'
|
||||
import { MarkdownRenderer } from '@/components/MarkdownRenderer'
|
||||
import { LazyRainbowText } from '@/components/LazyRainbowText'
|
||||
import { ToolCard } from '@/components/ToolCard/ToolCard'
|
||||
import { useHappyChatContext } from '@/components/AssistantChat/context'
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === 'object'
|
||||
}
|
||||
|
||||
function isToolCallBlock(value: unknown): value is ToolCallBlock {
|
||||
if (!isObject(value)) return false
|
||||
if (value.kind !== 'tool-call') return false
|
||||
if (typeof value.id !== 'string') return false
|
||||
if (!isObject(value.tool)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function ErrorIcon() {
|
||||
return (
|
||||
<svg className="h-[14px] w-[14px]" viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M8 5v4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<circle cx="8" cy="11" r="0.75" fill="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MessageStatusIndicator(props: {
|
||||
status?: MessageStatus
|
||||
onRetry?: () => void
|
||||
}) {
|
||||
if (props.status !== 'failed') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="text-red-500">
|
||||
<ErrorIcon />
|
||||
</span>
|
||||
{props.onRetry ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onRetry}
|
||||
className="text-xs text-blue-500 hover:underline"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function formatUnixTimestamp(value: number): string {
|
||||
const ms = value < 1_000_000_000_000 ? value * 1000 : value
|
||||
const date = new Date(ms)
|
||||
if (Number.isNaN(date.getTime())) return String(value)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
function renderEventLabel(event: AgentEvent): string {
|
||||
if (event.type === 'switch') {
|
||||
const mode = event.mode === 'local' ? 'local' : 'remote'
|
||||
return `🔄 Switched to ${mode}`
|
||||
}
|
||||
if (event.type === 'title-changed') {
|
||||
const title = typeof event.title === 'string' ? event.title : ''
|
||||
return title ? `Title changed to "${title}"` : 'Title changed'
|
||||
}
|
||||
if (event.type === 'permission-mode-changed') {
|
||||
const modeValue = (event as Record<string, unknown>).mode
|
||||
const mode = typeof modeValue === 'string' ? modeValue : 'default'
|
||||
return `🔐 Permission mode: ${mode}`
|
||||
}
|
||||
if (event.type === 'limit-reached') {
|
||||
const endsAt = typeof event.endsAt === 'number' ? event.endsAt : null
|
||||
return endsAt ? `⏳ Usage limit reached until ${formatUnixTimestamp(endsAt)}` : '⏳ Usage limit reached'
|
||||
}
|
||||
if (event.type === 'message') {
|
||||
return typeof event.message === 'string' ? event.message : 'Message'
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(event)
|
||||
} catch {
|
||||
return String(event.type)
|
||||
}
|
||||
}
|
||||
|
||||
function HappyNestedBlockList(props: {
|
||||
blocks: ChatBlock[]
|
||||
}) {
|
||||
const ctx = useHappyChatContext()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{props.blocks.map((block) => {
|
||||
if (block.kind === 'user-text') {
|
||||
const userBubbleClass = 'w-fit max-w-[92%] ml-auto rounded-xl bg-[var(--app-secondary-bg)] px-3 py-2 text-[var(--app-fg)] shadow-sm'
|
||||
const status = block.status
|
||||
const canRetry = status === 'failed' && typeof block.localId === 'string' && Boolean(ctx.onRetryMessage)
|
||||
const onRetry = canRetry ? () => ctx.onRetryMessage!(block.localId!) : undefined
|
||||
|
||||
return (
|
||||
<div key={`user:${block.id}`} className={userBubbleClass}>
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<LazyRainbowText text={block.text} />
|
||||
</div>
|
||||
{status ? (
|
||||
<div className="shrink-0 self-end pb-0.5">
|
||||
<MessageStatusIndicator status={status} onRetry={onRetry} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (block.kind === 'agent-text') {
|
||||
return (
|
||||
<div key={`agent:${block.id}`} className="px-1">
|
||||
<MarkdownRenderer content={block.text} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (block.kind === 'agent-event') {
|
||||
return (
|
||||
<div key={`event:${block.id}`} className="py-1">
|
||||
<div className="mx-auto w-fit max-w-[92%] px-2 text-center text-xs text-[var(--app-hint)] opacity-80">
|
||||
{renderEventLabel(block.event)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (block.kind === 'tool-call') {
|
||||
const isTask = block.tool.name === 'Task'
|
||||
|
||||
return (
|
||||
<div key={`tool:${block.id}`} className="py-1">
|
||||
<ToolCard
|
||||
api={ctx.api}
|
||||
sessionId={ctx.sessionId}
|
||||
metadata={ctx.metadata}
|
||||
disabled={ctx.disabled}
|
||||
onDone={ctx.onRefresh}
|
||||
block={block}
|
||||
/>
|
||||
{block.children.length > 0 ? (
|
||||
isTask ? (
|
||||
<details className="mt-2">
|
||||
<summary className="cursor-pointer text-xs text-[var(--app-hint)]">
|
||||
Task details ({block.children.length})
|
||||
</summary>
|
||||
<div className="mt-2 pl-3">
|
||||
<HappyNestedBlockList blocks={block.children} />
|
||||
</div>
|
||||
</details>
|
||||
) : (
|
||||
<div className="mt-2 pl-3">
|
||||
<HappyNestedBlockList blocks={block.children} />
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function HappyToolMessage(props: ToolCallMessagePartProps) {
|
||||
const ctx = useHappyChatContext()
|
||||
const artifact = props.artifact
|
||||
|
||||
if (!isToolCallBlock(artifact)) {
|
||||
return (
|
||||
<div className="py-1">
|
||||
<div className="text-xs text-[var(--app-hint)]">
|
||||
Tool call: {props.toolName}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const block = artifact
|
||||
const isTask = block.tool.name === 'Task'
|
||||
|
||||
return (
|
||||
<div className="py-1">
|
||||
<ToolCard
|
||||
api={ctx.api}
|
||||
sessionId={ctx.sessionId}
|
||||
metadata={ctx.metadata}
|
||||
disabled={ctx.disabled}
|
||||
onDone={ctx.onRefresh}
|
||||
block={block}
|
||||
/>
|
||||
{block.children.length > 0 ? (
|
||||
isTask ? (
|
||||
<details className="mt-2">
|
||||
<summary className="cursor-pointer text-xs text-[var(--app-hint)]">
|
||||
Task details ({block.children.length})
|
||||
</summary>
|
||||
<div className="mt-2 pl-3">
|
||||
<HappyNestedBlockList blocks={block.children} />
|
||||
</div>
|
||||
</details>
|
||||
) : (
|
||||
<div className="mt-2 pl-3">
|
||||
<HappyNestedBlockList blocks={block.children} />
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { MessagePrimitive, useAssistantState } from '@assistant-ui/react'
|
||||
import type { MessageStatus } from '@/types/api'
|
||||
import { LazyRainbowText } from '@/components/LazyRainbowText'
|
||||
import { useHappyChatContext } from '@/components/AssistantChat/context'
|
||||
import type { HappyChatMessageMetadata } from '@/lib/assistant-runtime'
|
||||
|
||||
function ErrorIcon() {
|
||||
return (
|
||||
<svg className="h-[14px] w-[14px]" viewBox="0 0 16 16" fill="none">
|
||||
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M8 5v4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<circle cx="8" cy="11" r="0.75" fill="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MessageStatusIndicator(props: {
|
||||
status?: MessageStatus
|
||||
onRetry?: () => void
|
||||
}) {
|
||||
if (props.status !== 'failed') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="text-red-500">
|
||||
<ErrorIcon />
|
||||
</span>
|
||||
{props.onRetry ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onRetry}
|
||||
className="text-xs text-blue-500 hover:underline"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function HappyUserMessage() {
|
||||
const ctx = useHappyChatContext()
|
||||
const message = useAssistantState(({ message }) => message)
|
||||
|
||||
if (message.role !== 'user') return null
|
||||
|
||||
const text = message.content.find((part) => part.type === 'text')?.text ?? ''
|
||||
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
|
||||
const status = custom?.status
|
||||
const localId = custom?.localId
|
||||
const canRetry = status === 'failed' && typeof localId === 'string' && Boolean(ctx.onRetryMessage)
|
||||
const onRetry = canRetry ? () => ctx.onRetryMessage!(localId) : undefined
|
||||
|
||||
const userBubbleClass = 'w-fit max-w-[92%] ml-auto rounded-xl bg-[var(--app-secondary-bg)] px-3 py-2 text-[var(--app-fg)] shadow-sm'
|
||||
|
||||
return (
|
||||
<MessagePrimitive.Root className={userBubbleClass}>
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<LazyRainbowText text={text} />
|
||||
</div>
|
||||
{status ? (
|
||||
<div className="shrink-0 self-end pb-0.5">
|
||||
<MessageStatusIndicator status={status} onRetry={onRetry} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</MessagePrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { memo } from 'react'
|
||||
import type { Suggestion } from '@/hooks/useActiveSuggestions'
|
||||
|
||||
interface AutocompleteProps {
|
||||
suggestions: Suggestion[]
|
||||
suggestions: readonly Suggestion[]
|
||||
selectedIndex: number
|
||||
onSelect: (index: number) => void
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { AssistantRuntimeProvider } from '@assistant-ui/react'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { DecryptedMessage, ModelMode, PermissionMode, Session } from '@/types/api'
|
||||
import type { NormalizedMessage } from '@/chat/types'
|
||||
import { normalizeDecryptedMessage } from '@/chat/normalize'
|
||||
import { reduceChatBlocks } from '@/chat/reducer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { HappyComposer } from '@/components/AssistantChat/HappyComposer'
|
||||
import { HappyThread } from '@/components/AssistantChat/HappyThread'
|
||||
import { useHappyRuntime } from '@/lib/assistant-runtime'
|
||||
import { SessionHeader } from '@/components/SessionHeader'
|
||||
import { MessageBubble } from '@/components/MessageBubble'
|
||||
import { ChatBlockList } from '@/components/ChatBlockList'
|
||||
import { ChatInput } from '@/components/ChatInput'
|
||||
import { useScrollToBottom } from '@/hooks/useScrollToBottom'
|
||||
import { getTelegramWebApp } from '@/hooks/useTelegram'
|
||||
|
||||
export function SessionChat(props: {
|
||||
@@ -61,7 +62,6 @@ export function SessionChat(props: {
|
||||
|
||||
const [debugViewMode, setDebugViewMode] = useState<'reduced' | 'raw'>('reduced')
|
||||
const viewMode = import.meta.env.DEV ? debugViewMode : 'reduced'
|
||||
const scrollRef = useScrollToBottom([props.messages.length, reduced.blocks.length, viewMode])
|
||||
|
||||
// Permission mode change handler
|
||||
const handlePermissionModeChange = useCallback(async (mode: PermissionMode) => {
|
||||
@@ -93,21 +93,25 @@ export function SessionChat(props: {
|
||||
props.onRefresh()
|
||||
}, [props.api, props.session.id, props.onRefresh])
|
||||
|
||||
const runtime = useHappyRuntime({
|
||||
session: props.session,
|
||||
blocks: viewMode === 'raw' ? [] : reduced.blocks,
|
||||
isSending: props.isSending,
|
||||
onSendMessage: props.onSend,
|
||||
onAbort: handleAbort
|
||||
})
|
||||
|
||||
const threadHeader = useMemo(() => {
|
||||
if (props.isLoadingMessages) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<SessionHeader
|
||||
session={props.session}
|
||||
onBack={props.onBack}
|
||||
/>
|
||||
|
||||
{controlsDisabled ? (
|
||||
<div className="bg-[var(--app-subtle-bg)] p-3 text-sm text-[var(--app-hint)]">
|
||||
Session is inactive. Controls are disabled.
|
||||
<div className="text-sm text-[var(--app-hint)]">
|
||||
Loading…
|
||||
</div>
|
||||
) : null}
|
||||
)
|
||||
}
|
||||
|
||||
<div ref={scrollRef} className="relative flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||
<div className="mx-auto w-full max-w-[720px] p-3">
|
||||
return (
|
||||
<>
|
||||
{props.messagesWarning ? (
|
||||
<div className="mb-3 rounded-md bg-amber-500/10 p-2 text-xs">
|
||||
{props.messagesWarning}
|
||||
@@ -146,10 +150,6 @@ export function SessionChat(props: {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{props.isLoadingMessages ? (
|
||||
<div className="text-sm text-[var(--app-hint)]">Loading…</div>
|
||||
) : (
|
||||
<>
|
||||
{import.meta.env.DEV && viewMode === 'reduced' && normalizedMessages.length === 0 && props.messages.length > 0 ? (
|
||||
<div className="mb-2 rounded-md bg-amber-500/10 p-2 text-xs">
|
||||
Message normalization returned 0 items for {props.messages.length} messages (see `hapi/web/src/chat/normalize.ts`).
|
||||
@@ -169,26 +169,49 @@ export function SessionChat(props: {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<ChatBlockList
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}, [
|
||||
props.isLoadingMessages,
|
||||
props.messagesWarning,
|
||||
props.hasMoreMessages,
|
||||
props.isLoadingMoreMessages,
|
||||
props.onLoadMore,
|
||||
props.messages,
|
||||
props.messages.length,
|
||||
props.onRetryMessage,
|
||||
viewMode,
|
||||
normalizedMessages.length
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<SessionHeader
|
||||
session={props.session}
|
||||
onBack={props.onBack}
|
||||
/>
|
||||
|
||||
{controlsDisabled ? (
|
||||
<div className="bg-[var(--app-subtle-bg)] p-3 text-sm text-[var(--app-hint)]">
|
||||
Session is inactive. Controls are disabled.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<AssistantRuntimeProvider runtime={runtime}>
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
<HappyThread
|
||||
api={props.api}
|
||||
sessionId={props.session.id}
|
||||
metadata={props.session.metadata}
|
||||
disabled={controlsDisabled}
|
||||
onRefresh={props.onRefresh}
|
||||
blocks={reduced.blocks}
|
||||
onRetryMessage={props.onRetryMessage}
|
||||
header={threadHeader}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="sticky bottom-0 z-10 mt-auto w-full overflow-visible">
|
||||
<ChatInput
|
||||
<HappyComposer
|
||||
disabled={props.isSending || controlsDisabled}
|
||||
onSend={props.onSend}
|
||||
sessionId={props.session.id}
|
||||
permissionMode={props.session.permissionMode}
|
||||
modelMode={props.session.modelMode}
|
||||
active={props.session.active}
|
||||
@@ -197,10 +220,9 @@ export function SessionChat(props: {
|
||||
contextSize={reduced.latestUsage?.contextSize}
|
||||
onPermissionModeChange={handlePermissionModeChange}
|
||||
onModelModeChange={handleModelModeChange}
|
||||
onAbort={handleAbort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AssistantRuntimeProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { AppendMessage, ExternalStoreAdapter, ThreadMessageLike } from '@assistant-ui/react'
|
||||
import { useExternalStoreRuntime } from '@assistant-ui/react'
|
||||
import type { ChatBlock } from '@/chat/types'
|
||||
import type { AgentEvent, ToolCallBlock } from '@/chat/types'
|
||||
import type { MessageStatus as HappyMessageStatus, Session } from '@/types/api'
|
||||
|
||||
function safeStringify(value: unknown): string {
|
||||
if (typeof value === 'string') return value
|
||||
try {
|
||||
const stringified = JSON.stringify(value, null, 2)
|
||||
return typeof stringified === 'string' ? stringified : String(value)
|
||||
} catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
function formatUnixTimestamp(value: number): string {
|
||||
const ms = value < 1_000_000_000_000 ? value * 1000 : value
|
||||
const date = new Date(ms)
|
||||
if (Number.isNaN(date.getTime())) return String(value)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
function renderEventLabel(event: AgentEvent): string {
|
||||
if (event.type === 'switch') {
|
||||
const mode = event.mode === 'local' ? 'local' : 'remote'
|
||||
return `🔄 Switched to ${mode}`
|
||||
}
|
||||
if (event.type === 'title-changed') {
|
||||
const title = typeof event.title === 'string' ? event.title : ''
|
||||
return title ? `Title changed to "${title}"` : 'Title changed'
|
||||
}
|
||||
if (event.type === 'permission-mode-changed') {
|
||||
const modeValue = (event as Record<string, unknown>).mode
|
||||
const mode = typeof modeValue === 'string' ? modeValue : 'default'
|
||||
return `🔐 Permission mode: ${mode}`
|
||||
}
|
||||
if (event.type === 'limit-reached') {
|
||||
const endsAt = typeof event.endsAt === 'number' ? event.endsAt : null
|
||||
return endsAt ? `⏳ Usage limit reached until ${formatUnixTimestamp(endsAt)}` : '⏳ Usage limit reached'
|
||||
}
|
||||
if (event.type === 'message') {
|
||||
return typeof event.message === 'string' ? event.message : 'Message'
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(event)
|
||||
} catch {
|
||||
return String(event.type)
|
||||
}
|
||||
}
|
||||
|
||||
export type HappyChatMessageMetadata = {
|
||||
kind: 'user' | 'assistant' | 'tool' | 'event'
|
||||
status?: HappyMessageStatus
|
||||
localId?: string | null
|
||||
originalText?: string
|
||||
toolCallId?: string
|
||||
event?: AgentEvent
|
||||
}
|
||||
|
||||
function toThreadMessageLike(block: ChatBlock): ThreadMessageLike {
|
||||
if (block.kind === 'user-text') {
|
||||
const messageId = `user:${block.id}`
|
||||
return {
|
||||
role: 'user',
|
||||
id: messageId,
|
||||
createdAt: new Date(block.createdAt),
|
||||
content: [{ type: 'text', text: block.text }],
|
||||
metadata: {
|
||||
custom: {
|
||||
kind: 'user',
|
||||
status: block.status,
|
||||
localId: block.localId,
|
||||
originalText: block.originalText
|
||||
} satisfies HappyChatMessageMetadata
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (block.kind === 'agent-text') {
|
||||
const messageId = `assistant:${block.id}`
|
||||
return {
|
||||
role: 'assistant',
|
||||
id: messageId,
|
||||
createdAt: new Date(block.createdAt),
|
||||
content: [{ type: 'text', text: block.text }],
|
||||
metadata: {
|
||||
custom: { kind: 'assistant' } satisfies HappyChatMessageMetadata
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (block.kind === 'agent-event') {
|
||||
const messageId = `event:${block.id}`
|
||||
return {
|
||||
role: 'system',
|
||||
id: messageId,
|
||||
createdAt: new Date(block.createdAt),
|
||||
content: renderEventLabel(block.event),
|
||||
metadata: {
|
||||
custom: { kind: 'event', event: block.event } satisfies HappyChatMessageMetadata
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toolBlock: ToolCallBlock = block
|
||||
const messageId = `tool:${toolBlock.id}`
|
||||
const inputText = safeStringify(toolBlock.tool.input)
|
||||
|
||||
return {
|
||||
role: 'assistant',
|
||||
id: messageId,
|
||||
createdAt: new Date(toolBlock.createdAt),
|
||||
content: [{
|
||||
type: 'tool-call',
|
||||
toolCallId: toolBlock.id,
|
||||
toolName: toolBlock.tool.name,
|
||||
argsText: inputText,
|
||||
result: toolBlock.tool.result,
|
||||
isError: toolBlock.tool.state === 'error',
|
||||
artifact: toolBlock
|
||||
}],
|
||||
metadata: {
|
||||
custom: { kind: 'tool', toolCallId: toolBlock.id } satisfies HappyChatMessageMetadata
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTextFromAppendMessage(message: AppendMessage): string | null {
|
||||
if (message.role !== 'user') return null
|
||||
|
||||
const parts = message.content
|
||||
const text = parts
|
||||
.filter((part): part is { type: 'text'; text: string } => part.type === 'text' && typeof part.text === 'string')
|
||||
.map((part) => part.text)
|
||||
.join('\n')
|
||||
.trim()
|
||||
|
||||
return text.length > 0 ? text : null
|
||||
}
|
||||
|
||||
export function useHappyRuntime(props: {
|
||||
session: Session
|
||||
blocks: readonly ChatBlock[]
|
||||
isSending: boolean
|
||||
onSendMessage: (text: string) => void
|
||||
onAbort: () => Promise<void>
|
||||
}) {
|
||||
const adapter = useMemo(() => {
|
||||
return {
|
||||
isDisabled: !props.session.active || props.isSending,
|
||||
isRunning: props.session.thinking,
|
||||
messages: props.blocks,
|
||||
onNew: async (message: AppendMessage) => {
|
||||
const text = getTextFromAppendMessage(message)
|
||||
if (!text) return
|
||||
props.onSendMessage(text)
|
||||
},
|
||||
onCancel: async () => {
|
||||
await props.onAbort()
|
||||
},
|
||||
convertMessage: (block: ChatBlock) => toThreadMessageLike(block),
|
||||
unstable_capabilities: { copy: true }
|
||||
} satisfies ExternalStoreAdapter<ChatBlock>
|
||||
}, [props.session.active, props.session.thinking, props.isSending, props.blocks, props.onSendMessage, props.onAbort])
|
||||
|
||||
return useExternalStoreRuntime(adapter)
|
||||
}
|
||||
@@ -8,4 +8,3 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user