mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-18 13:34:08 +01:00
feat: add chatbot endpoint
This commit is contained in:
parent
8c63c8d047
commit
df38e1c9f2
@ -9,6 +9,7 @@ require (
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/generative-ai-go v0.19.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.7.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
@ -17,12 +18,28 @@ require (
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
google.golang.org/api v0.186.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.115.0 // indirect
|
||||
cloud.google.com/go/ai v0.8.0 // indirect
|
||||
cloud.google.com/go/auth v0.6.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.7 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
@ -41,11 +58,24 @@ require (
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
|
||||
go.opentelemetry.io/otel v1.26.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.26.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.26.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect
|
||||
google.golang.org/grpc v1.64.1 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
162
backend/go.sum
162
backend/go.sum
@ -1,5 +1,22 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
|
||||
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
|
||||
cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w=
|
||||
cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE=
|
||||
cloud.google.com/go/auth v0.6.0 h1:5x+d6b5zdezZ7gmLWD1m/xNjnaQ2YDhmIz/HH3doy1g=
|
||||
cloud.google.com/go/auth v0.6.0/go.mod h1:b4acV+jLQDyjwm4OXHYjNvRi4jvGBzHWJRtJcy+2P4g=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
|
||||
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/danielgtaylor/huma/v2 v2.28.0 h1:W+hIT52MigO73edJNJWXU896uC99xSBWpKoE2PRyybM=
|
||||
github.com/danielgtaylor/huma/v2 v2.28.0/go.mod h1:67KO0zmYEkR+LVUs8uqrcvf44G1wXiMIu94LV/cH2Ek=
|
||||
@ -9,6 +26,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
@ -17,16 +40,52 @@ github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg=
|
||||
github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
@ -67,6 +126,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pressly/goose/v3 v3.24.1 h1:bZmxRco2uy5uu5Ng1MMVEfYsFlrMJI+e/VMXHQ3C4LY=
|
||||
github.com/pressly/goose/v3 v3.24.1/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
@ -94,27 +154,107 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
|
||||
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
|
||||
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
|
||||
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
|
||||
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
|
||||
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
|
||||
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.186.0 h1:n2OPp+PPXX0Axh4GuSsL5QL8xQCTb2oDwyzPnQvqUug=
|
||||
google.golang.org/api v0.186.0/go.mod h1:hvRbBmgoje49RV3xqVXrmP6w93n6ehGgIVPYrGtBFFc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@ -124,6 +264,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||
|
||||
@ -20,6 +20,7 @@ import (
|
||||
"github.com/forfarm/backend/internal/domain"
|
||||
m "github.com/forfarm/backend/internal/middlewares"
|
||||
"github.com/forfarm/backend/internal/repository"
|
||||
"github.com/forfarm/backend/internal/services"
|
||||
"github.com/forfarm/backend/internal/services/weather"
|
||||
"github.com/forfarm/backend/internal/utilities"
|
||||
)
|
||||
@ -38,12 +39,12 @@ type api struct {
|
||||
analyticsRepo domain.AnalyticsRepository
|
||||
|
||||
weatherFetcher domain.WeatherFetcher
|
||||
|
||||
chatService *services.ChatService
|
||||
}
|
||||
|
||||
var weatherFetcherInstance domain.WeatherFetcher
|
||||
|
||||
func GetWeatherFetcher() domain.WeatherFetcher {
|
||||
return weatherFetcherInstance
|
||||
func (a *api) GetWeatherFetcher() domain.WeatherFetcher {
|
||||
return a.weatherFetcher
|
||||
}
|
||||
|
||||
func NewAPI(
|
||||
@ -60,8 +61,8 @@ func NewAPI(
|
||||
client := &http.Client{}
|
||||
|
||||
userRepository := repository.NewPostgresUser(pool)
|
||||
plantRepository := repository.NewPostgresPlant(pool)
|
||||
harvestRepository := repository.NewPostgresHarvest(pool)
|
||||
plantRepository := repository.NewPostgresPlant(pool)
|
||||
|
||||
owmFetcher := weather.NewOpenWeatherMapFetcher(config.OPENWEATHER_API_KEY, client, logger)
|
||||
cacheTTL, err := time.ParseDuration(config.OPENWEATHER_CACHE_TTL)
|
||||
@ -74,7 +75,12 @@ func NewAPI(
|
||||
cleanupInterval = 5 * time.Minute
|
||||
}
|
||||
cachedWeatherFetcher := weather.NewCachedWeatherFetcher(owmFetcher, cacheTTL, cleanupInterval, logger)
|
||||
weatherFetcherInstance = cachedWeatherFetcher
|
||||
|
||||
chatService, chatErr := services.NewChatService(logger, analyticsRepo, farmRepo, croplandRepo, inventoryRepo, plantRepository)
|
||||
if chatErr != nil {
|
||||
logger.Error("Failed to initialize ChatService", "error", chatErr)
|
||||
chatService = nil
|
||||
}
|
||||
|
||||
return &api{
|
||||
logger: logger,
|
||||
@ -90,6 +96,8 @@ func NewAPI(
|
||||
analyticsRepo: analyticsRepo,
|
||||
|
||||
weatherFetcher: cachedWeatherFetcher,
|
||||
|
||||
chatService: chatService,
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,6 +139,8 @@ func (a *api) Routes() *chi.Mux {
|
||||
a.registerCropRoutes(r, api)
|
||||
a.registerPlantRoutes(r, api)
|
||||
a.registerOauthRoutes(r, api)
|
||||
a.registerChatRoutes(r, api)
|
||||
a.registerInventoryRoutes(r, api)
|
||||
})
|
||||
|
||||
router.Group(func(r chi.Router) {
|
||||
@ -138,7 +148,6 @@ func (a *api) Routes() *chi.Mux {
|
||||
a.registerHelloRoutes(r, api)
|
||||
a.registerFarmRoutes(r, api)
|
||||
a.registerUserRoutes(r, api)
|
||||
a.registerInventoryRoutes(r, api)
|
||||
a.registerAnalyticsRoutes(r, api)
|
||||
})
|
||||
|
||||
|
||||
142
backend/internal/api/chat.go
Normal file
142
backend/internal/api/chat.go
Normal file
@ -0,0 +1,142 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/forfarm/backend/internal/services"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/generative-ai-go/genai"
|
||||
)
|
||||
|
||||
func (a *api) registerChatRoutes(_ chi.Router, apiInstance huma.API) {
|
||||
tags := []string{"chat"}
|
||||
|
||||
huma.Register(apiInstance, huma.Operation{
|
||||
OperationID: "chatWithAssistantContextual",
|
||||
Method: http.MethodPost,
|
||||
Path: "/chat/specific",
|
||||
Tags: tags,
|
||||
Summary: "Send a message to the assistant with farm/crop context",
|
||||
Description: "Allows users to interact with the AI chatbot, providing farm/crop context.",
|
||||
}, a.chatHandler)
|
||||
|
||||
huma.Register(apiInstance, huma.Operation{
|
||||
OperationID: "chatWithAssistantGeneral",
|
||||
Method: http.MethodPost,
|
||||
Path: "/chat",
|
||||
Tags: tags,
|
||||
Summary: "Send a message to the general farming assistant",
|
||||
Description: "Allows users to interact with the AI chatbot without specific farm/crop context.",
|
||||
}, a.generalChatHandler)
|
||||
}
|
||||
|
||||
type HistoryItem struct {
|
||||
Role string `json:"role" example:"user" enum:"user,model" doc:"Role of the sender (user or model)"`
|
||||
Text string `json:"text" doc:"The content of the message"`
|
||||
}
|
||||
|
||||
type ChatInput struct {
|
||||
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
||||
Body struct {
|
||||
Message string `json:"message" required:"true" doc:"The user's message to the assistant"`
|
||||
FarmID string `json:"farmId,omitempty" doc:"Optional UUID of the farm context"`
|
||||
CropID string `json:"cropId,omitempty" doc:"Optional UUID of the crop context"`
|
||||
History []HistoryItem `json:"history,omitempty" doc:"Previous turns in the conversation"`
|
||||
}
|
||||
}
|
||||
|
||||
type ChatOutput struct {
|
||||
Body struct {
|
||||
Response string `json:"response" doc:"The assistant's response message"`
|
||||
}
|
||||
}
|
||||
|
||||
type GeneralChatInput struct {
|
||||
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
||||
Body struct {
|
||||
Message string `json:"message" required:"true" doc:"The user's message to the assistant"`
|
||||
History []HistoryItem `json:"history,omitempty" doc:"Previous turns in the conversation"`
|
||||
}
|
||||
}
|
||||
|
||||
type GeneralChatOutput = ChatOutput
|
||||
|
||||
// convertInputHistory converts the API HistoryItem slice to genai.Content slice.
|
||||
func convertInputHistory(apiHistory []HistoryItem) []*genai.Content {
|
||||
if apiHistory == nil {
|
||||
return nil
|
||||
}
|
||||
genaiHistory := make([]*genai.Content, 0, len(apiHistory))
|
||||
for _, item := range apiHistory {
|
||||
if (item.Role == "user" || item.Role == "model") && item.Text != "" {
|
||||
content := &genai.Content{
|
||||
Role: item.Role,
|
||||
Parts: []genai.Part{genai.Text(item.Text)},
|
||||
}
|
||||
genaiHistory = append(genaiHistory, content)
|
||||
}
|
||||
}
|
||||
return genaiHistory
|
||||
}
|
||||
|
||||
func (a *api) chatHandler(ctx context.Context, input *ChatInput) (*ChatOutput, error) {
|
||||
userID, err := a.getUserIDFromHeader(input.Header)
|
||||
if err != nil {
|
||||
return nil, huma.Error401Unauthorized("Authentication failed", err)
|
||||
}
|
||||
if a.chatService == nil {
|
||||
a.logger.Error("Chat service is not initialized")
|
||||
return nil, huma.Error500InternalServerError("Chat service is not available")
|
||||
}
|
||||
|
||||
genaiHistory := convertInputHistory(input.Body.History)
|
||||
|
||||
serviceInput := services.GenerateResponseInput{
|
||||
UserID: userID,
|
||||
Message: input.Body.Message,
|
||||
FarmID: input.Body.FarmID,
|
||||
CropID: input.Body.CropID,
|
||||
History: genaiHistory,
|
||||
}
|
||||
|
||||
botResponse, err := a.chatService.GenerateResponse(ctx, serviceInput)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to get response from chat service (contextual)", "userId", userID, "farmId", input.Body.FarmID, "cropId", input.Body.CropID, "error", err)
|
||||
return nil, huma.Error500InternalServerError("Failed to get response from assistant")
|
||||
}
|
||||
|
||||
resp := &ChatOutput{}
|
||||
resp.Body.Response = botResponse
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (a *api) generalChatHandler(ctx context.Context, input *GeneralChatInput) (*GeneralChatOutput, error) {
|
||||
userID, err := a.getUserIDFromHeader(input.Header)
|
||||
if err != nil {
|
||||
return nil, huma.Error401Unauthorized("Authentication failed", err)
|
||||
}
|
||||
if a.chatService == nil {
|
||||
a.logger.Error("Chat service is not initialized")
|
||||
return nil, huma.Error500InternalServerError("Chat service is not available")
|
||||
}
|
||||
|
||||
genaiHistory := convertInputHistory(input.Body.History)
|
||||
|
||||
serviceInput := services.GenerateResponseInput{
|
||||
UserID: userID,
|
||||
Message: input.Body.Message,
|
||||
History: genaiHistory,
|
||||
}
|
||||
|
||||
botResponse, err := a.chatService.GenerateResponse(ctx, serviceInput)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to get response from chat service (general)", "userId", userID, "error", err)
|
||||
return nil, huma.Error500InternalServerError("Failed to get response from assistant")
|
||||
}
|
||||
|
||||
resp := &GeneralChatOutput{}
|
||||
resp.Body.Response = botResponse
|
||||
return resp, nil
|
||||
}
|
||||
@ -68,18 +68,21 @@ func APICmd(ctx context.Context) *cobra.Command {
|
||||
}()
|
||||
logger.Info("Farm Analytics Projection started")
|
||||
|
||||
weatherFetcher := api.GetWeatherFetcher() // Get fetcher instance from API setup
|
||||
apiInstance := api.NewAPI(ctx, logger, pool, eventBus, analyticsRepo, inventoryRepo, croplandRepo, farmRepo)
|
||||
|
||||
weatherFetcher := apiInstance.GetWeatherFetcher()
|
||||
weatherInterval, err := time.ParseDuration(config.WEATHER_FETCH_INTERVAL)
|
||||
if err != nil {
|
||||
logger.Warn("Invalid WEATHER_FETCH_INTERVAL, using default 15m", "value", config.WEATHER_FETCH_INTERVAL, "error", err)
|
||||
weatherInterval = 15 * time.Minute
|
||||
}
|
||||
weatherUpdater := workers.NewWeatherUpdater(farmRepo, weatherFetcher, eventBus, logger, weatherInterval)
|
||||
weatherUpdater.Start(ctx) // Pass the main context
|
||||
weatherUpdater, err := workers.NewWeatherUpdater(farmRepo, weatherFetcher, eventBus, logger, weatherInterval)
|
||||
if err != nil {
|
||||
logger.Error("failed to create WeatherUpdater", "error", err)
|
||||
}
|
||||
weatherUpdater.Start(ctx)
|
||||
logger.Info("Weather Updater worker started", "interval", weatherInterval)
|
||||
|
||||
apiInstance := api.NewAPI(ctx, logger, pool, eventBus, analyticsRepo, inventoryRepo, croplandRepo, farmRepo) // Pass new repo
|
||||
|
||||
server := apiInstance.Server(port)
|
||||
|
||||
serverErrChan := make(chan error, 1)
|
||||
@ -87,7 +90,7 @@ func APICmd(ctx context.Context) *cobra.Command {
|
||||
logger.Info("starting API server", "port", port)
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Error("API server failed", "error", err)
|
||||
serverErrChan <- err // Send error to channel
|
||||
serverErrChan <- err
|
||||
}
|
||||
close(serverErrChan)
|
||||
}()
|
||||
@ -98,11 +101,10 @@ func APICmd(ctx context.Context) *cobra.Command {
|
||||
case <-ctx.Done():
|
||||
logger.Info("Shutdown signal received, initiating graceful shutdown...")
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) // 15-second grace period
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
weatherUpdater.Stop() // Signal and wait
|
||||
|
||||
weatherUpdater.Stop()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
logger.Error("HTTP server graceful shutdown failed", "error", err)
|
||||
} else {
|
||||
|
||||
@ -20,6 +20,7 @@ var (
|
||||
OPENWEATHER_API_KEY string
|
||||
OPENWEATHER_CACHE_TTL string
|
||||
WEATHER_FETCH_INTERVAL string
|
||||
GEMINI_API_KEY string
|
||||
)
|
||||
|
||||
func Load() {
|
||||
@ -36,6 +37,7 @@ func Load() {
|
||||
viper.SetDefault("OPENWEATHER_API_KEY", "openweather_api_key")
|
||||
viper.SetDefault("OPENWEATHER_CACHE_TTL", "15m")
|
||||
viper.SetDefault("WEATHER_FETCH_INTERVAL", "15m")
|
||||
viper.SetDefault("GEMINI_API_KEY", "gemini_api_key")
|
||||
|
||||
viper.SetConfigFile(".env")
|
||||
viper.AddConfigPath("../../.")
|
||||
@ -59,4 +61,5 @@ func Load() {
|
||||
OPENWEATHER_API_KEY = viper.GetString("OPENWEATHER_API_KEY")
|
||||
OPENWEATHER_CACHE_TTL = viper.GetString("OPENWEATHER_CACHE_TTL")
|
||||
WEATHER_FETCH_INTERVAL = viper.GetString("WEATHER_FETCH_INTERVAL")
|
||||
GEMINI_API_KEY = viper.GetString("GEMINI_API_KEY")
|
||||
}
|
||||
|
||||
445
backend/internal/services/chat_service.go
Normal file
445
backend/internal/services/chat_service.go
Normal file
@ -0,0 +1,445 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/forfarm/backend/internal/config"
|
||||
"github.com/forfarm/backend/internal/domain"
|
||||
"github.com/google/generative-ai-go/genai"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
type ChatService struct {
|
||||
client *genai.Client
|
||||
logger *slog.Logger
|
||||
analyticsRepo domain.AnalyticsRepository
|
||||
farmRepo domain.FarmRepository
|
||||
cropRepo domain.CroplandRepository
|
||||
inventoryRepo domain.InventoryRepository
|
||||
plantRepo domain.PlantRepository
|
||||
}
|
||||
|
||||
func NewChatService(
|
||||
logger *slog.Logger,
|
||||
analyticsRepo domain.AnalyticsRepository,
|
||||
farmRepo domain.FarmRepository,
|
||||
cropRepo domain.CroplandRepository,
|
||||
inventoryRepo domain.InventoryRepository,
|
||||
plantRepo domain.PlantRepository,
|
||||
) (*ChatService, error) {
|
||||
if config.GEMINI_API_KEY == "" {
|
||||
logger.Warn("GEMINI_API_KEY not set, ChatService will not function.")
|
||||
return &ChatService{client: nil, logger: logger}, nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := genai.NewClient(ctx, option.WithAPIKey(config.GEMINI_API_KEY))
|
||||
if err != nil {
|
||||
logger.Error("Failed to create Gemini client", "error", err)
|
||||
return nil, fmt.Errorf("error creating genai client: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Gemini client initialized successfully")
|
||||
return &ChatService{
|
||||
client: client,
|
||||
logger: logger,
|
||||
analyticsRepo: analyticsRepo,
|
||||
farmRepo: farmRepo,
|
||||
cropRepo: cropRepo,
|
||||
inventoryRepo: inventoryRepo,
|
||||
plantRepo: plantRepo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type GenerateResponseInput struct {
|
||||
UserID string
|
||||
Message string
|
||||
FarmID string
|
||||
CropID string
|
||||
History []*genai.Content
|
||||
}
|
||||
|
||||
// --- Context Building Helpers ---
|
||||
|
||||
func (s *ChatService) buildCropContextString(ctx context.Context, cropID, userID string) (string, error) {
|
||||
var contextBuilder strings.Builder
|
||||
contextBuilder.WriteString("## Current Crop & Plant Context ##\n")
|
||||
|
||||
cropAnalytics, err := s.analyticsRepo.GetCropAnalytics(ctx, cropID)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrNotFound) {
|
||||
s.logger.Warn("Crop analytics not found for context", "cropId", cropID)
|
||||
return "", fmt.Errorf("crop not found")
|
||||
}
|
||||
s.logger.Error("Failed to fetch crop analytics context", "cropId", cropID, "error", err)
|
||||
contextBuilder.WriteString(fmt.Sprintf("Error fetching crop details for ID %s.\n", cropID))
|
||||
return "", fmt.Errorf("failed to fetch crop details")
|
||||
}
|
||||
|
||||
farm, err := s.farmRepo.GetByID(ctx, cropAnalytics.FarmID)
|
||||
if err != nil || farm.OwnerID != userID {
|
||||
s.logger.Warn("Ownership check failed for crop context", "cropId", cropID, "farmId", cropAnalytics.FarmID, "userId", userID)
|
||||
return "", fmt.Errorf("unauthorized access to crop data")
|
||||
}
|
||||
|
||||
fmt.Fprintf(&contextBuilder, "Crop Name: %s (ID: %s)\n", cropAnalytics.CropName, cropAnalytics.CropID)
|
||||
fmt.Fprintf(&contextBuilder, "Farm: %s (ID: %s)\n", farm.Name, cropAnalytics.FarmID)
|
||||
fmt.Fprintf(&contextBuilder, "Plant: %s (Variety: %s)\n", cropAnalytics.PlantName, safeString(cropAnalytics.Variety))
|
||||
fmt.Fprintf(&contextBuilder, "Status: %s\n", cropAnalytics.CurrentStatus)
|
||||
fmt.Fprintf(&contextBuilder, "Growth Stage: %s\n", cropAnalytics.GrowthStage)
|
||||
fmt.Fprintf(&contextBuilder, "Land Size: %.2f ha\n", cropAnalytics.LandSize)
|
||||
fmt.Fprintf(&contextBuilder, "Growth Progress: %d%%\n", cropAnalytics.GrowthProgress)
|
||||
if cropAnalytics.PlantHealth != nil {
|
||||
fmt.Fprintf(&contextBuilder, "Health Status: %s\n", *cropAnalytics.PlantHealth)
|
||||
}
|
||||
if cropAnalytics.Temperature != nil {
|
||||
fmt.Fprintf(&contextBuilder, "Temperature: %.1f°C\n", *cropAnalytics.Temperature)
|
||||
}
|
||||
if cropAnalytics.Humidity != nil {
|
||||
fmt.Fprintf(&contextBuilder, "Humidity: %.0f%%\n", *cropAnalytics.Humidity)
|
||||
}
|
||||
if cropAnalytics.SoilMoisture != nil {
|
||||
fmt.Fprintf(&contextBuilder, "Soil Moisture: %.0f%%\n", *cropAnalytics.SoilMoisture)
|
||||
}
|
||||
if cropAnalytics.Rainfall != nil {
|
||||
fmt.Fprintf(&contextBuilder, "Rainfall (1h): %.1f mm\n", *cropAnalytics.Rainfall)
|
||||
}
|
||||
if cropAnalytics.WindSpeed != nil {
|
||||
fmt.Fprintf(&contextBuilder, "Wind Speed: %.1f m/s\n", *cropAnalytics.WindSpeed)
|
||||
}
|
||||
if cropAnalytics.Sunlight != nil {
|
||||
fmt.Fprintf(&contextBuilder, "Sunlight Exposure: %.0f%%\n", *cropAnalytics.Sunlight)
|
||||
}
|
||||
if cropAnalytics.NutrientLevels != nil {
|
||||
contextBuilder.WriteString("Nutrients: ")
|
||||
nutrients := []string{}
|
||||
if cropAnalytics.NutrientLevels.Nitrogen != nil {
|
||||
nutrients = append(nutrients, fmt.Sprintf("N=%.0f%%", *cropAnalytics.NutrientLevels.Nitrogen))
|
||||
}
|
||||
if cropAnalytics.NutrientLevels.Phosphorus != nil {
|
||||
nutrients = append(nutrients, fmt.Sprintf("P=%.0f%%", *cropAnalytics.NutrientLevels.Phosphorus))
|
||||
}
|
||||
if cropAnalytics.NutrientLevels.Potassium != nil {
|
||||
nutrients = append(nutrients, fmt.Sprintf("K=%.0f%%", *cropAnalytics.NutrientLevels.Potassium))
|
||||
}
|
||||
if len(nutrients) > 0 {
|
||||
contextBuilder.WriteString(strings.Join(nutrients, ", "))
|
||||
} else {
|
||||
contextBuilder.WriteString("Not Available")
|
||||
}
|
||||
contextBuilder.WriteString("\n")
|
||||
}
|
||||
if cropAnalytics.NextAction != nil {
|
||||
dueStr := ""
|
||||
if cropAnalytics.NextActionDue != nil {
|
||||
dueStr = fmt.Sprintf(" (Due: %s)", cropAnalytics.NextActionDue.Format(time.RFC1123))
|
||||
}
|
||||
fmt.Fprintf(&contextBuilder, "Suggested Next Action: %s%s\n", *cropAnalytics.NextAction, dueStr)
|
||||
}
|
||||
contextBuilder.WriteString("\n")
|
||||
|
||||
contextBuilder.WriteString("Plant Details:\n")
|
||||
plant, err := s.plantRepo.GetByName(ctx, cropAnalytics.PlantName)
|
||||
if err != nil {
|
||||
s.logger.Warn("Could not fetch plant details for context", "plantId", cropAnalytics.PlantName, "error", err)
|
||||
fmt.Fprintf(&contextBuilder, " - Could not retrieve plant details.\n")
|
||||
} else {
|
||||
fmt.Fprintf(&contextBuilder, " - Type: %s (Variety: %s)\n", plant.Name, safeString(plant.Variety))
|
||||
if plant.DaysToMaturity != nil {
|
||||
fmt.Fprintf(&contextBuilder, " - Days to Maturity: ~%d\n", *plant.DaysToMaturity)
|
||||
}
|
||||
if plant.OptimalTemp != nil {
|
||||
fmt.Fprintf(&contextBuilder, " - Optimal Temp: %.1f°C\n", *plant.OptimalTemp)
|
||||
}
|
||||
if plant.WaterNeeds != nil {
|
||||
fmt.Fprintf(&contextBuilder, " - Water Needs: %.1f (units unspecified)\n", *plant.WaterNeeds)
|
||||
}
|
||||
if plant.PHValue != nil {
|
||||
fmt.Fprintf(&contextBuilder, " - Soil pH: %.1f\n", *plant.PHValue)
|
||||
}
|
||||
if plant.RowSpacing != nil {
|
||||
fmt.Fprintf(&contextBuilder, " - Row Spacing: %.1f (units unspecified)\n", *plant.RowSpacing)
|
||||
}
|
||||
if plant.PlantingDepth != nil {
|
||||
fmt.Fprintf(&contextBuilder, " - Planting Depth: %.1f (units unspecified)\n", *plant.PlantingDepth)
|
||||
}
|
||||
}
|
||||
|
||||
contextBuilder.WriteString("\n")
|
||||
|
||||
inventoryContext, _ := s.buildInventoryContextString(ctx, userID)
|
||||
contextBuilder.WriteString(inventoryContext)
|
||||
|
||||
return contextBuilder.String(), nil
|
||||
}
|
||||
|
||||
func (s *ChatService) buildFarmContextString(ctx context.Context, farmID, userID string) (string, error) {
|
||||
var contextBuilder strings.Builder
|
||||
contextBuilder.WriteString("## Current Farm Context ##\n")
|
||||
|
||||
farmAnalytics, err := s.analyticsRepo.GetFarmAnalytics(ctx, farmID)
|
||||
if err != nil || farmAnalytics.OwnerID != userID {
|
||||
if errors.Is(err, domain.ErrNotFound) || farmAnalytics == nil || farmAnalytics.OwnerID != userID {
|
||||
s.logger.Warn("Farm analytics not found or ownership mismatch for context", "farmId", farmID, "userId", userID)
|
||||
return "", fmt.Errorf("farm not found or access denied")
|
||||
}
|
||||
s.logger.Error("Failed to fetch farm analytics context", "farmId", farmID, "error", err)
|
||||
return "", fmt.Errorf("failed to fetch farm details")
|
||||
}
|
||||
|
||||
fmt.Fprintf(&contextBuilder, "Farm Name: %s (ID: %s)\n", farmAnalytics.FarmName, farmAnalytics.FarmID)
|
||||
if farmAnalytics.FarmType != nil {
|
||||
fmt.Fprintf(&contextBuilder, "Type: %s\n", *farmAnalytics.FarmType)
|
||||
}
|
||||
if farmAnalytics.TotalSize != nil {
|
||||
fmt.Fprintf(&contextBuilder, "Size: %s\n", *farmAnalytics.TotalSize)
|
||||
}
|
||||
fmt.Fprintf(&contextBuilder, "Location: Lat %.4f, Lon %.4f\n", farmAnalytics.Latitude, farmAnalytics.Longitude)
|
||||
if farmAnalytics.OverallStatus != nil {
|
||||
fmt.Fprintf(&contextBuilder, "Overall Status: %s\n", *farmAnalytics.OverallStatus)
|
||||
}
|
||||
if farmAnalytics.Weather != nil {
|
||||
contextBuilder.WriteString("Weather:\n")
|
||||
if farmAnalytics.Weather.TempCelsius != nil {
|
||||
fmt.Fprintf(&contextBuilder, " - Temp: %.1f°C\n", *farmAnalytics.Weather.TempCelsius)
|
||||
}
|
||||
if farmAnalytics.Weather.Humidity != nil {
|
||||
fmt.Fprintf(&contextBuilder, " - Humidity: %.0f%%\n", *farmAnalytics.Weather.Humidity)
|
||||
}
|
||||
if farmAnalytics.Weather.Description != nil {
|
||||
fmt.Fprintf(&contextBuilder, " - Condition: %s\n", *farmAnalytics.Weather.Description)
|
||||
}
|
||||
if farmAnalytics.Weather.WindSpeed != nil {
|
||||
fmt.Fprintf(&contextBuilder, " - Wind: %.1f m/s\n", *farmAnalytics.Weather.WindSpeed)
|
||||
}
|
||||
if farmAnalytics.Weather.RainVolume1h != nil {
|
||||
fmt.Fprintf(&contextBuilder, " - Rain (1h): %.1f mm\n", *farmAnalytics.Weather.RainVolume1h)
|
||||
}
|
||||
if farmAnalytics.Weather.WeatherLastUpdated != nil {
|
||||
fmt.Fprintf(&contextBuilder, " - Last Updated: %s\n", farmAnalytics.Weather.WeatherLastUpdated.Format(time.RFC1123))
|
||||
}
|
||||
}
|
||||
|
||||
crops, err := s.cropRepo.GetByFarmID(ctx, farmID)
|
||||
if err == nil && len(crops) > 0 {
|
||||
contextBuilder.WriteString("Crops on Farm:\n")
|
||||
for i, crop := range crops {
|
||||
if i >= 5 {
|
||||
fmt.Fprintf(&contextBuilder, " - ... and %d more\n", len(crops)-5)
|
||||
break
|
||||
}
|
||||
fmt.Fprintf(&contextBuilder, " - %s (Status: %s, Stage: %s)\n", crop.Name, crop.Status, crop.GrowthStage)
|
||||
}
|
||||
} else if err != nil {
|
||||
s.logger.Warn("Failed to fetch crops for farm context", "farmId", farmID, "error", err)
|
||||
}
|
||||
|
||||
contextBuilder.WriteString("\n")
|
||||
inventoryContext, _ := s.buildInventoryContextString(ctx, userID)
|
||||
contextBuilder.WriteString(inventoryContext)
|
||||
|
||||
return contextBuilder.String(), nil
|
||||
}
|
||||
|
||||
func (s *ChatService) buildInventoryContextString(ctx context.Context, userID string) (string, error) {
|
||||
var contextBuilder strings.Builder
|
||||
contextBuilder.WriteString("## Inventory Summary ##\n")
|
||||
|
||||
filter := domain.InventoryFilter{UserID: userID}
|
||||
sort := domain.InventorySort{Field: "name", Direction: "asc"}
|
||||
items, err := s.inventoryRepo.GetByUserID(ctx, userID, filter, sort)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to fetch inventory for context", "userId", userID, "error", err)
|
||||
fmt.Fprintf(&contextBuilder, "Could not retrieve inventory details.\n")
|
||||
return contextBuilder.String(), err
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
fmt.Fprintf(&contextBuilder, "No inventory items found.\n")
|
||||
return contextBuilder.String(), nil
|
||||
}
|
||||
|
||||
lowStockCount := 0
|
||||
fmt.Fprintf(&contextBuilder, "Items (%d total):\n", len(items))
|
||||
limit := 10
|
||||
for i, item := range items {
|
||||
if i >= limit {
|
||||
fmt.Fprintf(&contextBuilder, "- ... and %d more\n", len(items)-limit)
|
||||
break
|
||||
}
|
||||
statusName := item.Status.Name
|
||||
if statusName == "" && item.StatusID != 0 {
|
||||
statusName = fmt.Sprintf("StatusID %d", item.StatusID)
|
||||
}
|
||||
unitName := item.Unit.Name
|
||||
if unitName == "" && item.UnitID != 0 {
|
||||
unitName = fmt.Sprintf("UnitID %d", item.UnitID)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&contextBuilder, "- %s: %.2f %s (Status: %s)\n", item.Name, item.Quantity, unitName, statusName)
|
||||
if strings.Contains(strings.ToLower(statusName), "low") {
|
||||
lowStockCount++
|
||||
}
|
||||
}
|
||||
if lowStockCount > 0 {
|
||||
fmt.Fprintf(&contextBuilder, "Note: %d item(s) are low on stock.\n", lowStockCount)
|
||||
}
|
||||
|
||||
return contextBuilder.String(), nil
|
||||
}
|
||||
|
||||
func (s *ChatService) buildGeneralContextString(ctx context.Context, userID string) (string, error) {
|
||||
var contextBuilder strings.Builder
|
||||
contextBuilder.WriteString("## General Farming Context ##\n")
|
||||
|
||||
farms, err := s.farmRepo.GetByOwnerID(ctx, userID)
|
||||
if err == nil && len(farms) > 0 {
|
||||
contextBuilder.WriteString("Your Farms:\n")
|
||||
for i, farm := range farms {
|
||||
if i >= 5 {
|
||||
fmt.Fprintf(&contextBuilder, "- ... and %d more\n", len(farms)-5)
|
||||
break
|
||||
}
|
||||
fmt.Fprintf(&contextBuilder, "- %s (Type: %s, Size: %s)\n", farm.Name, farm.FarmType, farm.TotalSize)
|
||||
}
|
||||
} else if err != nil {
|
||||
s.logger.Warn("Failed to fetch farms for general context", "userId", userID, "error", err)
|
||||
} else {
|
||||
contextBuilder.WriteString("No farms found for your account.\n")
|
||||
}
|
||||
contextBuilder.WriteString("\n")
|
||||
|
||||
inventoryContext, _ := s.buildInventoryContextString(ctx, userID)
|
||||
contextBuilder.WriteString(inventoryContext)
|
||||
|
||||
return contextBuilder.String(), nil
|
||||
}
|
||||
|
||||
func (s *ChatService) GenerateResponse(ctx context.Context, input GenerateResponseInput) (string, error) {
|
||||
var contextString string
|
||||
var contextErr error
|
||||
startTime := time.Now()
|
||||
|
||||
if input.CropID != "" {
|
||||
s.logger.Debug("Building context for CROP", "cropId", input.CropID)
|
||||
contextString, contextErr = s.buildCropContextString(ctx, input.CropID, input.UserID)
|
||||
} else if input.FarmID != "" {
|
||||
s.logger.Debug("Building context for FARM", "farmId", input.FarmID)
|
||||
contextString, contextErr = s.buildFarmContextString(ctx, input.FarmID, input.UserID)
|
||||
} else {
|
||||
s.logger.Debug("Building GENERAL context", "userId", input.UserID)
|
||||
contextString, contextErr = s.buildGeneralContextString(ctx, input.UserID)
|
||||
}
|
||||
|
||||
newsContext, _ := s.retrieveDummyNews(ctx)
|
||||
weatherOutlook, _ := s.retrieveDummyWeatherOutlook(ctx, input.FarmID)
|
||||
|
||||
fullContext := strings.Builder{}
|
||||
fullContext.WriteString(contextString)
|
||||
if contextErr != nil {
|
||||
s.logger.Warn("Error building primary context string", "error", contextErr, "userId", input.UserID, "farmId", input.FarmID, "cropId", input.CropID)
|
||||
fullContext.WriteString(fmt.Sprintf("Warning: Could not retrieve specific data (%s).\n\n", contextErr))
|
||||
}
|
||||
fullContext.WriteString(newsContext)
|
||||
fullContext.WriteString(weatherOutlook)
|
||||
|
||||
contextDuration := time.Since(startTime)
|
||||
s.logger.Debug("Context retrieval duration", "duration", contextDuration)
|
||||
|
||||
model := s.client.GenerativeModel("gemini-1.5-flash")
|
||||
systemInstruction := `You are ForFarm Assistant, an expert AI specialized in agriculture and farming practices.
|
||||
Your goal is to provide helpful, accurate, and concise advice to farmers using the ForFarm platform.
|
||||
Use the provided context data about the user's specific farm, crops, or inventory when available to tailor your response.
|
||||
If context is provided, prioritize answering based on that context.
|
||||
If no specific context is available or relevant, provide general best-practice farming advice.
|
||||
Focus on actionable recommendations where appropriate.
|
||||
Keep responses focused on farming, agriculture, crop management, pest control, soil health, weather impacts, and inventory management.`
|
||||
model.SystemInstruction = &genai.Content{Parts: []genai.Part{genai.Text(systemInstruction)}}
|
||||
fullPrompt := fmt.Sprintf("%s\nUser Question: %s", strings.TrimSpace(fullContext.String()), input.Message)
|
||||
|
||||
session := model.StartChat()
|
||||
session.History = input.History
|
||||
|
||||
s.logger.Info("Sending message to LLM", "userId", input.UserID, "historyLength", len(session.History), "contextLength", len(fullContext.String()))
|
||||
|
||||
resp, err := session.SendMessage(ctx, genai.Text(fullPrompt))
|
||||
llmDuration := time.Since(startTime) - contextDuration
|
||||
s.logger.Debug("LLM response duration", "duration", llmDuration)
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("Error sending message to Gemini", "error", err)
|
||||
return "Sorry, I encountered an error while generating a response.", fmt.Errorf("LLM communication failed: %w", err)
|
||||
}
|
||||
|
||||
var responseText strings.Builder
|
||||
if len(resp.Candidates) > 0 && resp.Candidates[0].Content != nil {
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
if txt, ok := part.(genai.Text); ok {
|
||||
responseText.WriteString(string(txt))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if responseText.Len() == 0 {
|
||||
s.logger.Warn("Received no valid text content from Gemini", "finishReason", resp.Candidates[0].FinishReason)
|
||||
if resp.Candidates[0].FinishReason != genai.FinishReasonStop {
|
||||
return fmt.Sprintf("My response generation was interrupted (%s). Could you please try rephrasing?", resp.Candidates[0].FinishReason), nil
|
||||
}
|
||||
return "I apologize, I couldn't generate a response for that request.", nil
|
||||
}
|
||||
|
||||
s.logger.Info("Successfully generated chat response", "userId", input.UserID, "responseLength", responseText.Len())
|
||||
return responseText.String(), nil
|
||||
}
|
||||
|
||||
// Simulates fetching news with artificial delay and random selection
|
||||
func (s *ChatService) retrieveDummyNews(ctx context.Context) (string, error) {
|
||||
s.logger.Debug("Retrieving dummy news context")
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if rand.Intn(10) > 2 {
|
||||
newsItems := []string{
|
||||
"Global wheat prices show slight increase.",
|
||||
"New organic pest control method using beneficial nematodes gaining traction.",
|
||||
"Research highlights drought-resistant corn variety performance.",
|
||||
"Government announces new subsidies for sustainable farming practices.",
|
||||
}
|
||||
return fmt.Sprintf("## Recent Agricultural News ##\n- %s\n\n", newsItems[rand.Intn(len(newsItems))]), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Simulates fetching weather forecast with artificial delay and random selection
|
||||
func (s *ChatService) retrieveDummyWeatherOutlook(ctx context.Context, farmID string) (string, error) {
|
||||
s.logger.Debug("Retrieving dummy weather outlook context", "farmId", farmID)
|
||||
time.Sleep(80 * time.Millisecond)
|
||||
forecasts := []string{
|
||||
"Clear skies expected for the next 3 days.",
|
||||
"Chance of scattered showers tomorrow afternoon.",
|
||||
"Temperature expected to rise towards the weekend.",
|
||||
"Slightly higher winds predicted for Thursday.",
|
||||
}
|
||||
return fmt.Sprintf("## Weather Outlook ##\n- %s\n\n", forecasts[rand.Intn(len(forecasts))]), nil
|
||||
}
|
||||
|
||||
func safeString(s *string) string {
|
||||
if s == nil {
|
||||
return "N/A"
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func (s *ChatService) Close() {
|
||||
if s.client != nil {
|
||||
if err := s.client.Close(); err != nil {
|
||||
s.logger.Error("Failed to close Gemini client", "error", err)
|
||||
} else {
|
||||
s.logger.Info("Gemini client closed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
73
frontend/api/chat.ts
Normal file
73
frontend/api/chat.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import axiosInstance from "./config";
|
||||
|
||||
interface HistoryItem {
|
||||
role: "user" | "model" | "assistant";
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface ChatResponse {
|
||||
response: string;
|
||||
// Include history if backend returns it, otherwise frontend manages it
|
||||
}
|
||||
|
||||
// Map frontend 'assistant' role to backend 'model' role if needed
|
||||
const mapRoleForApi = (role: "user" | "assistant"): "user" | "model" => {
|
||||
return role === "assistant" ? "model" : role;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a chat message to the backend.
|
||||
* Determines the endpoint based on the presence of farmId and cropId.
|
||||
*
|
||||
* @param message The user's message.
|
||||
* @param history The conversation history.
|
||||
* @param farmId Optional farm ID for context.
|
||||
* @param cropId Optional crop ID for context.
|
||||
* @returns The assistant's response.
|
||||
*/
|
||||
export async function sendChatMessage(
|
||||
message: string,
|
||||
history: HistoryItem[],
|
||||
farmId?: string | null,
|
||||
cropId?: string | null
|
||||
): Promise<ChatResponse> {
|
||||
const endpoint = farmId || cropId ? "/chat/specific" : "/chat"; // Use /chatbot if no IDs
|
||||
const apiHistory = history.map((item) => ({
|
||||
role: mapRoleForApi(item.role as "user" | "assistant"),
|
||||
text: item.text,
|
||||
}));
|
||||
|
||||
const payload: {
|
||||
message: string;
|
||||
farmId?: string;
|
||||
cropId?: string;
|
||||
history: { role: "user" | "model"; text: string }[];
|
||||
} = {
|
||||
message,
|
||||
history: apiHistory,
|
||||
};
|
||||
|
||||
// Only include IDs if calling the contextual endpoint
|
||||
if (farmId || cropId) {
|
||||
if (farmId) payload.farmId = farmId;
|
||||
if (cropId) payload.cropId = cropId;
|
||||
}
|
||||
|
||||
console.log(`Sending chat message to ${endpoint} with payload:`, payload);
|
||||
|
||||
try {
|
||||
const response = await axiosInstance.post<ChatResponse>(endpoint, payload);
|
||||
console.log("Received chat response:", response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Error sending chat message to ${endpoint}:`, error);
|
||||
// Provide a user-friendly error message
|
||||
const errorMessage =
|
||||
(error as any).response?.data?.message ||
|
||||
"Sorry, I couldn't connect to the assistant right now. Please try again later.";
|
||||
// Throw an error with a useful message or return a specific error structure
|
||||
// throw new Error(errorMessage);
|
||||
// Or return an error response structure:
|
||||
return { response: errorMessage };
|
||||
}
|
||||
}
|
||||
@ -2,204 +2,69 @@
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
ChevronLeft,
|
||||
Send,
|
||||
Clock,
|
||||
X,
|
||||
Leaf,
|
||||
MessageSquare,
|
||||
History,
|
||||
PanelRightClose,
|
||||
PanelRightOpen,
|
||||
Search,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { Send, MessageSquare, Sparkles, Loader2, User, Bot } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import type { Farm, Crop } from "@/types";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; // Assuming Avatar is in ui folder
|
||||
import { sendChatMessage } from "@/api/chat"; // Import the API function
|
||||
|
||||
// Mock data for farms and crops
|
||||
const mockFarms: Farm[] = [
|
||||
{
|
||||
id: "farm1",
|
||||
name: "Green Valley Farm",
|
||||
location: "California",
|
||||
type: "Organic",
|
||||
createdAt: new Date("2023-01-15"),
|
||||
area: "120 acres",
|
||||
crops: 8,
|
||||
weather: {
|
||||
temperature: 24,
|
||||
humidity: 65,
|
||||
rainfall: "2mm",
|
||||
sunlight: 80,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "farm2",
|
||||
name: "Sunrise Fields",
|
||||
location: "Iowa",
|
||||
type: "Conventional",
|
||||
createdAt: new Date("2022-11-05"),
|
||||
area: "350 acres",
|
||||
crops: 5,
|
||||
weather: {
|
||||
temperature: 22,
|
||||
humidity: 58,
|
||||
rainfall: "0mm",
|
||||
sunlight: 90,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockCrops: Crop[] = [
|
||||
{
|
||||
id: "crop1",
|
||||
farmId: "farm1",
|
||||
name: "Organic Tomatoes",
|
||||
plantedDate: new Date("2023-03-10"),
|
||||
status: "Growing",
|
||||
variety: "Roma",
|
||||
area: "15 acres",
|
||||
healthScore: 92,
|
||||
progress: 65,
|
||||
},
|
||||
{
|
||||
id: "crop2",
|
||||
farmId: "farm1",
|
||||
name: "Sweet Corn",
|
||||
plantedDate: new Date("2023-04-05"),
|
||||
status: "Growing",
|
||||
variety: "Golden Bantam",
|
||||
area: "25 acres",
|
||||
healthScore: 88,
|
||||
progress: 45,
|
||||
},
|
||||
{
|
||||
id: "crop3",
|
||||
farmId: "farm2",
|
||||
name: "Soybeans",
|
||||
plantedDate: new Date("2023-05-15"),
|
||||
status: "Growing",
|
||||
variety: "Pioneer",
|
||||
area: "120 acres",
|
||||
healthScore: 95,
|
||||
progress: 30,
|
||||
},
|
||||
];
|
||||
|
||||
// Mock chat history
|
||||
// Interface for chat messages
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
role: "user" | "assistant"; // Changed sender to role
|
||||
content: string;
|
||||
sender: "user" | "bot";
|
||||
timestamp: Date;
|
||||
relatedTo?: {
|
||||
type: "farm" | "crop";
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
const mockChatHistory: ChatMessage[] = [
|
||||
{
|
||||
id: "msg1",
|
||||
content: "When should I harvest my tomatoes?",
|
||||
sender: "user",
|
||||
timestamp: new Date("2023-07-15T10:30:00"),
|
||||
relatedTo: {
|
||||
type: "crop",
|
||||
id: "crop1",
|
||||
name: "Organic Tomatoes",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "msg2",
|
||||
content:
|
||||
"Based on the current growth stage of your Roma tomatoes, they should be ready for harvest in approximately 2-3 weeks. The ideal time to harvest is when they've developed their full red color but are still firm to the touch. Keep monitoring the soil moisture levels as consistent watering during the final ripening stage is crucial for flavor development.",
|
||||
sender: "bot",
|
||||
timestamp: new Date("2023-07-15T10:30:30"),
|
||||
},
|
||||
{
|
||||
id: "msg3",
|
||||
content: "What's the best fertilizer for corn?",
|
||||
sender: "user",
|
||||
timestamp: new Date("2023-07-16T14:22:00"),
|
||||
relatedTo: {
|
||||
type: "crop",
|
||||
id: "crop2",
|
||||
name: "Sweet Corn",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "msg4",
|
||||
content:
|
||||
"For your Sweet Corn at Green Valley Farm, I recommend a nitrogen-rich fertilizer with an NPK ratio of approximately 16-4-8. Corn is a heavy nitrogen feeder, especially during its growth phase. Apply the fertilizer when the plants are knee-high and again when they begin to tassel. Based on your soil analysis, consider supplementing with sulfur to address the slight deficiency detected in your last soil test.",
|
||||
sender: "bot",
|
||||
timestamp: new Date("2023-07-16T14:22:45"),
|
||||
},
|
||||
];
|
||||
|
||||
// Recommended prompts
|
||||
// Recommended prompts (keep or adjust as needed)
|
||||
const recommendedPrompts = [
|
||||
{
|
||||
id: "prompt1",
|
||||
text: "When should I water my crops?",
|
||||
category: "Irrigation",
|
||||
text: "What are common signs of nutrient deficiency in plants?",
|
||||
category: "Plant Health",
|
||||
},
|
||||
{
|
||||
id: "prompt2",
|
||||
text: "How can I improve soil health?",
|
||||
text: "How can I improve soil drainage?",
|
||||
category: "Soil Management",
|
||||
},
|
||||
{
|
||||
id: "prompt3",
|
||||
text: "What pests might affect my crops this season?",
|
||||
text: "Explain integrated pest management (IPM).",
|
||||
category: "Pest Control",
|
||||
},
|
||||
{
|
||||
id: "prompt4",
|
||||
text: "Recommend a crop rotation plan",
|
||||
text: "What are the benefits of crop rotation?",
|
||||
category: "Planning",
|
||||
},
|
||||
{
|
||||
id: "prompt5",
|
||||
text: "How to maximize yield for my current crops?",
|
||||
category: "Optimization",
|
||||
text: "Tell me about sustainable farming practices.",
|
||||
category: "Sustainability",
|
||||
},
|
||||
{
|
||||
id: "prompt6",
|
||||
text: "What's the best time to harvest?",
|
||||
category: "Harvesting",
|
||||
text: "How does weather affect crop yield?",
|
||||
category: "Weather",
|
||||
},
|
||||
];
|
||||
|
||||
export default function ChatbotPage() {
|
||||
export default function GeneralChatbotPage() {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
||||
const [selectedFarm, setSelectedFarm] = useState<string | null>(null);
|
||||
const [selectedCrop, setSelectedCrop] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false); // Loading state for API call
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const router = useRouter();
|
||||
|
||||
// Initialize with a welcome message
|
||||
useEffect(() => {
|
||||
setMessages([
|
||||
{
|
||||
id: "welcome",
|
||||
id: `assistant-${Date.now()}`,
|
||||
role: "assistant",
|
||||
content:
|
||||
"👋 Hello! I'm ForFarm Assistant, your farming AI companion. How can I help you today? You can ask me about crop management, pest control, weather impacts, or select a specific farm or crop to get tailored advice.",
|
||||
sender: "bot",
|
||||
"👋 Hello! I'm ForFarm Assistant, your general farming AI companion. Ask me anything about agriculture, crops, soil, weather, or best practices!",
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
@ -210,483 +75,181 @@ export default function ChatbotPage() {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages]);
|
||||
|
||||
// Filter crops based on selected farm
|
||||
const filteredCrops = selectedFarm ? mockCrops.filter((crop) => crop.farmId === selectedFarm) : mockCrops;
|
||||
|
||||
// Handle sending a message
|
||||
const handleSendMessage = (content: string = inputValue) => {
|
||||
if (!content.trim()) return;
|
||||
const handleSendMessage = async (content: string = inputValue) => {
|
||||
if (!content.trim() || isLoading) return;
|
||||
|
||||
// Create user message
|
||||
const userMessage: ChatMessage = {
|
||||
id: `user-${Date.now()}`,
|
||||
role: "user",
|
||||
content,
|
||||
sender: "user",
|
||||
timestamp: new Date(),
|
||||
...(selectedFarm || selectedCrop
|
||||
? {
|
||||
relatedTo: {
|
||||
type: selectedCrop ? "crop" : "farm",
|
||||
id: selectedCrop || selectedFarm || "",
|
||||
name: selectedCrop
|
||||
? mockCrops.find((c) => c.id === selectedCrop)?.name || ""
|
||||
: mockFarms.find((f) => f.id === selectedFarm)?.name || "",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInputValue("");
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate bot response after a delay
|
||||
setTimeout(() => {
|
||||
const botResponse: ChatMessage = {
|
||||
id: `bot-${Date.now()}`,
|
||||
content: generateBotResponse(content, selectedFarm, selectedCrop),
|
||||
sender: "bot",
|
||||
// Prepare history for the API call
|
||||
const apiHistory = messages
|
||||
.filter((msg) => msg.role === "user" || msg.role === "assistant")
|
||||
.map((msg) => ({ role: msg.role, text: msg.content }));
|
||||
|
||||
try {
|
||||
// Call the API function *without* farmId and cropId
|
||||
const response = await sendChatMessage(userMessage.content, apiHistory);
|
||||
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: `assistant-${Date.now()}`,
|
||||
role: "assistant",
|
||||
content: response.response,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, botResponse]);
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
} catch (error) {
|
||||
console.error("Error sending general chat message:", error);
|
||||
const errorMessage: ChatMessage = {
|
||||
id: `error-${Date.now()}`,
|
||||
role: "assistant",
|
||||
content: `Sorry, I encountered an issue. ${(error as Error).message || "Please try again later."}`,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages((prev) => [...prev, errorMessage]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
// Generate a bot response based on the user's message and selected farm/crop
|
||||
const generateBotResponse = (message: string, farmId: string | null, cropId: string | null): string => {
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
// Get farm and crop details if selected
|
||||
const farm = farmId ? mockFarms.find((f) => f.id === farmId) : null;
|
||||
const crop = cropId ? mockCrops.find((c) => c.id === cropId) : null;
|
||||
|
||||
// Personalize response based on selected farm/crop
|
||||
let contextPrefix = "";
|
||||
if (crop) {
|
||||
contextPrefix = `For your ${crop.name} (${crop.variety}) at ${farm?.name || "your farm"}, `;
|
||||
} else if (farm) {
|
||||
contextPrefix = `For ${farm.name}, `;
|
||||
}
|
||||
|
||||
// Generate response based on message content
|
||||
if (lowerMessage.includes("water") || lowerMessage.includes("irrigation")) {
|
||||
return `${contextPrefix}I recommend watering deeply but infrequently to encourage strong root growth. Based on the current weather conditions${
|
||||
farm ? ` in ${farm.location}` : ""
|
||||
} (${farm?.weather?.rainfall || "minimal"} rainfall recently), you should water ${
|
||||
crop ? `your ${crop.name}` : "your crops"
|
||||
} approximately 2-3 times per week, ensuring the soil remains moist but not waterlogged.`;
|
||||
} else if (lowerMessage.includes("fertiliz") || lowerMessage.includes("nutrient")) {
|
||||
return `${contextPrefix}a balanced NPK fertilizer with a ratio of 10-10-10 would be suitable for general application. ${
|
||||
crop
|
||||
? `For ${crop.name} specifically, consider increasing ${
|
||||
crop.name.toLowerCase().includes("tomato")
|
||||
? "potassium"
|
||||
: crop.name.toLowerCase().includes("corn")
|
||||
? "nitrogen"
|
||||
: "phosphorus"
|
||||
} for optimal growth during the current ${
|
||||
crop.progress && crop.progress < 30 ? "early" : crop.progress && crop.progress < 70 ? "middle" : "late"
|
||||
} growth stage.`
|
||||
: ""
|
||||
}`;
|
||||
} else if (lowerMessage.includes("pest") || lowerMessage.includes("insect") || lowerMessage.includes("disease")) {
|
||||
return `${contextPrefix}monitor for ${
|
||||
crop
|
||||
? crop.name.toLowerCase().includes("tomato")
|
||||
? "tomato hornworms, aphids, and early blight"
|
||||
: crop.name.toLowerCase().includes("corn")
|
||||
? "corn borers, rootworms, and rust"
|
||||
: "common agricultural pests"
|
||||
: "common agricultural pests like aphids, beetles, and fungal diseases"
|
||||
}. I recommend implementing integrated pest management (IPM) practices, including regular scouting, beneficial insects, and targeted treatments only when necessary.`;
|
||||
} else if (lowerMessage.includes("harvest") || lowerMessage.includes("yield")) {
|
||||
return `${contextPrefix}${
|
||||
crop
|
||||
? `your ${crop.name} should be ready to harvest in approximately ${Math.max(
|
||||
1,
|
||||
Math.round((100 - (crop.progress || 50)) / 10)
|
||||
)} weeks based on the current growth stage. Look for ${
|
||||
crop.name.toLowerCase().includes("tomato")
|
||||
? "firm, fully colored fruits"
|
||||
: crop.name.toLowerCase().includes("corn")
|
||||
? "full ears with dried silk and plump kernels"
|
||||
: "signs of maturity specific to this crop type"
|
||||
}`
|
||||
: "harvest timing depends on the specific crops you're growing, but generally you should look for visual cues of ripeness and maturity"
|
||||
}.`;
|
||||
} else if (lowerMessage.includes("soil") || lowerMessage.includes("compost")) {
|
||||
return `${contextPrefix}improving soil health is crucial for sustainable farming. I recommend regular soil testing, adding organic matter through compost or cover crops, practicing crop rotation, and minimizing soil disturbance. ${
|
||||
farm
|
||||
? `Based on the soil type common in ${farm.location}, you might also consider adding ${
|
||||
farm.location.includes("California") ? "gypsum to improve drainage" : "lime to adjust pH levels"
|
||||
}.`
|
||||
: ""
|
||||
}`;
|
||||
} else if (lowerMessage.includes("weather") || lowerMessage.includes("forecast") || lowerMessage.includes("rain")) {
|
||||
return `${contextPrefix}${
|
||||
farm
|
||||
? `the current conditions show temperature at ${farm.weather?.temperature}°C with ${farm.weather?.humidity}% humidity. There's been ${farm.weather?.rainfall} of rainfall recently, and sunlight levels are at ${farm.weather?.sunlight}% of optimal.`
|
||||
: "I recommend checking your local agricultural weather service for the most accurate forecast for your specific location."
|
||||
} ${
|
||||
crop
|
||||
? `For your ${crop.name}, ${
|
||||
farm?.weather?.rainfall === "0mm"
|
||||
? "the dry conditions mean you should increase irrigation"
|
||||
: "the recent rainfall means you can reduce irrigation temporarily"
|
||||
}.`
|
||||
: ""
|
||||
}`;
|
||||
} else {
|
||||
return `${contextPrefix}I understand you're asking about "${message}". To provide the most helpful advice, could you provide more specific details about your farming goals or challenges? I'm here to help with crop management, pest control, irrigation strategies, and more.`;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle selecting a farm
|
||||
const handleFarmSelect = (farmId: string) => {
|
||||
setSelectedFarm(farmId);
|
||||
setSelectedCrop(null); // Reset crop selection when farm changes
|
||||
};
|
||||
|
||||
// Handle selecting a crop
|
||||
const handleCropSelect = (cropId: string) => {
|
||||
setSelectedCrop(cropId);
|
||||
};
|
||||
|
||||
// Handle clicking a recommended prompt
|
||||
const handlePromptClick = (promptText: string) => {
|
||||
setInputValue(promptText);
|
||||
handleSendMessage(promptText);
|
||||
};
|
||||
|
||||
// Handle loading a chat history item
|
||||
const handleLoadChatHistory = (messageId: string) => {
|
||||
// Find the message in history
|
||||
const historyItem = mockChatHistory.find((msg) => msg.id === messageId);
|
||||
if (!historyItem) return;
|
||||
|
||||
// Set related farm/crop if available
|
||||
if (historyItem.relatedTo) {
|
||||
if (historyItem.relatedTo.type === "farm") {
|
||||
setSelectedFarm(historyItem.relatedTo.id);
|
||||
setSelectedCrop(null);
|
||||
} else if (historyItem.relatedTo.type === "crop") {
|
||||
const crop = mockCrops.find((c) => c.id === historyItem.relatedTo?.id);
|
||||
if (crop) {
|
||||
setSelectedFarm(crop.farmId);
|
||||
setSelectedCrop(historyItem.relatedTo.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load the conversation
|
||||
const conversation = mockChatHistory.filter(
|
||||
(msg) =>
|
||||
msg.id === messageId ||
|
||||
(msg.timestamp >= historyItem.timestamp && msg.timestamp <= new Date(historyItem.timestamp.getTime() + 60000))
|
||||
);
|
||||
|
||||
setMessages(conversation);
|
||||
setIsHistoryOpen(false);
|
||||
// Directly send message after setting input
|
||||
// The handleSendMessage function will pick up the new inputValue
|
||||
// No need to call handleSendMessage here if the button triggers submit or calls it
|
||||
// Let's assume the button just sets the input and the user clicks send
|
||||
// OR: uncomment the line below if the button should send immediately
|
||||
// handleSendMessage(promptText);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-gradient-to-br from-green-50 to-blue-50 dark:from-green-950 dark:to-blue-950">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-white dark:bg-gray-950 shadow-sm py-4 px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => router.push("/farms")} aria-label="Back to farms">
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageSquare className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
<h1 className="text-xl font-semibold">ForFarm Assistant</h1>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsHistoryOpen(!isHistoryOpen)}
|
||||
aria-label={isHistoryOpen ? "Close history" : "Open history"}>
|
||||
{isHistoryOpen ? <PanelRightClose className="h-5 w-5" /> : <PanelRightOpen className="h-5 w-5" />}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col h-[calc(100vh-4rem)] bg-gradient-to-br from-green-50 to-blue-50 dark:from-green-950 dark:to-blue-950">
|
||||
{/* Header (Optional - can be simplified as it's part of the main layout) */}
|
||||
<header className="border-b bg-white dark:bg-gray-950 shadow-sm py-4 px-6 flex items-center gap-2">
|
||||
<MessageSquare className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
<h1 className="text-xl font-semibold">General Farming Assistant</h1>
|
||||
</header>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Chat area */}
|
||||
<div className="flex-1 flex flex-col h-full overflow-hidden">
|
||||
{/* Farm/Crop selector */}
|
||||
<div className="bg-white dark:bg-gray-900 p-4 border-b">
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<div className="flex-1">
|
||||
<label className="text-sm font-medium mb-1 block text-gray-700 dark:text-gray-300">
|
||||
Select Farm (Optional)
|
||||
</label>
|
||||
<Select value={selectedFarm || ""} onValueChange={handleFarmSelect}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="All Farms" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Farms</SelectItem>
|
||||
{mockFarms.map((farm) => (
|
||||
<SelectItem key={farm.id} value={farm.id}>
|
||||
{farm.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="text-sm font-medium mb-1 block text-gray-700 dark:text-gray-300">
|
||||
Select Crop (Optional)
|
||||
</label>
|
||||
<Select value={selectedCrop || ""} onValueChange={handleCropSelect} disabled={!selectedFarm}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={selectedFarm ? "All Crops" : "Select a farm first"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{selectedFarm && <SelectItem value="all">All Crops</SelectItem>}
|
||||
{filteredCrops.map((crop) => (
|
||||
<SelectItem key={crop.id} value={crop.id}>
|
||||
{crop.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<div className="space-y-4 max-w-3xl mx-auto">
|
||||
{messages.map((message) => (
|
||||
<div key={message.id} className={`flex ${message.sender === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-lg p-4 ${
|
||||
message.sender === "user"
|
||||
? "bg-green-600 text-white dark:bg-green-700"
|
||||
: "bg-white dark:bg-gray-800 border dark:border-gray-700 shadow-sm"
|
||||
}`}>
|
||||
{message.relatedTo && (
|
||||
<div className="mb-1">
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
{message.relatedTo.type === "farm" ? "🏡 " : "🌱 "}
|
||||
{message.relatedTo.name}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm">{message.content}</div>
|
||||
<div className="mt-1 text-xs opacity-70 text-right">
|
||||
{message.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="max-w-[80%] rounded-lg p-4 bg-white dark:bg-gray-800 border dark:border-gray-700 shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse delay-150"></div>
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse delay-300"></div>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 ml-1">
|
||||
ForFarm Assistant is typing...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Chat Area */}
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<div className="space-y-6 max-w-4xl mx-auto pb-4">
|
||||
{messages.map((message, i) => (
|
||||
<div
|
||||
key={message.id || i}
|
||||
className={`flex items-start gap-3 ${message.role === "user" ? "justify-end" : ""}`}>
|
||||
{message.role === "assistant" && (
|
||||
<Avatar className="h-8 w-8 border bg-white dark:bg-gray-700 flex-shrink-0">
|
||||
<AvatarFallback>
|
||||
<Bot className="h-5 w-5 text-green-600" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
<div
|
||||
className={`max-w-[80%] rounded-lg p-3 shadow-sm ${
|
||||
message.role === "user"
|
||||
? "bg-green-600 text-white dark:bg-green-700"
|
||||
: "bg-white dark:bg-gray-800 border dark:border-gray-700"
|
||||
}`}>
|
||||
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
|
||||
<p
|
||||
className={`mt-1 text-xs opacity-70 ${
|
||||
message.role === "user" ? "text-green-100" : "text-gray-500 dark:text-gray-400"
|
||||
} text-right`}>
|
||||
{message.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
||||
</p>
|
||||
</div>
|
||||
{message.role === "user" && (
|
||||
<Avatar className="h-8 w-8 border bg-gray-200 dark:bg-gray-600 flex-shrink-0">
|
||||
<AvatarFallback>
|
||||
<User className="h-5 w-5 text-gray-600 dark:text-gray-300" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Recommended prompts */}
|
||||
<div className="bg-white dark:bg-gray-900 border-t p-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-1">
|
||||
<Sparkles className="h-4 w-4 text-green-500" />
|
||||
Recommended Questions
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{recommendedPrompts.map((prompt) => (
|
||||
<Button
|
||||
key={prompt.id}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs rounded-full bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900/40 text-green-800 dark:text-green-300"
|
||||
onClick={() => handlePromptClick(prompt.text)}>
|
||||
{prompt.text}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input area */}
|
||||
<div className="bg-white dark:bg-gray-900 border-t p-4">
|
||||
<div className="flex gap-2 max-w-3xl mx-auto">
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
placeholder="Ask about your farm or crops..."
|
||||
className="flex-1"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => handleSendMessage()}
|
||||
disabled={!inputValue.trim() || isLoading}
|
||||
className="bg-green-600 hover:bg-green-700 text-white">
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat history sidebar */}
|
||||
{isHistoryOpen && (
|
||||
<div className="w-80 border-l bg-white dark:bg-gray-900 flex flex-col">
|
||||
<div className="p-4 border-b flex items-center justify-between">
|
||||
<h2 className="font-medium flex items-center gap-1">
|
||||
<History className="h-4 w-4" />
|
||||
Chat History
|
||||
</h2>
|
||||
<Button variant="ghost" size="icon" onClick={() => setIsHistoryOpen(false)}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500 dark:text-gray-400" />
|
||||
<Input placeholder="Search conversations..." className="pl-9" />
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Avatar className="h-8 w-8 border bg-white dark:bg-gray-700 flex-shrink-0">
|
||||
<AvatarFallback>
|
||||
<Bot className="h-5 w-5 text-green-600" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="max-w-[80%] rounded-lg p-3 bg-white dark:bg-gray-800 border dark:border-gray-700 shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse delay-150"></div>
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse delay-300"></div>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 ml-1">Assistant is thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<Tabs defaultValue="recent" className="flex-1 flex flex-col">
|
||||
<TabsList className="mx-3 mb-2">
|
||||
<TabsTrigger value="recent" className="flex-1">
|
||||
Recent
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="farms" className="flex-1">
|
||||
By Farm
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="crops" className="flex-1">
|
||||
By Crop
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
{/* Recommended prompts */}
|
||||
<div className="bg-white dark:bg-gray-900 border-t p-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-1">
|
||||
<Sparkles className="h-4 w-4 text-green-500" />
|
||||
Try asking...
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{recommendedPrompts.slice(0, 5).map(
|
||||
(
|
||||
prompt // Limit displayed prompts
|
||||
) => (
|
||||
<Button
|
||||
key={prompt.id}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs rounded-full bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900/40 text-green-800 dark:text-green-300"
|
||||
onClick={() => handlePromptClick(prompt.text)}>
|
||||
{prompt.text}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<TabsContent value="recent" className="m-0 p-0">
|
||||
<div className="space-y-1 p-2">
|
||||
{mockChatHistory
|
||||
.filter((msg) => msg.sender === "user")
|
||||
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
|
||||
.map((message) => (
|
||||
<Card
|
||||
key={message.id}
|
||||
className="p-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
onClick={() => handleLoadChatHistory(message.id)}>
|
||||
<div className="flex items-start gap-3">
|
||||
<Avatar className="h-8 w-8 bg-green-100 dark:bg-green-900">
|
||||
<div className="text-xs font-medium text-green-700 dark:text-green-300">
|
||||
{message.relatedTo?.name.substring(0, 2) || "Me"}
|
||||
</div>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{message.relatedTo ? message.relatedTo.name : "General Question"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{message.timestamp.toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-300 truncate mt-1">
|
||||
{message.content}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="farms" className="m-0 p-0">
|
||||
<div className="space-y-3 p-3">
|
||||
{mockFarms.map((farm) => (
|
||||
<div key={farm.id}>
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{farm.name}</h3>
|
||||
<div className="space-y-1">
|
||||
{mockChatHistory
|
||||
.filter(
|
||||
(msg) =>
|
||||
msg.sender === "user" && msg.relatedTo?.type === "farm" && msg.relatedTo.id === farm.id
|
||||
)
|
||||
.map((message) => (
|
||||
<Card
|
||||
key={message.id}
|
||||
className="p-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
onClick={() => handleLoadChatHistory(message.id)}>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-300 truncate">{message.content}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{message.timestamp.toLocaleDateString()}
|
||||
</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Separator className="my-3" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="crops" className="m-0 p-0">
|
||||
<div className="space-y-3 p-3">
|
||||
{mockCrops.map((crop) => (
|
||||
<div key={crop.id}>
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-1">
|
||||
<Leaf className="h-3 w-3 text-green-600" />
|
||||
{crop.name}
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
({mockFarms.find((f) => f.id === crop.farmId)?.name})
|
||||
</span>
|
||||
</h3>
|
||||
<div className="space-y-1">
|
||||
{mockChatHistory
|
||||
.filter(
|
||||
(msg) =>
|
||||
msg.sender === "user" && msg.relatedTo?.type === "crop" && msg.relatedTo.id === crop.id
|
||||
)
|
||||
.map((message) => (
|
||||
<Card
|
||||
key={message.id}
|
||||
className="p-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
onClick={() => handleLoadChatHistory(message.id)}>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-300 truncate">{message.content}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{message.timestamp.toLocaleDateString()}
|
||||
</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Separator className="my-3" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
{/* Input area */}
|
||||
<div className="bg-white dark:bg-gray-900 border-t p-4 sticky bottom-0">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}}
|
||||
className="flex gap-2 max-w-4xl mx-auto">
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
placeholder="Ask the farming assistant..."
|
||||
className="flex-1 h-11"
|
||||
disabled={isLoading}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!inputValue.trim() || isLoading}
|
||||
className="bg-green-600 hover:bg-green-700 text-white h-11 px-5">
|
||||
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
<span className="sr-only">Send</span>
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Send } from "lucide-react";
|
||||
import { Send, Bot, Loader2, User } from "lucide-react";
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import { useParams } from "next/navigation";
|
||||
import { sendChatMessage } from "@/api/chat";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
interface ChatbotDialogProps {
|
||||
@ -20,52 +25,158 @@ interface ChatbotDialogProps {
|
||||
}
|
||||
|
||||
export function ChatbotDialog({ open, onOpenChange, cropName }: ChatbotDialogProps) {
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
role: "assistant",
|
||||
content: `Hello! I'm your farming assistant. How can I help you with your ${cropName} today?`,
|
||||
},
|
||||
]);
|
||||
const params = useParams<{ farmId: string; cropId: string }>();
|
||||
const { farmId, cropId } = params;
|
||||
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false); // Loading state for API call
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim()) return;
|
||||
// Initialize with a welcome message when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setMessages([
|
||||
{
|
||||
id: `assistant-${Date.now()}`,
|
||||
role: "assistant",
|
||||
content: `Hello! How can I help you with ${cropName} (Crop ID: ${cropId}) today?`,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
// Reset input when opening
|
||||
setInput("");
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [open, cropName, cropId]); // Add dependencies
|
||||
|
||||
const newMessages: Message[] = [
|
||||
...messages,
|
||||
{ role: "user", content: input },
|
||||
{ role: "assistant", content: `Here's some information about ${cropName}: [AI response placeholder]` },
|
||||
];
|
||||
setMessages(newMessages);
|
||||
// Scroll to bottom of messages
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages, isLoading]); // Add isLoading to scroll when loading appears/disappears
|
||||
|
||||
const handleSend = async () => {
|
||||
const messageText = input.trim();
|
||||
if (!messageText || isLoading) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: `user-${Date.now()}`,
|
||||
role: "user",
|
||||
content: messageText,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
// Add user message immediately and clear input, set loading
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInput("");
|
||||
setIsLoading(true); // Show loading indicator *now*
|
||||
|
||||
// Prepare history for API call (use messages *before* adding the placeholder)
|
||||
const apiHistory = [...messages, userMessage] // Include the just-added user message
|
||||
.map((msg) => ({ role: msg.role, text: msg.content }));
|
||||
|
||||
try {
|
||||
// Call the API function with context
|
||||
const response = await sendChatMessage(
|
||||
userMessage.content,
|
||||
apiHistory,
|
||||
farmId, // Pass farmId
|
||||
cropId // Pass cropId
|
||||
);
|
||||
|
||||
const assistantMessage: Message = {
|
||||
id: `assistant-${Date.now()}`,
|
||||
role: "assistant",
|
||||
content: response.response,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
// Replace loading state with the actual response
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
} catch (error) {
|
||||
console.error("Error sending chat message:", error);
|
||||
const errorMessage: Message = {
|
||||
id: `error-${Date.now()}`,
|
||||
role: "assistant",
|
||||
content: `Sorry, something went wrong. ${(error as Error).message || "Please try again."}`,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
// Replace loading state with the error message
|
||||
setMessages((prev) => [...prev, errorMessage]);
|
||||
} finally {
|
||||
setIsLoading(false); // Hide loading indicator
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>Farming Assistant Chat</DialogTitle>
|
||||
<DialogTitle>Farming Assistant Chat for {cropName}</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
<DialogContent className="sm:max-w-[500px] p-0 dark:bg-background">
|
||||
<div className="flex flex-col h-[600px]">
|
||||
<div className="p-4 border-b dark:border-slate-700">
|
||||
<h2 className="text-lg font-semibold">Farming Assistant</h2>
|
||||
<p className="text-sm text-muted-foreground">Ask questions about your {cropName}</p>
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Bot className="h-5 w-5 text-green-600" />
|
||||
Farming Assistant
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">Ask about {cropName}</p>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<div className="space-y-4">
|
||||
{messages.map((message, i) => (
|
||||
<div key={i} className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||
{messages.map(
|
||||
(
|
||||
message // Render existing messages
|
||||
) => (
|
||||
<div
|
||||
className={`rounded-lg px-4 py-2 max-w-[80%] ${
|
||||
message.role === "user"
|
||||
? "bg-primary text-primary-foreground dark:bg-primary dark:text-primary-foreground"
|
||||
: "bg-muted dark:bg-muted dark:text-muted-foreground"
|
||||
}`}>
|
||||
{message.content}
|
||||
key={message.id}
|
||||
className={`flex items-start gap-3 ${message.role === "user" ? "justify-end" : ""}`}>
|
||||
{message.role === "assistant" && (
|
||||
<Avatar className="h-8 w-8 border bg-white dark:bg-gray-700 flex-shrink-0 shadow-sm">
|
||||
<AvatarFallback>
|
||||
<Bot className="h-5 w-5 text-green-600" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
<div
|
||||
className={`rounded-lg px-4 py-2 max-w-[80%] shadow-sm ${
|
||||
message.role === "user"
|
||||
? "bg-primary text-primary-foreground dark:bg-primary dark:text-primary-foreground"
|
||||
: "bg-muted dark:bg-muted dark:text-muted-foreground"
|
||||
}`}>
|
||||
<p className="text-sm whitespace-pre-wrap break-words">{message.content}</p>
|
||||
<p
|
||||
className={`mt-1 text-xs opacity-70 ${
|
||||
message.role === "user" ? "text-green-100" : "text-gray-500 dark:text-gray-400"
|
||||
} text-right`}>
|
||||
{message.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
||||
</p>
|
||||
</div>
|
||||
{message.role === "user" && (
|
||||
<Avatar className="h-8 w-8 border bg-gray-200 dark:bg-gray-600 flex-shrink-0 shadow-sm">
|
||||
<AvatarFallback>
|
||||
<User className="h-5 w-5 text-gray-600 dark:text-gray-300" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{/* Conditional Loading Indicator */}
|
||||
{isLoading && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Avatar className="h-8 w-8 border bg-white dark:bg-gray-700 flex-shrink-0">
|
||||
<AvatarFallback>
|
||||
<Bot className="h-5 w-5 text-green-600" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex items-center gap-2 p-2 rounded-lg bg-muted dark:bg-muted">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">Assistant is thinking...</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@ -76,9 +187,22 @@ export function ChatbotDialog({ open, onOpenChange, cropName }: ChatbotDialogPro
|
||||
handleSend();
|
||||
}}
|
||||
className="flex gap-2">
|
||||
<Input placeholder="Type your message..." value={input} onChange={(e) => setInput(e.target.value)} />
|
||||
<Button type="submit" size="icon">
|
||||
<Send className="h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Type your message..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
disabled={isLoading}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
className="h-11"
|
||||
/>
|
||||
<Button type="submit" size="icon" disabled={isLoading || !input.trim()} className="h-11 w-11">
|
||||
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
<span className="sr-only">Send</span>
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
141
frontend/components/ui/alert-dialog.tsx
Normal file
141
frontend/components/ui/alert-dialog.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^4.0.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
|
||||
@ -11,6 +11,9 @@ importers:
|
||||
'@hookform/resolvers':
|
||||
specifier: ^4.0.0
|
||||
version: 4.1.3(react-hook-form@7.54.2(react@19.0.0))
|
||||
'@radix-ui/react-alert-dialog':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@radix-ui/react-avatar':
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@ -476,6 +479,19 @@ packages:
|
||||
'@radix-ui/primitive@1.1.1':
|
||||
resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
|
||||
|
||||
'@radix-ui/react-alert-dialog@1.1.6':
|
||||
resolution: {integrity: sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==}
|
||||
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-arrow@1.1.2':
|
||||
resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==}
|
||||
peerDependencies:
|
||||
@ -2999,6 +3015,20 @@ snapshots:
|
||||
|
||||
'@radix-ui/primitive@1.1.1': {}
|
||||
|
||||
'@radix-ui/react-alert-dialog@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0)
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0)
|
||||
'@radix-ui/react-dialog': 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@radix-ui/react-slot': 1.1.2(@types/react@19.0.10)(react@19.0.0)
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.10
|
||||
'@types/react-dom': 19.0.4(@types/react@19.0.10)
|
||||
|
||||
'@radix-ui/react-arrow@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user