Compare commits

..

224 Commits

Author SHA1 Message Date
dependabot[bot]
2037848b4e build(deps): bump packaging from 24.2 to 25.0 in /application
Bumps [packaging](https://github.com/pypa/packaging) from 24.2 to 25.0.
- [Release notes](https://github.com/pypa/packaging/releases)
- [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/packaging/compare/24.2...25.0)

---
updated-dependencies:
- dependency-name: packaging
  dependency-version: '25.0'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 20:54:56 +00:00
Alex
50fbe3d5af Merge pull request #1798 from arc53/dependabot/pip/application/markdownify-1.1.0
build(deps): bump markdownify from 0.14.1 to 1.1.0 in /application
2025-05-19 14:01:13 +03:00
dependabot[bot]
8af9a5e921 build(deps): bump markdownify from 0.14.1 to 1.1.0 in /application
Bumps [markdownify](https://github.com/matthewwithanm/python-markdownify) from 0.14.1 to 1.1.0.
- [Release notes](https://github.com/matthewwithanm/python-markdownify/releases)
- [Commits](https://github.com/matthewwithanm/python-markdownify/compare/0.14.1...1.1.0)

---
updated-dependencies:
- dependency-name: markdownify
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-18 12:07:13 +00:00
Alex
9807788ecb Merge pull request #1795 from arc53/dependabot/pip/application/pypdf-5.5.0
build(deps): bump pypdf from 5.2.0 to 5.5.0 in /application
2025-05-18 15:05:57 +03:00
dependabot[bot]
5e2f329f15 build(deps): bump pypdf from 5.2.0 to 5.5.0 in /application
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 5.2.0 to 5.5.0.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/5.2.0...5.5.0)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 5.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-18 12:02:27 +00:00
Alex
9572a7adaa Merge pull request #1800 from arc53/dependabot/pip/application/boto3-1.38.18
build(deps): bump boto3 from 1.35.97 to 1.38.18 in /application
2025-05-18 15:00:45 +03:00
dependabot[bot]
1ba94f4f5f build(deps): bump boto3 from 1.35.97 to 1.38.18 in /application
Bumps [boto3](https://github.com/boto/boto3) from 1.35.97 to 1.38.18.
- [Release notes](https://github.com/boto/boto3/releases)
- [Commits](https://github.com/boto/boto3/compare/1.35.97...1.38.18)

---
updated-dependencies:
- dependency-name: boto3
  dependency-version: 1.38.18
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-18 11:54:08 +00:00
Alex
237afa0a3a Merge pull request #1799 from arc53/dependabot/pip/application/beautifulsoup4-4.13.4
build(deps): bump beautifulsoup4 from 4.12.3 to 4.13.4 in /application
2025-05-18 14:53:01 +03:00
Alex
9d8073d468 Merge pull request #1794 from arc53/remove-duplicate-docs
remove duplicate chat widget page
2025-05-16 12:45:59 +03:00
dependabot[bot]
3bf7f67adf build(deps): bump beautifulsoup4 from 4.12.3 to 4.13.4 in /application
Bumps [beautifulsoup4](https://www.crummy.com/software/BeautifulSoup/bs4/) from 4.12.3 to 4.13.4.

---
updated-dependencies:
- dependency-name: beautifulsoup4
  dependency-version: 4.13.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-15 20:15:17 +00:00
Pavel
594ce05292 remove duplicate chat widget page 2025-05-15 21:32:59 +04:00
Alex
fe02ca68d5 fix: update API key for DocsGPTWidget 2025-05-15 18:18:12 +01:00
Alex
21ef27ee9b Merge pull request #1791 from arc53/fix-openai-conflict
fix for OPENAI_BASE_URL + ollama can't connect to container
2025-05-15 19:04:21 +03:00
Alex
09d37f669f Merge pull request #1793 from arc53/prompts-update
refactor: enhance AI assistant prompts for clarity and creativity
2025-05-15 16:19:32 +03:00
Pavel
416b776062 redundant f string 2025-05-15 16:57:41 +04:00
Alex
5ed05d4020 refactor: enhance AI assistant prompts for clarity and creativity 2025-05-15 13:30:18 +01:00
Alex
4004bfb5ef Merge pull request #1792 from arc53/read_webpage_tools
feat: add ReadWebpageTool for fetching and converting webpage content…
2025-05-15 15:14:02 +03:00
Alex
45aace8966 feat: add ReadWebpageTool for fetching and converting webpage content to Markdown 2025-05-15 12:56:06 +01:00
Alex
d9fc623dcb Merge pull request #1790 from ManishMadan2882/setup-fix
(fix:dockercompose) volumes, user
2025-05-15 13:37:11 +03:00
Pavel
dbb822f6b0 fix for OPENAI_BASE_URL + ollama can't connect to container
- fix for OpenAI trying to use base_url=""
- fix for ollama container error:
`Error code: 404 - {'error': {'message': 'model "MODEL_NAME" not found, try pulling it first', 'type': 'api_error', 'param': None, 'code': None}}`
2025-05-15 13:50:08 +04:00
ManishMadan2882
3d64dffc32 (fix:dockercompose) volumes, user 2025-05-15 15:19:34 +05:30
Alex
b2809b2e9a remove old deps 2025-05-14 22:03:27 +01:00
Alex
29e89d2965 Merge pull request #1785 from arc53/dependabot/pip/application/yarl-1.20.0
build(deps): bump yarl from 1.18.3 to 1.20.0 in /application
2025-05-14 23:55:09 +03:00
dependabot[bot]
e7d54a639e build(deps): bump yarl from 1.18.3 to 1.20.0 in /application
Bumps [yarl](https://github.com/aio-libs/yarl) from 1.18.3 to 1.20.0.
- [Release notes](https://github.com/aio-libs/yarl/releases)
- [Changelog](https://github.com/aio-libs/yarl/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/yarl/compare/v1.18.3...v1.20.0)

---
updated-dependencies:
- dependency-name: yarl
  dependency-version: 1.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-14 20:50:26 +00:00
Alex
22df98e9bb Merge pull request #1782 from arc53/dependabot/pip/application/prompt-toolkit-3.0.51
build(deps): bump prompt-toolkit from 3.0.50 to 3.0.51 in /application
2025-05-14 23:49:14 +03:00
dependabot[bot]
0d45c44c6f build(deps): bump prompt-toolkit from 3.0.50 to 3.0.51 in /application
Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.50 to 3.0.51.
- [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases)
- [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/main/CHANGELOG)
- [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.50...3.0.51)

---
updated-dependencies:
- dependency-name: prompt-toolkit
  dependency-version: 3.0.51
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-14 20:47:44 +00:00
Alex
63c6912841 lazy load elasticsearch 2025-05-14 21:45:30 +01:00
Alex
73bce73034 Merge pull request #1786 from arc53/dependabot/pip/application/flask-3.1.1
build(deps): bump flask from 3.1.0 to 3.1.1 in /application
2025-05-14 23:40:58 +03:00
Alex
8babb6e68f Merge pull request #1787 from siiddhantt/refactor/agents
refactor: handle empty sources and chunks in agent
2025-05-14 14:39:01 +03:00
Siddhant Rai
d1d28df8a1 fix: handle empty chunks and retriever values in agent creation and update 2025-05-14 09:26:36 +05:30
dependabot[bot]
23bfd4683c build(deps): bump flask from 3.1.0 to 3.1.1 in /application
Bumps [flask](https://github.com/pallets/flask) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.1.0...3.1.1)

---
updated-dependencies:
- dependency-name: flask
  dependency-version: 3.1.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 20:32:17 +00:00
Alex
a52a3e3158 Merge pull request #1750 from arc53/dependabot/pip/application/packaging-25.0
build(deps): bump packaging from 24.1 to 25.0 in /application
2025-05-13 18:34:56 +03:00
Alex
44e524e3c3 build(deps): update langchain and openai dependencies 2025-05-13 16:29:24 +01:00
dependabot[bot]
9a430f73e2 build(deps): bump packaging from 24.1 to 25.0 in /application
Bumps [packaging](https://github.com/pypa/packaging) from 24.1 to 25.0.
- [Release notes](https://github.com/pypa/packaging/releases)
- [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/packaging/compare/24.1...25.0)

---
updated-dependencies:
- dependency-name: packaging
  dependency-version: '25.0'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 15:10:29 +00:00
Alex
fdea40ec11 Merge pull request #1735 from arc53/dependabot/pip/application/referencing-0.36.2
build(deps): bump referencing from 0.30.2 to 0.36.2 in /application
2025-05-13 18:09:10 +03:00
Alex
526d340849 fix: stale deps 2025-05-13 16:03:48 +01:00
dependabot[bot]
fe95f6ad81 build(deps): bump referencing from 0.30.2 to 0.36.2 in /application
Bumps [referencing](https://github.com/python-jsonschema/referencing) from 0.30.2 to 0.36.2.
- [Release notes](https://github.com/python-jsonschema/referencing/releases)
- [Changelog](https://github.com/python-jsonschema/referencing/blob/main/docs/changes.rst)
- [Commits](https://github.com/python-jsonschema/referencing/compare/v0.30.2...v0.36.2)

---
updated-dependencies:
- dependency-name: referencing
  dependency-version: 0.36.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 13:20:27 +00:00
Alex
39e73c37ab Merge pull request #1576 from arc53/dependabot/pip/application/portalocker-3.1.1
build(deps): bump portalocker from 2.10.1 to 3.1.1 in /application
2025-05-13 16:06:44 +03:00
Alex
39b36b6857 Feat: Add MD gen script, enable Qdrant lazy loading 2025-05-13 14:03:05 +01:00
dependabot[bot]
44e98748c5 build(deps): bump portalocker from 2.10.1 to 3.1.1 in /application
Bumps [portalocker](https://github.com/wolph/portalocker) from 2.10.1 to 3.1.1.
- [Release notes](https://github.com/wolph/portalocker/releases)
- [Changelog](https://github.com/wolph/portalocker/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/wolph/portalocker/compare/v2.10.1...v3.1.1)

---
updated-dependencies:
- dependency-name: portalocker
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 12:44:30 +00:00
Alex
8a7aeee955 Merge pull request #1751 from arc53/dependabot/pip/application/torch-2.7.0
build(deps): bump torch from 2.5.1 to 2.7.0 in /application
2025-05-13 15:41:10 +03:00
dependabot[bot]
1c7befb8d3 build(deps): bump torch from 2.5.1 to 2.7.0 in /application
Bumps [torch](https://github.com/pytorch/pytorch) from 2.5.1 to 2.7.0.
- [Release notes](https://github.com/pytorch/pytorch/releases)
- [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md)
- [Commits](https://github.com/pytorch/pytorch/compare/v2.5.1...v2.7.0)

---
updated-dependencies:
- dependency-name: torch
  dependency-version: 2.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 12:20:22 +00:00
Alex
d5d59ac62c Merge pull request #1759 from arc53/dependabot/pip/application/transformers-4.51.3
build(deps): bump transformers from 4.49.0 to 4.51.3 in /application
2025-05-13 15:19:18 +03:00
Alex
562f0762a0 Merge pull request #1775 from siiddhantt/feat/enhance-agents
feat: share and pin agents
2025-05-13 11:54:29 +03:00
Siddhant Rai
e46aedce21 Merge branch 'feat/enhance-agents' of https://github.com/siiddhantt/DocsGPT into feat/enhance-agents 2025-05-13 13:05:50 +05:30
Siddhant Rai
57cc09b1d7 feat: update tool display name and improve SharedAgent component layout 2025-05-13 13:05:20 +05:30
Alex
cbfa5a5118 Delete .jwt_secret_key 2025-05-12 15:11:42 +03:00
Siddhant Rai
ec7f14b82d Merge branch 'main' into feat/enhance-agents 2025-05-12 13:45:18 +05:30
Siddhant Rai
6520be5b85 feat: shared and pinning agents + fix for streaming tools 2025-05-12 06:06:11 +05:30
Alex
d84c416421 Merge pull request #1774 from arc53/limit-yield-on-tools
fix: truncate tool call results to 50 characters for cleaner output
2025-05-10 00:53:28 +03:00
Alex
32803c89a3 fix: truncate tool call results to 50 characters for cleaner output 2025-05-09 22:52:17 +01:00
Alex
a86bcb5c29 Merge pull request #1773 from arc53/cols-agent-fix
fix:(style) update layout for agent list and card components
2025-05-10 00:21:05 +03:00
Alex
7d76a33790 fix:(style) update layout for agent list and card components 2025-05-09 22:15:55 +01:00
Alex
24bb2e469d Merge pull request #1772 from ManishMadan2882/main
Bug Fixes
2025-05-09 11:01:01 +03:00
ManishMadan2882
e1aa2cc0b8 (fix:ingestion) store file name as metadata, not path 2025-05-09 02:26:35 +05:30
ManishMadan2882
d073947f3b Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-05-08 16:11:14 +05:30
ManishMadan2882
3243740dd1 (fix-bubble) inconsistent width with snippets 2025-05-08 16:10:31 +05:30
Alex
f9bd566a3b Merge pull request #1768 from ManishMadan2882/main
Attachments : File storage changes
2025-05-08 01:41:08 +03:00
Alex
183251487c lint: remove unused import of 'io' in worker.py 2025-05-07 23:37:35 +01:00
ManishMadan2882
ff532210f7 lint 2025-05-08 00:15:28 +05:30
ManishMadan2882
d0a04d9801 (fix/lint) empty methods 2025-05-08 00:14:42 +05:30
ManishMadan2882
ea6533db4e (fix:attachment) strictly use filename 2025-05-08 00:11:02 +05:30
ManishMadan2882
89d5e7bee5 (feat:attachment) store file in endpoint layer 2025-05-07 19:15:36 +05:30
Alex
7e6cdee592 Merge pull request #1732 from asminkarki012/feature/mermaid-integration
feat[mermaid]:integration of mermaid
2025-05-07 11:52:50 +03:00
ManishMadan2882
990c2fb416 Merge branch 'main' into 'feature/mermaid-integration' 2025-05-07 03:44:47 +05:30
ManishMadan2882
09e054c6aa (fix/scroll) bring back arrowDown button, smoother scroll 2025-05-07 02:48:49 +05:30
ManishMadan2882
23f648f53a (feat/mermaid) zoom as per the requirement 2025-05-07 02:07:57 +05:30
Siddhant Rai
07fa656e7c feat: implement pinning functionality for agents with UI updates 2025-05-06 16:12:55 +05:30
Alex
7858c48f11 fix: zip file uploads 2025-05-06 11:12:26 +01:00
Alex
e56d54c3f0 fix: improve source and description handling in GetAgent and GetAgents responses 2025-05-06 10:59:25 +01:00
ManishMadan2882
f37ca95c10 (fix/mermaid loading): load only the diagrams which stream 2025-05-06 15:16:14 +05:30
ManishMadan2882
72e51bb072 (feat:mermaid) dont pass isDarkTheme 2025-05-06 04:38:38 +05:30
Alex
dcfcbf54be Merge pull request #1767 from arc53/sources-icon-fix-agent-menu
fix: sources icon mini fix
2025-05-06 01:36:29 +03:00
Alex
204936b2d0 fix: sources icon mini fix 2025-05-05 23:34:13 +01:00
ManishMadan2882
98856b39ac (feat:mermaid) zoom onhover, throw syntax errors 2025-05-06 00:53:33 +05:30
Alex
ad5f707486 lint: ruff fix 2025-05-05 18:03:45 +01:00
Alex
5ecfb0ce6d fix: enhance error logging 2025-05-05 17:59:37 +01:00
Alex
2147b3f06f lint: mini fix 2025-05-05 13:14:56 +01:00
Alex
7daed3daaf Merge pull request #1764 from arc53/feat/better-logs
fix: enhance error logging with exception info across multiple modules
2025-05-05 15:13:21 +03:00
Alex
481df4d604 fix: enhance error logging with exception info across multiple modules 2025-05-05 13:12:39 +01:00
Alex
cf333873fd fix: json body 2025-05-05 00:08:56 +01:00
Alex
ae700e8f3a fix: display only 2 demos buttons on mobile 2025-05-04 18:56:33 +01:00
ManishMadan2882
16386a9524 (feat:mermaid) zoom on hover 2025-05-04 19:39:24 +05:30
ManishMadan2882
7e7ce276b2 (fix:mermaid/flicker) separated from markdown 2025-05-02 14:12:24 +05:30
Alex
71c6b41b83 Merge pull request #1762 from arc53/dartpain-patch-2
Update README.md
2025-05-01 17:19:10 +03:00
Alex
4b2faae29a Update README.md 2025-05-01 17:15:08 +03:00
ManishMadan2882
7e28e562d0 (fix:mermaid) download svg/png 2025-05-01 19:37:03 +05:30
dependabot[bot]
93c2e2a597 build(deps): bump transformers from 4.49.0 to 4.51.3 in /application
Bumps [transformers](https://github.com/huggingface/transformers) from 4.49.0 to 4.51.3.
- [Release notes](https://github.com/huggingface/transformers/releases)
- [Commits](https://github.com/huggingface/transformers/compare/v4.49.0...v4.51.3)

---
updated-dependencies:
- dependency-name: transformers
  dependency-version: 4.51.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-29 20:50:32 +00:00
Alex
c45d13d834 Merge pull request #1755 from siiddhantt/feat/agent-menu
feat: agent webhook and minor fixes
2025-04-29 00:41:36 +03:00
Alex
330276cdf7 fix: lint for ruff 2025-04-28 22:32:13 +01:00
Siddhant Rai
22c7015c69 refactor: webhook listener handle both POST and GET requests 2025-04-29 00:29:16 +05:30
Alex
cc67d4a1e2 process all request data implicitly 2025-04-28 17:49:29 +01:00
Siddhant Rai
eeb9da696f Merge remote-tracking branch 'upstream/main' into feat/agent-menu 2025-04-28 17:01:46 +05:30
Siddhant Rai
4979e1ac9a feat: add clsx dependency, enhance logging in agent logic, and improve agent logs component 2025-04-28 14:18:28 +05:30
ManishMadan2882
545353dabf (feat:mermaid) use contentLoaded to render 2025-04-27 03:42:28 +05:30
ManishMadan2882
545376740c (fix:re-render) useRef to check for bottom 2025-04-26 19:33:24 +05:30
Siddhant Rai
8289b02ab0 feat: add agent webhook endpoint and implement related functionality 2025-04-26 12:00:29 +05:30
ManishMadan2882
fc0060662b (fix:mermaid) suppress mermaid injected errors in DOM 2025-04-25 21:18:49 +05:30
Alex
df9d432d29 fix: mongo db database name in settings 2025-04-24 17:29:41 +01:00
Alex
76fd6e15cc Update Dockerfile 2025-04-24 18:54:58 +03:00
Alex
06982efda5 Merge pull request #1742 from ManishMadan2882/main
File System Abstraction
2025-04-24 01:32:27 +03:00
Alex
3cd9a72495 add storage type to the settings cofig 2025-04-23 23:13:39 +01:00
ManishMadan2882
0ce27f274a (feat:storage) file indexes/faiss 2025-04-23 04:28:45 +05:30
ManishMadan2882
e60f78ac4a (feat:storage) file uploads 2025-04-23 03:39:35 +05:30
ManishMadan2882
637d3a24a1 Revert "(feat:storage) file, indexes uploads"
This reverts commit 64c42f0ddf.
2025-04-23 00:52:55 +05:30
ManishMadan2882
24c8b24b1f Revert "(fix:indexes) look for the right path"
This reverts commit 5ad34e2216.
2025-04-23 00:52:22 +05:30
ManishMadan2882
5ad34e2216 (fix:indexes) look for the right path 2025-04-22 17:34:25 +05:30
ManishMadan2882
64c42f0ddf (feat:storage) file, indexes uploads 2025-04-22 05:18:07 +05:30
ManishMadan2882
0a31ddaae6 (feat:storage) use get storage 2025-04-22 01:41:53 +05:30
ManishMadan2882
38476cfeb8 (gfeat:storage) get storage instance based on settings 2025-04-22 00:57:57 +05:30
asminkarki012
decc31f1f0 update latest changes 2025-04-21 21:05:12 +05:45
asminkarki012
ea0aa64330 fix: added renderer in answer bubble 2025-04-21 20:50:04 +05:45
Manish Madan
e9a6044645 Merge branch 'main' into main 2025-04-20 16:01:13 +05:30
Alex
474d700df2 Merge pull request #1745 from arc53/dependabot/pip/application/google-generativeai-0.8.5
build(deps): bump google-generativeai from 0.8.3 to 0.8.5 in /application
2025-04-20 01:39:11 +03:00
ManishMadan2882
c50ff6faa3 (feat:fs abstract) googleLLM class 2025-04-18 21:03:28 +05:30
ManishMadan2882
c8efef8f04 (fix:openai) image uplads, use lambda in process_files 2025-04-18 18:27:02 +05:30
dependabot[bot]
1d22f77568 build(deps): bump google-generativeai in /application
Bumps [google-generativeai](https://github.com/google/generative-ai-python) from 0.8.3 to 0.8.5.
- [Release notes](https://github.com/google/generative-ai-python/releases)
- [Changelog](https://github.com/google-gemini/deprecated-generative-ai-python/blob/main/RELEASE.md)
- [Commits](https://github.com/google/generative-ai-python/compare/v0.8.3...v0.8.5)

---
updated-dependencies:
- dependency-name: google-generativeai
  dependency-version: 0.8.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-17 20:06:10 +00:00
ManishMadan2882
5aa51f5f36 (feat:file_abstract) openai attachments comply 2025-04-18 01:27:21 +05:30
ManishMadan2882
335c21c48a (fix:attachment) dont calculate MIME again 2025-04-17 16:36:40 +05:30
ManishMadan2882
c35d1cecfe (feat:file_abstract) return storage metadata after upload 2025-04-17 16:29:34 +05:30
ManishMadan2882
0d3e6157cd (feat:attachmentUpload) parse content before upload 2025-04-17 16:23:01 +05:30
ManishMadan2882
68e4cf4d14 (feat:fsabstract) add factory class 2025-04-17 02:40:53 +05:30
ManishMadan2882
9454150f7d (fix:s3) processor func 2025-04-17 02:36:55 +05:30
ManishMadan2882
0a0e16547e (feat:fs_abstract) attachment uploads 2025-04-17 02:35:45 +05:30
Alex
0aec1b9969 Merge pull request #1739 from siiddhantt/feat/agent-menu
feat: new agents section
2025-04-16 17:50:11 +03:00
Alex
3e1ec23409 fix: lint 2025-04-16 15:47:39 +01:00
Siddhant Rai
2f9f428a2f feat: add prompt management functionality with modal integration 2025-04-16 19:41:16 +05:30
Siddhant Rai
da15cde49c Merge branch 'main' into feat/agent-menu 2025-04-16 18:35:38 +05:30
Siddhant Rai
e6ed37139a feat: implement agent preview functionality and enhance conversation handling 2025-04-16 16:26:47 +05:30
ManishMadan2882
377e33c148 (feat:file_abstract) process files method 2025-04-16 03:36:45 +05:30
ManishMadan2882
e567d88951 ((feat:fs_abstact) s3 2025-04-16 03:31:42 +05:30
ManishMadan2882
89b2937b11 ((feat:fs_abstact) local 2025-04-16 03:31:28 +05:30
ManishMadan2882
142ed75468 ((feat:fs_abstact) base 2025-04-16 03:31:06 +05:30
Siddhant Rai
d80eeb044c feat: add agent timestamps and improve agent retrieval logic 2025-04-15 14:06:20 +05:30
Siddhant Rai
7c69e99914 feat: Enhance agent selection and conversation handling
- Added functionality to select agents in the Navigation component, allowing users to reset conversations and set the selected agent.
- Updated the MessageInput component to conditionally show source and tool buttons based on the selected agent.
- Modified the Conversation component to handle agent-specific queries and manage file uploads.
- Improved conversation fetching logic to include agent IDs and handle attachments.
- Introduced new types for conversation summaries and results to streamline API responses.
- Refactored Redux slices to manage selected agent state and improve overall state management.
- Enhanced error handling and loading states across components for better user experience.
2025-04-15 11:53:53 +05:30
Alex
5e1aaf5a44 Merge pull request #1733 from ManishMadan2882/main
Attachments: Enhancements , strategy specific to certain LLMs
2025-04-15 01:55:46 +03:00
Alex
ad610d2f90 fix: lint ruff 2025-04-14 23:49:40 +01:00
Alex
02934452d6 fix: remove comment 2025-04-14 23:48:17 +01:00
ManishMadan2882
8b054010e1 (Feat:input) sleek attachment pills 2025-04-15 03:30:11 +05:30
Alex
5b77f3839b fix: maybe 2025-04-14 20:24:05 +01:00
Alex
231b792452 fix: streaming with tools google and openai halfway 2025-04-14 18:54:40 +01:00
ManishMadan2882
b468e0c164 (fix:generation) attach + tools 2025-04-13 18:33:26 +05:30
Siddhant Rai
fa1f9d7009 feat: agents route replacing chatbots
- Removed API Keys tab from SettingsBar and adjusted tab layout.
- Improved styling for tab scrolling buttons and gradient indicators.
- Introduced AgentDetailsModal for displaying agent access details.
- Updated Analytics component to fetch agent data and handle analytics for selected agent.
- Refactored Logs component to accept agentId as a prop for filtering logs.
- Enhanced type definitions for InputProps to include textSize.
- Cleaned up unused imports and optimized component structure across various files.
2025-04-11 17:24:22 +05:30
asminkarki012
c5a8f3abcd refact:generate unique meraid id using crypto.randomUUID 2025-04-11 11:48:07 +05:45
ManishMadan2882
dfe6a8d3e3 (feat:attach) simplify the format for files 2025-04-11 01:53:17 +05:30
ManishMadan2882
292257770c (refactor:attach) centralize attachment state 2025-04-10 03:02:39 +05:30
ManishMadan2882
b4c6b2b08b (feat:utils) getOS, isTouchDevice 2025-04-10 01:44:49 +05:30
ManishMadan2882
6cb4577e1b (feat:input) hotkey for sources open 2025-04-10 01:43:46 +05:30
ManishMadan2882
456784db48 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-04-10 01:29:52 +05:30
ManishMadan2882
dd9ea46e58 (feat:attach) strategy specific to google genai 2025-04-10 01:29:01 +05:30
GH Action - Upstream Sync
ed3af2fac0 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-04-09 01:27:51 +00:00
Alex
02f8132f3a Update README.md 2025-04-08 15:56:34 +03:00
ManishMadan2882
55bd90fad9 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-04-08 17:51:27 +05:30
ManishMadan2882
cd7bbb45c3 (fix:popups) minor hover 2025-04-08 17:51:18 +05:30
Alex
6c7fc0ed22 Merge pull request #1729 from arc53/setup-windows
win-setup
2025-04-08 13:58:28 +03:00
ManishMadan2882
5421bc1386 (feat:attach) extend support for imgs 2025-04-08 15:51:37 +05:30
GH Action - Upstream Sync
051841e566 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-04-08 01:27:09 +00:00
ManishMadan2882
0c68815cf2 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-04-07 20:16:12 +05:30
ManishMadan2882
0c1138179b (feat:attch) store file mime type 2025-04-07 20:16:03 +05:30
ManishMadan2882
1f3d1cc73e (feat:attach) handle unsupported attachments 2025-04-07 20:15:11 +05:30
Alex
707d1332de Merge pull request #1734 from arc53/fix-thought-param
fix: thought param in /api/answer
2025-04-07 13:08:23 +03:00
Alex
f6c88da81b fix: thought param in /api/answer 2025-04-07 11:03:49 +01:00
GH Action - Upstream Sync
a651e6e518 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-04-07 01:37:46 +00:00
Alex
bea89b93eb Merge pull request #1730 from arc53/dependabot/pip/application/multidict-6.3.2
build(deps): bump multidict from 6.1.0 to 6.3.2 in /application
2025-04-06 22:00:31 +03:00
ManishMadan2882
244c9b96a2 (fix:attach) pass attachment docs as it is 2025-04-06 16:02:30 +05:30
ManishMadan2882
a37bd76950 (feat:storeAttach) store in inputs, raise errors from worker 2025-04-06 16:01:57 +05:30
ManishMadan2882
9d70032de8 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-04-06 15:57:26 +05:30
ManishMadan2882
e4945b41e9 (feat:files) link attachment to openai_api 2025-04-06 15:57:18 +05:30
Pavel
493dc8689c guide-updates 2025-04-05 17:49:56 +04:00
GH Action - Upstream Sync
bdac2ffa27 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-04-05 01:25:36 +00:00
asminkarki012
b1235f3ce0 feat[mermaid]:clean comment 2025-04-04 18:40:57 +05:45
asminkarki012
ba4bb63a1f feat[mermaid]:integration of mermaid 2025-04-04 18:36:45 +05:45
Alex
3227b0e69c Merge pull request #1722 from asminkarki012/fix/csv-parser-include-headers
fix[csv_parser]:missing header
2025-04-04 14:53:45 +03:00
ManishMadan2882
29c899627e Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-04-04 03:31:45 +05:30
ManishMadan2882
5923781484 (feat:attach) warning for error, progress, removal 2025-04-04 03:29:01 +05:30
dependabot[bot]
8bb263a2ec build(deps): bump multidict from 6.1.0 to 6.3.2 in /application
Bumps [multidict](https://github.com/aio-libs/multidict) from 6.1.0 to 6.3.2.
- [Release notes](https://github.com/aio-libs/multidict/releases)
- [Changelog](https://github.com/aio-libs/multidict/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/multidict/compare/v6.1.0...v6.3.2)

---
updated-dependencies:
- dependency-name: multidict
  dependency-version: 6.3.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 20:24:09 +00:00
Alex
94c7bba168 Merge pull request #1716 from ManishMadan2882/main
Sources + Tools
2025-04-03 02:03:47 +03:00
ManishMadan2882
f9ad4c068a (feat:attach) fallback strategy to process docs 2025-04-03 03:26:37 +05:30
ManishMadan2882
19d68252cd (fix/attach): inputs are created in application 2025-04-02 16:36:58 +05:30
ManishMadan2882
72bbe3b1ce Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-04-02 15:57:45 +05:30
ManishMadan2882
856824316b (feat: attach) handle them locally from message input 2025-04-02 15:33:35 +05:30
ManishMadan2882
95e189d1d8 (feat:base/agents) default attachment 2025-04-02 15:29:04 +05:30
ManishMadan2882
c629460acb (feat:attach) extract contents in endpoint layer 2025-04-02 15:21:33 +05:30
ManishMadan2882
f235a94986 (feat:attach) pass attachments for generation 2025-04-02 15:14:56 +05:30
GH Action - Upstream Sync
632cba86e9 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-04-02 01:36:06 +00:00
Alex
6b92c7eccc Update README.md 2025-04-01 12:27:55 +03:00
Alex
ab0da1abac Merge pull request #1721 from siiddhantt/feat/react-agent
feat: ReActAgent and agent refactor
2025-04-01 12:08:09 +03:00
Siddhant Rai
7f31ac7bcb refactor: minor changes 2025-04-01 12:33:43 +05:30
Pavel
57a6fb31b2 periodic header injection 2025-03-31 22:28:04 +04:00
Siddhant Rai
fd2b6c111c feat: enhance ClassicAgent and ReActAgent with tool preparation steps 2025-03-31 17:02:36 +05:30
GH Action - Upstream Sync
302458b505 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-30 01:40:32 +00:00
GH Action - Upstream Sync
8978a4cf2d Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-29 01:25:46 +00:00
asminkarki012
c70be12bfd fix[csv_parser]:missing header 2025-03-28 22:46:11 +05:45
ManishMadan2882
4241307990 (fix:responsive/messageInput) sleek badge buttons, aligned send btn 2025-03-28 20:42:12 +05:30
ManishMadan2882
727a8ef13d (fix:responsive) tools and source pop 2025-03-28 20:40:46 +05:30
ManishMadan2882
7c92558ad1 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-03-28 18:13:42 +05:30
ManishMadan2882
45083d29a6 (feat:attach) show files on conversations 2025-03-28 18:13:24 +05:30
ManishMadan2882
5089d86095 (feat:attach) send attachment ids 2025-03-28 18:12:38 +05:30
ManishMadan2882
80e55ef385 (feat:attach) functionality to upload files 2025-03-28 18:10:18 +05:30
GH Action - Upstream Sync
b5ed98445f Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-28 01:26:07 +00:00
Siddhant Rai
82d377abf5 feat: add support for thought processing in conversation flow and introduce ReActAgent 2025-03-27 23:19:08 +05:30
ManishMadan2882
2dbea5d1b2 Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-03-27 16:55:49 +05:30
ManishMadan2882
4ba35d6189 (feat: attachment) integrate upload on fe 2025-03-27 16:54:21 +05:30
GH Action - Upstream Sync
cec3f987f2 Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-27 01:26:14 +00:00
ManishMadan2882
55050a9f58 (feat:attachment) upload single file 2025-03-27 03:28:03 +05:30
ManishMadan2882
502dc9ec52 (feat:attachments) store and ingest files shared 2025-03-26 18:01:31 +05:30
ManishMadan2882
9c8999a3ae (fix:sources) doc selection for preLoaded 2025-03-25 19:04:31 +05:30
ManishMadan2882
90db42ce3a Revert "(fix:sources) preloaded docs have ids too"
This reverts commit 02c8bd06f5.
2025-03-25 16:08:59 +05:30
ManishMadan2882
551130f0e1 (feat:sources/tools) ui perfection for pop-ups 2025-03-25 03:22:21 +05:30
ManishMadan2882
2940a60b3c (faet:input) tools pop-up 2025-03-24 17:16:24 +05:30
ManishMadan2882
76b9bc0d56 (feat:input) sources pop-up 2025-03-24 17:15:57 +05:30
ManishMadan2882
42422ccdcd (feat:settings) routes for tab 2025-03-24 13:56:12 +05:30
ManishMadan2882
e9702ae2de Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-03-21 23:57:09 +05:30
ManishMadan2882
5c54852ebe (fix:tools) no tools placeholder 2025-03-21 23:56:47 +05:30
GH Action - Upstream Sync
718a86ecda Merge branch 'main' of https://github.com/arc53/DocsGPT 2025-03-21 01:26:21 +00:00
Pavel
1223fd2149 win-setup 2025-03-20 21:48:59 +03:00
ManishMadan2882
b09386d102 (clean:nav) rm source dropdown 2025-03-20 11:26:45 +05:30
ManishMadan2882
6464698b6d Merge branch 'main' of https://github.com/manishmadan2882/docsgpt 2025-03-20 09:47:38 +05:30
ManishMadan2882
9230fd3bd6 Revert "(feat:nav) clean-up source dropdown"
This reverts commit 561a125c92.
2025-03-20 09:47:29 +05:30
ManishMadan2882
7771609ea0 (locales) udpate placeholder 2025-03-20 09:43:36 +05:30
ManishMadan2882
561a125c92 (feat:nav) clean-up source dropdown 2025-03-20 09:42:57 +05:30
ManishMadan2882
7149461d8e (feat:sources) add pop-up to switch sources 2025-03-20 09:42:08 +05:30
ManishMadan2882
02c8bd06f5 (fix:sources) preloaded docs have ids too 2025-03-20 09:39:30 +05:30
ManishMadan2882
9f17eb1d28 feat(textInput) new design 2025-03-18 19:33:26 +05:30
156 changed files with 10978 additions and 3230 deletions

1
.gitignore vendored
View File

@@ -113,6 +113,7 @@ venv.bak/
# Spyder project settings
.spyderproject
.spyproject
.jwt_secret_key
# Rope project settings
.ropeproject

View File

@@ -48,10 +48,13 @@
- [x] Add tools (Jan 2025)
- [x] Manually updating chunks in the app UI (Feb 2025)
- [x] Devcontainer for easy development (Feb 2025)
- [ ] Anthropic Tool compatibility
- [ ] Add triggerable actions / tools (webhook)
- [x] ReACT agent (March 2025)
- [x] Chatbots menu re-design to handle tools, agent types, and more (April 2025)
- [x] New input box in the conversation menu (April 2025)
- [x] Add triggerable actions / tools (webhook) (April 2025)
- [ ] Anthropic Tool compatibility (May 2025)
- [ ] Add OAuth 2.0 authentication for tools and sources
- [ ] Chatbots menu re-design to handle tools, scheduling, and more
- [ ] Agent scheduling
You can find our full roadmap [here](https://github.com/orgs/arc53/projects/2). Please don't hesitate to contribute or create issues, it helps us improve DocsGPT!
@@ -92,13 +95,15 @@ A more detailed [Quickstart](https://docs.docsgpt.cloud/quickstart) is available
./setup.sh
```
This interactive script will guide you through setting up DocsGPT. It offers four options: using the public API, running locally, connecting to a local inference engine, or using a cloud API provider. The script will automatically configure your `.env` file and handle necessary downloads and installations based on your chosen option.
**For Windows:**
2. **Follow the Docker Deployment Guide:**
2. **Run the PowerShell setup script:**
Please refer to the [Docker Deployment documentation](https://docs.docsgpt.cloud/Deploying/Docker-Deploying) for detailed step-by-step instructions on setting up DocsGPT using Docker.
```powershell
PowerShell -ExecutionPolicy Bypass -File .\setup.ps1
```
Either script will guide you through setting up DocsGPT. Four options available: using the public API, running locally, connecting to a local inference engine, or using a cloud API provider. Scripts will automatically configure your `.env` file and handle necessary downloads and installations based on your chosen option.
**Navigate to http://localhost:5173/**
@@ -107,7 +112,7 @@ To stop DocsGPT, open a terminal in the `DocsGPT` directory and run:
```bash
docker compose -f deployment/docker-compose.yaml down
```
(or use the specific `docker compose down` command shown after running `setup.sh`).
(or use the specific `docker compose down` command shown after running the setup script).
> [!Note]
> For development environment setup instructions, please refer to the [Development Environment Guide](https://docs.docsgpt.cloud/Deploying/Development-Environment).

View File

@@ -84,4 +84,4 @@ EXPOSE 7091
USER appuser
# Start Gunicorn
CMD ["gunicorn", "-w", "2", "--timeout", "120", "--bind", "0.0.0.0:7091", "application.wsgi:app"]
CMD ["gunicorn", "-w", "1", "--timeout", "120", "--bind", "0.0.0.0:7091", "--preload", "application.wsgi:app"]

View File

View File

@@ -1,9 +1,11 @@
from application.agents.classic_agent import ClassicAgent
from application.agents.react_agent import ReActAgent
class AgentCreator:
agents = {
"classic": ClassicAgent,
"react": ReActAgent,
}
@classmethod
@@ -11,7 +13,4 @@ class AgentCreator:
agent_class = cls.agents.get(type.lower())
if not agent_class:
raise ValueError(f"No agent class found for type {type}")
config = kwargs.pop('config', None)
if isinstance(config, dict) and 'proxy_id' in config and 'proxy_id' not in kwargs:
kwargs['proxy_id'] = config['proxy_id']
return agent_class(*args, **kwargs)

View File

@@ -1,4 +1,6 @@
from typing import Dict, Generator
import uuid
from abc import ABC, abstractmethod
from typing import Dict, Generator, List, Optional
from application.agents.llm_handler import get_llm_handler
from application.agents.tools.tool_action_parser import ToolActionParser
@@ -6,20 +8,37 @@ from application.agents.tools.tool_manager import ToolManager
from application.core.mongo_db import MongoDB
from application.llm.llm_creator import LLMCreator
from application.logging import build_stack_data, log_activity, LogContext
from application.retriever.base import BaseRetriever
from application.core.settings import settings
from bson.objectid import ObjectId
class BaseAgent:
class BaseAgent(ABC):
def __init__(
self,
endpoint,
llm_name,
gpt_model,
api_key,
user_api_key=None,
decoded_token=None,
proxy_id=None,
endpoint: str,
llm_name: str,
gpt_model: str,
api_key: str,
user_api_key: Optional[str] = None,
prompt: str = "",
chat_history: Optional[List[Dict]] = None,
decoded_token: Optional[Dict] = None,
attachments: Optional[List[Dict]] = None,
):
self.endpoint = endpoint
self.llm_name = llm_name
self.gpt_model = gpt_model
self.api_key = api_key
self.user_api_key = user_api_key
self.prompt = prompt
self.decoded_token = decoded_token or {}
self.user: str = decoded_token.get("sub")
self.tool_config: Dict = {}
self.tools: List[Dict] = []
self.tool_calls: List[Dict] = []
self.chat_history: List[Dict] = chat_history if chat_history is not None else []
self.llm = LLMCreator.create_llm(
llm_name,
api_key=api_key,
@@ -27,27 +46,48 @@ class BaseAgent:
decoded_token=decoded_token,
)
self.llm_handler = get_llm_handler(llm_name)
self.gpt_model = gpt_model
self.tools = []
self.tool_config = {}
self.tool_calls = []
self.proxy_id = proxy_id
self.attachments = attachments or []
def gen(self, *args, **kwargs) -> Generator[Dict, None, None]:
raise NotImplementedError('Method "gen" must be implemented in the child class')
@log_activity()
def gen(
self, query: str, retriever: BaseRetriever, log_context: LogContext = None
) -> Generator[Dict, None, None]:
yield from self._gen_inner(query, retriever, log_context)
@abstractmethod
def _gen_inner(
self, query: str, retriever: BaseRetriever, log_context: LogContext
) -> Generator[Dict, None, None]:
pass
def _get_tools(self, api_key: str = None) -> Dict[str, Dict]:
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
agents_collection = db["agents"]
tools_collection = db["user_tools"]
agent_data = agents_collection.find_one({"key": api_key or self.user_api_key})
tool_ids = agent_data.get("tools", []) if agent_data else []
tools = (
tools_collection.find(
{"_id": {"$in": [ObjectId(tool_id) for tool_id in tool_ids]}}
)
if tool_ids
else []
)
tools = list(tools)
tools_by_id = {str(tool["_id"]): tool for tool in tools} if tools else {}
return tools_by_id
def _get_user_tools(self, user="local"):
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
db = mongo[settings.MONGO_DB_NAME]
user_tools_collection = db["user_tools"]
user_tools = user_tools_collection.find({"user": user, "status": True})
user_tools = list(user_tools)
tools_by_id = {str(tool["_id"]): tool for tool in user_tools}
if hasattr(self, 'proxy_id') and self.proxy_id:
for tool_id, tool in tools_by_id.items():
if 'config' not in tool:
tool['config'] = {}
tool['config']['proxy_id'] = self.proxy_id
return tools_by_id
def _build_tool_parameters(self, action):
@@ -116,14 +156,12 @@ class BaseAgent:
for param, details in action_data[param_type]["properties"].items():
if param not in call_args and "value" in details:
target_dict[param] = details["value"]
for param, value in call_args.items():
for param_type, target_dict in param_types.items():
if param_type in action_data and param in action_data[param_type].get(
"properties", {}
):
target_dict[param] = value
tm = ToolManager(config={})
tool = tm.load_tool(
tool_data["name"],
@@ -133,7 +171,6 @@ class BaseAgent:
"method": tool_data["config"]["actions"][action_name]["method"],
"headers": headers,
"query_params": query_params,
"proxy_id": self.proxy_id,
}
if tool_data["name"] == "api_tool"
else tool_data["config"]
@@ -159,3 +196,82 @@ class BaseAgent:
self.tool_calls.append(tool_call_data)
return result, call_id
def _build_messages(
self,
system_prompt: str,
query: str,
retrieved_data: List[Dict],
) -> List[Dict]:
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
p_chat_combine = system_prompt.replace("{summaries}", docs_together)
messages_combine = [{"role": "system", "content": p_chat_combine}]
for i in self.chat_history:
if "prompt" in i and "response" in i:
messages_combine.append({"role": "user", "content": i["prompt"]})
messages_combine.append({"role": "assistant", "content": i["response"]})
if "tool_calls" in i:
for tool_call in i["tool_calls"]:
call_id = tool_call.get("call_id") or str(uuid.uuid4())
function_call_dict = {
"function_call": {
"name": tool_call.get("action_name"),
"args": tool_call.get("arguments"),
"call_id": call_id,
}
}
function_response_dict = {
"function_response": {
"name": tool_call.get("action_name"),
"response": {"result": tool_call.get("result")},
"call_id": call_id,
}
}
messages_combine.append(
{"role": "assistant", "content": [function_call_dict]}
)
messages_combine.append(
{"role": "tool", "content": [function_response_dict]}
)
messages_combine.append({"role": "user", "content": query})
return messages_combine
def _retriever_search(
self,
retriever: BaseRetriever,
query: str,
log_context: Optional[LogContext] = None,
) -> List[Dict]:
retrieved_data = retriever.search(query)
if log_context:
data = build_stack_data(retriever, exclude_attributes=["llm"])
log_context.stacks.append({"component": "retriever", "data": data})
return retrieved_data
def _llm_gen(self, messages: List[Dict], log_context: Optional[LogContext] = None):
resp = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=self.tools
)
if log_context:
data = build_stack_data(self.llm, exclude_attributes=["client"])
log_context.stacks.append({"component": "llm", "data": data})
return resp
def _llm_handler(
self,
resp,
tools_dict: Dict,
messages: List[Dict],
log_context: Optional[LogContext] = None,
attachments: Optional[List[Dict]] = None,
):
resp = self.llm_handler.handle_response(
self, resp, tools_dict, messages, attachments
)
if log_context:
data = build_stack_data(self.llm_handler, exclude_attributes=["tool_calls"])
log_context.stacks.append({"component": "llm_handler", "data": data})
return resp

View File

@@ -1,87 +1,30 @@
import uuid
from typing import Dict, Generator
from application.agents.base import BaseAgent
from application.logging import build_stack_data, log_activity, LogContext
from application.logging import LogContext
from application.retriever.base import BaseRetriever
import logging
logger = logging.getLogger(__name__)
class ClassicAgent(BaseAgent):
def __init__(
self,
endpoint,
llm_name,
gpt_model,
api_key,
user_api_key=None,
prompt="",
chat_history=None,
decoded_token=None,
proxy_id=None,
):
super().__init__(
endpoint, llm_name, gpt_model, api_key, user_api_key, decoded_token, proxy_id
)
self.user = decoded_token.get("sub")
self.prompt = prompt
self.chat_history = chat_history if chat_history is not None else []
@log_activity()
def gen(
self, query: str, retriever: BaseRetriever, log_context: LogContext = None
) -> Generator[Dict, None, None]:
yield from self._gen_inner(query, retriever, log_context)
def _gen_inner(
self, query: str, retriever: BaseRetriever, log_context: LogContext
) -> Generator[Dict, None, None]:
retrieved_data = self._retriever_search(retriever, query, log_context)
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
p_chat_combine = self.prompt.replace("{summaries}", docs_together)
messages_combine = [{"role": "system", "content": p_chat_combine}]
if len(self.chat_history) > 0:
for i in self.chat_history:
if "prompt" in i and "response" in i:
messages_combine.append({"role": "user", "content": i["prompt"]})
messages_combine.append(
{"role": "assistant", "content": i["response"]}
)
if "tool_calls" in i:
for tool_call in i["tool_calls"]:
call_id = tool_call.get("call_id")
if call_id is None or call_id == "None":
call_id = str(uuid.uuid4())
function_call_dict = {
"function_call": {
"name": tool_call.get("action_name"),
"args": tool_call.get("arguments"),
"call_id": call_id,
}
}
function_response_dict = {
"function_response": {
"name": tool_call.get("action_name"),
"response": {"result": tool_call.get("result")},
"call_id": call_id,
}
}
messages_combine.append(
{"role": "assistant", "content": [function_call_dict]}
)
messages_combine.append(
{"role": "tool", "content": [function_response_dict]}
)
messages_combine.append({"role": "user", "content": query})
tools_dict = self._get_user_tools(self.user)
if self.user_api_key:
tools_dict = self._get_tools(self.user_api_key)
else:
tools_dict = self._get_user_tools(self.user)
self._prepare_tools(tools_dict)
resp = self._llm_gen(messages_combine, log_context)
messages = self._build_messages(self.prompt, query, retrieved_data)
resp = self._llm_gen(messages, log_context)
attachments = self.attachments
if isinstance(resp, str):
yield {"answer": resp}
@@ -94,7 +37,7 @@ class ClassicAgent(BaseAgent):
yield {"answer": resp.message.content}
return
resp = self._llm_handler(resp, tools_dict, messages_combine, log_context)
resp = self._llm_handler(resp, tools_dict, messages, log_context, attachments)
if isinstance(resp, str):
yield {"answer": resp}
@@ -105,37 +48,17 @@ class ClassicAgent(BaseAgent):
):
yield {"answer": resp.message.content}
else:
completion = self.llm.gen_stream(
model=self.gpt_model, messages=messages_combine, tools=self.tools
)
for line in completion:
for line in resp:
if isinstance(line, str):
yield {"answer": line}
log_context.stacks.append(
{"component": "agent", "data": {"tool_calls": self.tool_calls.copy()}}
)
yield {"sources": retrieved_data}
# clean tool_call_data only send first 50 characters of tool_call['result']
for tool_call in self.tool_calls:
if len(str(tool_call["result"])) > 50:
tool_call["result"] = str(tool_call["result"])[:50] + "..."
yield {"tool_calls": self.tool_calls.copy()}
def _retriever_search(self, retriever, query, log_context):
retrieved_data = retriever.search(query)
if log_context:
data = build_stack_data(retriever, exclude_attributes=["llm"])
log_context.stacks.append({"component": "retriever", "data": data})
return retrieved_data
def _llm_gen(self, messages_combine, log_context):
resp = self.llm.gen_stream(
model=self.gpt_model, messages=messages_combine, tools=self.tools
)
if log_context:
data = build_stack_data(self.llm)
log_context.stacks.append({"component": "llm", "data": data})
return resp
def _llm_handler(self, resp, tools_dict, messages_combine, log_context):
resp = self.llm_handler.handle_response(
self, resp, tools_dict, messages_combine
)
if log_context:
data = build_stack_data(self.llm_handler)
log_context.stacks.append({"component": "llm_handler", "data": data})
return resp

View File

@@ -1,8 +1,11 @@
import json
import logging
from abc import ABC, abstractmethod
from application.logging import build_stack_data
logger = logging.getLogger(__name__)
class LLMHandler(ABC):
def __init__(self):
@@ -10,12 +13,90 @@ class LLMHandler(ABC):
self.tool_calls = []
@abstractmethod
def handle_response(self, agent, resp, tools_dict, messages, **kwargs):
def handle_response(self, agent, resp, tools_dict, messages, attachments=None, **kwargs):
pass
def prepare_messages_with_attachments(self, agent, messages, attachments=None):
"""
Prepare messages with attachment content if available.
Args:
agent: The current agent instance.
messages (list): List of message dictionaries.
attachments (list): List of attachment dictionaries with content.
Returns:
list: Messages with attachment context added to the system prompt.
"""
if not attachments:
return messages
logger.info(f"Preparing messages with {len(attachments)} attachments")
supported_types = agent.llm.get_supported_attachment_types()
supported_attachments = []
unsupported_attachments = []
for attachment in attachments:
mime_type = attachment.get('mime_type')
if mime_type in supported_types:
supported_attachments.append(attachment)
else:
unsupported_attachments.append(attachment)
# Process supported attachments with the LLM's custom method
prepared_messages = messages
if supported_attachments:
logger.info(f"Processing {len(supported_attachments)} supported attachments with {agent.llm.__class__.__name__}'s method")
prepared_messages = agent.llm.prepare_messages_with_attachments(messages, supported_attachments)
# Process unsupported attachments with the default method
if unsupported_attachments:
logger.info(f"Processing {len(unsupported_attachments)} unsupported attachments with default method")
prepared_messages = self._append_attachment_content_to_system(prepared_messages, unsupported_attachments)
return prepared_messages
def _append_attachment_content_to_system(self, messages, attachments):
"""
Default method to append attachment content to the system prompt.
Args:
messages (list): List of message dictionaries.
attachments (list): List of attachment dictionaries with content.
Returns:
list: Messages with attachment context added to the system prompt.
"""
prepared_messages = messages.copy()
attachment_texts = []
for attachment in attachments:
logger.info(f"Adding attachment {attachment.get('id')} to context")
if 'content' in attachment:
attachment_texts.append(f"Attached file content:\n\n{attachment['content']}")
if attachment_texts:
combined_attachment_text = "\n\n".join(attachment_texts)
system_found = False
for i in range(len(prepared_messages)):
if prepared_messages[i].get("role") == "system":
prepared_messages[i]["content"] += f"\n\n{combined_attachment_text}"
system_found = True
break
if not system_found:
prepared_messages.insert(0, {"role": "system", "content": combined_attachment_text})
return prepared_messages
class OpenAILLMHandler(LLMHandler):
def handle_response(self, agent, resp, tools_dict, messages, stream: bool = True):
def handle_response(self, agent, resp, tools_dict, messages, attachments=None, stream: bool = True):
messages = self.prepare_messages_with_attachments(agent, messages, attachments)
logger.info(f"Messages with attachments: {messages}")
if not stream:
while hasattr(resp, "finish_reason") and resp.finish_reason == "tool_calls":
message = json.loads(resp.model_dump_json())["message"]
@@ -54,7 +135,9 @@ class OpenAILLMHandler(LLMHandler):
{"role": "tool", "content": [function_response_dict]}
)
messages = self.prepare_messages_with_attachments(agent, messages, attachments)
except Exception as e:
logging.error(f"Error executing tool: {str(e)}", exc_info=True)
messages.append(
{
"role": "tool",
@@ -69,12 +152,14 @@ class OpenAILLMHandler(LLMHandler):
return resp
else:
text_buffer = ""
while True:
tool_calls = {}
for chunk in resp:
if isinstance(chunk, str) and len(chunk) > 0:
return
elif hasattr(chunk, "delta"):
yield chunk
continue
elif hasattr(chunk, "delta"):
chunk_delta = chunk.delta
if (
@@ -145,6 +230,7 @@ class OpenAILLMHandler(LLMHandler):
)
except Exception as e:
logging.error(f"Error executing tool: {str(e)}", exc_info=True)
messages.append(
{
"role": "assistant",
@@ -152,15 +238,21 @@ class OpenAILLMHandler(LLMHandler):
}
)
tool_calls = {}
if hasattr(chunk_delta, "content") and chunk_delta.content:
# Add to buffer or yield immediately based on your preference
text_buffer += chunk_delta.content
yield text_buffer
text_buffer = ""
if (
hasattr(chunk, "finish_reason")
and chunk.finish_reason == "stop"
):
return
return resp
elif isinstance(chunk, str) and len(chunk) == 0:
continue
logger.info(f"Regenerating with messages: {messages}")
resp = agent.llm.gen_stream(
model=agent.gpt_model, messages=messages, tools=agent.tools
)
@@ -168,9 +260,11 @@ class OpenAILLMHandler(LLMHandler):
class GoogleLLMHandler(LLMHandler):
def handle_response(self, agent, resp, tools_dict, messages, stream: bool = True):
def handle_response(self, agent, resp, tools_dict, messages, attachments=None, stream: bool = True):
from google.genai import types
messages = self.prepare_messages_with_attachments(agent, messages, attachments)
while True:
if not stream:
response = agent.llm.gen(
@@ -241,6 +335,9 @@ class GoogleLLMHandler(LLMHandler):
"content": [function_response_part.to_json_dict()],
}
)
else:
tool_call_found = False
yield result
if not tool_call_found:
return response

View File

@@ -0,0 +1,132 @@
import os
from typing import Dict, Generator, List
from application.agents.base import BaseAgent
from application.logging import build_stack_data, LogContext
from application.retriever.base import BaseRetriever
current_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
with open(
os.path.join(current_dir, "application/prompts", "react_planning_prompt.txt"), "r"
) as f:
planning_prompt = f.read()
with open(
os.path.join(current_dir, "application/prompts", "react_final_prompt.txt"),
"r",
) as f:
final_prompt = f.read()
class ReActAgent(BaseAgent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.plan = ""
self.observations: List[str] = []
def _gen_inner(
self, query: str, retriever: BaseRetriever, log_context: LogContext
) -> Generator[Dict, None, None]:
retrieved_data = self._retriever_search(retriever, query, log_context)
if self.user_api_key:
tools_dict = self._get_tools(self.user_api_key)
else:
tools_dict = self._get_user_tools(self.user)
self._prepare_tools(tools_dict)
docs_together = "\n".join([doc["text"] for doc in retrieved_data])
plan = self._create_plan(query, docs_together, log_context)
for line in plan:
if isinstance(line, str):
self.plan += line
yield {"thought": line}
prompt = self.prompt + f"\nFollow this plan: {self.plan}"
messages = self._build_messages(prompt, query, retrieved_data)
resp = self._llm_gen(messages, log_context)
if isinstance(resp, str):
self.observations.append(resp)
if (
hasattr(resp, "message")
and hasattr(resp.message, "content")
and resp.message.content is not None
):
self.observations.append(resp.message.content)
resp = self._llm_handler(resp, tools_dict, messages, log_context)
for tool_call in self.tool_calls:
observation = (
f"Action '{tool_call['action_name']}' of tool '{tool_call['tool_name']}' "
f"with arguments '{tool_call['arguments']}' returned: '{tool_call['result']}'"
)
self.observations.append(observation)
if isinstance(resp, str):
self.observations.append(resp)
elif (
hasattr(resp, "message")
and hasattr(resp.message, "content")
and resp.message.content is not None
):
self.observations.append(resp.message.content)
else:
completion = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=self.tools
)
for line in completion:
if isinstance(line, str):
self.observations.append(line)
log_context.stacks.append(
{"component": "agent", "data": {"tool_calls": self.tool_calls.copy()}}
)
yield {"sources": retrieved_data}
# clean tool_call_data only send first 50 characters of tool_call['result']
for tool_call in self.tool_calls:
if len(str(tool_call["result"])) > 50:
tool_call["result"] = str(tool_call["result"])[:50] + "..."
yield {"tool_calls": self.tool_calls.copy()}
final_answer = self._create_final_answer(query, self.observations, log_context)
for line in final_answer:
if isinstance(line, str):
yield {"answer": line}
def _create_plan(
self, query: str, docs_data: str, log_context: LogContext = None
) -> Generator[str, None, None]:
plan_prompt = planning_prompt.replace("{query}", query)
if "{summaries}" in planning_prompt:
summaries = docs_data
plan_prompt = plan_prompt.replace("{summaries}", summaries)
messages = [{"role": "user", "content": plan_prompt}]
print(self.tools)
plan = self.llm.gen_stream(
model=self.gpt_model, messages=messages, tools=self.tools
)
if log_context:
data = build_stack_data(self.llm)
log_context.stacks.append({"component": "planning_llm", "data": data})
return plan
def _create_final_answer(
self, query: str, observations: List[str], log_context: LogContext = None
) -> str:
observation_string = "\n".join(observations)
final_answer_prompt = final_prompt.format(
query=query, observations=observation_string
)
messages = [{"role": "user", "content": final_answer_prompt}]
final_answer = self.llm.gen_stream(model=self.gpt_model, messages=messages)
if log_context:
data = build_stack_data(self.llm)
log_context.stacks.append({"component": "final_answer_llm", "data": data})
return final_answer

View File

@@ -23,43 +23,15 @@ class APITool(Tool):
)
def _make_api_call(self, url, method, headers, query_params, body):
sanitized_headers = {}
for key, value in headers.items():
if isinstance(value, str):
sanitized_value = value.encode('latin-1', errors='ignore').decode('latin-1')
sanitized_headers[key] = sanitized_value
else:
sanitized_headers[key] = value
if query_params:
url = f"{url}?{requests.compat.urlencode(query_params)}"
if isinstance(body, dict):
body = json.dumps(body)
response = None
# if isinstance(body, dict):
# body = json.dumps(body)
try:
print(f"Making API call: {method} {url} with body: {body}")
if body == "{}":
body = None
proxy_id = self.config.get("proxy_id", None)
request_kwargs = {
'method': method,
'url': url,
'headers': sanitized_headers,
'data': body
}
try:
if proxy_id:
from application.agents.tools.proxy_handler import apply_proxy_to_request
response = apply_proxy_to_request(
requests.request,
proxy_id=proxy_id,
**request_kwargs
)
else:
response = requests.request(**request_kwargs)
except ImportError:
response = requests.request(**request_kwargs)
response = requests.request(method, url, headers=headers, data=body)
response.raise_for_status()
content_type = response.headers.get(
"Content-Type", "application/json"

View File

@@ -1,63 +0,0 @@
import logging
import requests
from typing import Dict, Optional
from bson.objectid import ObjectId
from application.core.mongo_db import MongoDB
logger = logging.getLogger(__name__)
# Get MongoDB connection
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
proxies_collection = db["proxies"]
def get_proxy_config(proxy_id: str) -> Optional[Dict[str, str]]:
"""
Retrieve proxy configuration from the database.
Args:
proxy_id: The ID of the proxy configuration
Returns:
A dictionary with proxy configuration or None if not found
"""
if not proxy_id or proxy_id == "none":
return None
try:
if ObjectId.is_valid(proxy_id):
proxy_config = proxies_collection.find_one({"_id": ObjectId(proxy_id)})
if proxy_config and "connection" in proxy_config:
connection_str = proxy_config["connection"].strip()
if connection_str:
# Format proxy for requests library
return {
"http": connection_str,
"https": connection_str
}
return None
except Exception as e:
logger.error(f"Error retrieving proxy configuration: {e}")
return None
def apply_proxy_to_request(request_func, proxy_id=None, **kwargs):
"""
Apply proxy configuration to a requests function if available.
This is a minimal wrapper that doesn't change the function signature.
Args:
request_func: The requests function to call (e.g., requests.get, requests.post)
proxy_id: Optional proxy ID to use
**kwargs: Arguments to pass to the request function
Returns:
The response from the request
"""
if proxy_id:
proxy_config = get_proxy_config(proxy_id)
if proxy_config:
kwargs['proxies'] = proxy_config
logger.info(f"Using proxy for request")
return request_func(**kwargs)

View File

@@ -0,0 +1,83 @@
import requests
from markdownify import markdownify
from application.agents.tools.base import Tool
from urllib.parse import urlparse
class ReadWebpageTool(Tool):
"""
Read Webpage (browser)
A tool to fetch the HTML content of a URL and convert it to Markdown.
"""
def __init__(self, config=None):
"""
Initializes the tool.
:param config: Optional configuration dictionary. Not used by this tool.
"""
self.config = config
def execute_action(self, action_name: str, **kwargs) -> str:
"""
Executes the specified action. For this tool, the only action is 'read_webpage'.
:param action_name: The name of the action to execute. Should be 'read_webpage'.
:param kwargs: Keyword arguments, must include 'url'.
:return: The Markdown content of the webpage or an error message.
"""
if action_name != "read_webpage":
return f"Error: Unknown action '{action_name}'. This tool only supports 'read_webpage'."
url = kwargs.get("url")
if not url:
return "Error: URL parameter is missing."
# Ensure the URL has a scheme (if not, default to http)
parsed_url = urlparse(url)
if not parsed_url.scheme:
url = "http://" + url
try:
response = requests.get(url, timeout=10, headers={'User-Agent': 'DocsGPT-Agent/1.0'})
response.raise_for_status() # Raise an exception for HTTP errors (4xx or 5xx)
html_content = response.text
#soup = BeautifulSoup(html_content, 'html.parser')
markdown_content = markdownify(html_content, heading_style="ATX", newline_style="BACKSLASH")
return markdown_content
except requests.exceptions.RequestException as e:
return f"Error fetching URL {url}: {e}"
except Exception as e:
return f"Error processing URL {url}: {e}"
def get_actions_metadata(self):
"""
Returns metadata for the actions supported by this tool.
"""
return [
{
"name": "read_webpage",
"description": "Fetches the HTML content of a given URL and returns it as clean Markdown text. Input must be a valid URL.",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The fully qualified URL of the webpage to read (e.g., 'https://www.example.com').",
}
},
"required": ["url"],
"additionalProperties": False,
},
}
]
def get_config_requirements(self):
"""
Returns a dictionary describing the configuration requirements for the tool.
This tool does not require any specific configuration.
"""
return {}

View File

@@ -23,12 +23,13 @@ from application.utils import check_required_fields, limit_chat_history
logger = logging.getLogger(__name__)
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
db = mongo[settings.MONGO_DB_NAME]
conversations_collection = db["conversations"]
sources_collection = db["sources"]
prompts_collection = db["prompts"]
api_key_collection = db["api_keys"]
agents_collection = db["agents"]
user_logs_collection = db["user_logs"]
attachments_collection = db["attachments"]
answer = Blueprint("answer", __name__)
answer_ns = Namespace("answer", description="Answer related operations", path="/")
@@ -85,19 +86,51 @@ def run_async_chain(chain, question, chat_history):
return result
def get_data_from_api_key(api_key):
data = api_key_collection.find_one({"key": api_key})
# # Raise custom exception if the API key is not found
if data is None:
raise Exception("Invalid API Key, please generate new key", 401)
def get_agent_key(agent_id, user_id):
if not agent_id:
return None, False, None
if "source" in data and isinstance(data["source"], DBRef):
source_doc = db.dereference(data["source"])
try:
agent = agents_collection.find_one({"_id": ObjectId(agent_id)})
if agent is None:
raise Exception("Agent not found", 404)
is_owner = agent.get("user") == user_id
if is_owner:
agents_collection.update_one(
{"_id": ObjectId(agent_id)},
{"$set": {"lastUsedAt": datetime.datetime.now(datetime.timezone.utc)}},
)
return str(agent["key"]), False, None
is_shared_with_user = agent.get(
"shared_publicly", False
) or user_id in agent.get("shared_with", [])
if is_shared_with_user:
return str(agent["key"]), True, agent.get("shared_token")
raise Exception("Unauthorized access to the agent", 403)
except Exception as e:
logger.error(f"Error in get_agent_key: {str(e)}", exc_info=True)
raise
def get_data_from_api_key(api_key):
data = agents_collection.find_one({"key": api_key})
if not data:
raise Exception("Invalid API Key, please generate a new key", 401)
source = data.get("source")
if isinstance(source, DBRef):
source_doc = db.dereference(source)
data["source"] = str(source_doc["_id"])
if "retriever" in source_doc:
data["retriever"] = source_doc["retriever"]
data["retriever"] = source_doc.get("retriever", data.get("retriever"))
else:
data["source"] = {}
return data
@@ -121,12 +154,16 @@ def save_conversation(
conversation_id,
question,
response,
thought,
source_log_docs,
tool_calls,
llm,
decoded_token,
index=None,
api_key=None,
agent_id=None,
is_shared_usage=False,
shared_token=None,
):
current_time = datetime.datetime.now(datetime.timezone.utc)
if conversation_id is not None and index is not None:
@@ -136,6 +173,7 @@ def save_conversation(
"$set": {
f"queries.{index}.prompt": question,
f"queries.{index}.response": response,
f"queries.{index}.thought": thought,
f"queries.{index}.sources": source_log_docs,
f"queries.{index}.tool_calls": tool_calls,
f"queries.{index}.timestamp": current_time,
@@ -155,6 +193,7 @@ def save_conversation(
"queries": {
"prompt": question,
"response": response,
"thought": thought,
"sources": source_log_docs,
"tool_calls": tool_calls,
"timestamp": current_time,
@@ -190,6 +229,7 @@ def save_conversation(
{
"prompt": question,
"response": response,
"thought": thought,
"sources": source_log_docs,
"tool_calls": tool_calls,
"timestamp": current_time,
@@ -197,7 +237,12 @@ def save_conversation(
],
}
if api_key:
api_key_doc = api_key_collection.find_one({"key": api_key})
if agent_id:
conversation_data["agent_id"] = agent_id
if is_shared_usage:
conversation_data["is_shared_usage"] = is_shared_usage
conversation_data["shared_token"] = shared_token
api_key_doc = agents_collection.find_one({"key": api_key})
if api_key_doc:
conversation_data["api_key"] = api_key_doc["key"]
conversation_id = conversations_collection.insert_one(
@@ -228,11 +273,20 @@ def complete_stream(
isNoneDoc=False,
index=None,
should_save_conversation=True,
attachments=None,
agent_id=None,
is_shared_usage=False,
shared_token=None,
):
try:
response_full = ""
source_log_docs = []
tool_calls = []
response_full, thought, source_log_docs, tool_calls = "", "", [], []
attachment_ids = []
if attachments:
attachment_ids = [attachment["id"] for attachment in attachments]
logger.info(
f"Processing request with {len(attachments)} attachments: {attachment_ids}"
)
answer = agent.gen(query=question, retriever=retriever)
@@ -258,6 +312,10 @@ def complete_stream(
tool_calls = line["tool_calls"]
data = json.dumps({"type": "tool_calls", "tool_calls": tool_calls})
yield f"data: {data}\n\n"
elif "thought" in line:
thought += line["thought"]
data = json.dumps({"type": "thought", "thought": line["thought"]})
yield f"data: {data}\n\n"
if isNoneDoc:
for doc in source_log_docs:
@@ -275,12 +333,16 @@ def complete_stream(
conversation_id,
question,
response_full,
thought,
source_log_docs,
tool_calls,
llm,
decoded_token,
index,
api_key=user_api_key,
agent_id=agent_id,
is_shared_usage=is_shared_usage,
shared_token=shared_token,
)
else:
conversation_id = None
@@ -300,14 +362,14 @@ def complete_stream(
"response": response_full,
"sources": source_log_docs,
"retriever_params": retriever_params,
"attachments": attachment_ids,
"timestamp": datetime.datetime.now(datetime.timezone.utc),
}
)
data = json.dumps({"type": "end"})
yield f"data: {data}\n\n"
except Exception as e:
logger.error(f"Error in stream: {str(e)}")
logger.error(traceback.format_exc())
logger.error(f"Error in stream: {str(e)}", exc_info=True)
data = json.dumps(
{
"type": "error",
@@ -335,9 +397,6 @@ class Stream(Resource):
"prompt_id": fields.String(
required=False, default="default", description="Prompt ID"
),
"proxy_id": fields.String(
required=False, description="Proxy ID to use for API calls"
),
"chunks": fields.Integer(
required=False, default=2, description="Number of chunks"
),
@@ -351,10 +410,15 @@ class Stream(Resource):
required=False, description="Flag indicating if no document is used"
),
"index": fields.Integer(
required=False, description="The position where query is to be updated"
required=False, description="Index of the query to update"
),
"save_conversation": fields.Boolean(
required=False, default=True, description="Flag to save conversation"
required=False,
default=True,
description="Whether to save the conversation",
),
"attachments": fields.List(
fields.String, required=False, description="List of attachment IDs"
),
},
)
@@ -379,22 +443,36 @@ class Stream(Resource):
)
conversation_id = data.get("conversation_id")
prompt_id = data.get("prompt_id", "default")
proxy_id = data.get("proxy_id", None)
attachment_ids = data.get("attachments", [])
index = data.get("index", None)
chunks = int(data.get("chunks", 2))
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
retriever_name = data.get("retriever", "classic")
agent_id = data.get("agent_id", None)
agent_type = settings.AGENT_NAME
agent_key, is_shared_usage, shared_token = get_agent_key(
agent_id, request.decoded_token.get("sub")
)
if agent_key:
data.update({"api_key": agent_key})
else:
agent_id = None
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key.get("chunks", 2))
prompt_id = data_key.get("prompt_id", "default")
proxy_id = data_key.get("proxy_id", None)
source = {"active_docs": data_key.get("source")}
retriever_name = data_key.get("retriever", retriever_name)
user_api_key = data["api_key"]
decoded_token = {"sub": data_key.get("user")}
agent_type = data_key.get("agent_type", agent_type)
if is_shared_usage:
decoded_token = request.decoded_token
else:
decoded_token = {"sub": data_key.get("user")}
is_shared_usage = False
elif "active_docs" in data:
source = {"active_docs": data["active_docs"]}
@@ -410,8 +488,12 @@ class Stream(Resource):
if not decoded_token:
return make_response({"error": "Unauthorized"}, 401)
attachments = get_attachments_content(
attachment_ids, decoded_token.get("sub")
)
logger.info(
f"/stream - request_data: {data}, source: {source}",
f"/stream - request_data: {data}, source: {source}, attachments: {len(attachments)}",
extra={"data": json.dumps({"request_data": data, "source": source})},
)
@@ -420,16 +502,16 @@ class Stream(Resource):
chunks = 0
agent = AgentCreator.create_agent(
settings.AGENT_NAME,
agent_type,
endpoint="stream",
llm_name=settings.LLM_NAME,
gpt_model=gpt_model,
api_key=settings.API_KEY,
user_api_key=user_api_key,
prompt=prompt,
proxy_id=proxy_id,
chat_history=history,
decoded_token=decoded_token,
attachments=attachments,
)
retriever = RetrieverCreator.create_retriever(
@@ -443,7 +525,8 @@ class Stream(Resource):
user_api_key=user_api_key,
decoded_token=decoded_token,
)
is_shared_usage_val = data.get("is_shared_usage", False)
is_shared_token_val = data.get("shared_token", None)
return Response(
complete_stream(
question=question,
@@ -455,6 +538,9 @@ class Stream(Resource):
isNoneDoc=data.get("isNoneDoc"),
index=index,
should_save_conversation=save_conv,
agent_id=agent_id,
is_shared_usage=is_shared_usage_val,
shared_token=is_shared_token_val,
),
mimetype="text/event-stream",
)
@@ -502,9 +588,6 @@ class Answer(Resource):
"prompt_id": fields.String(
required=False, default="default", description="Prompt ID"
),
"proxy_id": fields.String(
required=False, description="Proxy ID to use for API calls"
),
"chunks": fields.Integer(
required=False, default=2, description="Number of chunks"
),
@@ -536,19 +619,19 @@ class Answer(Resource):
)
conversation_id = data.get("conversation_id")
prompt_id = data.get("prompt_id", "default")
proxy_id = data.get("proxy_id", None)
chunks = int(data.get("chunks", 2))
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
retriever_name = data.get("retriever", "classic")
agent_type = settings.AGENT_NAME
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key.get("chunks", 2))
prompt_id = data_key.get("prompt_id", "default")
proxy_id = data_key.get("proxy_id", None)
source = {"active_docs": data_key.get("source")}
retriever_name = data_key.get("retriever", retriever_name)
user_api_key = data["api_key"]
agent_type = data_key.get("agent_type", agent_type)
decoded_token = {"sub": data_key.get("user")}
elif "active_docs" in data:
@@ -573,14 +656,13 @@ class Answer(Resource):
)
agent = AgentCreator.create_agent(
settings.AGENT_NAME,
agent_type,
endpoint="api/answer",
llm_name=settings.LLM_NAME,
gpt_model=gpt_model,
api_key=settings.API_KEY,
user_api_key=user_api_key,
prompt=prompt,
proxy_id=proxy_id,
chat_history=history,
decoded_token=decoded_token,
)
@@ -601,6 +683,7 @@ class Answer(Resource):
source_log_docs = []
tool_calls = []
stream_ended = False
thought = ""
for line in complete_stream(
question=question,
@@ -623,6 +706,8 @@ class Answer(Resource):
source_log_docs = event["source"]
elif event["type"] == "tool_calls":
tool_calls = event["tool_calls"]
elif event["type"] == "thought":
thought = event["thought"]
elif event["type"] == "error":
logger.error(f"Error from stream: {event['error']}")
return bad_request(500, event["error"])
@@ -654,6 +739,7 @@ class Answer(Resource):
conversation_id,
question,
response_full,
thought,
source_log_docs,
tool_calls,
llm,
@@ -796,3 +882,34 @@ class Search(Resource):
return bad_request(500, str(e))
return make_response(docs, 200)
def get_attachments_content(attachment_ids, user):
"""
Retrieve content from attachment documents based on their IDs.
Args:
attachment_ids (list): List of attachment document IDs
user (str): User identifier to verify ownership
Returns:
list: List of dictionaries containing attachment content and metadata
"""
if not attachment_ids:
return []
attachments = []
for attachment_id in attachment_ids:
try:
attachment_doc = attachments_collection.find_one(
{"_id": ObjectId(attachment_id), "user": user}
)
if attachment_doc:
attachments.append(attachment_doc)
except Exception as e:
logger.error(
f"Error retrieving attachment {attachment_id}: {e}", exc_info=True
)
return attachments

View File

@@ -3,12 +3,15 @@ import datetime
from flask import Blueprint, request, send_from_directory
from werkzeug.utils import secure_filename
from bson.objectid import ObjectId
import logging
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.storage.storage_creator import StorageCreator
logger = logging.getLogger(__name__)
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
db = mongo[settings.MONGO_DB_NAME]
conversations_collection = db["conversations"]
sources_collection = db["sources"]
@@ -45,26 +48,26 @@ def upload_index_files():
remote_data = request.form["remote_data"] if "remote_data" in request.form else None
sync_frequency = secure_filename(request.form["sync_frequency"]) if "sync_frequency" in request.form else None
save_dir = os.path.join(current_dir, "indexes", str(id))
storage = StorageCreator.get_storage()
index_base_path = f"indexes/{id}"
if settings.VECTOR_STORE == "faiss":
if "file_faiss" not in request.files:
print("No file part")
logger.error("No file_faiss part")
return {"status": "no file"}
file_faiss = request.files["file_faiss"]
if file_faiss.filename == "":
return {"status": "no file name"}
if "file_pkl" not in request.files:
print("No file part")
logger.error("No file_pkl part")
return {"status": "no file"}
file_pkl = request.files["file_pkl"]
if file_pkl.filename == "":
return {"status": "no file name"}
# saves index files
if not os.path.exists(save_dir):
os.makedirs(save_dir)
file_faiss.save(os.path.join(save_dir, "index.faiss"))
file_pkl.save(os.path.join(save_dir, "index.pkl"))
# Save index files to storage
storage.save_file(file_faiss, f"{index_base_path}/index.faiss")
storage.save_file(file_pkl, f"{index_base_path}/index.pkl")
existing_entry = sources_collection.find_one({"_id": ObjectId(id)})
if existing_entry:

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,13 @@
from datetime import timedelta
from application.celery_init import celery
from application.worker import ingest_worker, remote_worker, sync_worker
from application.worker import (
agent_webhook_worker,
attachment_worker,
ingest_worker,
remote_worker,
sync_worker,
)
@celery.task(bind=True)
@@ -22,6 +28,18 @@ def schedule_syncs(self, frequency):
return resp
@celery.task(bind=True)
def store_attachment(self, file_info, user):
resp = attachment_worker(self, file_info, user)
return resp
@celery.task(bind=True)
def process_agent_webhook(self, agent_id, payload):
resp = agent_webhook_worker(self, agent_id, payload)
return resp
@celery.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
sender.add_periodic_task(

View File

@@ -61,14 +61,14 @@ def gen_cache(func):
if cached_response:
return cached_response.decode("utf-8")
except Exception as e:
logger.error(f"Error getting cached response: {e}")
logger.error(f"Error getting cached response: {e}", exc_info=True)
result = func(self, model, messages, stream, tools, *args, **kwargs)
if redis_client and isinstance(result, str):
try:
redis_client.set(cache_key, result, ex=1800)
except Exception as e:
logger.error(f"Error setting cache: {e}")
logger.error(f"Error setting cache: {e}", exc_info=True)
return result
@@ -100,7 +100,7 @@ def stream_cache(func):
time.sleep(0.03) # Simulate streaming delay
return
except Exception as e:
logger.error(f"Error getting cached stream: {e}")
logger.error(f"Error getting cached stream: {e}", exc_info=True)
stream_cache_data = []
for chunk in func(self, model, messages, stream, tools, *args, **kwargs):
@@ -112,6 +112,6 @@ def stream_cache(func):
redis_client.set(cache_key, json.dumps(stream_cache_data), ex=1800)
logger.info(f"Stream cache saved for key: {cache_key}")
except Exception as e:
logger.error(f"Error setting stream cache: {e}")
logger.error(f"Error setting stream cache: {e}", exc_info=True)
return wrapper

View File

@@ -19,6 +19,7 @@ class Settings(BaseSettings):
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
MONGO_URI: str = "mongodb://localhost:27017/docsgpt"
MONGO_DB_NAME: str = "docsgpt"
MODEL_PATH: str = os.path.join(current_dir, "models/docsgpt-7b-f16.gguf")
DEFAULT_MAX_HISTORY: int = 150
MODEL_TOKEN_LIMITS: dict = {
@@ -98,6 +99,8 @@ class Settings(BaseSettings):
BRAVE_SEARCH_API_KEY: Optional[str] = None
FLASK_DEBUG_MODE: bool = False
STORAGE_TYPE: str = "local" # local or s3
JWT_SECRET_KEY: str = ""

View File

@@ -55,3 +55,12 @@ class BaseLLM(ABC):
def _supports_tools(self):
raise NotImplementedError("Subclass must implement _supports_tools method")
def get_supported_attachment_types(self):
"""
Return a list of MIME types supported by this LLM for file uploads.
Returns:
list: List of supported MIME types
"""
return [] # Default: no attachments supported

View File

@@ -1,7 +1,11 @@
from google import genai
from google.genai import types
import logging
import json
from application.llm.base import BaseLLM
from application.storage.storage_creator import StorageCreator
from application.core.settings import settings
class GoogleLLM(BaseLLM):
@@ -9,6 +13,126 @@ class GoogleLLM(BaseLLM):
super().__init__(*args, **kwargs)
self.api_key = api_key
self.user_api_key = user_api_key
self.client = genai.Client(api_key=self.api_key)
self.storage = StorageCreator.get_storage()
def get_supported_attachment_types(self):
"""
Return a list of MIME types supported by Google Gemini for file uploads.
Returns:
list: List of supported MIME types
"""
return [
'application/pdf',
'image/png',
'image/jpeg',
'image/jpg',
'image/webp',
'image/gif'
]
def prepare_messages_with_attachments(self, messages, attachments=None):
"""
Process attachments using Google AI's file API for more efficient handling.
Args:
messages (list): List of message dictionaries.
attachments (list): List of attachment dictionaries with content and metadata.
Returns:
list: Messages formatted with file references for Google AI API.
"""
if not attachments:
return messages
prepared_messages = messages.copy()
# Find the user message to attach files to the last one
user_message_index = None
for i in range(len(prepared_messages) - 1, -1, -1):
if prepared_messages[i].get("role") == "user":
user_message_index = i
break
if user_message_index is None:
user_message = {"role": "user", "content": []}
prepared_messages.append(user_message)
user_message_index = len(prepared_messages) - 1
if isinstance(prepared_messages[user_message_index].get("content"), str):
text_content = prepared_messages[user_message_index]["content"]
prepared_messages[user_message_index]["content"] = [
{"type": "text", "text": text_content}
]
elif not isinstance(prepared_messages[user_message_index].get("content"), list):
prepared_messages[user_message_index]["content"] = []
files = []
for attachment in attachments:
mime_type = attachment.get('mime_type')
if mime_type in self.get_supported_attachment_types():
try:
file_uri = self._upload_file_to_google(attachment)
logging.info(f"GoogleLLM: Successfully uploaded file, got URI: {file_uri}")
files.append({"file_uri": file_uri, "mime_type": mime_type})
except Exception as e:
logging.error(f"GoogleLLM: Error uploading file: {e}", exc_info=True)
if 'content' in attachment:
prepared_messages[user_message_index]["content"].append({
"type": "text",
"text": f"[File could not be processed: {attachment.get('path', 'unknown')}]"
})
if files:
logging.info(f"GoogleLLM: Adding {len(files)} files to message")
prepared_messages[user_message_index]["content"].append({
"files": files
})
return prepared_messages
def _upload_file_to_google(self, attachment):
"""
Upload a file to Google AI and return the file URI.
Args:
attachment (dict): Attachment dictionary with path and metadata.
Returns:
str: Google AI file URI for the uploaded file.
"""
if 'google_file_uri' in attachment:
return attachment['google_file_uri']
file_path = attachment.get('path')
if not file_path:
raise ValueError("No file path provided in attachment")
if not self.storage.file_exists(file_path):
raise FileNotFoundError(f"File not found: {file_path}")
try:
file_uri = self.storage.process_file(
file_path,
lambda local_path, **kwargs: self.client.files.upload(file=local_path).uri
)
from application.core.mongo_db import MongoDB
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
attachments_collection = db["attachments"]
if '_id' in attachment:
attachments_collection.update_one(
{"_id": attachment['_id']},
{"$set": {"google_file_uri": file_uri}}
)
return file_uri
except Exception as e:
logging.error(f"Error uploading file to Google AI: {e}", exc_info=True)
raise
def _clean_messages_google(self, messages):
cleaned_messages = []
@@ -26,7 +150,7 @@ class GoogleLLM(BaseLLM):
elif isinstance(content, list):
for item in content:
if "text" in item:
parts.append(types.Part.from_text(item["text"]))
parts.append(types.Part.from_text(text=item["text"]))
elif "function_call" in item:
parts.append(
types.Part.from_function_call(
@@ -41,6 +165,14 @@ class GoogleLLM(BaseLLM):
response=item["function_response"]["response"],
)
)
elif "files" in item:
for file_data in item["files"]:
parts.append(
types.Part.from_uri(
file_uri=file_data["file_uri"],
mime_type=file_data["mime_type"]
)
)
else:
raise ValueError(
f"Unexpected content dictionary format:{item}"
@@ -146,11 +278,25 @@ class GoogleLLM(BaseLLM):
cleaned_tools = self._clean_tools_format(tools)
config.tools = cleaned_tools
# Check if we have both tools and file attachments
has_attachments = False
for message in messages:
for part in message.parts:
if hasattr(part, 'file_data') and part.file_data is not None:
has_attachments = True
break
if has_attachments:
break
logging.info(f"GoogleLLM: Starting stream generation. Model: {model}, Messages: {json.dumps(messages, default=str)}, Has attachments: {has_attachments}")
response = client.models.generate_content_stream(
model=model,
contents=messages,
config=config,
)
for chunk in response:
if hasattr(chunk, "candidates") and chunk.candidates:
for candidate in chunk.candidates:

View File

@@ -1,7 +1,10 @@
import json
import base64
import logging
from application.core.settings import settings
from application.llm.base import BaseLLM
from application.storage.storage_creator import StorageCreator
class OpenAILLM(BaseLLM):
@@ -10,12 +13,14 @@ class OpenAILLM(BaseLLM):
from openai import OpenAI
super().__init__(*args, **kwargs)
if settings.OPENAI_BASE_URL:
if isinstance(settings.OPENAI_BASE_URL, str) and settings.OPENAI_BASE_URL.strip():
self.client = OpenAI(api_key=api_key, base_url=settings.OPENAI_BASE_URL)
else:
self.client = OpenAI(api_key=api_key)
DEFAULT_OPENAI_API_BASE = "https://api.openai.com/v1"
self.client = OpenAI(api_key=api_key, base_url=DEFAULT_OPENAI_API_BASE)
self.api_key = api_key
self.user_api_key = user_api_key
self.storage = StorageCreator.get_storage()
def _clean_messages_openai(self, messages):
cleaned_messages = []
@@ -65,6 +70,17 @@ class OpenAILLM(BaseLLM):
),
}
)
elif isinstance(item, dict):
content_parts = []
if "text" in item:
content_parts.append({"type": "text", "text": item["text"]})
elif "type" in item and item["type"] == "text" and "text" in item:
content_parts.append(item)
elif "type" in item and item["type"] == "file" and "file" in item:
content_parts.append(item)
elif "type" in item and item["type"] == "image_url" and "image_url" in item:
content_parts.append(item)
cleaned_messages.append({"role": role, "content": content_parts})
else:
raise ValueError(
f"Unexpected content dictionary format: {item}"
@@ -133,11 +149,167 @@ class OpenAILLM(BaseLLM):
def _supports_tools(self):
return True
def get_supported_attachment_types(self):
"""
Return a list of MIME types supported by OpenAI for file uploads.
Returns:
list: List of supported MIME types
"""
return [
'application/pdf',
'image/png',
'image/jpeg',
'image/jpg',
'image/webp',
'image/gif'
]
def prepare_messages_with_attachments(self, messages, attachments=None):
"""
Process attachments using OpenAI's file API for more efficient handling.
Args:
messages (list): List of message dictionaries.
attachments (list): List of attachment dictionaries with content and metadata.
Returns:
list: Messages formatted with file references for OpenAI API.
"""
if not attachments:
return messages
prepared_messages = messages.copy()
# Find the user message to attach file_id to the last one
user_message_index = None
for i in range(len(prepared_messages) - 1, -1, -1):
if prepared_messages[i].get("role") == "user":
user_message_index = i
break
if user_message_index is None:
user_message = {"role": "user", "content": []}
prepared_messages.append(user_message)
user_message_index = len(prepared_messages) - 1
if isinstance(prepared_messages[user_message_index].get("content"), str):
text_content = prepared_messages[user_message_index]["content"]
prepared_messages[user_message_index]["content"] = [
{"type": "text", "text": text_content}
]
elif not isinstance(prepared_messages[user_message_index].get("content"), list):
prepared_messages[user_message_index]["content"] = []
for attachment in attachments:
mime_type = attachment.get('mime_type')
if mime_type and mime_type.startswith('image/'):
try:
base64_image = self._get_base64_image(attachment)
prepared_messages[user_message_index]["content"].append({
"type": "image_url",
"image_url": {
"url": f"data:{mime_type};base64,{base64_image}"
}
})
except Exception as e:
logging.error(f"Error processing image attachment: {e}", exc_info=True)
if 'content' in attachment:
prepared_messages[user_message_index]["content"].append({
"type": "text",
"text": f"[Image could not be processed: {attachment.get('path', 'unknown')}]"
})
# Handle PDFs using the file API
elif mime_type == 'application/pdf':
try:
file_id = self._upload_file_to_openai(attachment)
prepared_messages[user_message_index]["content"].append({
"type": "file",
"file": {"file_id": file_id}
})
except Exception as e:
logging.error(f"Error uploading PDF to OpenAI: {e}", exc_info=True)
if 'content' in attachment:
prepared_messages[user_message_index]["content"].append({
"type": "text",
"text": f"File content:\n\n{attachment['content']}"
})
return prepared_messages
def _get_base64_image(self, attachment):
"""
Convert an image file to base64 encoding.
Args:
attachment (dict): Attachment dictionary with path and metadata.
Returns:
str: Base64-encoded image data.
"""
file_path = attachment.get('path')
if not file_path:
raise ValueError("No file path provided in attachment")
try:
with self.storage.get_file(file_path) as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
except FileNotFoundError:
raise FileNotFoundError(f"File not found: {file_path}")
def _upload_file_to_openai(self, attachment):
"""
Upload a file to OpenAI and return the file_id.
Args:
attachment (dict): Attachment dictionary with path and metadata.
Expected keys:
- path: Path to the file
- id: Optional MongoDB ID for caching
Returns:
str: OpenAI file_id for the uploaded file.
"""
import logging
if 'openai_file_id' in attachment:
return attachment['openai_file_id']
file_path = attachment.get('path')
if not self.storage.file_exists(file_path):
raise FileNotFoundError(f"File not found: {file_path}")
try:
file_id = self.storage.process_file(
file_path,
lambda local_path, **kwargs: self.client.files.create(
file=open(local_path, 'rb'),
purpose="assistants"
).id
)
from application.core.mongo_db import MongoDB
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
attachments_collection = db["attachments"]
if '_id' in attachment:
attachments_collection.update_one(
{"_id": attachment['_id']},
{"$set": {"openai_file_id": file_id}}
)
return file_id
except Exception as e:
logging.error(f"Error uploading file to OpenAI: {e}", exc_info=True)
raise
class AzureOpenAILLM(OpenAILLM):
def __init__(
self, api_key, user_api_key, *args, **kwargs
self, api_key, user_api_key, *args, **kwargs
):
super().__init__(api_key)

View File

@@ -7,6 +7,7 @@ import uuid
from typing import Any, Callable, Dict, Generator, List
from application.core.mongo_db import MongoDB
from application.core.settings import settings
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
@@ -29,6 +30,8 @@ def build_stack_data(
exclude_attributes: List[str] = None,
custom_data: Dict = None,
) -> Dict:
if obj is None:
raise ValueError("The 'obj' parameter cannot be None")
data = {}
if include_attributes is None:
include_attributes = []
@@ -56,8 +59,8 @@ def build_stack_data(
data[attr_name] = [str(item) for item in attr_value]
elif isinstance(attr_value, dict):
data[attr_name] = {k: str(v) for k, v in attr_value.items()}
else:
data[attr_name] = str(attr_value)
except AttributeError as e:
logging.warning(f"AttributeError while accessing {attr_name}: {e}")
except AttributeError:
pass
if custom_data:
@@ -131,7 +134,7 @@ def _log_to_mongodb(
) -> None:
try:
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
db = mongo[settings.MONGO_DB_NAME]
user_logs_collection = db["stack_logs"]
log_entry = {
@@ -148,4 +151,4 @@ def _log_to_mongodb(
logging.debug(f"Logged activity to MongoDB: {activity_id}")
except Exception as e:
logging.error(f"Failed to log to MongoDB: {e}")
logging.error(f"Failed to log to MongoDB: {e}", exc_info=True)

View File

@@ -19,7 +19,7 @@ def add_text_to_store_with_retry(store, doc, source_id):
doc.metadata["source_id"] = str(source_id)
store.add_texts([doc.page_content], metadatas=[doc.metadata])
except Exception as e:
logging.error(f"Failed to add document with retry: {e}")
logging.error(f"Failed to add document with retry: {e}", exc_info=True)
raise
@@ -75,7 +75,7 @@ def embed_and_store_documents(docs, folder_name, source_id, task_status):
# Add document to vector store
add_text_to_store_with_retry(store, doc, source_id)
except Exception as e:
logging.error(f"Error embedding document {idx}: {e}")
logging.error(f"Error embedding document {idx}: {e}", exc_info=True)
logging.info(f"Saving progress at document {idx} out of {total_docs}")
store.save_local(folder_name)
break

View File

@@ -158,7 +158,7 @@ class SimpleDirectoryReader(BaseReader):
data = f.read()
# Prepare metadata for this file
if self.file_metadata is not None:
file_metadata = self.file_metadata(str(input_file))
file_metadata = self.file_metadata(input_file.name)
else:
# Provide a default empty metadata
file_metadata = {'title': '', 'store': ''}

View File

@@ -73,7 +73,13 @@ class PandasCSVParser(BaseParser):
for more information.
Set to empty dict by default, this means pandas will try to figure
out the separators, table head, etc. on its own.
header_period (int): Controls how headers are included in output:
- 0: Headers only at the beginning
- 1: Headers in every row
- N > 1: Headers every N rows
header_prefix (str): Prefix for header rows. Default is "HEADERS: ".
"""
def __init__(
@@ -83,6 +89,8 @@ class PandasCSVParser(BaseParser):
col_joiner: str = ", ",
row_joiner: str = "\n",
pandas_config: dict = {},
header_period: int = 20,
header_prefix: str = "HEADERS: ",
**kwargs: Any
) -> None:
"""Init params."""
@@ -91,6 +99,8 @@ class PandasCSVParser(BaseParser):
self._col_joiner = col_joiner
self._row_joiner = row_joiner
self._pandas_config = pandas_config
self._header_period = header_period
self._header_prefix = header_prefix
def _init_parser(self) -> Dict:
"""Init parser."""
@@ -104,15 +114,26 @@ class PandasCSVParser(BaseParser):
raise ValueError("pandas module is required to read CSV files.")
df = pd.read_csv(file, **self._pandas_config)
headers = df.columns.tolist()
header_row = f"{self._header_prefix}{self._col_joiner.join(headers)}"
text_list = df.apply(
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
).tolist()
if not self._concat_rows:
return df.apply(
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
).tolist()
text_list = []
if self._header_period != 1:
text_list.append(header_row)
for i, row in df.iterrows():
if (self._header_period > 1 and i > 0 and i % self._header_period == 0):
text_list.append(header_row)
text_list.append(self._col_joiner.join(row.astype(str).tolist()))
if self._header_period == 1 and i < len(df) - 1:
text_list.append(header_row)
if self._concat_rows:
return (self._row_joiner).join(text_list)
else:
return text_list
return self._row_joiner.join(text_list)
class ExcelParser(BaseParser):
@@ -138,7 +159,13 @@ class ExcelParser(BaseParser):
for more information.
Set to empty dict by default, this means pandas will try to figure
out the table structure on its own.
header_period (int): Controls how headers are included in output:
- 0: Headers only at the beginning (default)
- 1: Headers in every row
- N > 1: Headers every N rows
header_prefix (str): Prefix for header rows. Default is "HEADERS: ".
"""
def __init__(
@@ -148,6 +175,8 @@ class ExcelParser(BaseParser):
col_joiner: str = ", ",
row_joiner: str = "\n",
pandas_config: dict = {},
header_period: int = 20,
header_prefix: str = "HEADERS: ",
**kwargs: Any
) -> None:
"""Init params."""
@@ -156,6 +185,8 @@ class ExcelParser(BaseParser):
self._col_joiner = col_joiner
self._row_joiner = row_joiner
self._pandas_config = pandas_config
self._header_period = header_period
self._header_prefix = header_prefix
def _init_parser(self) -> Dict:
"""Init parser."""
@@ -169,12 +200,22 @@ class ExcelParser(BaseParser):
raise ValueError("pandas module is required to read Excel files.")
df = pd.read_excel(file, **self._pandas_config)
headers = df.columns.tolist()
header_row = f"{self._header_prefix}{self._col_joiner.join(headers)}"
if not self._concat_rows:
return df.apply(
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
).tolist()
text_list = []
if self._header_period != 1:
text_list.append(header_row)
text_list = df.apply(
lambda row: (self._col_joiner).join(row.astype(str).tolist()), axis=1
).tolist()
if self._concat_rows:
return (self._row_joiner).join(text_list)
else:
return text_list
for i, row in df.iterrows():
if (self._header_period > 1 and i > 0 and i % self._header_period == 0):
text_list.append(header_row)
text_list.append(self._col_joiner.join(row.astype(str).tolist()))
if self._header_period == 1 and i < len(df) - 1:
text_list.append(header_row)
return self._row_joiner.join(text_list)

View File

@@ -1,3 +1,4 @@
import logging
import requests
from urllib.parse import urlparse, urljoin
from bs4 import BeautifulSoup
@@ -42,7 +43,7 @@ class CrawlerLoader(BaseRemote):
)
)
except Exception as e:
print(f"Error processing URL {current_url}: {e}")
logging.error(f"Error processing URL {current_url}: {e}", exc_info=True)
continue
# Parse the HTML content to extract all links
@@ -61,4 +62,4 @@ class CrawlerLoader(BaseRemote):
if self.limit is not None and len(visited_urls) >= self.limit:
break
return loaded_content
return loaded_content

View File

@@ -1,3 +1,4 @@
import logging
import requests
import re # Import regular expression library
import xml.etree.ElementTree as ET
@@ -32,7 +33,7 @@ class SitemapLoader(BaseRemote):
documents.extend(loader.load())
processed_urls += 1 # Increment the counter after processing each URL
except Exception as e:
print(f"Error processing URL {url}: {e}")
logging.error(f"Error processing URL {url}: {e}", exc_info=True)
continue
return documents

View File

@@ -1,3 +1,4 @@
import logging
from application.parser.remote.base import BaseRemote
from application.parser.schema.base import Document
from langchain_community.document_loaders import WebBaseLoader
@@ -39,6 +40,6 @@ class WebLoader(BaseRemote):
)
)
except Exception as e:
print(f"Error processing URL {url}: {e}")
logging.error(f"Error processing URL {url}: {e}", exc_info=True)
continue
return documents
return documents

View File

@@ -1,9 +1,15 @@
You are a DocsGPT, friendly and helpful AI assistant by Arc53 that provides help with documents. You give thorough answers with code examples if possible.
Use the following pieces of context to help answer the users question. If its not relevant to the question, provide friendly responses.
You have access to chat history, and can use it to help answer the question.
When using code examples, use the following format:
You are a helpful AI assistant, DocsGPT. You are proactive and helpful. Try to use tools, if they are available to you,
be proactive and fill in missing information.
Users can Upload documents for your context as attachments or sources via UI using the Conversation input box.
If appropriate, your answers can include code examples, formatted as follows:
```(language)
(code)
```
Users are also able to see charts and diagrams if you use them with valid mermaid syntax in your responses.
Try to respond with mermaid charts if visualization helps with users queries.
You effectively utilize chat history, ensuring relevant and tailored responses.
Try to use additional provided context if it's available, otherwise use your knowledge and tool capabilities.
Allow yourself to be very creative and use your imagination.
----------------
Possible additional context from uploaded sources:
{summaries}

View File

@@ -1,9 +1,14 @@
You are a helpful AI assistant, DocsGPT, specializing in document assistance, designed to offer detailed and informative responses.
You are a helpful AI assistant, DocsGPT. You are proactive and helpful. Try to use tools, if they are available to you,
be proactive and fill in missing information.
Users can Upload documents for your context as attachments or sources via UI using the Conversation input box.
If appropriate, your answers can include code examples, formatted as follows:
```(language)
(code)
```
Users are also able to see charts and diagrams if you use them with valid mermaid syntax in your responses.
Try to respond with mermaid charts if visualization helps with users queries.
You effectively utilize chat history, ensuring relevant and tailored responses.
If a question doesn't align with your context, you provide friendly and helpful replies.
Try to use additional provided context if it's available, otherwise use your knowledge and tool capabilities.
----------------
Possible additional context from uploaded sources:
{summaries}

View File

@@ -1,13 +1,17 @@
You are an AI Assistant, DocsGPT, adept at offering document assistance.
Your expertise lies in providing answer on top of provided context.
You can leverage the chat history if needed.
Answer the question based on the context below.
Keep the answer concise. Respond "Irrelevant context" if not sure about the answer.
If question is not related to the context, respond "Irrelevant context".
When using code examples, use the following format:
You are a helpful AI assistant, DocsGPT. You are proactive and helpful. Try to use tools, if they are available to you,
be proactive and fill in missing information.
Users can Upload documents for your context as attachments or sources via UI using the Conversation input box.
If appropriate, your answers can include code examples, formatted as follows:
```(language)
(code)
```
----------------
Context:
{summaries}
Users are also able to see charts and diagrams if you use them with valid mermaid syntax in your responses.
Try to respond with mermaid charts if visualization helps with users queries.
You effectively utilize chat history, ensuring relevant and tailored responses.
Use context provided below or use available tools tool capabilities to answer user queries.
If you dont have enough information from the context or tools, answer "I don't know" or "I don't have enough information".
Never make up information or provide false information!
Allow yourself to be very creative and use your imagination.
----------------
Context from uploaded sources:
{summaries}

View File

@@ -0,0 +1,3 @@
Query: {query}
Observations: {observations}
Now, using the insights from the observations, formulate a well-structured and precise final answer.

View File

@@ -0,0 +1,10 @@
You are an AI assistant and talk like you're thinking out loud. Given the following query, outline a concise thought process that includes key steps and considerations necessary for effective analysis and response. Avoid pointwise formatting. The goal is to break down the query into manageable components without excessive detail, focusing on clarity and logical progression.
Include the following elements in your thought process:
1. Identify the main objective of the query.
2. Determine any relevant context or background information needed to understand the query.
3. List potential approaches or methods to address the query.
4. Highlight any critical factors or constraints that may influence the outcome.
Query: {query}
Summaries: {summaries}

View File

@@ -1,25 +1,20 @@
anthropic==0.49.0
boto3==1.35.97
beautifulsoup4==4.12.3
boto3==1.38.18
beautifulsoup4==4.13.4
celery==5.4.0
dataclasses-json==0.6.7
docx2txt==0.8
duckduckgo-search==7.5.2
ebooklib==0.18
elastic-transport==8.17.0
elasticsearch==8.17.1
escodegen==1.0.11
esprima==4.0.1
esutils==1.0.1
Flask==3.1.0
Flask==3.1.1
faiss-cpu==1.9.0.post1
flask-restx==1.3.0
gevent==24.11.1
google-genai==1.3.0
google-generativeai==0.8.3
gTTS==2.5.4
gunicorn==23.0.0
html2text==2024.2.26
javalang==0.13.0
jinja2==3.1.6
jiter==0.8.2
@@ -27,63 +22,56 @@ jmespath==1.0.1
joblib==1.4.2
jsonpatch==1.33
jsonpointer==3.0.0
jsonschema==4.23.0
jsonschema-spec==0.2.4
jsonschema-specifications==2023.7.1
kombu==5.4.2
langchain==0.3.20
langchain-community==0.3.19
langchain-core==0.3.45
langchain-openai==0.3.8
langchain-text-splitters==0.3.6
langsmith==0.3.15
langchain-core==0.3.59
langchain-openai==0.3.16
langchain-text-splitters==0.3.8
langsmith==0.3.42
lazy-object-proxy==1.10.0
lxml==5.3.1
markupsafe==3.0.2
marshmallow==3.26.1
mpmath==1.3.0
multidict==6.1.0
multidict==6.4.3
mypy-extensions==1.0.0
networkx==3.4.2
numpy==2.2.1
openai==1.66.3
openapi-schema-validator==0.6.3
openapi-spec-validator==0.6.0
openapi3-parser==1.1.19
openai==1.78.1
openapi3-parser==1.1.21
orjson==3.10.14
packaging==24.1
packaging==25.0
pandas==2.2.3
openpyxl==3.1.5
pathable==0.4.4
pillow==11.1.0
portalocker==2.10.1
portalocker==3.1.1
prance==23.6.21.0
primp==0.14.0
prompt-toolkit==3.0.50
prompt-toolkit==3.0.51
protobuf==5.29.3
psycopg2-binary==2.9.10
py==1.11.0
pydantic==2.10.6
pydantic-core==2.27.2
pydantic-settings==2.7.1
pymongo==4.10.1
pypdf==5.2.0
pymongo==4.11.3
pypdf==5.5.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-jose==3.4.0
python-pptx==1.0.2
qdrant-client==1.13.2
redis==5.2.1
referencing==0.30.2
referencing==0.36.2
regex==2024.11.6
requests==2.32.3
retry==0.9.2
sentence-transformers==3.3.1
tiktoken==0.8.0
tokenizers==0.21.0
torch==2.5.1
torch==2.7.0
tqdm==4.67.1
transformers==4.49.0
transformers==4.51.3
typing-extensions==4.12.2
typing-inspect==0.9.0
tzdata==2024.2
@@ -91,7 +79,7 @@ urllib3==2.3.0
vine==5.1.0
wcwidth==0.2.13
werkzeug==3.1.3
yarl==1.18.3
markdownify==0.14.1
yarl==1.20.0
markdownify==1.1.0
tldextract==5.1.3
websockets==14.1

View File

@@ -1,3 +1,4 @@
import logging
from application.core.settings import settings
from application.llm.llm_creator import LLMCreator
from application.retriever.base import BaseRetriever
@@ -72,7 +73,7 @@ class ClassicRAG(BaseRetriever):
print(f"Rephrased query: {rephrased_query}")
return rephrased_query if rephrased_query else self.original_question
except Exception as e:
print(f"Error rephrasing query: {e}")
logging.error(f"Error rephrasing query: {e}", exc_info=True)
return self.original_question
def _get_data(self):

View File

@@ -0,0 +1,94 @@
"""Base storage class for file system abstraction."""
from abc import ABC, abstractmethod
from typing import BinaryIO, List, Callable
class BaseStorage(ABC):
"""Abstract base class for storage implementations."""
@abstractmethod
def save_file(self, file_data: BinaryIO, path: str) -> dict:
"""
Save a file to storage.
Args:
file_data: File-like object containing the data
path: Path where the file should be stored
Returns:
dict: A dictionary containing metadata about the saved file, including:
- 'path': The path where the file was saved
- 'storage_type': The type of storage (e.g., 'local', 's3')
- Other storage-specific metadata (e.g., 'uri', 'bucket_name', etc.)
"""
pass
@abstractmethod
def get_file(self, path: str) -> BinaryIO:
"""
Retrieve a file from storage.
Args:
path: Path to the file
Returns:
BinaryIO: File-like object containing the file data
"""
pass
@abstractmethod
def process_file(self, path: str, processor_func: Callable, **kwargs):
"""
Process a file using the provided processor function.
This method handles the details of retrieving the file and providing
it to the processor function in an appropriate way based on the storage type.
Args:
path: Path to the file
processor_func: Function that processes the file
**kwargs: Additional arguments to pass to the processor function
Returns:
The result of the processor function
"""
pass
@abstractmethod
def delete_file(self, path: str) -> bool:
"""
Delete a file from storage.
Args:
path: Path to the file
Returns:
bool: True if deletion was successful
"""
pass
@abstractmethod
def file_exists(self, path: str) -> bool:
"""
Check if a file exists.
Args:
path: Path to the file
Returns:
bool: True if the file exists
"""
pass
@abstractmethod
def list_files(self, directory: str) -> List[str]:
"""
List all files in a directory.
Args:
directory: Directory path to list
Returns:
List[str]: List of file paths
"""
pass

View File

@@ -0,0 +1,103 @@
"""Local file system implementation."""
import os
import shutil
from typing import BinaryIO, List, Callable
from application.storage.base import BaseStorage
class LocalStorage(BaseStorage):
"""Local file system storage implementation."""
def __init__(self, base_dir: str = None):
"""
Initialize local storage.
Args:
base_dir: Base directory for all operations. If None, uses current directory.
"""
self.base_dir = base_dir or os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
def _get_full_path(self, path: str) -> str:
"""Get absolute path by combining base_dir and path."""
if os.path.isabs(path):
return path
return os.path.join(self.base_dir, path)
def save_file(self, file_data: BinaryIO, path: str) -> dict:
"""Save a file to local storage."""
full_path = self._get_full_path(path)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
if hasattr(file_data, 'save'):
file_data.save(full_path)
else:
with open(full_path, 'wb') as f:
shutil.copyfileobj(file_data, f)
return {
'storage_type': 'local'
}
def get_file(self, path: str) -> BinaryIO:
"""Get a file from local storage."""
full_path = self._get_full_path(path)
if not os.path.exists(full_path):
raise FileNotFoundError(f"File not found: {full_path}")
return open(full_path, 'rb')
def delete_file(self, path: str) -> bool:
"""Delete a file from local storage."""
full_path = self._get_full_path(path)
if not os.path.exists(full_path):
return False
os.remove(full_path)
return True
def file_exists(self, path: str) -> bool:
"""Check if a file exists in local storage."""
full_path = self._get_full_path(path)
return os.path.exists(full_path)
def list_files(self, directory: str) -> List[str]:
"""List all files in a directory in local storage."""
full_path = self._get_full_path(directory)
if not os.path.exists(full_path):
return []
result = []
for root, _, files in os.walk(full_path):
for file in files:
rel_path = os.path.relpath(os.path.join(root, file), self.base_dir)
result.append(rel_path)
return result
def process_file(self, path: str, processor_func: Callable, **kwargs):
"""
Process a file using the provided processor function.
For local storage, we can directly pass the full path to the processor.
Args:
path: Path to the file
processor_func: Function that processes the file
**kwargs: Additional arguments to pass to the processor function
Returns:
The result of the processor function
"""
full_path = self._get_full_path(path)
if not os.path.exists(full_path):
raise FileNotFoundError(f"File not found: {full_path}")
return processor_func(local_path=full_path, **kwargs)

120
application/storage/s3.py Normal file
View File

@@ -0,0 +1,120 @@
"""S3 storage implementation."""
import io
from typing import BinaryIO, List, Callable
import os
import boto3
from botocore.exceptions import ClientError
from application.storage.base import BaseStorage
from application.core.settings import settings
class S3Storage(BaseStorage):
"""AWS S3 storage implementation."""
def __init__(self, bucket_name=None):
"""
Initialize S3 storage.
Args:
bucket_name: S3 bucket name (optional, defaults to settings)
"""
self.bucket_name = bucket_name or getattr(settings, "S3_BUCKET_NAME", "docsgpt-test-bucket")
# Get credentials from settings
aws_access_key_id = getattr(settings, "SAGEMAKER_ACCESS_KEY", None)
aws_secret_access_key = getattr(settings, "SAGEMAKER_SECRET_KEY", None)
region_name = getattr(settings, "SAGEMAKER_REGION", None)
self.s3 = boto3.client(
's3',
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
region_name=region_name
)
def save_file(self, file_data: BinaryIO, path: str) -> dict:
"""Save a file to S3 storage."""
self.s3.upload_fileobj(file_data, self.bucket_name, path)
region = getattr(settings, "SAGEMAKER_REGION", None)
return {
'storage_type': 's3',
'bucket_name': self.bucket_name,
'uri': f's3://{self.bucket_name}/{path}',
'region': region
}
def get_file(self, path: str) -> BinaryIO:
"""Get a file from S3 storage."""
if not self.file_exists(path):
raise FileNotFoundError(f"File not found: {path}")
file_obj = io.BytesIO()
self.s3.download_fileobj(self.bucket_name, path, file_obj)
file_obj.seek(0)
return file_obj
def delete_file(self, path: str) -> bool:
"""Delete a file from S3 storage."""
try:
self.s3.delete_object(Bucket=self.bucket_name, Key=path)
return True
except ClientError:
return False
def file_exists(self, path: str) -> bool:
"""Check if a file exists in S3 storage."""
try:
self.s3.head_object(Bucket=self.bucket_name, Key=path)
return True
except ClientError:
return False
def list_files(self, directory: str) -> List[str]:
"""List all files in a directory in S3 storage."""
# Ensure directory ends with a slash if it's not empty
if directory and not directory.endswith('/'):
directory += '/'
result = []
paginator = self.s3.get_paginator('list_objects_v2')
pages = paginator.paginate(Bucket=self.bucket_name, Prefix=directory)
for page in pages:
if 'Contents' in page:
for obj in page['Contents']:
result.append(obj['Key'])
return result
def process_file(self, path: str, processor_func: Callable, **kwargs):
"""
Process a file using the provided processor function.
Args:
path: Path to the file
processor_func: Function that processes the file
**kwargs: Additional arguments to pass to the processor function
Returns:
The result of the processor function
"""
import tempfile
import logging
if not self.file_exists(path):
raise FileNotFoundError(f"File not found in S3: {path}")
with tempfile.NamedTemporaryFile(suffix=os.path.splitext(path)[1], delete=True) as temp_file:
try:
# Download the file from S3 to the temporary file
self.s3.download_fileobj(self.bucket_name, path, temp_file)
temp_file.flush()
return processor_func(local_path=temp_file.name, **kwargs)
except Exception as e:
logging.error(f"Error processing S3 file {path}: {e}", exc_info=True)
raise

View File

@@ -0,0 +1,32 @@
"""Storage factory for creating different storage implementations."""
from typing import Dict, Type
from application.storage.base import BaseStorage
from application.storage.local import LocalStorage
from application.storage.s3 import S3Storage
from application.core.settings import settings
class StorageCreator:
storages: Dict[str, Type[BaseStorage]] = {
"local": LocalStorage,
"s3": S3Storage,
}
_instance = None
@classmethod
def get_storage(cls) -> BaseStorage:
if cls._instance is None:
storage_type = getattr(settings, "STORAGE_TYPE", "local")
cls._instance = cls.create_storage(storage_type)
return cls._instance
@classmethod
def create_storage(cls, type_name: str, *args, **kwargs) -> BaseStorage:
storage_class = cls.storages.get(type_name.lower())
if not storage_class:
raise ValueError(f"No storage implementation found for type {type_name}")
return storage_class(*args, **kwargs)

View File

@@ -2,10 +2,11 @@ import sys
from datetime import datetime
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.utils import num_tokens_from_object_or_list, num_tokens_from_string
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
db = mongo[settings.MONGO_DB_NAME]
usage_collection = db["token_usage"]

View File

@@ -1,9 +1,6 @@
from application.vectorstore.base import BaseVectorStore
from application.core.settings import settings
from application.vectorstore.document_class import Document
import elasticsearch
class ElasticsearchStore(BaseVectorStore):
@@ -26,8 +23,7 @@ class ElasticsearchStore(BaseVectorStore):
else:
raise ValueError("Please provide either elasticsearch_url or cloud_id.")
import elasticsearch
ElasticsearchStore._es_connection = elasticsearch.Elasticsearch(**connection_params)
self.docsearch = ElasticsearchStore._es_connection
@@ -155,8 +151,6 @@ class ElasticsearchStore(BaseVectorStore):
**kwargs,
):
from elasticsearch.helpers import BulkIndexError, bulk
bulk_kwargs = bulk_kwargs or {}
import uuid
embeddings = []
@@ -189,6 +183,7 @@ class ElasticsearchStore(BaseVectorStore):
if len(requests) > 0:
from elasticsearch.helpers import BulkIndexError, bulk
try:
success, failed = bulk(
self._es_connection,

View File

@@ -1,17 +1,19 @@
import os
import tempfile
from langchain_community.vectorstores import FAISS
from application.core.settings import settings
from application.parser.schema.base import Document
from application.vectorstore.base import BaseVectorStore
from application.storage.storage_creator import StorageCreator
def get_vectorstore(path: str) -> str:
if path:
vectorstore = os.path.join("application", "indexes", path)
vectorstore = f"indexes/{path}"
else:
vectorstore = os.path.join("application")
vectorstore = "indexes"
return vectorstore
@@ -21,16 +23,36 @@ class FaissStore(BaseVectorStore):
self.source_id = source_id
self.path = get_vectorstore(source_id)
self.embeddings = self._get_embeddings(settings.EMBEDDINGS_NAME, embeddings_key)
self.storage = StorageCreator.get_storage()
try:
if docs_init:
self.docsearch = FAISS.from_documents(docs_init, self.embeddings)
else:
self.docsearch = FAISS.load_local(
self.path, self.embeddings, allow_dangerous_deserialization=True
)
except Exception:
raise
with tempfile.TemporaryDirectory() as temp_dir:
faiss_path = f"{self.path}/index.faiss"
pkl_path = f"{self.path}/index.pkl"
if not self.storage.file_exists(faiss_path) or not self.storage.file_exists(pkl_path):
raise FileNotFoundError(f"Index files not found in storage at {self.path}")
faiss_file = self.storage.get_file(faiss_path)
pkl_file = self.storage.get_file(pkl_path)
local_faiss_path = os.path.join(temp_dir, "index.faiss")
local_pkl_path = os.path.join(temp_dir, "index.pkl")
with open(local_faiss_path, 'wb') as f:
f.write(faiss_file.read())
with open(local_pkl_path, 'wb') as f:
f.write(pkl_file.read())
self.docsearch = FAISS.load_local(
temp_dir, self.embeddings, allow_dangerous_deserialization=True
)
except Exception as e:
raise Exception(f"Error loading FAISS index: {str(e)}")
self.assert_embedding_dimensions(self.embeddings)

View File

@@ -1,3 +1,4 @@
import logging
from application.core.settings import settings
from application.vectorstore.base import BaseVectorStore
from application.vectorstore.document_class import Document
@@ -146,7 +147,7 @@ class MongoDBVectorStore(BaseVectorStore):
return chunks
except Exception as e:
print(f"Error getting chunks: {e}")
logging.error(f"Error getting chunks: {e}", exc_info=True)
return []
def add_chunk(self, text, metadata=None):
@@ -172,5 +173,5 @@ class MongoDBVectorStore(BaseVectorStore):
result = self._collection.delete_one({"_id": object_id})
return result.deleted_count > 0
except Exception as e:
print(f"Error deleting chunk: {e}")
logging.error(f"Error deleting chunk: {e}", exc_info=True)
return False

View File

@@ -1,11 +1,12 @@
from langchain_community.vectorstores.qdrant import Qdrant
from application.vectorstore.base import BaseVectorStore
from application.core.settings import settings
from qdrant_client import models
class QdrantStore(BaseVectorStore):
def __init__(self, source_id: str = "", embeddings_key: str = "embeddings"):
from qdrant_client import models
from langchain_community.vectorstores.qdrant import Qdrant
self._filter = models.Filter(
must=[
models.FieldCondition(

View File

@@ -1,25 +1,37 @@
import datetime
import json
import logging
import mimetypes
import os
import shutil
import string
import tempfile
import zipfile
from collections import Counter
from urllib.parse import urljoin
import requests
from bson.dbref import DBRef
from bson.objectid import ObjectId
from application.agents.agent_creator import AgentCreator
from application.api.answer.routes import get_prompt
from application.core.mongo_db import MongoDB
from application.core.settings import settings
from application.parser.file.bulk import SimpleDirectoryReader
from application.parser.chunking import Chunker
from application.parser.embedding_pipeline import embed_and_store_documents
from application.parser.file.bulk import SimpleDirectoryReader
from application.parser.remote.remote_creator import RemoteCreator
from application.parser.schema.base import Document
from application.parser.chunking import Chunker
from application.utils import count_tokens_docs
from application.retriever.retriever_creator import RetrieverCreator
from application.storage.storage_creator import StorageCreator
from application.utils import count_tokens_docs, num_tokens_from_string
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
db = mongo[settings.MONGO_DB_NAME]
sources_collection = db["sources"]
# Constants
@@ -27,18 +39,22 @@ MIN_TOKENS = 150
MAX_TOKENS = 1250
RECURSION_DEPTH = 2
# Define a function to extract metadata from a given filename.
def metadata_from_filename(title):
return {"title": title}
# Define a function to generate a random string of a given length.
def generate_random_string(length):
return "".join([string.ascii_letters[i % 52] for i in range(length)])
current_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
def extract_zip_recursive(zip_path, extract_to, current_depth=0, max_depth=5):
"""
Recursively extract zip files with a limit on recursion depth.
@@ -58,7 +74,7 @@ def extract_zip_recursive(zip_path, extract_to, current_depth=0, max_depth=5):
zip_ref.extractall(extract_to)
os.remove(zip_path) # Remove the zip file after extracting
except Exception as e:
logging.error(f"Error extracting zip file {zip_path}: {e}")
logging.error(f"Error extracting zip file {zip_path}: {e}", exc_info=True)
return
# Check for nested zip files and extract them
@@ -69,6 +85,7 @@ def extract_zip_recursive(zip_path, extract_to, current_depth=0, max_depth=5):
file_path = os.path.join(root, file)
extract_zip_recursive(file_path, root, current_depth + 1, max_depth)
def download_file(url, params, dest_path):
try:
response = requests.get(url, params=params)
@@ -79,6 +96,7 @@ def download_file(url, params, dest_path):
logging.error(f"Error downloading file: {e}")
raise
def upload_index(full_path, file_data):
try:
if settings.VECTOR_STORE == "faiss":
@@ -87,7 +105,9 @@ def upload_index(full_path, file_data):
"file_pkl": open(full_path + "/index.pkl", "rb"),
}
response = requests.post(
urljoin(settings.API_URL, "/api/upload_index"), files=files, data=file_data
urljoin(settings.API_URL, "/api/upload_index"),
files=files,
data=file_data,
)
else:
response = requests.post(
@@ -102,6 +122,76 @@ def upload_index(full_path, file_data):
for file in files.values():
file.close()
def run_agent_logic(agent_config, input_data):
try:
source = agent_config.get("source")
retriever = agent_config.get("retriever", "classic")
if isinstance(source, DBRef):
source_doc = db.dereference(source)
source = str(source_doc["_id"])
retriever = source_doc.get("retriever", agent_config.get("retriever"))
else:
source = {}
source = {"active_docs": source}
chunks = int(agent_config.get("chunks", 2))
prompt_id = agent_config.get("prompt_id", "default")
user_api_key = agent_config["key"]
agent_type = agent_config.get("agent_type", "classic")
decoded_token = {"sub": agent_config.get("user")}
prompt = get_prompt(prompt_id)
agent = AgentCreator.create_agent(
agent_type,
endpoint="webhook",
llm_name=settings.LLM_NAME,
gpt_model=settings.MODEL_NAME,
api_key=settings.API_KEY,
user_api_key=user_api_key,
prompt=prompt,
chat_history=[],
decoded_token=decoded_token,
attachments=[],
)
retriever = RetrieverCreator.create_retriever(
retriever,
source=source,
chat_history=[],
prompt=prompt,
chunks=chunks,
token_limit=settings.DEFAULT_MAX_HISTORY,
gpt_model=settings.MODEL_NAME,
user_api_key=user_api_key,
decoded_token=decoded_token,
)
answer = agent.gen(query=input_data, retriever=retriever)
response_full = ""
thought = ""
source_log_docs = []
tool_calls = []
for line in answer:
if "answer" in line:
response_full += str(line["answer"])
elif "sources" in line:
source_log_docs.extend(line["sources"])
elif "tool_calls" in line:
tool_calls.extend(line["tool_calls"])
elif "thought" in line:
thought += line["thought"]
result = {
"answer": response_full,
"sources": source_log_docs,
"tool_calls": tool_calls,
"thought": thought,
}
logging.info(f"Agent response: {result}")
return result
except Exception as e:
logging.error(f"Error in run_agent_logic: {e}", exc_info=True)
raise
# Define the main function for ingesting and processing documents.
def ingest_worker(
self, directory, formats, name_job, filename, user, retriever="classic"
@@ -126,62 +216,87 @@ def ingest_worker(
limit = None
exclude = True
sample = False
storage = StorageCreator.get_storage()
full_path = os.path.join(directory, user, name_job)
source_file_path = os.path.join(full_path, filename)
logging.info(f"Ingest file: {full_path}", extra={"user": user, "job": name_job})
file_data = {"name": name_job, "file": filename, "user": user}
if not os.path.exists(full_path):
os.makedirs(full_path)
download_file(urljoin(settings.API_URL, "/api/download"), file_data, os.path.join(full_path, filename))
# Create temporary working directory
with tempfile.TemporaryDirectory() as temp_dir:
try:
os.makedirs(temp_dir, exist_ok=True)
# check if file is .zip and extract it
if filename.endswith(".zip"):
extract_zip_recursive(
os.path.join(full_path, filename), full_path, 0, RECURSION_DEPTH
)
# Download file from storage to temp directory
temp_file_path = os.path.join(temp_dir, filename)
file_data = storage.get_file(source_file_path)
self.update_state(state="PROGRESS", meta={"current": 1})
with open(temp_file_path, "wb") as f:
f.write(file_data.read())
raw_docs = SimpleDirectoryReader(
input_dir=full_path,
input_files=input_files,
recursive=recursive,
required_exts=formats,
num_files_limit=limit,
exclude_hidden=exclude,
file_metadata=metadata_from_filename,
).load_data()
self.update_state(state="PROGRESS", meta={"current": 1})
chunker = Chunker(
chunking_strategy="classic_chunk",
max_tokens=MAX_TOKENS,
min_tokens=MIN_TOKENS,
duplicate_headers=False
)
raw_docs = chunker.chunk(documents=raw_docs)
# Handle zip files
if filename.endswith(".zip"):
logging.info(f"Extracting zip file: {filename}")
extract_zip_recursive(
temp_file_path, temp_dir, current_depth=0, max_depth=RECURSION_DEPTH
)
docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
id = ObjectId()
if sample:
logging.info(f"Sample mode enabled. Using {limit} documents.")
embed_and_store_documents(docs, full_path, id, self)
tokens = count_tokens_docs(docs)
self.update_state(state="PROGRESS", meta={"current": 100})
reader = SimpleDirectoryReader(
input_dir=temp_dir,
input_files=input_files,
recursive=recursive,
required_exts=formats,
exclude_hidden=exclude,
file_metadata=metadata_from_filename,
)
raw_docs = reader.load_data()
if sample:
for i in range(min(5, len(raw_docs))):
logging.info(f"Sample document {i}: {raw_docs[i]}")
chunker = Chunker(
chunking_strategy="classic_chunk",
max_tokens=MAX_TOKENS,
min_tokens=MIN_TOKENS,
duplicate_headers=False,
)
raw_docs = chunker.chunk(documents=raw_docs)
file_data.update({
"tokens": tokens,
"retriever": retriever,
"id": str(id),
"type": "local",
})
upload_index(full_path, file_data)
docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
# delete local
shutil.rmtree(full_path)
id = ObjectId()
vector_store_path = os.path.join(temp_dir, "vector_store")
os.makedirs(vector_store_path, exist_ok=True)
embed_and_store_documents(docs, vector_store_path, id, self)
tokens = count_tokens_docs(docs)
self.update_state(state="PROGRESS", meta={"current": 100})
if sample:
for i in range(min(5, len(raw_docs))):
logging.info(f"Sample document {i}: {raw_docs[i]}")
file_data = {
"name": name_job,
"file": filename,
"user": user,
"tokens": tokens,
"retriever": retriever,
"id": str(id),
"type": "local",
}
upload_index(vector_store_path, file_data)
except Exception as e:
logging.error(f"Error in ingest_worker: {e}", exc_info=True)
raise
return {
"directory": directory,
@@ -192,6 +307,7 @@ def ingest_worker(
"limited": False,
}
def remote_worker(
self,
source_data,
@@ -203,7 +319,7 @@ def remote_worker(
sync_frequency="never",
operation_mode="upload",
doc_id=None,
):
):
full_path = os.path.join(directory, user, name_job)
if not os.path.exists(full_path):
os.makedirs(full_path)
@@ -218,7 +334,7 @@ def remote_worker(
chunking_strategy="classic_chunk",
max_tokens=MAX_TOKENS,
min_tokens=MIN_TOKENS,
duplicate_headers=False
duplicate_headers=False,
)
docs = chunker.chunk(documents=raw_docs)
docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
@@ -260,6 +376,7 @@ def remote_worker(
logging.info("remote_worker task completed successfully")
return {"urls": source_data, "name_job": name_job, "user": user, "limited": False}
def sync(
self,
source_data,
@@ -285,10 +402,11 @@ def sync(
doc_id,
)
except Exception as e:
logging.error(f"Error during sync: {e}")
logging.error(f"Error during sync: {e}", exc_info=True)
return {"status": "error", "error": str(e)}
return {"status": "success"}
def sync_worker(self, frequency):
sync_counts = Counter()
sources = sources_collection.find()
@@ -312,3 +430,121 @@ def sync_worker(self, frequency):
key: sync_counts[key]
for key in ["total_sync_count", "sync_success", "sync_failure"]
}
def attachment_worker(self, file_info, user):
"""
Process and store a single attachment without vectorization.
"""
mongo = MongoDB.get_client()
db = mongo[settings.MONGO_DB_NAME]
attachments_collection = db["attachments"]
filename = file_info["filename"]
attachment_id = file_info["attachment_id"]
relative_path = file_info["path"]
metadata = file_info.get("metadata", {})
try:
self.update_state(state="PROGRESS", meta={"current": 10})
storage = StorageCreator.get_storage()
self.update_state(
state="PROGRESS", meta={"current": 30, "status": "Processing content"}
)
content = storage.process_file(
relative_path,
lambda local_path, **kwargs: SimpleDirectoryReader(
input_files=[local_path], exclude_hidden=True, errors="ignore"
).load_data()[0].text
)
token_count = num_tokens_from_string(content)
self.update_state(
state="PROGRESS", meta={"current": 80, "status": "Storing in database"}
)
mime_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
doc_id = ObjectId(attachment_id)
attachments_collection.insert_one(
{
"_id": doc_id,
"user": user,
"path": relative_path,
"content": content,
"token_count": token_count,
"mime_type": mime_type,
"date": datetime.datetime.now(),
"metadata": metadata,
}
)
logging.info(
f"Stored attachment with ID: {attachment_id}", extra={"user": user}
)
self.update_state(
state="PROGRESS", meta={"current": 100, "status": "Complete"}
)
return {
"filename": filename,
"path": relative_path,
"token_count": token_count,
"attachment_id": attachment_id,
"mime_type": mime_type,
"metadata": metadata,
}
except Exception as e:
logging.error(
f"Error processing file {filename}: {e}",
extra={"user": user},
exc_info=True,
)
raise
def agent_webhook_worker(self, agent_id, payload):
"""
Process the webhook payload for an agent.
Args:
self: Reference to the instance of the task.
agent_id (str): Unique identifier for the agent.
payload (dict): The payload data from the webhook.
Returns:
dict: Information about the processed webhook.
"""
mongo = MongoDB.get_client()
db = mongo["docsgpt"]
agents_collection = db["agents"]
self.update_state(state="PROGRESS", meta={"current": 1})
try:
agent_oid = ObjectId(agent_id)
agent_config = agents_collection.find_one({"_id": agent_oid})
if not agent_config:
raise ValueError(f"Agent with ID {agent_id} not found.")
input_data = json.dumps(payload)
except Exception as e:
logging.error(f"Error processing agent webhook: {e}", exc_info=True)
return {"status": "error", "error": str(e)}
self.update_state(state="PROGRESS", meta={"current": 50})
try:
result = run_agent_logic(agent_config, input_data)
except Exception as e:
logging.error(f"Error running agent logic: {e}", exc_info=True)
return {"status": "error", "error": str(e)}
finally:
self.update_state(state="PROGRESS", meta={"current": 100})
logging.info(
f"Webhook processed for agent {agent_id}", extra={"agent_id": agent_id}
)
return {"status": "success", "result": result}

View File

@@ -12,6 +12,7 @@ services:
- backend
backend:
user: root
build: ../application
environment:
- API_KEY=$API_KEY
@@ -27,15 +28,16 @@ services:
- "7091:7091"
volumes:
- ../application/indexes:/app/application/indexes
- ../application/inputs:/app/application/inputs
- ../application/inputs:/app/inputs
- ../application/vectors:/app/application/vectors
depends_on:
- redis
- mongo
worker:
user: root
build: ../application
command: celery -A application.app.celery worker -l INFO --pool=gevent -B
command: celery -A application.app.celery worker -l INFO -B
environment:
- API_KEY=$API_KEY
- EMBEDDINGS_KEY=$API_KEY
@@ -45,6 +47,10 @@ services:
- MONGO_URI=mongodb://mongo:27017/docsgpt
- API_URL=http://backend:7091
- CACHE_REDIS_URL=redis://redis:6379/2
volumes:
- ../application/indexes:/app/application/indexes
- ../application/inputs:/app/inputs
- ../application/vectors:/app/application/vectors
depends_on:
- redis
- mongo

View File

@@ -1,212 +0,0 @@
# Setting up the DocsGPT Widget in Your React Project
## Introduction:
The DocsGPT Widget is a powerful tool that allows you to integrate AI-powered documentation assistance into your web applications. This guide will walk you through the installation and usage of the DocsGPT Widget in your React project. Whether you're building a web app or a knowledge base, this widget can enhance your user experience.
## Installation
First, make sure you have Node.js and npm installed in your project. Then go to your project and install a new dependency: `npm install docsgpt`.
## Usage
In the file where you want to use the widget, import it and include the CSS file:
```js
import { DocsGPTWidget } from "docsgpt";
```
Now, you can use the widget in your component like this :
```jsx
<DocsGPTWidget
apiHost="https://your-docsgpt-api.com"
apiKey=""
avatar = "https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"
title = "Get AI assistance"
description = "DocsGPT's AI Chatbot is here to help"
heroTitle = "Welcome to DocsGPT !"
heroDescription="This chatbot is built with DocsGPT and utilises GenAI,
please review important information using sources."
theme = "dark"
buttonIcon = "https://your-icon"
buttonBg = "#222327"
/>
```
## Props Table for DocsGPT Widget
| **Prop** | **Type** | **Default Value** | **Description** |
|--------------------|------------------|-------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| **`apiHost`** | `string` | `"https://gptcloud.arc53.com"` | The URL of your DocsGPT API for vector search and chatbot queries. |
| **`apiKey`** | `string` | `""` | Your API key for authentication. Can be left empty if authentication is not required. |
| **`avatar`** | `string` | `"https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"` | Specifies the URL of the avatar or image representing the chatbot. |
| **`title`** | `string` | `"Get AI assistance"` | Sets the title text displayed in the chatbot interface. |
| **`description`** | `string` | `"DocsGPT's AI Chatbot is here to help"` | Provides a brief description of the chatbot's purpose or functionality. |
| **`heroTitle`** | `string` | `"Welcome to DocsGPT !"` | Displays a welcome title when users interact with the chatbot. |
| **`heroDescription`** | `string` | `"This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources."` | Provides additional introductory text or information about the chatbot's capabilities. |
| **`theme`** | `"dark" \| "light"` | `"dark"` | Allows you to select the theme for the chatbot interface. Accepts `"dark"` or `"light"`. |
| **`buttonIcon`** | `string` | `"https://your-icon"` | Specifies the URL of the icon image for the widget's launch button. |
| **`buttonBg`** | `string` | `"#222327"` | Sets the background color of the widget's launch button. |
| **`size`** | `"small" \| "medium"` | `"medium"` | Sets the size of the widget. Options are `"small"` or `"medium"`. |
---
## Notes
- **Customizing Props:** All properties can be overridden when embedding the widget. For example, you can provide a unique avatar, title, or color scheme to better align with your brand.
- **Default Theme:** The widget defaults to the dark theme unless explicitly set to `"light"`.
- **API Key:** If the `apiKey` is not required for your application, leave it empty.
This table provides a clear overview of the customization options available for tailoring the DocsGPT widget to fit your application.
## How to use DocsGPTWidget with [Nextra](https://nextra.site/) (Next.js + MDX)
Install your widget as described above and then go to your `pages/` folder and create a new file `_app.js` with the following content:
```js
import { DocsGPTWidget } from "docsgpt";
export default function MyApp({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
<DocsGPTWidget selectDocs="local/docsgpt-sep.zip/"/>
</>
)
}
```
## How to use DocsGPTWidget with HTML
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>HTML + CSS</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<h1>This is a simple HTML + CSS template!</h1>
<div id="app"></div>
<!-- Include the widget script from dist/modern or dist/legacy -->
<script
src="https://unpkg.com/docsgpt/dist/modern/main.js"
type="module"
></script>
<script type="module">
window.onload = function () {
renderDocsGPTWidget("app", {
apiKey: "",
size: "medium",
});
};
</script>
</body>
</html>
```
To link the widget to your api and your documents you can pass parameters to the renderDocsGPTWidget('div id', { parameters }).
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocsGPT Widget</title>
<script src="https://unpkg.com/docsgpt/dist/modern/main.js" type="module"></script>
</head>
<body>
<div id="app"></div>
<!-- Include the widget script from dist/modern or dist/legacy -->
<script type="module">
window.onload = function() {
renderDocsGPTWidget('app', {
apiHost: 'http://localhost:7001',
apiKey:"",
avatar: 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png',
title: 'Get AI assistance',
description: "DocsGPT's AI Chatbot is here to help",
heroTitle: 'Welcome to DocsGPT!',
heroDescription: 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.',
theme:"dark",
buttonIcon:"https://your-icon",
buttonBg:"#222327"
});
}
</script>
</body>
</html>
```
# SearchBar
The `SearchBar` component is an interactive search bar designed to provide search results based on **vector similarity search**. It also includes the capability to open the AI Chatbot, enabling users to query.
---
### Importing the Component
```tsx
import { SearchBar } from "docsgpt-react";
```
---
### Usage Example
```tsx
<SearchBar
apiKey="your-api-key"
apiHost="https://gptcloud.arc53.com"
theme="light"
placeholder="Search or Ask AI..."
width="300px"
/>
```
---
## HTML embedding for Search bar
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SearchBar Embedding</title>
<script src="https://unpkg.com/docsgpt/dist/modern/main.js"></script> <!-- The bundled JavaScript file -->
</head>
<body>
<!-- Element where the SearchBar will render -->
<div id="search-bar-container"></div>
<script>
// Render the SearchBar into the specified element
renderSearchBar('search-bar-container', {
apiKey: 'your-api-key-here',
apiHost: 'https://your-api-host.com',
theme: 'light',
placeholder: 'Search here...',
width: '300px'
});
</script>
</body>
</html>
```
### Props
| **Prop** | **Type** | **Default Value** | **Description** |
|-----------------|-----------|-------------------------------------|--------------------------------------------------------------------------------------------------|
| **`apiKey`** | `string` | `"74039c6d-bff7-44ce-ae55-2973cbf13837"` | Your API key generated from the app. Used for authenticating requests. |
| **`apiHost`** | `string` | `"https://gptcloud.arc53.com"` | The base URL of the server hosting the vector similarity search and chatbot services. |
| **`theme`** | `"dark" \| "light"` | `"dark"` | The theme of the search bar. Accepts `"dark"` or `"light"`. |
| **`placeholder`** | `string` | `"Search or Ask AI..."` | Placeholder text displayed in the search input field. |
| **`width`** | `string` | `"256px"` | Width of the search bar. Accepts any valid CSS width value (e.g., `"300px"`, `"100%"`, `"20rem"`). |
Feel free to reach out if you need help customizing or extending the `SearchBar`!
## Our github
[DocsGPT](https://github.com/arc53/DocsGPT)
You can find the source code in the extensions/react-widget folder.
For more information about React, refer to this [link here](https://react.dev/learn)

View File

@@ -4,7 +4,7 @@ export default function MyApp({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
<DocsGPTWidget showSources={true} apiKey="6dd66edf-d374-4904-93af-ab0c6d41ee56" theme="dark" size="medium" />
<DocsGPTWidget showSources={true} apiKey="5d8270cb-735f-484e-9dc9-5b407f24e652" theme="dark" size="medium" />
</>
)
}

View File

@@ -73,9 +73,44 @@ The easiest way to launch DocsGPT is using the provided `setup.sh` script. This
## Launching DocsGPT (Windows)
For Windows users, we recommend following the Docker deployment guide for detailed instructions. Please refer to the [Docker Deployment documentation](/Deploying/Docker-Deploying) for step-by-step instructions on setting up DocsGPT on Windows using Docker.
For Windows users, we provide a PowerShell script that offers the same functionality as the macOS/Linux setup script.
**Important for Windows:** Ensure Docker Desktop is installed and running correctly on your Windows system before proceeding.
**Steps:**
1. **Download the DocsGPT Repository:**
First, you need to download the DocsGPT repository to your local machine. You can do this using Git:
```powershell
git clone https://github.com/arc53/DocsGPT.git
cd DocsGPT
```
2. **Run the `setup.ps1` script:**
Execute the PowerShell setup script:
```powershell
PowerShell -ExecutionPolicy Bypass -File .\setup.ps1
```
3. **Follow the interactive setup:**
Just like the Linux/macOS script, the PowerShell script will guide you through setting DocsGPT.
The script will handle environment configuration and start DocsGPT based on your selections.
4. **Access DocsGPT in your browser:**
Once the setup is complete and Docker containers are running, navigate to [http://localhost:5173/](http://localhost:5173/) in your web browser to access the DocsGPT web application.
5. **Stopping DocsGPT:**
To stop DocsGPT run the Docker Compose down command displayed at the end of the setup script's execution.
**Important for Windows:** Ensure Docker Desktop is installed and running correctly on your Windows system before proceeding. The script will attempt to start Docker if it's not running, but you may need to start it manually if there are issues.
**Alternative Method:**
If you prefer a more manual approach, you can follow our [Docker Deployment documentation](/Deploying/Docker-Deploying) for detailed instructions on setting up DocsGPT on Windows using Docker commands directly.
## Advanced Configuration

View File

@@ -1,3 +1,4 @@
# Please put appropriate value
VITE_API_HOST=http://0.0.0.0:7091
VITE_BASE_URL=http://localhost:5173
VITE_API_HOST=http://127.0.0.1:7091
VITE_API_STREAMING=true

File diff suppressed because it is too large Load Diff

View File

@@ -21,15 +21,17 @@
"dependencies": {
"@reduxjs/toolkit": "^2.5.1",
"chart.js": "^4.4.4",
"clsx": "^2.1.1",
"i18next": "^24.2.0",
"i18next-browser-languagedetector": "^8.0.2",
"mermaid": "^11.6.0",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-chartjs-2": "^5.3.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-dropzone": "^14.3.5",
"react-helmet": "^6.1.0",
"react-i18next": "^15.4.0",
"react-markdown": "^9.0.1",
"react-redux": "^8.0.5",
@@ -40,6 +42,7 @@
"remark-math": "^6.0.0"
},
"devDependencies": {
"@types/mermaid": "^9.1.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.3.0",
"@types/react-helmet": "^6.1.11",
@@ -60,8 +63,8 @@
"husky": "^8.0.0",
"lint-staged": "^15.3.0",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"vite": "^5.4.14",

View File

@@ -4,4 +4,5 @@ module.exports = {
semi: true,
singleQuote: true,
printWidth: 80,
}
plugins: ['prettier-plugin-tailwindcss'],
};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/></svg>

After

Width:  |  Height:  |  Size: 976 B

View File

@@ -29,7 +29,7 @@ export default function About() {
If you want to add your own documentation, please follow the
instruction below:
</p>
<p className="mt-4 ml-2">
<p className="ml-2 mt-4">
1. Navigate to{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">
{' '}
@@ -37,13 +37,13 @@ export default function About() {
</span>{' '}
folder
</p>
<p className="mt-4 ml-2">
<p className="ml-2 mt-4">
2. Install dependencies from{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">
pip install -r requirements.txt
</span>
</p>
<p className="mt-4 ml-2">
<p className="ml-2 mt-4">
3. Prepare a{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">.env</span>{' '}
file. Copy{' '}
@@ -54,7 +54,7 @@ export default function About() {
<span className="bg-gray-200 italic dark:bg-outer-space">.env</span>{' '}
with your OpenAI API token
</p>
<p className="mt-4 ml-2">
<p className="ml-2 mt-4">
4. Run the app with{' '}
<span className="bg-gray-200 italic dark:bg-outer-space">
python app.py
@@ -64,9 +64,9 @@ export default function About() {
<p>
Currently It uses{' '}
<span className="text-blue-950 font-medium">DocsGPT</span>{' '}
<span className="font-medium text-blue-950">DocsGPT</span>{' '}
documentation, so it will respond to information relevant to{' '}
<span className="text-blue-950 font-medium">DocsGPT</span>. If you
<span className="font-medium text-blue-950">DocsGPT</span>. If you
want to train it on different documentation - please follow
<a
className="text-blue-500"

View File

@@ -12,13 +12,14 @@ import useTokenAuth from './hooks/useTokenAuth';
import Navigation from './Navigation';
import PageNotFound from './PageNotFound';
import Setting from './settings';
import Agents from './agents';
function AuthWrapper({ children }: { children: React.ReactNode }) {
const { isAuthLoading } = useTokenAuth();
if (isAuthLoading) {
return (
<div className="h-screen flex items-center justify-center">
<div className="flex h-screen items-center justify-center">
<Spinner />
</div>
);
@@ -31,7 +32,7 @@ function MainLayout() {
const [navOpen, setNavOpen] = useState(!isMobile);
return (
<div className="dark:bg-raisin-black relative h-screen overflow-auto">
<div className="relative h-screen overflow-auto dark:bg-raisin-black">
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
<div
className={`h-[calc(100dvh-64px)] md:h-screen ${
@@ -52,7 +53,7 @@ export default function App() {
return <div />;
}
return (
<div className="h-full relative overflow-auto">
<div className="relative h-full overflow-auto">
<Routes>
<Route
element={
@@ -63,7 +64,8 @@ export default function App() {
>
<Route index element={<Conversation />} />
<Route path="/about" element={<About />} />
<Route path="/settings" element={<Setting />} />
<Route path="/settings/*" element={<Setting />} />
<Route path="/agents/*" element={<Agents />} />
</Route>
<Route path="/share/:identifier" element={<SharedConversation />} />
<Route path="/*" element={<PageNotFound />} />

View File

@@ -19,18 +19,18 @@ export default function Hero({
}>;
return (
<div className="flex h-full w-full flex-col text-black-1000 dark:text-bright-gray items-center justify-between">
<div className="flex h-full w-full flex-col items-center justify-between text-black-1000 dark:text-bright-gray">
{/* Header Section */}
<div className="flex flex-col items-center justify-center flex-grow pt-8 md:pt-0">
<div className="flex items-center mb-4">
<div className="flex flex-grow flex-col items-center justify-center pt-8 md:pt-0">
<div className="mb-4 flex items-center">
<span className="text-4xl font-semibold">DocsGPT</span>
<img className="mb-1 inline w-14" src={DocsGPT3} alt="docsgpt" />
</div>
</div>
{/* Demo Buttons Section */}
<div className="w-full max-w-full mb-8 md:mb-16">
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 gap-3 md:gap-4 text-xs">
<div className="mb-8 w-full max-w-full md:mb-16">
<div className="grid grid-cols-1 gap-3 text-xs md:grid-cols-1 md:gap-4 lg:grid-cols-2">
{demos?.map(
(demo: { header: string; query: string }, key: number) =>
demo.header &&
@@ -38,14 +38,12 @@ export default function Hero({
<button
key={key}
onClick={() => handleQuestion({ question: demo.query })}
className="w-full rounded-[66px] border bg-transparent px-6 py-[14px] text-left transition-colors
border-dark-gray text-just-black hover:bg-cultured
dark:border-dim-gray dark:text-chinese-white dark:hover:bg-charleston-green"
className={`w-full rounded-[66px] border border-dark-gray bg-transparent px-6 py-[14px] text-left text-just-black transition-colors hover:bg-cultured dark:border-dim-gray dark:text-chinese-white dark:hover:bg-charleston-green ${key >= 2 ? 'hidden md:block' : ''} // Show only 2 buttons on mobile`}
>
<p className="mb-2 font-semibold text-black-1000 dark:text-bright-gray">
{demo.header}
</p>
<span className="text-gray-700 dark:text-gray-300 opacity-60 line-clamp-2">
<span className="line-clamp-2 text-gray-700 opacity-60 dark:text-gray-300">
{demo.query}
</span>
</button>

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { NavLink, useNavigate } from 'react-router-dom';
import { Agent } from './agents/types';
import conversationService from './api/services/conversationService';
import userService from './api/services/userService';
import Add from './assets/add.svg';
@@ -12,13 +13,15 @@ import Expand from './assets/expand.svg';
import Github from './assets/github.svg';
import Hamburger from './assets/hamburger.svg';
import openNewChat from './assets/openNewChat.svg';
import Pin from './assets/pin.svg';
import Robot from './assets/robot.svg';
import SettingGear from './assets/settingGear.svg';
import Spark from './assets/spark.svg';
import SpinnerDark from './assets/spinner-dark.svg';
import Spinner from './assets/spinner.svg';
import Twitter from './assets/TwitterX.svg';
import UploadIcon from './assets/upload.svg';
import UnPin from './assets/unpin.svg';
import Help from './components/Help';
import SourceDropdown from './components/SourceDropdown';
import {
handleAbort,
selectQueries,
@@ -31,22 +34,19 @@ import useDefaultDocument from './hooks/useDefaultDocument';
import useTokenAuth from './hooks/useTokenAuth';
import DeleteConvModal from './modals/DeleteConvModal';
import JWTModal from './modals/JWTModal';
import { ActiveState, Doc } from './models/misc';
import { getConversations, getDocs } from './preferences/preferenceApi';
import { ActiveState } from './models/misc';
import { getConversations } from './preferences/preferenceApi';
import {
selectApiKeyStatus,
selectAgents,
selectConversationId,
selectConversations,
selectModalStateDeleteConv,
selectPaginatedDocuments,
selectSelectedDocs,
selectSourceDocs,
selectSelectedAgent,
selectToken,
setAgents,
setConversations,
setModalStateDeleteConv,
setPaginatedDocuments,
setSelectedDocs,
setSourceDocs,
setSelectedAgent,
} from './preferences/preferenceSlice';
import Upload from './upload/Upload';
@@ -57,39 +57,63 @@ interface NavigationProps {
export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const dispatch = useDispatch();
const navigate = useNavigate();
const { t } = useTranslation();
const token = useSelector(selectToken);
const queries = useSelector(selectQueries);
const docs = useSelector(selectSourceDocs);
const selectedDocs = useSelector(selectSelectedDocs);
const conversations = useSelector(selectConversations);
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
const conversationId = useSelector(selectConversationId);
const paginatedDocuments = useSelector(selectPaginatedDocuments);
const [isDeletingConversation, setIsDeletingConversation] = useState(false);
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
const agents = useSelector(selectAgents);
const selectedAgent = useSelector(selectSelectedAgent);
const { isMobile } = useMediaQuery();
const [isDarkTheme] = useDarkTheme();
const [isDocsListOpen, setIsDocsListOpen] = useState(false);
const { t } = useTranslation();
const isApiKeySet = useSelector(selectApiKeyStatus);
const { showTokenModal, handleTokenSubmit } = useTokenAuth();
const [isDeletingConversation, setIsDeletingConversation] = useState(false);
const [uploadModalState, setUploadModalState] =
useState<ActiveState>('INACTIVE');
const [recentAgents, setRecentAgents] = useState<Agent[]>([]);
const navRef = useRef(null);
const navigate = useNavigate();
useEffect(() => {
if (!conversations?.data) {
fetchConversations();
async function fetchRecentAgents() {
try {
const response = await userService.getPinnedAgents(token);
if (!response.ok) throw new Error('Failed to fetch pinned agents');
const pinnedAgents: Agent[] = await response.json();
if (pinnedAgents.length >= 3) {
setRecentAgents(pinnedAgents);
return;
}
let tempAgents: Agent[] = [];
if (!agents) {
const response = await userService.getAgents(token);
if (!response.ok) throw new Error('Failed to fetch agents');
const data: Agent[] = await response.json();
dispatch(setAgents(data));
tempAgents = data;
} else tempAgents = agents;
const additionalAgents = tempAgents
.filter(
(agent: Agent) =>
agent.status === 'published' &&
!pinnedAgents.some((pinned) => pinned.id === agent.id),
)
.sort(
(a: Agent, b: Agent) =>
new Date(b.last_used_at ?? 0).getTime() -
new Date(a.last_used_at ?? 0).getTime(),
)
.slice(0, 3 - pinnedAgents.length);
setRecentAgents([...pinnedAgents, ...additionalAgents]);
} catch (error) {
console.error('Failed to fetch recent agents: ', error);
}
if (queries.length === 0) {
resetConversation();
}
}, [conversations?.data, dispatch]);
}
async function fetchConversations() {
dispatch(setConversations({ ...conversations, loading: true }));
@@ -103,6 +127,15 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
});
}
useEffect(() => {
fetchRecentAgents();
}, [agents, token, dispatch]);
useEffect(() => {
if (!conversations?.data) fetchConversations();
if (queries.length === 0) resetConversation();
}, [conversations?.data, dispatch]);
const handleDeleteAllConversations = () => {
setIsDeletingConversation(true);
conversationService
@@ -124,44 +157,61 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
.catch((error) => console.error(error));
};
const handleDeleteClick = (doc: Doc) => {
userService
.deletePath(doc.id ?? '', token)
.then(() => {
return getDocs(token);
})
.then((updatedDocs) => {
dispatch(setSourceDocs(updatedDocs));
const updatedPaginatedDocs = paginatedDocuments?.filter(
(document) => document.id !== doc.id,
const handleAgentClick = (agent: Agent) => {
resetConversation();
dispatch(setSelectedAgent(agent));
if (isMobile) setNavOpen(!navOpen);
navigate('/');
};
const handleTogglePin = (agent: Agent) => {
userService.togglePinAgent(agent.id ?? '', token).then((response) => {
if (response.ok) {
const updatedAgents = agents?.map((a) =>
a.id === agent.id ? { ...a, pinned: !a.pinned } : a,
);
dispatch(
setPaginatedDocuments(updatedPaginatedDocs || paginatedDocuments),
);
dispatch(
setSelectedDocs(
Array.isArray(updatedDocs) &&
updatedDocs?.find(
(doc: Doc) => doc.name.toLowerCase() === 'default',
),
),
);
})
.catch((error) => console.error(error));
dispatch(setAgents(updatedAgents));
}
});
};
const handleConversationClick = (index: string) => {
dispatch(setSelectedAgent(null));
conversationService
.getConversation(index, token)
.then((response) => response.json())
.then((data) => {
navigate('/');
dispatch(setConversation(data));
dispatch(setConversation(data.queries));
dispatch(
updateConversationId({
query: { conversationId: index },
}),
);
if (data.agent_id) {
if (data.is_shared_usage) {
userService
.getSharedAgent(data.shared_token, token)
.then((response) => {
if (response.ok) {
response.json().then((agent: Agent) => {
navigate(`/agents/shared/${agent.shared_token}`);
});
}
});
} else {
userService.getAgent(data.agent_id, token).then((response) => {
if (response.ok) {
response.json().then((agent: Agent) => {
navigate('/');
dispatch(setSelectedAgent(agent));
});
}
});
}
} else {
navigate('/');
dispatch(setSelectedAgent(null));
}
});
};
@@ -173,12 +223,15 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
query: { conversationId: null },
}),
);
dispatch(setSelectedAgent(null));
};
const newChat = () => {
if (queries && queries?.length > 0) {
resetConversation();
}
};
async function updateConversationName(updatedConversation: {
name: string;
id: string;
@@ -197,10 +250,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
});
}
/*
Needed to fix bug where if mobile nav was closed and then window was resized to desktop, nav would still be closed but the button to open would be gone, as per #1 on issue #146
*/
useEffect(() => {
setNavOpen(!isMobile);
}, [isMobile]);
@@ -209,8 +258,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
return (
<>
{!navOpen && (
<div className="duration-25 absolute top-3 left-3 z-20 hidden transition-all md:block">
<div className="flex gap-3 items-center">
<div className="duration-25 absolute left-3 top-3 z-20 hidden transition-all md:block">
<div className="flex items-center gap-3">
<button
onClick={() => {
setNavOpen(!navOpen);
@@ -237,7 +286,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
/>
</button>
)}
<div className="text-[#949494] font-medium text-[20px]">
<div className="text-[20px] font-medium text-[#949494]">
DocsGPT
</div>
</div>
@@ -247,13 +296,13 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
ref={navRef}
className={`${
!navOpen && '-ml-96 md:-ml-[18rem]'
} duration-20 fixed top-0 z-20 flex h-full w-72 flex-col border-r-[1px] border-b-0 bg-lotion dark:bg-chinese-black transition-all dark:border-r-purple-taupe dark:text-white`}
} duration-20 fixed top-0 z-20 flex h-full w-72 flex-col border-b-0 border-r-[1px] bg-lotion transition-all dark:border-r-purple-taupe dark:bg-chinese-black dark:text-white`}
>
<div
className={'visible mt-2 flex h-[6vh] w-full justify-between md:h-12'}
>
<div
className="my-auto mx-4 flex cursor-pointer gap-1.5"
className="mx-4 my-auto flex cursor-pointer gap-1.5"
onClick={() => {
if (isMobile) {
setNavOpen(!navOpen);
@@ -291,7 +340,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
className={({ isActive }) =>
`${
isActive ? 'bg-transparent' : ''
} group sticky mx-4 mt-4 flex cursor-pointer gap-2.5 rounded-3xl border border-silver p-3 hover:border-rainy-gray dark:border-purple-taupe dark:text-white hover:bg-transparent`
} group sticky mx-4 mt-4 flex cursor-pointer gap-2.5 rounded-3xl border border-silver p-3 hover:border-rainy-gray hover:bg-transparent dark:border-purple-taupe dark:text-white`
}
>
<img
@@ -299,7 +348,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
alt="Create new chat"
className="opacity-80 group-hover:opacity-100"
/>
<p className=" text-sm text-dove-gray group-hover:text-neutral-600 dark:text-chinese-silver dark:group-hover:text-bright-gray">
<p className="text-sm text-dove-gray group-hover:text-neutral-600 dark:text-chinese-silver dark:group-hover:text-bright-gray">
{t('newChat')}
</p>
</NavLink>
@@ -308,7 +357,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
className="mb-auto h-[78vh] overflow-y-auto overflow-x-hidden dark:text-white"
>
{conversations?.loading && !isDeletingConversation && (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform">
<img
src={isDarkTheme ? SpinnerDark : Spinner}
className="animate-spin cursor-pointer bg-transparent"
@@ -316,10 +365,95 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
/>
</div>
)}
{conversations?.data && conversations.data.length > 0 ? (
{recentAgents?.length > 0 ? (
<div>
<div className=" my-auto mx-4 mt-2 flex h-6 items-center justify-between gap-4 rounded-3xl">
<p className="mt-1 ml-4 text-sm font-semibold">{t('chats')}</p>
<div className="mx-4 my-auto mt-2 flex h-6 items-center">
<p className="ml-4 mt-1 text-sm font-semibold">Agents</p>
</div>
<div className="agents-container">
<div>
{recentAgents.map((agent, idx) => (
<div
key={idx}
className={`group mx-4 my-auto mt-4 flex h-9 cursor-pointer items-center justify-between rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal ${
agent.id === selectedAgent?.id && !conversationId
? 'bg-bright-gray dark:bg-dark-charcoal'
: ''
}`}
onClick={() => handleAgentClick(agent)}
>
<div className="flex items-center gap-2">
<div className="flex w-6 justify-center">
<img
src={agent.image ?? Robot}
alt="agent-logo"
className="h-6 w-6"
/>
</div>
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
{agent.name}
</p>
</div>
<div
className={`${isMobile ? 'flex' : 'invisible flex group-hover:visible'} items-center px-3`}
>
<button
className="rounded-full hover:opacity-75"
onClick={(e) => {
e.stopPropagation();
handleTogglePin(agent);
}}
>
<img
src={agent.pinned ? UnPin : Pin}
className="h-4 w-4"
></img>
</button>
</div>
</div>
))}
</div>
<div
className="mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
onClick={() => {
dispatch(setSelectedAgent(null));
navigate('/agents');
}}
>
<div className="flex w-6 justify-center">
<img
src={Spark}
alt="manage-agents"
className="h-[18px] w-[18px]"
/>
</div>
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
Manage Agents
</p>
</div>
</div>
</div>
) : (
<div
className="mx-4 my-auto mt-2 flex h-9 cursor-pointer items-center gap-2 rounded-3xl pl-4 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
onClick={() => navigate('/agents')}
>
<div className="flex w-6 justify-center">
<img
src={Spark}
alt="manage-agents"
className="h-[18px] w-[18px]"
/>
</div>
<p className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm leading-6 text-eerie-black dark:text-bright-gray">
Manage Agents
</p>
</div>
)}
{conversations?.data && conversations.data.length > 0 ? (
<div className="mt-7">
<div className="mx-4 my-auto mt-2 flex h-6 items-center justify-between gap-4 rounded-3xl">
<p className="ml-4 mt-1 text-sm font-semibold">{t('chats')}</p>
</div>
<div className="conversations-container">
{conversations.data?.map((conversation) => (
@@ -345,37 +479,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
)}
</div>
<div className="flex h-auto flex-col justify-end text-eerie-black dark:text-white">
<div className="flex flex-col-reverse border-b-[1px] dark:border-b-purple-taupe">
<div className="relative my-4 mx-4 flex gap-4 items-center">
<SourceDropdown
options={docs}
selectedDocs={selectedDocs}
setSelectedDocs={setSelectedDocs}
isDocsListOpen={isDocsListOpen}
setIsDocsListOpen={setIsDocsListOpen}
handleDeleteClick={handleDeleteClick}
handlePostDocumentSelect={(option?: string) => {
if (isMobile) {
setNavOpen(!navOpen);
}
}}
/>
<img
className="hover:cursor-pointer"
src={UploadIcon}
width={28}
height={25}
alt="Upload document"
onClick={() => {
setUploadModalState('ACTIVE');
if (isMobile) {
setNavOpen(!navOpen);
}
}}
></img>
</div>
<p className="ml-5 mt-3 text-sm font-semibold">{t('sourceDocs')}</p>
</div>
<div className="flex flex-col gap-2 border-b-[1px] py-2 dark:border-b-purple-taupe">
<NavLink
onClick={() => {
@@ -386,7 +489,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
}}
to="/settings"
className={({ isActive }) =>
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
`mx-4 my-auto flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
isActive ? 'bg-gray-3000 dark:bg-transparent' : ''
}`
}
@@ -394,15 +497,15 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
<img
src={SettingGear}
alt="Settings"
className="ml-2 w- filter dark:invert"
className="w- ml-2 filter dark:invert"
/>
<p className="my-auto text-sm text-eerie-black dark:text-white">
<p className="my-auto text-sm text-eerie-black dark:text-white">
{t('settings.label')}
</p>
</NavLink>
</div>
<div className="flex flex-col justify-end text-eerie-black dark:text-white">
<div className="flex justify-between items-center py-1">
<div className="flex items-center justify-between py-1">
<Help />
<div className="flex items-center gap-1 pr-4">
@@ -451,9 +554,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
</div>
</div>
<div className="sticky z-10 h-16 w-full border-b-2 bg-gray-50 dark:border-b-purple-taupe dark:bg-chinese-black md:hidden">
<div className="flex gap-6 items-center h-full ml-6 ">
<div className="ml-6 flex h-full items-center gap-6">
<button
className=" h-6 w-6 md:hidden"
className="h-6 w-6 md:hidden"
onClick={() => setNavOpen(true)}
>
<img
@@ -462,7 +565,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
className="w-7 filter dark:invert"
/>
</button>
<div className="text-[#949494] font-medium text-[20px]">DocsGPT</div>
<div className="text-[20px] font-medium text-[#949494]">DocsGPT</div>
</div>
</div>
<DeleteConvModal

View File

@@ -6,7 +6,7 @@ export default function PageNotFound() {
<p className="mx-auto my-auto mt-20 flex w-full max-w-6xl flex-col place-items-center gap-6 rounded-3xl bg-gray-100 p-6 text-jet dark:bg-outer-space dark:text-gray-100 lg:p-10 xl:p-16">
<h1>404</h1>
<p>The page you are looking for does not exist.</p>
<button className="pointer-cursor mr-4 flex cursor-pointer items-center justify-center rounded-full bg-blue-1000 py-2 px-4 text-white transition-colors duration-100 hover:bg-blue-3000">
<button className="pointer-cursor mr-4 flex cursor-pointer items-center justify-center rounded-full bg-blue-1000 px-4 py-2 text-white transition-colors duration-100 hover:bg-blue-3000">
<Link to="/">Go Back Home</Link>
</button>
</p>

View File

@@ -0,0 +1,123 @@
import { useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import userService from '../api/services/userService';
import Robot from '../assets/robot.svg';
import ThreeDots from '../assets/three-dots.svg';
import ContextMenu, { MenuOption } from '../components/ContextMenu';
import ConfirmationModal from '../modals/ConfirmationModal';
import { ActiveState } from '../models/misc';
import {
selectToken,
setAgents,
setSelectedAgent,
} from '../preferences/preferenceSlice';
import { Agent } from './types';
type AgentCardProps = {
agent: Agent;
agents: Agent[];
menuOptions?: MenuOption[];
onDelete?: (agentId: string) => void;
};
export default function AgentCard({
agent,
agents,
menuOptions,
onDelete,
}: AgentCardProps) {
const navigate = useNavigate();
const dispatch = useDispatch();
const token = useSelector(selectToken);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [deleteConfirmation, setDeleteConfirmation] =
useState<ActiveState>('INACTIVE');
const menuRef = useRef<HTMLDivElement>(null);
const handleCardClick = () => {
if (agent.status === 'published') {
dispatch(setSelectedAgent(agent));
navigate('/');
}
};
const defaultDelete = async (agentId: string) => {
const response = await userService.deleteAgent(agentId, token);
if (!response.ok) throw new Error('Failed to delete agent');
const data = await response.json();
dispatch(setAgents(agents.filter((prevAgent) => prevAgent.id !== data.id)));
};
return (
<div
className={`relative flex h-44 w-48 flex-col justify-between rounded-[1.2rem] bg-[#F6F6F6] px-6 py-5 hover:bg-[#ECECEC] dark:bg-[#383838] hover:dark:bg-[#383838]/80 ${
agent.status === 'published' ? 'cursor-pointer' : ''
}`}
onClick={handleCardClick}
>
<div
ref={menuRef}
onClick={(e) => {
e.stopPropagation();
setIsMenuOpen(true);
}}
className="absolute right-4 top-4 z-10 cursor-pointer"
>
<img src={ThreeDots} alt="options" className="h-[19px] w-[19px]" />
{menuOptions && (
<ContextMenu
isOpen={isMenuOpen}
setIsOpen={setIsMenuOpen}
options={menuOptions}
anchorRef={menuRef}
position="top-right"
offset={{ x: 0, y: 0 }}
/>
)}
</div>
<div className="w-full">
<div className="flex w-full items-center gap-1 px-1">
<img
src={agent.image ?? Robot}
alt={`${agent.name}`}
className="h-7 w-7 rounded-full"
/>
{agent.status === 'draft' && (
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">
(Draft)
</p>
)}
</div>
<div className="mt-2">
<p
title={agent.name}
className="truncate px-1 text-[13px] font-semibold capitalize leading-relaxed text-[#020617] dark:text-[#E0E0E0]"
>
{agent.name}
</p>
<p className="mt-1 h-20 overflow-auto px-1 text-[12px] leading-relaxed text-[#64748B] dark:text-sonic-silver-light">
{agent.description}
</p>
</div>
</div>
<ConfirmationModal
message="Are you sure you want to delete this agent?"
modalState={deleteConfirmation}
setModalState={setDeleteConfirmation}
submitLabel="Delete"
handleSubmit={() => {
onDelete ? onDelete(agent.id || '') : defaultDelete(agent.id || '');
setDeleteConfirmation('INACTIVE');
}}
cancelLabel="Cancel"
variant="danger"
/>
</div>
);
}

View File

@@ -0,0 +1,86 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import userService from '../api/services/userService';
import ArrowLeft from '../assets/arrow-left.svg';
import Spinner from '../components/Spinner';
import { selectToken } from '../preferences/preferenceSlice';
import Analytics from '../settings/Analytics';
import Logs from '../settings/Logs';
import { Agent } from './types';
export default function AgentLogs() {
const navigate = useNavigate();
const { agentId } = useParams();
const token = useSelector(selectToken);
const [agent, setAgent] = useState<Agent>();
const [loadingAgent, setLoadingAgent] = useState<boolean>(true);
const fetchAgent = async (agentId: string) => {
setLoadingAgent(true);
try {
const response = await userService.getAgent(agentId ?? '', token);
if (!response.ok) throw new Error('Failed to fetch Chatbots');
const agent = await response.json();
setAgent(agent);
} catch (error) {
console.error(error);
} finally {
setLoadingAgent(false);
}
};
useEffect(() => {
if (agentId) fetchAgent(agentId);
}, [agentId, token]);
return (
<div className="p-4 md:p-12">
<div className="flex items-center gap-3 px-4">
<button
className="rounded-full border p-3 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
onClick={() => navigate('/agents')}
>
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
</button>
<p className="mt-px text-sm font-semibold text-eerie-black dark:text-bright-gray">
Back to all agents
</p>
</div>
<div className="mt-5 flex w-full flex-wrap items-center justify-between gap-2 px-4">
<h1 className="m-0 text-[40px] font-bold text-[#212121] dark:text-white">
Agent Logs
</h1>
</div>
<div className="mt-6 flex flex-col gap-3 px-4">
{agent && (
<div className="flex flex-col gap-1">
<p className="text-[#28292E] dark:text-[#E0E0E0]">{agent.name}</p>
<p className="text-xs text-[#28292E] dark:text-[#E0E0E0]/40">
{agent.last_used_at
? 'Last used at ' +
new Date(agent.last_used_at).toLocaleString()
: 'No usage history'}
</p>
</div>
)}
</div>
{loadingAgent ? (
<div className="flex h-[345px] w-full items-center justify-center">
<Spinner />
</div>
) : (
agent && <Analytics agentId={agent.id} />
)}
{loadingAgent ? (
<div className="flex h-[55vh] w-full items-center justify-center">
{' '}
<Spinner />
</div>
) : (
agent && <Logs agentId={agent.id} tableHeader="Agent endpoint logs" />
)}
</div>
);
}

View File

@@ -0,0 +1,154 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import MessageInput from '../components/MessageInput';
import ConversationMessages from '../conversation/ConversationMessages';
import { Query } from '../conversation/conversationModels';
import {
addQuery,
fetchAnswer,
handleAbort,
resendQuery,
resetConversation,
selectQueries,
selectStatus,
} from '../conversation/conversationSlice';
import { selectSelectedAgent } from '../preferences/preferenceSlice';
import { AppDispatch } from '../store';
export default function AgentPreview() {
const dispatch = useDispatch<AppDispatch>();
const queries = useSelector(selectQueries);
const status = useSelector(selectStatus);
const selectedAgent = useSelector(selectSelectedAgent);
const [input, setInput] = useState('');
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
const fetchStream = useRef<any>(null);
const handleFetchAnswer = useCallback(
({ question, index }: { question: string; index?: number }) => {
fetchStream.current = dispatch(
fetchAnswer({ question, indx: index, isPreview: true }),
);
},
[dispatch],
);
const handleQuestion = useCallback(
({
question,
isRetry = false,
index = undefined,
}: {
question: string;
isRetry?: boolean;
index?: number;
}) => {
const trimmedQuestion = question.trim();
if (trimmedQuestion === '') return;
if (index !== undefined) {
if (!isRetry) dispatch(resendQuery({ index, prompt: trimmedQuestion }));
handleFetchAnswer({ question: trimmedQuestion, index });
} else {
if (!isRetry) {
const newQuery: Query = { prompt: trimmedQuestion };
dispatch(addQuery(newQuery));
}
handleFetchAnswer({ question: trimmedQuestion, index: undefined });
}
},
[dispatch, handleFetchAnswer],
);
const handleQuestionSubmission = (
updatedQuestion?: string,
updated?: boolean,
indx?: number,
) => {
if (
updated === true &&
updatedQuestion !== undefined &&
indx !== undefined
) {
handleQuestion({
question: updatedQuestion,
index: indx,
isRetry: false,
});
} else if (input.trim() && status !== 'loading') {
const currentInput = input.trim();
if (lastQueryReturnedErr && queries.length > 0) {
const lastQueryIndex = queries.length - 1;
handleQuestion({
question: currentInput,
isRetry: true,
index: lastQueryIndex,
});
} else {
handleQuestion({
question: currentInput,
isRetry: false,
index: undefined,
});
}
setInput('');
}
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleQuestionSubmission();
}
};
useEffect(() => {
dispatch(resetConversation());
return () => {
if (fetchStream.current) fetchStream.current.abort();
handleAbort();
dispatch(resetConversation());
};
}, [dispatch]);
useEffect(() => {
if (queries.length > 0) {
const lastQuery = queries[queries.length - 1];
setLastQueryReturnedErr(!!lastQuery.error);
} else setLastQueryReturnedErr(false);
}, [queries]);
return (
<div>
<div className="flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden dark:bg-raisin-black">
<div className="h-[512px] w-full overflow-y-auto">
<ConversationMessages
handleQuestion={handleQuestion}
handleQuestionSubmission={handleQuestionSubmission}
queries={queries}
status={status}
showHeroOnEmpty={false}
/>
</div>
<div className="flex w-[95%] max-w-[1500px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
<MessageInput
value={input}
onChange={(e) => setInput(e.target.value)}
onSubmit={() => handleQuestionSubmission()}
loading={status === 'loading'}
showSourceButton={selectedAgent ? false : true}
showToolButton={selectedAgent ? false : true}
autoFocus={false}
/>
<p className="w-full self-center bg-transparent pt-2 text-center text-xs text-gray-4000 dark:text-sonic-silver md:inline">
This is a preview of the agent. You can publish it to start using it
in conversations.
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,611 @@
import React, { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import userService from '../api/services/userService';
import ArrowLeft from '../assets/arrow-left.svg';
import SourceIcon from '../assets/source.svg';
import Dropdown from '../components/Dropdown';
import MultiSelectPopup, { OptionType } from '../components/MultiSelectPopup';
import AgentDetailsModal from '../modals/AgentDetailsModal';
import ConfirmationModal from '../modals/ConfirmationModal';
import { ActiveState, Doc, Prompt } from '../models/misc';
import {
selectSelectedAgent,
selectSourceDocs,
selectToken,
setSelectedAgent,
} from '../preferences/preferenceSlice';
import PromptsModal from '../preferences/PromptsModal';
import { UserToolType } from '../settings/types';
import AgentPreview from './AgentPreview';
import { Agent } from './types';
const embeddingsName =
import.meta.env.VITE_EMBEDDINGS_NAME ||
'huggingface_sentence-transformers/all-mpnet-base-v2';
export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
const navigate = useNavigate();
const dispatch = useDispatch();
const { agentId } = useParams();
const token = useSelector(selectToken);
const sourceDocs = useSelector(selectSourceDocs);
const selectedAgent = useSelector(selectSelectedAgent);
const [effectiveMode, setEffectiveMode] = useState(mode);
const [agent, setAgent] = useState<Agent>({
id: agentId || '',
name: '',
description: '',
image: '',
source: '',
chunks: '',
retriever: '',
prompt_id: '',
tools: [],
agent_type: '',
status: '',
});
const [prompts, setPrompts] = useState<
{ name: string; id: string; type: string }[]
>([]);
const [userTools, setUserTools] = useState<OptionType[]>([]);
const [isSourcePopupOpen, setIsSourcePopupOpen] = useState(false);
const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false);
const [selectedSourceIds, setSelectedSourceIds] = useState<
Set<string | number>
>(new Set());
const [selectedToolIds, setSelectedToolIds] = useState<Set<string | number>>(
new Set(),
);
const [deleteConfirmation, setDeleteConfirmation] =
useState<ActiveState>('INACTIVE');
const [agentDetails, setAgentDetails] = useState<ActiveState>('INACTIVE');
const [addPromptModal, setAddPromptModal] = useState<ActiveState>('INACTIVE');
const sourceAnchorButtonRef = useRef<HTMLButtonElement>(null);
const toolAnchorButtonRef = useRef<HTMLButtonElement>(null);
const modeConfig = {
new: {
heading: 'New Agent',
buttonText: 'Create Agent',
showDelete: false,
showSaveDraft: true,
showLogs: false,
showAccessDetails: false,
},
edit: {
heading: 'Edit Agent',
buttonText: 'Save Changes',
showDelete: true,
showSaveDraft: false,
showLogs: true,
showAccessDetails: true,
},
draft: {
heading: 'New Agent (Draft)',
buttonText: 'Publish Draft',
showDelete: true,
showSaveDraft: true,
showLogs: false,
showAccessDetails: false,
},
};
const chunks = ['0', '2', '4', '6', '8', '10'];
const agentTypes = [
{ label: 'Classic', value: 'classic' },
{ label: 'ReAct', value: 'react' },
];
const isPublishable = () => {
return (
agent.name && agent.description && agent.prompt_id && agent.agent_type
);
};
const handleCancel = () => {
if (selectedAgent) dispatch(setSelectedAgent(null));
navigate('/agents');
};
const handleDelete = async (agentId: string) => {
const response = await userService.deleteAgent(agentId, token);
if (!response.ok) throw new Error('Failed to delete agent');
navigate('/agents');
};
const handleSaveDraft = async () => {
const response =
effectiveMode === 'new'
? await userService.createAgent({ ...agent, status: 'draft' }, token)
: await userService.updateAgent(
agent.id || '',
{ ...agent, status: 'draft' },
token,
);
if (!response.ok) throw new Error('Failed to create agent draft');
const data = await response.json();
if (effectiveMode === 'new') {
setEffectiveMode('draft');
setAgent((prev) => ({ ...prev, id: data.id }));
}
};
const handlePublish = async () => {
const response =
effectiveMode === 'new'
? await userService.createAgent(
{ ...agent, status: 'published' },
token,
)
: await userService.updateAgent(
agent.id || '',
{ ...agent, status: 'published' },
token,
);
if (!response.ok) throw new Error('Failed to publish agent');
const data = await response.json();
if (data.id) setAgent((prev) => ({ ...prev, id: data.id }));
if (data.key) setAgent((prev) => ({ ...prev, key: data.key }));
if (effectiveMode === 'new' || effectiveMode === 'draft') {
setEffectiveMode('edit');
setAgent((prev) => ({ ...prev, status: 'published' }));
setAgentDetails('ACTIVE');
}
};
useEffect(() => {
const getTools = async () => {
const response = await userService.getUserTools(token);
if (!response.ok) throw new Error('Failed to fetch tools');
const data = await response.json();
const tools: OptionType[] = data.tools.map((tool: UserToolType) => ({
id: tool.id,
label: tool.displayName,
icon: `/toolIcons/tool_${tool.name}.svg`,
}));
setUserTools(tools);
};
const getPrompts = async () => {
const response = await userService.getPrompts(token);
if (!response.ok) {
throw new Error('Failed to fetch prompts');
}
const data = await response.json();
setPrompts(data);
};
getTools();
getPrompts();
}, [token]);
useEffect(() => {
if ((mode === 'edit' || mode === 'draft') && agentId) {
const getAgent = async () => {
const response = await userService.getAgent(agentId, token);
if (!response.ok) {
navigate('/agents');
throw new Error('Failed to fetch agent');
}
const data = await response.json();
if (data.source) setSelectedSourceIds(new Set([data.source]));
else if (data.retriever)
setSelectedSourceIds(new Set([data.retriever]));
if (data.tools) setSelectedToolIds(new Set(data.tools));
if (data.status === 'draft') setEffectiveMode('draft');
setAgent(data);
};
getAgent();
}
}, [agentId, mode, token]);
useEffect(() => {
const selectedSource = Array.from(selectedSourceIds).map((id) =>
sourceDocs?.find(
(source) =>
source.id === id || source.retriever === id || source.name === id,
),
);
if (selectedSource[0]?.model === embeddingsName) {
if (selectedSource[0] && 'id' in selectedSource[0]) {
setAgent((prev) => ({
...prev,
source: selectedSource[0]?.id || 'default',
retriever: '',
}));
} else
setAgent((prev) => ({
...prev,
source: '',
retriever: selectedSource[0]?.retriever || 'classic',
}));
}
}, [selectedSourceIds]);
useEffect(() => {
const selectedTool = Array.from(selectedToolIds).map((id) =>
userTools.find((tool) => tool.id === id),
);
setAgent((prev) => ({
...prev,
tools: selectedTool
.map((tool) => tool?.id)
.filter((id): id is string => typeof id === 'string'),
}));
}, [selectedToolIds]);
useEffect(() => {
if (isPublishable()) dispatch(setSelectedAgent(agent));
}, [agent, dispatch]);
return (
<div className="p-4 md:p-12">
<div className="flex items-center gap-3 px-4">
<button
className="rounded-full border p-3 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
onClick={handleCancel}
>
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
</button>
<p className="mt-px text-sm font-semibold text-eerie-black dark:text-bright-gray">
Back to all agents
</p>
</div>
<div className="mt-5 flex w-full flex-wrap items-center justify-between gap-2 px-4">
<h1 className="m-0 text-[40px] font-bold text-[#212121] dark:text-white">
{modeConfig[effectiveMode].heading}
</h1>
<div className="flex flex-wrap items-center gap-1">
<button
className="mr-4 rounded-3xl py-2 text-sm font-medium text-purple-30 dark:bg-transparent dark:text-light-gray"
onClick={handleCancel}
>
Cancel
</button>
{modeConfig[effectiveMode].showDelete && agent.id && (
<button
className="group flex items-center gap-2 rounded-3xl border border-solid border-red-2000 px-5 py-2 text-sm font-medium text-red-2000 transition-colors hover:bg-red-2000 hover:text-white"
onClick={() => setDeleteConfirmation('ACTIVE')}
>
<span className="block h-4 w-4 bg-[url('/src/assets/red-trash.svg')] bg-contain bg-center bg-no-repeat transition-all group-hover:bg-[url('/src/assets/white-trash.svg')]" />
Delete
</button>
)}
{modeConfig[effectiveMode].showSaveDraft && (
<button
className="hover:bg-vi</button>olets-are-blue rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white"
onClick={handleSaveDraft}
>
Save Draft
</button>
)}
{modeConfig[effectiveMode].showAccessDetails && (
<button
className="group flex items-center gap-2 rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white"
onClick={() => navigate(`/agents/logs/${agent.id}`)}
>
<span className="block h-5 w-5 bg-[url('/src/assets/monitoring-purple.svg')] bg-contain bg-center bg-no-repeat transition-all group-hover:bg-[url('/src/assets/monitoring-white.svg')]" />
Logs
</button>
)}
{modeConfig[effectiveMode].showAccessDetails && (
<button
className="hover:bg-vi</button>olets-are-blue rounded-3xl border border-solid border-violets-are-blue px-5 py-2 text-sm font-medium text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white"
onClick={() => setAgentDetails('ACTIVE')}
>
Access Details
</button>
)}
<button
disabled={!isPublishable()}
className={`${!isPublishable() && 'cursor-not-allowed opacity-30'} rounded-3xl bg-purple-30 px-5 py-2 text-sm font-medium text-white hover:bg-violets-are-blue`}
onClick={handlePublish}
>
Publish
</button>
</div>
</div>
<div className="mt-5 flex w-full grid-cols-5 flex-col gap-10 min-[1180px]:grid min-[1180px]:gap-5">
<div className="col-span-2 flex flex-col gap-5">
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Meta</h2>
<input
className="mt-3 w-full rounded-3xl border border-silver bg-white px-5 py-3 text-sm text-jet outline-none placeholder:text-gray-400 dark:border-[#7E7E7E] dark:bg-[#222327] dark:text-bright-gray placeholder:dark:text-silver"
type="text"
value={agent.name}
placeholder="Agent name"
onChange={(e) => setAgent({ ...agent, name: e.target.value })}
/>
<textarea
className="mt-3 h-32 w-full rounded-3xl border border-silver bg-white px-5 py-4 text-sm text-jet outline-none placeholder:text-gray-400 dark:border-[#7E7E7E] dark:bg-[#222327] dark:text-bright-gray placeholder:dark:text-silver"
placeholder="Describe your agent"
value={agent.description}
onChange={(e) =>
setAgent({ ...agent, description: e.target.value })
}
/>
</div>
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Source</h2>
<div className="mt-3">
<div className="flex flex-wrap items-center gap-1">
<button
ref={sourceAnchorButtonRef}
onClick={() => setIsSourcePopupOpen(!isSourcePopupOpen)}
className="w-full truncate rounded-3xl border border-silver bg-white px-5 py-3 text-left text-sm text-gray-400 dark:border-[#7E7E7E] dark:bg-[#222327] dark:text-silver"
>
{selectedSourceIds.size > 0
? Array.from(selectedSourceIds)
.map(
(id) =>
sourceDocs?.find(
(source) =>
source.id === id ||
source.name === id ||
source.retriever === id,
)?.name,
)
.filter(Boolean)
.join(', ')
: 'Select source'}
</button>
<MultiSelectPopup
isOpen={isSourcePopupOpen}
onClose={() => setIsSourcePopupOpen(false)}
anchorRef={sourceAnchorButtonRef}
options={
sourceDocs?.map((doc: Doc) => ({
id: doc.id || doc.retriever || doc.name,
label: doc.name,
icon: <img src={SourceIcon} alt="" />,
})) || []
}
selectedIds={selectedSourceIds}
onSelectionChange={(newSelectedIds: Set<string | number>) => {
setSelectedSourceIds(newSelectedIds);
setIsSourcePopupOpen(false);
}}
title="Select Source"
searchPlaceholder="Search sources..."
noOptionsMessage="No source available"
singleSelect={true}
/>
</div>
<div className="mt-3">
<Dropdown
options={chunks}
selectedValue={agent.chunks ? agent.chunks : null}
onSelect={(value: string) =>
setAgent({ ...agent, chunks: value })
}
size="w-full"
rounded="3xl"
buttonDarkBackgroundColor="[#222327]"
border="border"
darkBorderColor="[#7E7E7E]"
placeholder="Chunks per query"
placeholderTextColor="gray-400"
darkPlaceholderTextColor="silver"
contentSize="text-sm"
/>
</div>
</div>
</div>
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Prompt</h2>
<div className="mt-3 flex flex-wrap items-center gap-1">
<div className="min-w-20 flex-grow basis-full sm:basis-0">
<Dropdown
options={prompts.map((prompt) => ({
label: prompt.name,
value: prompt.id,
}))}
selectedValue={
agent.prompt_id
? prompts.filter(
(prompt) => prompt.id === agent.prompt_id,
)[0]?.name || null
: null
}
onSelect={(option: { label: string; value: string }) =>
setAgent({ ...agent, prompt_id: option.value })
}
size="w-full"
rounded="3xl"
buttonDarkBackgroundColor="[#222327]"
border="border"
darkBorderColor="[#7E7E7E]"
placeholder="Select a prompt"
placeholderTextColor="gray-400"
darkPlaceholderTextColor="silver"
contentSize="text-sm"
/>
</div>
<button
className="w-20 flex-shrink-0 basis-full rounded-3xl border-2 border-solid border-violets-are-blue px-5 py-[11px] text-sm text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white sm:basis-auto"
onClick={() => setAddPromptModal('ACTIVE')}
>
Add
</button>
</div>
</div>
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Tools</h2>
<div className="mt-3 flex flex-wrap items-center gap-1">
<button
ref={toolAnchorButtonRef}
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
className="w-full truncate rounded-3xl border border-silver bg-white px-5 py-3 text-left text-sm text-gray-400 dark:border-[#7E7E7E] dark:bg-[#222327] dark:text-silver"
>
{selectedToolIds.size > 0
? Array.from(selectedToolIds)
.map(
(id) => userTools.find((tool) => tool.id === id)?.label,
)
.filter(Boolean)
.join(', ')
: 'Select tools'}
</button>
<MultiSelectPopup
isOpen={isToolsPopupOpen}
onClose={() => setIsToolsPopupOpen(false)}
anchorRef={toolAnchorButtonRef}
options={userTools}
selectedIds={selectedToolIds}
onSelectionChange={(newSelectedIds: Set<string | number>) =>
setSelectedToolIds(newSelectedIds)
}
title="Select Tools"
searchPlaceholder="Search tools..."
noOptionsMessage="No tools available"
/>
</div>
</div>
<div className="rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Agent type</h2>
<div className="mt-3">
<Dropdown
options={agentTypes}
selectedValue={
agent.agent_type
? agentTypes.find((type) => type.value === agent.agent_type)
?.label || null
: null
}
onSelect={(option: { label: string; value: string }) =>
setAgent({ ...agent, agent_type: option.value })
}
size="w-full"
rounded="3xl"
buttonDarkBackgroundColor="[#222327]"
border="border"
darkBorderColor="[#7E7E7E]"
placeholder="Select type"
placeholderTextColor="gray-400"
darkPlaceholderTextColor="silver"
contentSize="text-sm"
/>
</div>
</div>
</div>
<div className="col-span-3 flex flex-col gap-3 rounded-[30px] bg-[#F6F6F6] px-6 py-3 dark:bg-[#383838] dark:text-[#E0E0E0]">
<h2 className="text-lg font-semibold">Preview</h2>
<AgentPreviewArea />
</div>
</div>
<ConfirmationModal
message="Are you sure you want to delete this agent?"
modalState={deleteConfirmation}
setModalState={setDeleteConfirmation}
submitLabel="Delete"
handleSubmit={() => {
handleDelete(agent.id || '');
setDeleteConfirmation('INACTIVE');
}}
cancelLabel="Cancel"
variant="danger"
/>
<AgentDetailsModal
agent={agent}
mode={effectiveMode}
modalState={agentDetails}
setModalState={setAgentDetails}
/>
<AddPromptModal
prompts={prompts}
setPrompts={setPrompts}
isOpen={addPromptModal}
onClose={() => setAddPromptModal('INACTIVE')}
onSelect={(name: string, id: string, type: string) => {
setAgent({ ...agent, prompt_id: id });
}}
/>
</div>
);
}
function AgentPreviewArea() {
const selectedAgent = useSelector(selectSelectedAgent);
return (
<div className="h-full w-full rounded-[30px] border border-[#F6F6F6] bg-white dark:border-[#7E7E7E] dark:bg-[#222327] max-[1180px]:h-[48rem]">
{selectedAgent?.status === 'published' ? (
<div className="flex h-full w-full flex-col justify-end overflow-auto rounded-[30px]">
<AgentPreview />
</div>
) : (
<div className="flex h-full w-full flex-col items-center justify-center gap-2">
<span className="block h-12 w-12 bg-[url('/src/assets/science-spark.svg')] bg-contain bg-center bg-no-repeat transition-all dark:bg-[url('/src/assets/science-spark-dark.svg')]" />{' '}
<p className="text-xs text-[#18181B] dark:text-[#949494]">
Published agents can be previewed here
</p>
</div>
)}
</div>
);
}
function AddPromptModal({
prompts,
setPrompts,
isOpen,
onClose,
onSelect,
}: {
prompts: Prompt[];
setPrompts?: React.Dispatch<React.SetStateAction<Prompt[]>>;
isOpen: ActiveState;
onClose: () => void;
onSelect?: (name: string, id: string, type: string) => void;
}) {
const token = useSelector(selectToken);
const [newPromptName, setNewPromptName] = useState('');
const [newPromptContent, setNewPromptContent] = useState('');
const handleAddPrompt = async () => {
try {
const response = await userService.createPrompt(
{
name: newPromptName,
content: newPromptContent,
},
token,
);
if (!response.ok) {
throw new Error('Failed to add prompt');
}
const newPrompt = await response.json();
if (setPrompts) {
setPrompts([
...prompts,
{ name: newPromptName, id: newPrompt.id, type: 'private' },
]);
}
onClose();
setNewPromptName('');
setNewPromptContent('');
onSelect?.(newPromptName, newPrompt.id, newPromptContent);
} catch (error) {
console.error(error);
}
};
return (
<PromptsModal
modalState={isOpen}
setModalState={onClose}
type="ADD"
existingPrompts={prompts}
newPromptName={newPromptName}
setNewPromptName={setNewPromptName}
newPromptContent={newPromptContent}
setNewPromptContent={setNewPromptContent}
editPromptName={''}
setEditPromptName={() => undefined}
editPromptContent={''}
setEditPromptContent={() => undefined}
currentPromptEdit={{ id: '', name: '', type: '' }}
handleAddPrompt={handleAddPrompt}
/>
);
}

View File

@@ -0,0 +1,263 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import userService from '../api/services/userService';
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
import NoFilesIcon from '../assets/no-files.svg';
import Robot from '../assets/robot.svg';
import MessageInput from '../components/MessageInput';
import Spinner from '../components/Spinner';
import ConversationMessages from '../conversation/ConversationMessages';
import { Query } from '../conversation/conversationModels';
import {
addQuery,
fetchAnswer,
resendQuery,
selectQueries,
selectStatus,
} from '../conversation/conversationSlice';
import { useDarkTheme } from '../hooks';
import { selectToken, setSelectedAgent } from '../preferences/preferenceSlice';
import { AppDispatch } from '../store';
import { Agent } from './types';
export default function SharedAgent() {
const { t } = useTranslation();
const { agentId } = useParams();
const dispatch = useDispatch<AppDispatch>();
const [isDarkTheme] = useDarkTheme();
const token = useSelector(selectToken);
const queries = useSelector(selectQueries);
const status = useSelector(selectStatus);
const [sharedAgent, setSharedAgent] = useState<Agent>();
const [isLoading, setIsLoading] = useState(true);
const [input, setInput] = useState('');
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
const fetchStream = useRef<any>(null);
const getSharedAgent = async () => {
try {
setIsLoading(true);
const response = await userService.getSharedAgent(agentId ?? '', token);
if (!response.ok) throw new Error('Failed to fetch Shared Agent');
const agent: Agent = await response.json();
setSharedAgent(agent);
} catch (error) {
console.error('Error: ', error);
} finally {
setIsLoading(false);
}
};
const handleFetchAnswer = useCallback(
({ question, index }: { question: string; index?: number }) => {
fetchStream.current = dispatch(
fetchAnswer({ question, indx: index, isPreview: false }),
);
},
[dispatch],
);
const handleQuestion = useCallback(
({
question,
isRetry = false,
index = undefined,
}: {
question: string;
isRetry?: boolean;
index?: number;
}) => {
const trimmedQuestion = question.trim();
if (trimmedQuestion === '') return;
if (index !== undefined) {
if (!isRetry) dispatch(resendQuery({ index, prompt: trimmedQuestion }));
handleFetchAnswer({ question: trimmedQuestion, index });
} else {
if (!isRetry) {
const newQuery: Query = { prompt: trimmedQuestion };
dispatch(addQuery(newQuery));
}
handleFetchAnswer({ question: trimmedQuestion, index: undefined });
}
},
[dispatch, handleFetchAnswer],
);
const handleQuestionSubmission = (
updatedQuestion?: string,
updated?: boolean,
indx?: number,
) => {
if (
updated === true &&
updatedQuestion !== undefined &&
indx !== undefined
) {
handleQuestion({
question: updatedQuestion,
index: indx,
isRetry: false,
});
} else if (input.trim() && status !== 'loading') {
const currentInput = input.trim();
if (lastQueryReturnedErr && queries.length > 0) {
const lastQueryIndex = queries.length - 1;
handleQuestion({
question: currentInput,
isRetry: true,
index: lastQueryIndex,
});
} else {
handleQuestion({
question: currentInput,
isRetry: false,
index: undefined,
});
}
setInput('');
}
};
useEffect(() => {
if (agentId) getSharedAgent();
}, [agentId, token]);
useEffect(() => {
if (sharedAgent) dispatch(setSelectedAgent(sharedAgent));
}, [sharedAgent, dispatch]);
if (isLoading)
return (
<div className="flex h-full w-full items-center justify-center">
<Spinner />
</div>
);
if (!sharedAgent)
return (
<div className="flex h-full w-full items-center justify-center">
<div className="flex w-full flex-col items-center justify-center gap-4">
<img
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
alt="No agent found"
className="mx-auto mb-6 h-32 w-32"
/>
<p className="text-center text-lg text-[#71717A] dark:text-[#949494]">
No agent found. Please ensure the agent is shared.
</p>
</div>
</div>
);
return (
<div className="relative h-full w-full">
<div className="absolute left-4 top-5 hidden items-center gap-3 sm:flex">
<img
src={sharedAgent.image ?? Robot}
alt="agent-logo"
className="h-6 w-6"
/>
<h2 className="text-lg font-semibold text-[#212121] dark:text-[#E0E0E0]">
{sharedAgent.name}
</h2>
</div>
<div className="flex h-full w-full flex-col items-center justify-between sm:pt-12">
<div className="flex w-full flex-col items-center overflow-y-auto">
<ConversationMessages
handleQuestion={handleQuestion}
handleQuestionSubmission={handleQuestionSubmission}
queries={queries}
status={status}
showHeroOnEmpty={false}
headerContent={
<div className="flex w-full items-center justify-center py-4">
<SharedAgentCard agent={sharedAgent} />
</div>
}
/>
</div>
<div className="flex w-[95%] max-w-[1500px] flex-col items-center gap-4 pb-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
<MessageInput
value={input}
onChange={(e) => setInput(e.target.value)}
onSubmit={() => handleQuestionSubmission()}
loading={status === 'loading'}
showSourceButton={sharedAgent ? false : true}
showToolButton={sharedAgent ? false : true}
autoFocus={false}
/>
<p className="hidden w-[100vw] self-center bg-transparent py-2 text-center text-xs text-gray-4000 dark:text-sonic-silver md:inline md:w-full">
{t('tagline')}
</p>
</div>
</div>
</div>
);
}
function SharedAgentCard({ agent }: { agent: Agent }) {
return (
<div className="flex w-full max-w-[720px] flex-col rounded-3xl border border-dark-gray p-6 shadow-sm dark:border-grey sm:w-fit sm:min-w-[480px]">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center overflow-hidden rounded-full p-1">
<img src={Robot} className="h-full w-full object-contain" />
</div>
<div className="flex max-h-[92px] w-[80%] flex-col gap-px">
<h2 className="text-base font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-lg">
{agent.name}
</h2>
<p className="overflow-y-auto text-wrap break-all text-xs text-[#71717A] dark:text-[#949494] sm:text-sm">
{agent.description}
</p>
</div>
</div>
<div className="mt-4 flex items-center gap-8">
{agent.shared_metadata?.shared_by && (
<p className="text-xs font-light text-[#212121] dark:text-[#E0E0E0] sm:text-sm">
by {agent.shared_metadata.shared_by}
</p>
)}
{agent.shared_metadata?.shared_at && (
<p className="text-xs font-light text-[#71717A] dark:text-[#949494] sm:text-sm">
Shared on{' '}
{new Date(agent.shared_metadata.shared_at).toLocaleString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true,
})}
</p>
)}
</div>
{agent.tools.length > 0 && (
<div className="mt-8">
<p className="text-sm font-semibold text-[#212121] dark:text-[#E0E0E0] sm:text-base">
Connected Tools
</p>
<div className="mt-2 flex flex-wrap gap-2">
{agent.tools.map((tool, index) => (
<span
key={index}
className="flex items-center gap-1 rounded-full bg-bright-gray px-3 py-1 text-xs font-light text-[#212121] dark:bg-dark-charcoal dark:text-[#E0E0E0]"
>
<img
src={`/toolIcons/tool_${tool}.svg`}
alt={`${tool} icon`}
className="h-3 w-3"
/>{' '}
{tool}
</span>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,418 @@
import { SyntheticEvent, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Route, Routes, useNavigate } from 'react-router-dom';
import userService from '../api/services/userService';
import Edit from '../assets/edit.svg';
import Monitoring from '../assets/monitoring.svg';
import Pin from '../assets/pin.svg';
import Trash from '../assets/red-trash.svg';
import Robot from '../assets/robot.svg';
import Link from '../assets/link-gray.svg';
import ThreeDots from '../assets/three-dots.svg';
import UnPin from '../assets/unpin.svg';
import ContextMenu, { MenuOption } from '../components/ContextMenu';
import Spinner from '../components/Spinner';
import {
setConversation,
updateConversationId,
} from '../conversation/conversationSlice';
import ConfirmationModal from '../modals/ConfirmationModal';
import { ActiveState } from '../models/misc';
import {
selectAgents,
selectSelectedAgent,
selectToken,
setAgents,
setSelectedAgent,
} from '../preferences/preferenceSlice';
import AgentLogs from './AgentLogs';
import NewAgent from './NewAgent';
import SharedAgent from './SharedAgent';
import { Agent } from './types';
export default function Agents() {
return (
<Routes>
<Route path="/" element={<AgentsList />} />
<Route path="/new" element={<NewAgent mode="new" />} />
<Route path="/edit/:agentId" element={<NewAgent mode="edit" />} />
<Route path="/logs/:agentId" element={<AgentLogs />} />
<Route path="/shared/:agentId" element={<SharedAgent />} />
</Routes>
);
}
const sectionConfig = {
user: {
title: 'By me',
description: 'Agents created or published by you',
showNewAgentButton: true,
emptyStateDescription: 'You dont have any created agents yet',
},
shared: {
title: 'Shared with me',
description: 'Agents imported by using a public link',
showNewAgentButton: false,
emptyStateDescription: 'No shared agents found',
},
};
function AgentsList() {
const navigate = useNavigate();
const dispatch = useDispatch();
const token = useSelector(selectToken);
const agents = useSelector(selectAgents);
const selectedAgent = useSelector(selectSelectedAgent);
const [sharedAgents, setSharedAgents] = useState<Agent[]>([]);
const [loadingUserAgents, setLoadingUserAgents] = useState<boolean>(true);
const [loadingSharedAgents, setLoadingSharedAgents] = useState<boolean>(true);
const getAgents = async () => {
try {
setLoadingUserAgents(true);
const response = await userService.getAgents(token);
if (!response.ok) throw new Error('Failed to fetch agents');
const data = await response.json();
dispatch(setAgents(data));
setLoadingUserAgents(false);
} catch (error) {
console.error('Error:', error);
setLoadingUserAgents(false);
}
};
const getSharedAgents = async () => {
try {
setLoadingSharedAgents(true);
const response = await userService.getSharedAgents(token);
if (!response.ok) throw new Error('Failed to fetch shared agents');
const data = await response.json();
setSharedAgents(data);
setLoadingSharedAgents(false);
} catch (error) {
console.error('Error:', error);
setLoadingSharedAgents(false);
}
};
useEffect(() => {
getAgents();
getSharedAgents();
dispatch(setConversation([]));
dispatch(
updateConversationId({
query: { conversationId: null },
}),
);
if (selectedAgent) dispatch(setSelectedAgent(null));
}, [token]);
return (
<div className="p-4 md:p-12">
<h1 className="mb-0 text-[40px] font-bold text-[#212121] dark:text-[#E0E0E0]">
Agents
</h1>
<p className="mt-5 text-[15px] text-[#71717A] dark:text-[#949494]">
Discover and create custom versions of DocsGPT that combine
instructions, extra knowledge, and any combination of skills
</p>
{/* Premade agents section */}
{/* <div className="mt-6">
<h2 className="text-[18px] font-semibold text-[#18181B] dark:text-[#E0E0E0]">
Premade by DocsGPT
</h2>
<div className="mt-4 flex w-full flex-wrap gap-4">
{Array.from({ length: 5 }, (_, index) => (
<div
key={index}
className="relative flex h-44 w-48 flex-col justify-between rounded-[1.2rem] bg-[#F6F6F6] px-6 py-5 dark:bg-[#383838]"
>
<button onClick={() => {}} className="absolute right-4 top-4">
<img
src={Copy}
alt={'use-agent'}
className="h-[19px] w-[19px]"
/>
</button>
<div className="w-full">
<div className="flex w-full items-center px-1">
<img
src={Robot}
alt="agent-logo"
className="h-7 w-7 rounded-full"
/>
</div>
<div className="mt-2">
<p
title={''}
className="truncate px-1 text-[13px] font-semibold capitalize leading-relaxed text-raisin-black-light dark:text-bright-gray"
>
{}
</p>
<p className="mt-1 h-20 overflow-auto px-1 text-[12px] leading-relaxed text-old-silver dark:text-sonic-silver-light">
{}
</p>
</div>
</div>
<div className="absolute bottom-4 right-4"></div>
</div>
))}
</div>
</div> */}
<AgentSection
agents={agents ?? []}
loading={loadingUserAgents}
section="user"
/>
<AgentSection
agents={sharedAgents ?? []}
loading={loadingSharedAgents}
section="shared"
/>
</div>
);
}
function AgentSection({
agents,
loading,
section,
}: {
agents: Agent[];
loading: boolean;
section: keyof typeof sectionConfig;
}) {
const navigate = useNavigate();
return (
<div className="mt-8 flex flex-col gap-4">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col gap-2">
<h2 className="text-[18px] font-semibold text-[#18181B] dark:text-[#E0E0E0]">
{sectionConfig[section].title}
</h2>
<p className="text-[13px] text-[#71717A]">
{sectionConfig[section].description}
</p>
</div>
{sectionConfig[section].showNewAgentButton && (
<button
className="rounded-full bg-purple-30 px-4 py-2 text-sm text-white hover:bg-violets-are-blue"
onClick={() => navigate('/agents/new')}
>
New Agent
</button>
)}
</div>
<div className="grid w-full grid-cols-2 gap-2 md:flex md:flex-wrap md:gap-4">
{loading ? (
<div className="flex h-72 w-full items-center justify-center">
<Spinner />
</div>
) : agents && agents.length > 0 ? (
agents.map((agent) => (
<AgentCard
key={agent.id}
agent={agent}
agents={agents}
section={section}
/>
))
) : (
<div className="flex h-72 w-full flex-col items-center justify-center gap-3 text-base text-[#18181B] dark:text-[#E0E0E0]">
<p>{sectionConfig[section].emptyStateDescription}</p>
{sectionConfig[section].showNewAgentButton && (
<button
className="ml-2 rounded-full bg-purple-30 px-4 py-2 text-sm text-white hover:bg-violets-are-blue"
onClick={() => navigate('/agents/new')}
>
New Agent
</button>
)}
</div>
)}
</div>
</div>
);
}
function AgentCard({
agent,
agents,
section,
}: {
agent: Agent;
agents: Agent[];
section: keyof typeof sectionConfig;
}) {
const navigate = useNavigate();
const dispatch = useDispatch();
const token = useSelector(selectToken);
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
const [deleteConfirmation, setDeleteConfirmation] =
useState<ActiveState>('INACTIVE');
const menuRef = useRef<HTMLDivElement>(null);
const togglePin = async () => {
try {
const response = await userService.togglePinAgent(agent.id ?? '', token);
if (!response.ok) throw new Error('Failed to pin agent');
const updatedAgents = agents.map((prevAgent) => {
if (prevAgent.id === agent.id)
return { ...prevAgent, pinned: !prevAgent.pinned };
return prevAgent;
});
dispatch(setAgents(updatedAgents));
} catch (error) {
console.error('Error:', error);
}
};
const menuOptionsConfig: Record<string, MenuOption[]> = {
user: [
{
icon: Monitoring,
label: 'Logs',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
navigate(`/agents/logs/${agent.id}`);
},
variant: 'primary',
iconWidth: 14,
iconHeight: 14,
},
{
icon: Edit,
label: 'Edit',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
navigate(`/agents/edit/${agent.id}`);
},
variant: 'primary',
iconWidth: 14,
iconHeight: 14,
},
{
icon: agent.pinned ? UnPin : Pin,
label: agent.pinned ? 'Unpin' : 'Pin agent',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
togglePin();
},
variant: 'primary',
iconWidth: 18,
iconHeight: 18,
},
{
icon: Trash,
label: 'Delete',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
setDeleteConfirmation('ACTIVE');
},
variant: 'danger',
iconWidth: 13,
iconHeight: 13,
},
],
shared: [
{
icon: Link,
label: 'Open',
onClick: (e: SyntheticEvent) => {
e.stopPropagation();
navigate(`/agents/shared/${agent.shared_token}`);
},
variant: 'primary',
iconWidth: 14,
iconHeight: 14,
},
],
};
const menuOptions = menuOptionsConfig[section] || [];
const handleClick = () => {
if (section === 'user') {
if (agent.status === 'published') {
dispatch(setSelectedAgent(agent));
navigate(`/`);
}
}
if (section === 'shared') {
navigate(`/agents/shared/${agent.shared_token}`);
}
};
const handleDelete = async (agentId: string) => {
const response = await userService.deleteAgent(agentId, token);
if (!response.ok) throw new Error('Failed to delete agent');
const data = await response.json();
dispatch(setAgents(agents.filter((prevAgent) => prevAgent.id !== data.id)));
};
return (
<div
className={`relative flex h-44 w-full flex-col justify-between rounded-[1.2rem] bg-[#F6F6F6] px-6 py-5 hover:bg-[#ECECEC] dark:bg-[#383838] hover:dark:bg-[#383838]/80 md:w-48 ${agent.status === 'published' && 'cursor-pointer'}`}
onClick={(e) => {
e.stopPropagation();
handleClick();
}}
>
<div
ref={menuRef}
onClick={(e) => {
e.stopPropagation();
setIsMenuOpen(true);
}}
className="absolute right-4 top-4 z-10 cursor-pointer"
>
<img src={ThreeDots} alt={'use-agent'} className="h-[19px] w-[19px]" />
<ContextMenu
isOpen={isMenuOpen}
setIsOpen={setIsMenuOpen}
options={menuOptions}
anchorRef={menuRef}
position="top-right"
offset={{ x: 0, y: 0 }}
/>
</div>
<div className="w-full">
<div className="flex w-full items-center gap-1 px-1">
<img
src={agent.image ?? Robot}
alt={`${agent.name}`}
className="h-7 w-7 rounded-full"
/>
{agent.status === 'draft' && (
<p className="text-xs text-black opacity-50 dark:text-[#E0E0E0]">{`(Draft)`}</p>
)}
</div>
<div className="mt-2">
<p
title={agent.name}
className="truncate px-1 text-[13px] font-semibold capitalize leading-relaxed text-[#020617] dark:text-[#E0E0E0]"
>
{agent.name}
</p>
<p className="mt-1 h-20 overflow-auto px-1 text-[12px] leading-relaxed text-[#64748B] dark:text-sonic-silver-light">
{agent.description}
</p>
</div>
</div>
<ConfirmationModal
message="Are you sure you want to delete this agent?"
modalState={deleteConfirmation}
setModalState={setDeleteConfirmation}
submitLabel="Delete"
handleSubmit={() => {
handleDelete(agent.id || '');
setDeleteConfirmation('INACTIVE');
}}
cancelLabel="Cancel"
variant="danger"
/>
</div>
);
}

View File

@@ -0,0 +1,22 @@
export type Agent = {
id?: string;
name: string;
description: string;
image: string;
source: string;
chunks: string;
retriever: string;
prompt_id: string;
tools: string[];
agent_type: string;
status: string;
key?: string;
incoming_webhook_token?: string;
pinned?: boolean;
shared?: boolean;
shared_token?: string;
shared_metadata?: any;
created_at?: string;
updated_at?: string;
last_used_at?: string;
};

View File

@@ -1,4 +1,5 @@
const baseURL = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
export const baseURL =
import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const defaultHeaders = {
'Content-Type': 'application/json',

View File

@@ -8,16 +8,22 @@ const endpoints = {
API_KEYS: '/api/get_api_keys',
CREATE_API_KEY: '/api/create_api_key',
DELETE_API_KEY: '/api/delete_api_key',
AGENT: (id: string) => `/api/get_agent?id=${id}`,
AGENTS: '/api/get_agents',
CREATE_AGENT: '/api/create_agent',
UPDATE_AGENT: (agent_id: string) => `/api/update_agent/${agent_id}`,
DELETE_AGENT: (id: string) => `/api/delete_agent?id=${id}`,
PINNED_AGENTS: '/api/pinned_agents',
TOGGLE_PIN_AGENT: (id: string) => `/api/pin_agent?id=${id}`,
SHARED_AGENT: (id: string) => `/api/shared_agent?token=${id}`,
SHARED_AGENTS: '/api/shared_agents',
SHARE_AGENT: `/api/share_agent`,
AGENT_WEBHOOK: (id: string) => `/api/agent_webhook?id=${id}`,
PROMPTS: '/api/get_prompts',
CREATE_PROMPT: '/api/create_prompt',
DELETE_PROMPT: '/api/delete_prompt',
UPDATE_PROMPT: '/api/update_prompt',
SINGLE_PROMPT: (id: string) => `/api/get_single_prompt?id=${id}`,
PROXIES: '/api/get_proxies',
CREATE_PROXY: '/api/create_proxy',
DELETE_PROXY: '/api/delete_proxy',
UPDATE_PROXY: '/api/update_proxy',
SINGLE_PROXY: (id: string) => `/api/get_single_proxy?id=${id}`,
DELETE_PATH: (docPath: string) => `/api/delete_old?source_id=${docPath}`,
TASK_STATUS: (task_id: string) => `/api/task_status?task_id=${task_id}`,
MESSAGE_ANALYTICS: '/api/get_message_analytics',
@@ -37,6 +43,7 @@ const endpoints = {
DELETE_CHUNK: (docId: string, chunkId: string) =>
`/api/delete_chunk?id=${docId}&chunk_id=${chunkId}`,
UPDATE_CHUNK: '/api/update_chunk',
STORE_ATTACHMENT: '/api/store_attachment',
},
CONVERSATION: {
ANSWER: '/api/answer',

View File

@@ -17,6 +17,32 @@ const userService = {
apiClient.post(endpoints.USER.CREATE_API_KEY, data, token),
deleteAPIKey: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.DELETE_API_KEY, data, token),
getAgent: (id: string, token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.AGENT(id), token),
getAgents: (token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.AGENTS, token),
createAgent: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.CREATE_AGENT, data, token),
updateAgent: (
agent_id: string,
data: any,
token: string | null,
): Promise<any> =>
apiClient.put(endpoints.USER.UPDATE_AGENT(agent_id), data, token),
deleteAgent: (id: string, token: string | null): Promise<any> =>
apiClient.delete(endpoints.USER.DELETE_AGENT(id), token),
getPinnedAgents: (token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.PINNED_AGENTS, token),
togglePinAgent: (id: string, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.TOGGLE_PIN_AGENT(id), {}, token),
getSharedAgent: (id: string, token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.SHARED_AGENT(id), token),
getSharedAgents: (token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.SHARED_AGENTS, token),
shareAgent: (data: any, token: string | null): Promise<any> =>
apiClient.put(endpoints.USER.SHARE_AGENT, data, token),
getAgentWebhook: (id: string, token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.AGENT_WEBHOOK(id), token),
getPrompts: (token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.PROMPTS, token),
createPrompt: (data: any, token: string | null): Promise<any> =>
@@ -27,16 +53,6 @@ const userService = {
apiClient.post(endpoints.USER.UPDATE_PROMPT, data, token),
getSinglePrompt: (id: string, token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.SINGLE_PROMPT(id), token),
getProxies: (token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.PROXIES, token),
createProxy: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.CREATE_PROXY, data, token),
deleteProxy: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.DELETE_PROXY, data, token),
updateProxy: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.UPDATE_PROXY, data, token),
getSingleProxy: (id: string, token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.SINGLE_PROXY(id), token),
deletePath: (docPath: string, token: string | null): Promise<any> =>
apiClient.get(endpoints.USER.DELETE_PATH(docPath), token),
getTaskStatus: (task_id: string, token: string | null): Promise<any> =>

View File

@@ -0,0 +1,3 @@
<svg width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.08485 4.07133L3.08485 9.20109C3.08485 9.49263 3.20066 9.77222 3.40681 9.97837C3.61295 10.1845 3.89255 10.3003 4.18408 10.3003C4.47562 10.3003 4.75521 10.1845 4.96136 9.97837C5.1675 9.77222 5.28332 9.49263 5.28332 9.20109L5.28332 3.70492C5.28332 3.12185 5.05169 2.56266 4.6394 2.15037C4.22711 1.73808 3.66792 1.50645 3.08485 1.50645C2.50178 1.50645 1.94259 1.73808 1.5303 2.15037C1.118 2.56266 0.88638 3.12185 0.88638 3.70492L0.886379 9.20109C0.886379 10.0757 1.23381 10.9145 1.85225 11.5329C2.47069 12.1514 3.30948 12.4988 4.18408 12.4988C5.05869 12.4988 5.89747 12.1514 6.51591 11.5329C7.13435 10.9145 7.48178 10.0757 7.48178 9.20109L7.48178 4.07133" stroke="#5D5D5D" stroke-width="1.03637" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 854 B

View File

@@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 16C2.53043 16 3.03914 16.2107 3.41421 16.5858C3.78929 16.9609 4 17.4696 4 18C4 18.5304 3.78929 19.0391 3.41421 19.4142C3.03914 19.7893 2.53043 20 2 20C1.46957 20 0.960859 19.7893 0.585786 19.4142C0.210714 19.0391 0 18.5304 0 18C0 17.4696 0.210714 16.9609 0.585786 16.5858C0.960859 16.2107 1.46957 16 2 16ZM7.5 13C8.16304 13 8.79893 13.2634 9.26777 13.7322C9.73661 14.2011 10 14.837 10 15.5C10 16.163 9.73661 16.7989 9.26777 17.2678C8.79893 17.7366 8.16304 18 7.5 18C6.83696 18 6.20107 17.7366 5.73223 17.2678C5.26339 16.7989 5 16.163 5 15.5C5 14.837 5.26339 14.2011 5.73223 13.7322C6.20107 13.2634 6.83696 13 7.5 13ZM10 0C11.272 0.000250351 12.5033 0.448355 13.4779 1.26572C14.4525 2.08308 15.1082 3.2175 15.33 4.47H15.412C16.4105 4.47 17.3682 4.86667 18.0743 5.57274C18.7803 6.27882 19.177 7.23646 19.177 8.235C19.177 9.23354 18.7803 10.1912 18.0743 10.8973C17.3682 11.6033 16.4105 12 15.412 12H4.588C3.58946 12 2.63182 11.6033 1.92574 10.8973C1.21967 10.1912 0.823 9.23354 0.823 8.235C0.823 7.23646 1.21967 6.27882 1.92574 5.57274C2.63182 4.86667 3.58946 4.47 4.588 4.47H4.67C4.89179 3.2175 5.54749 2.08308 6.52211 1.26572C7.49673 0.448355 8.72801 0.000250351 10 0Z" fill="url(#paint0_linear_6161_11984)"/>
<defs>
<linearGradient id="paint0_linear_6161_11984" x1="0" y1="10" x2="19.177" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#70FDF7"/>
<stop offset="0.325" stop-color="#747696"/>
<stop offset="0.68" stop-color="#BD5372"/>
<stop offset="1" stop-color="#F5A06C"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,4 @@
<svg width="17" height="19" viewBox="0 0 17 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.75 8.70825C3.75 6.46942 3.75 5.34921 4.44588 4.65413C5.14096 3.95825 6.26117 3.95825 8.5 3.95825H10.875C13.1138 3.95825 14.234 3.95825 14.9291 4.65413C15.625 5.34921 15.625 6.46942 15.625 8.70825V12.6666C15.625 14.9054 15.625 16.0256 14.9291 16.7207C14.234 17.4166 13.1138 17.4166 10.875 17.4166H8.5C6.26117 17.4166 5.14096 17.4166 4.44588 16.7207C3.75 16.0256 3.75 14.9054 3.75 12.6666V8.70825Z" stroke="#B9B9B9" stroke-width="1.5"/>
<path d="M3.75 15.0416C3.12011 15.0416 2.51602 14.7914 2.07062 14.346C1.62522 13.9006 1.375 13.2965 1.375 12.6666V7.91658C1.375 4.93121 1.375 3.43813 2.30283 2.51109C3.23067 1.58404 4.72296 1.58325 7.70833 1.58325H10.875C11.5049 1.58325 12.109 1.83347 12.5544 2.27887C12.9998 2.72427 13.25 3.32836 13.25 3.95825" stroke="#B9B9B9" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 901 B

View File

@@ -0,0 +1,3 @@
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1.5H3C2.46957 1.5 1.96086 1.71071 1.58579 2.08579C1.21071 2.46086 1 2.96957 1 3.5V15.5C1 16.0304 1.21071 16.5391 1.58579 16.9142C1.96086 17.2893 2.46957 17.5 3 17.5H15C15.5304 17.5 16.0391 17.2893 16.4142 16.9142C16.7893 16.5391 17 16.0304 17 15.5V11.5M9 9.5L17 1.5M17 1.5V6.5M17 1.5H12" stroke="#949494" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.50195 17.1494C3.68072 17.1503 3.83797 17.2119 3.96289 17.3359C4.08842 17.4607 4.15029 17.6188 4.15039 17.7988V18.5869C4.15039 18.7666 4.08775 18.9248 3.96191 19.0498C3.83677 19.1741 3.68038 19.2364 3.50098 19.2373H3.5C3.36498 19.2373 3.24235 19.2024 3.13672 19.1318L3.03613 19.0488C2.91173 18.9234 2.84961 18.7657 2.84961 18.5869V17.7988C2.84971 17.6193 2.91265 17.4614 3.03809 17.3359C3.16377 17.2104 3.32208 17.1486 3.50195 17.1494ZM7.50195 12.6494C7.68072 12.6503 7.83797 12.7119 7.96289 12.8359C8.08842 12.9607 8.15029 13.1188 8.15039 13.2988V18.5869C8.15039 18.7666 8.08775 18.9248 7.96191 19.0498C7.83656 19.1743 7.67907 19.2364 7.5 19.2373H7.49902C7.31988 19.2373 7.16245 19.175 7.03711 19.0498C6.91161 18.9243 6.84884 18.7663 6.84961 18.5869V13.2988C6.84971 13.1193 6.91265 12.9614 7.03809 12.8359C7.16377 12.7104 7.32208 12.6486 7.50195 12.6494ZM11.502 14.6494C11.6807 14.6503 11.838 14.7119 11.9629 14.8359C12.0884 14.9607 12.1503 15.1188 12.1504 15.2988V18.5869C12.1504 18.7666 12.0878 18.9248 11.9619 19.0498C11.8366 19.1743 11.6791 19.2364 11.5 19.2373H11.499C11.3193 19.2373 11.1611 19.1747 11.0361 19.0488C10.9117 18.9234 10.8496 18.7657 10.8496 18.5869V15.2988C10.8497 15.1193 10.9127 14.9614 11.0381 14.8359C11.1638 14.7104 11.3221 14.6486 11.502 14.6494ZM15.502 13.1494C15.6807 13.1503 15.838 13.2119 15.9629 13.3359C16.0884 13.4607 16.1503 13.6188 16.1504 13.7988V18.5869C16.1504 18.7666 16.0878 18.9248 15.9619 19.0498C15.8366 19.1743 15.6791 19.2364 15.5 19.2373H15.499C15.3199 19.2373 15.1625 19.175 15.0371 19.0498C14.9116 18.9243 14.8488 18.7663 14.8496 18.5869V13.7988C14.8497 13.6193 14.9127 13.4614 15.0381 13.3359C15.1638 13.2104 15.3221 13.1486 15.502 13.1494ZM19.502 9.14941C19.6807 9.15031 19.838 9.2119 19.9629 9.33594C20.0884 9.46066 20.1503 9.61875 20.1504 9.79883V18.5869C20.1504 18.7666 20.0878 18.9248 19.9619 19.0498C19.8366 19.1743 19.6791 19.2364 19.5 19.2373H19.499C19.3199 19.2373 19.1625 19.175 19.0371 19.0498C18.9116 18.9243 18.8488 18.7663 18.8496 18.5869V9.79883C18.8497 9.61927 18.9127 9.46137 19.0381 9.33594C19.1638 9.21036 19.3221 9.14857 19.502 9.14941ZM19.499 3.35156C19.6838 3.34567 19.8427 3.41642 19.9678 3.55469H19.9688C20.0596 3.64961 20.1156 3.761 20.1357 3.88477L20.1436 4.0127C20.1385 4.18363 20.079 4.33405 19.9609 4.45312H19.96L14.7422 9.6709C14.5779 9.83222 14.3846 9.95688 14.1641 10.0439C13.9451 10.1304 13.7235 10.1738 13.5 10.1738C13.3327 10.1738 13.1678 10.1497 13.0059 10.1006L12.8457 10.043C12.6325 9.95623 12.4382 9.83371 12.2637 9.67578L12.2578 9.6709L12.3643 9.56445L12.2578 9.66992L9.83594 7.24902C9.75169 7.16486 9.64381 7.12012 9.5 7.12012C9.39199 7.12012 9.30408 7.14514 9.23145 7.19336L9.16406 7.24902L3.95996 12.4531C3.83505 12.578 3.67971 12.6445 3.50195 12.6504H3.50098C3.31619 12.6556 3.1575 12.583 3.0332 12.4443V12.4453C2.91072 12.3194 2.85137 12.1633 2.85645 11.9873L2.87012 11.8623C2.89478 11.7425 2.95119 11.6357 3.04004 11.5469L8.25879 6.32812L8.39453 6.20703C8.53435 6.09371 8.68597 6.00535 8.84961 5.94434L9.00977 5.89258C9.17067 5.84784 9.33418 5.8252 9.5 5.8252C9.72114 5.8252 9.94159 5.86551 10.1602 5.94434C10.3843 6.02524 10.5789 6.15388 10.7422 6.3291L13.1641 8.75L13.2314 8.80566C13.3041 8.854 13.3919 8.87891 13.5 8.87891C13.6439 8.87891 13.7517 8.83427 13.8359 8.75L19.04 3.54688C19.165 3.42139 19.321 3.35565 19.499 3.35059V3.35156Z" fill="#7D54D1" stroke="#7D54D1" stroke-width="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.50195 17.1494C3.68072 17.1503 3.83797 17.2119 3.96289 17.3359C4.08842 17.4607 4.15029 17.6188 4.15039 17.7988V18.5869C4.15039 18.7666 4.08775 18.9248 3.96191 19.0498C3.83677 19.1741 3.68038 19.2364 3.50098 19.2373H3.5C3.36498 19.2373 3.24235 19.2024 3.13672 19.1318L3.03613 19.0488C2.91173 18.9234 2.84961 18.7657 2.84961 18.5869V17.7988C2.84971 17.6193 2.91265 17.4614 3.03809 17.3359C3.16377 17.2104 3.32208 17.1486 3.50195 17.1494ZM7.50195 12.6494C7.68072 12.6503 7.83797 12.7119 7.96289 12.8359C8.08842 12.9607 8.15029 13.1188 8.15039 13.2988V18.5869C8.15039 18.7666 8.08775 18.9248 7.96191 19.0498C7.83656 19.1743 7.67907 19.2364 7.5 19.2373H7.49902C7.31988 19.2373 7.16245 19.175 7.03711 19.0498C6.91161 18.9243 6.84884 18.7663 6.84961 18.5869V13.2988C6.84971 13.1193 6.91265 12.9614 7.03809 12.8359C7.16377 12.7104 7.32208 12.6486 7.50195 12.6494ZM11.502 14.6494C11.6807 14.6503 11.838 14.7119 11.9629 14.8359C12.0884 14.9607 12.1503 15.1188 12.1504 15.2988V18.5869C12.1504 18.7666 12.0878 18.9248 11.9619 19.0498C11.8366 19.1743 11.6791 19.2364 11.5 19.2373H11.499C11.3193 19.2373 11.1611 19.1747 11.0361 19.0488C10.9117 18.9234 10.8496 18.7657 10.8496 18.5869V15.2988C10.8497 15.1193 10.9127 14.9614 11.0381 14.8359C11.1638 14.7104 11.3221 14.6486 11.502 14.6494ZM15.502 13.1494C15.6807 13.1503 15.838 13.2119 15.9629 13.3359C16.0884 13.4607 16.1503 13.6188 16.1504 13.7988V18.5869C16.1504 18.7666 16.0878 18.9248 15.9619 19.0498C15.8366 19.1743 15.6791 19.2364 15.5 19.2373H15.499C15.3199 19.2373 15.1625 19.175 15.0371 19.0498C14.9116 18.9243 14.8488 18.7663 14.8496 18.5869V13.7988C14.8497 13.6193 14.9127 13.4614 15.0381 13.3359C15.1638 13.2104 15.3221 13.1486 15.502 13.1494ZM19.502 9.14941C19.6807 9.15031 19.838 9.2119 19.9629 9.33594C20.0884 9.46066 20.1503 9.61875 20.1504 9.79883V18.5869C20.1504 18.7666 20.0878 18.9248 19.9619 19.0498C19.8366 19.1743 19.6791 19.2364 19.5 19.2373H19.499C19.3199 19.2373 19.1625 19.175 19.0371 19.0498C18.9116 18.9243 18.8488 18.7663 18.8496 18.5869V9.79883C18.8497 9.61927 18.9127 9.46137 19.0381 9.33594C19.1638 9.21036 19.3221 9.14857 19.502 9.14941ZM19.499 3.35156C19.6838 3.34567 19.8427 3.41642 19.9678 3.55469H19.9688C20.0596 3.64961 20.1156 3.761 20.1357 3.88477L20.1436 4.0127C20.1385 4.18363 20.079 4.33405 19.9609 4.45312H19.96L14.7422 9.6709C14.5779 9.83222 14.3846 9.95688 14.1641 10.0439C13.9451 10.1304 13.7235 10.1738 13.5 10.1738C13.3327 10.1738 13.1678 10.1497 13.0059 10.1006L12.8457 10.043C12.6325 9.95623 12.4382 9.83371 12.2637 9.67578L12.2578 9.6709L12.3643 9.56445L12.2578 9.66992L9.83594 7.24902C9.75169 7.16486 9.64381 7.12012 9.5 7.12012C9.39199 7.12012 9.30408 7.14514 9.23145 7.19336L9.16406 7.24902L3.95996 12.4531C3.83505 12.578 3.67971 12.6445 3.50195 12.6504H3.50098C3.31619 12.6556 3.1575 12.583 3.0332 12.4443V12.4453C2.91072 12.3194 2.85137 12.1633 2.85645 11.9873L2.87012 11.8623C2.89478 11.7425 2.95119 11.6357 3.04004 11.5469L8.25879 6.32812L8.39453 6.20703C8.53435 6.09371 8.68597 6.00535 8.84961 5.94434L9.00977 5.89258C9.17067 5.84784 9.33418 5.8252 9.5 5.8252C9.72114 5.8252 9.94159 5.86551 10.1602 5.94434C10.3843 6.02524 10.5789 6.15388 10.7422 6.3291L13.1641 8.75L13.2314 8.80566C13.3041 8.854 13.3919 8.87891 13.5 8.87891C13.6439 8.87891 13.7517 8.83427 13.8359 8.75L19.04 3.54688C19.165 3.42139 19.321 3.35565 19.499 3.35059V3.35156Z" fill="#FFFFFF" stroke="#FFFFFF" stroke-width="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,3 @@
<svg width="19" height="17" viewBox="0 0 19 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.85 16.087C4.84924 16.2663 4.91145 16.4239 5.03693 16.5494C5.16233 16.6747 5.31971 16.7373 5.499 16.7373L5.4997 16.7373C5.67889 16.7364 5.83627 16.6743 5.9617 16.5497C6.08758 16.4247 6.15 16.267 6.15 16.0873V10.7993C6.15 10.619 6.08842 10.4607 5.96269 10.3358C5.83772 10.2117 5.68058 10.1501 5.5017 10.1493C5.32173 10.1484 5.16366 10.2105 5.03793 10.3362C4.9124 10.4618 4.85 10.6195 4.85 10.7993V16.087ZM4.85 16.087C4.85 16.0868 4.85 16.0867 4.85 16.0866L5 16.0873M4.85 16.087V16.0873H5M5 16.0873C4.99933 16.2286 5.047 16.3473 5.143 16.4433C5.239 16.5393 5.35767 16.5873 5.499 16.5873L5 16.0873ZM17.4984 0.850356C17.6844 0.844257 17.8447 0.916616 17.9702 1.05647C18.0903 1.18276 18.1491 1.33782 18.1439 1.51269C18.1389 1.6837 18.0787 1.83386 17.9605 1.95296L17.9601 1.95335L12.7431 7.17035L12.7421 7.17131C12.5779 7.33262 12.3846 7.45674 12.1641 7.54381C11.9451 7.63023 11.7235 7.67428 11.5 7.67428C11.277 7.67428 11.0585 7.63017 10.8453 7.54316M17.4984 0.850356L10.8453 7.54316M17.4984 0.850356C17.3204 0.855502 17.1648 0.921665 17.0399 1.04721L17.4984 0.850356ZM10.8453 7.54316C10.632 7.45637 10.4379 7.33351 10.2633 7.17548L10.2578 7.1705L10.258 7.17037L7.83596 4.74937L7.83593 4.74935C7.75166 4.66508 7.6439 4.62029 7.5 4.62029C7.3561 4.62029 7.24834 4.66508 7.16407 4.74935L1.96007 9.95335C1.83516 10.0783 1.67973 10.1443 1.50197 10.1502L1.50128 10.1502C1.31531 10.1555 1.15551 10.0825 1.03084 9.94215C0.910023 9.81657 0.850921 9.66177 0.856065 9.48688C0.861095 9.31586 0.921352 9.1658 1.03993 9.04722L6.25793 3.82922L6.25859 3.82856C6.43436 3.65497 6.63139 3.52612 6.84951 3.44477C7.06174 3.36537 7.27883 3.32528 7.5 3.32528C7.72114 3.32528 7.94133 3.36536 8.15989 3.44418C8.38508 3.5254 8.58031 3.65486 8.74407 3.83122L11.164 6.2502L11.1641 6.25022C11.2483 6.33449 11.3561 6.37928 11.5 6.37928C11.6439 6.37928 11.7517 6.33449 11.8359 6.25022L11.8359 6.25021L17.0396 1.04758L10.8453 7.54316ZM1.5 16.7373L1.5007 16.7373C1.68022 16.7364 1.83649 16.6741 1.9617 16.5497C2.08758 16.4247 2.15 16.267 2.15 16.0873V15.2993C2.15 15.119 2.08842 14.9607 1.96269 14.8358C1.83772 14.7117 1.68058 14.6501 1.5017 14.6493C1.32173 14.6484 1.16366 14.7105 1.03793 14.8362C0.912396 14.9618 0.85 15.1195 0.85 15.2993V16.0873C0.85 16.2663 0.91197 16.4235 1.03657 16.549C1.16172 16.675 1.31979 16.7373 1.5 16.7373ZM9.499 16.7373L9.4997 16.7373C9.67889 16.7364 9.83627 16.6743 9.9617 16.5497C10.0876 16.4247 10.15 16.267 10.15 16.0873V12.7993C10.15 12.619 10.0884 12.4607 9.96269 12.3358C9.83772 12.2117 9.68058 12.1501 9.5017 12.1493C9.32173 12.1484 9.16366 12.2105 9.03793 12.3362C8.9124 12.4618 8.85 12.6195 8.85 12.7993V16.0873C8.85 16.2663 8.91197 16.4235 9.03656 16.549C9.16158 16.6749 9.31924 16.7373 9.499 16.7373ZM13.499 16.7373L13.4997 16.7373C13.6789 16.7364 13.8363 16.6743 13.9617 16.5497C14.0876 16.4247 14.15 16.267 14.15 16.0873V11.2993C14.15 11.119 14.0884 10.9607 13.9627 10.8358C13.8377 10.7117 13.6806 10.6501 13.5017 10.6493C13.3217 10.6484 13.1637 10.7105 13.0379 10.8362C12.9124 10.9618 12.85 11.1195 12.85 11.2993L12.85 16.0866L13 16.0873H12.85V16.0868C12.8492 16.2662 12.9114 16.4238 13.0369 16.5494C13.1623 16.6747 13.3197 16.7373 13.499 16.7373ZM17.499 16.7373L17.4997 16.7373C17.6789 16.7364 17.8363 16.6743 17.9617 16.5497C18.0876 16.4247 18.15 16.267 18.15 16.0873V7.29928C18.15 7.11895 18.0884 6.9607 17.9627 6.83585C17.8377 6.71175 17.6806 6.65013 17.5017 6.64929C17.3217 6.64844 17.1637 6.7105 17.0379 6.83622C16.9124 6.96176 16.85 7.11954 16.85 7.29928L16.85 16.0866L17 16.0873H16.85V16.0868C16.8492 16.2662 16.9114 16.4238 17.0369 16.5494C17.1623 16.6747 17.3197 16.7373 17.499 16.7373Z" fill="#747474" stroke="#747474" stroke-width="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.6294 0.371048C15.4618 0.204652 15.2516 0.0876771 15.0218 0.0329776C14.7921 -0.0217218 14.5517 -0.012028 14.3271 0.0609937L1.36688 4.38403C1.12652 4.4602 0.914122 4.60588 0.756509 4.80269C0.598897 4.9995 0.503143 5.2386 0.481341 5.48979C0.459539 5.74099 0.512667 5.99301 0.634015 6.21403C0.755364 6.43505 0.939488 6.61515 1.16313 6.73159L6.54036 9.38919L9.19796 14.7841C9.30478 14.9953 9.46824 15.1726 9.67006 15.2962C9.87187 15.4198 10.1041 15.4848 10.3407 15.484H10.4293C10.6828 15.4653 10.9247 15.3708 11.1238 15.2129C11.3228 15.0549 11.4698 14.8407 11.5455 14.5981L15.9306 1.67327C16.0089 1.44998 16.0221 1.20903 15.9688 0.978485C15.9155 0.747942 15.7978 0.537288 15.6294 0.371048ZM1.91612 5.60653L13.2287 1.83273L6.94786 8.11354L1.91612 5.60653ZM10.4027 14.0843L7.88688 9.05256L14.1677 2.77175L10.4027 14.0843Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 947 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#747474" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pin-icon lucide-pin"><path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/></svg>

After

Width:  |  Height:  |  Size: 458 B

View File

@@ -1,3 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.66427 17.7747C6.66427 18.2167 6.84488 18.6406 7.16637 18.9532C7.48787 19.2658 7.9239 19.4413 8.37856 19.4413H15.2357C15.6904 19.4413 16.1264 19.2658 16.4479 18.9532C16.7694 18.6406 16.95 18.2167 16.95 17.7747V7.77468H6.66427V17.7747ZM8.37856 9.44135H15.2357V17.7747H8.37856V9.44135ZM14.8071 5.27468L13.95 4.44135H9.66427L8.80713 5.27468H5.80713V6.94135H17.8071V5.27468H14.8071Z" fill="#D30000"/>
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.66574 13.7747C1.66574 14.2168 1.84635 14.6407 2.16784 14.9533C2.48933 15.2658 2.92537 15.4414 3.38002 15.4414H10.2372C10.6918 15.4414 11.1279 15.2658 11.4493 14.9533C11.7708 14.6407 11.9515 14.2168 11.9515 13.7747V3.77474H1.66574V13.7747ZM3.38002 5.44141H10.2372V13.7747H3.38002V5.44141ZM9.80859 1.27474L8.95145 0.441406H4.66574L3.80859 1.27474H0.808594V2.94141H12.8086V1.27474H9.80859Z" fill="#f44336"/>
</svg>

Before

Width:  |  Height:  |  Size: 511 B

After

Width:  |  Height:  |  Size: 520 B

View File

@@ -0,0 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.22066 0.46934C5.36129 0.328889 5.55191 0.25 5.75066 0.25C5.94941 0.25 6.14003 0.328889 6.28066 0.46934L10.5307 4.71934C10.6711 4.85997 10.75 5.05059 10.75 5.24934C10.75 5.44809 10.6711 5.63871 10.5307 5.77934L6.28066 10.0293C6.13851 10.162 5.95041 10.2342 5.75602 10.2309C5.56163 10.2275 5.37614 10.1488 5.23866 10.0113C5.10119 9.87386 5.02247 9.68837 5.01911 9.49398C5.01576 9.29959 5.08802 9.11149 5.22066 8.96934L8.19066 5.99934L0.75066 5.99934C0.551747 5.99934 0.360983 5.92032 0.22033 5.77967C0.0796773 5.63902 0.000659715 5.44825 0.000659724 5.24934C0.000659733 5.05043 0.0796774 4.85966 0.22033 4.71901C0.360983 4.57836 0.551747 4.49934 0.75066 4.49934L8.19066 4.49934L5.22066 1.52934C5.08021 1.38871 5.00132 1.19809 5.00132 0.99934C5.00132 0.800589 5.08021 0.609965 5.22066 0.46934Z" fill="#7D54D1"/>
</svg>

After

Width:  |  Height:  |  Size: 924 B

View File

@@ -0,0 +1,22 @@
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.394 1.001H7.982C7.32776 1.00074 6.67989 1.12939 6.07539 1.3796C5.47089 1.62982 4.92162 1.99669 4.45896 2.45926C3.9963 2.92182 3.62932 3.47102 3.37898 4.07547C3.12865 4.67992 2.99987 5.32776 3 5.982V11.394C2.99974 12.0483 3.12842 12.6963 3.3787 13.3008C3.62897 13.9054 3.99593 14.4547 4.45861 14.9174C4.92128 15.3801 5.4706 15.747 6.07516 15.9973C6.67972 16.2476 7.32768 16.3763 7.982 16.376H13.394C14.0483 16.3763 14.6963 16.2476 15.3008 15.9973C15.9054 15.747 16.4547 15.3801 16.9174 14.9174C17.3801 14.4547 17.747 13.9054 17.9973 13.3008C18.2476 12.6963 18.3763 12.0483 18.376 11.394V5.982C18.3763 5.32768 18.2476 4.67972 17.9973 4.07516C17.747 3.4706 17.3801 2.92128 16.9174 2.45861C16.4547 1.99593 15.9054 1.62897 15.3008 1.3787C14.6963 1.12842 14.0483 0.999738 13.394 1V1.001Z" stroke="url(#paint0_linear_8958_15228)" stroke-width="1.5"/>
<path d="M18.606 12.5881H20.225C20.4968 12.5881 20.7576 12.4801 20.9498 12.2879C21.142 12.0956 21.25 11.8349 21.25 11.5631V6.43809C21.25 6.16624 21.142 5.90553 20.9498 5.7133C20.7576 5.52108 20.4968 5.41309 20.225 5.41309H18.605M3.395 12.5881H1.775C1.6404 12.5881 1.50711 12.5616 1.38275 12.5101C1.25839 12.4586 1.1454 12.3831 1.05022 12.2879C0.955035 12.1927 0.879535 12.0797 0.828023 11.9553C0.776512 11.831 0.75 11.6977 0.75 11.5631V6.43809C0.75 6.16624 0.857991 5.90553 1.05022 5.7133C1.24244 5.52108 1.50315 5.41309 1.775 5.41309H3.395" stroke="url(#paint1_linear_8958_15228)" stroke-width="1.5"/>
<path d="M1.76562 5.41323V1.31323M20.2256 5.41323L20.2156 1.31323M7.91562 5.76323V8.46123M14.0656 5.76323V8.46123M8.94062 12.5882H13.0406" stroke="url(#paint2_linear_8958_15228)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<linearGradient id="paint0_linear_8958_15228" x1="10.688" y1="1" x2="10.688" y2="16.376" gradientUnits="userSpaceOnUse">
<stop stop-color="#58E2E1"/>
<stop offset="0.524038" stop-color="#657797"/>
<stop offset="1" stop-color="#CC7871"/>
</linearGradient>
<linearGradient id="paint1_linear_8958_15228" x1="11" y1="5.41309" x2="11" y2="12.5881" gradientUnits="userSpaceOnUse">
<stop stop-color="#58E2E1"/>
<stop offset="0.524038" stop-color="#657797"/>
<stop offset="1" stop-color="#CC7871"/>
</linearGradient>
<linearGradient id="paint2_linear_8958_15228" x1="10.9956" y1="1.31323" x2="10.9956" y2="12.5882" gradientUnits="userSpaceOnUse">
<stop stop-color="#58E2E1"/>
<stop offset="0.524038" stop-color="#657797"/>
<stop offset="1" stop-color="#CC7871"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,12 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_8836_14948)">
<path d="M25.7094 18.5522C22.8007 20.7575 20.0485 23.1618 17.4725 25.7479C5.33468 37.8856 -0.811037 51.4105 3.7385 55.9643C8.28803 60.5138 21.8171 54.3638 33.9507 42.2303C36.5369 39.6529 38.9412 36.8992 41.1463 33.9891" stroke="#3F4147" stroke-width="4.26786" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M41.1421 33.989C48.2993 43.4892 51.2142 52.4261 47.6804 55.9599C43.1266 60.5137 29.6018 54.3637 17.464 42.2302C5.33477 30.0881 -0.810942 16.5676 3.73859 12.0138C7.27238 8.48424 16.2093 11.3992 25.7095 18.5521" stroke="#3F4147" stroke-width="4.26786" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M23.5781 33.9891C23.5781 34.5551 23.8029 35.0978 24.2031 35.498C24.6033 35.8982 25.1461 36.123 25.7121 36.123C26.278 36.123 26.8208 35.8982 27.221 35.498C27.6212 35.0978 27.846 34.5551 27.846 33.9891C27.846 33.4232 27.6212 32.8804 27.221 32.4802C26.8208 32.08 26.278 31.8552 25.7121 31.8552C25.1461 31.8552 24.6033 32.08 24.2031 32.4802C23.8029 32.8804 23.5781 33.4232 23.5781 33.9891ZM34.2904 15.4069C32.9461 15.1721 32.9461 13.2473 34.2904 13.0169C36.6655 12.6055 38.864 11.4952 40.6049 9.82805C42.3458 8.16089 43.5501 6.01251 44.0638 3.65746L44.1407 3.28616C44.4309 1.96312 46.3173 1.95459 46.616 3.27335L46.7184 3.70441C47.2541 6.04779 48.4698 8.18083 50.2131 9.83599C51.9563 11.4912 54.1495 12.5947 56.5174 13.0083C57.8661 13.2431 57.8661 15.1807 56.5174 15.4111C54.1495 15.8247 51.9563 16.9283 50.2131 18.5835C48.4698 20.2386 47.2541 22.3717 46.7184 24.7151L46.616 25.1461C46.3173 26.4692 44.4309 26.4606 44.1407 25.1376L44.0553 24.7663C43.5415 22.4112 42.3372 20.2629 40.5963 18.5957C38.8554 16.9285 36.657 15.8183 34.2819 15.4069H34.2904Z" stroke="#5F6167" stroke-width="4.26786" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_8836_14948">
<rect width="59.75" height="59.75" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,12 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_8612_14680)">
<path d="M25.7094 18.5525C22.8007 20.7577 20.0485 23.162 17.4725 25.7481C5.33468 37.8859 -0.811037 51.4107 3.7385 55.9645C8.28803 60.5141 21.8171 54.3641 33.9507 42.2306C36.5369 39.6532 38.9412 36.8995 41.1463 33.9893" stroke="#DCDCDC" stroke-width="4.26786" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M41.1421 33.9891C48.2993 43.4893 51.2142 52.4262 47.6804 55.96C43.1266 60.5138 29.6018 54.3638 17.464 42.2303C5.33477 30.0883 -0.810942 16.5677 3.73859 12.0139C7.27238 8.48436 16.2093 11.3993 25.7095 18.5522" stroke="#DCDCDC" stroke-width="4.26786" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M23.5781 33.9891C23.5781 34.5551 23.8029 35.0978 24.2031 35.498C24.6033 35.8982 25.1461 36.123 25.7121 36.123C26.278 36.123 26.8208 35.8982 27.221 35.498C27.6212 35.0978 27.846 34.5551 27.846 33.9891C27.846 33.4232 27.6212 32.8804 27.221 32.4802C26.8208 32.08 26.278 31.8552 25.7121 31.8552C25.1461 31.8552 24.6033 32.08 24.2031 32.4802C23.8029 32.8804 23.5781 33.4232 23.5781 33.9891ZM34.2904 15.4069C32.9461 15.1721 32.9461 13.2473 34.2904 13.0169C36.6655 12.6055 38.864 11.4952 40.6049 9.82805C42.3458 8.16089 43.5501 6.01251 44.0638 3.65746L44.1407 3.28616C44.4309 1.96312 46.3173 1.95459 46.616 3.27335L46.7184 3.70441C47.2541 6.04779 48.4698 8.18083 50.2131 9.83599C51.9563 11.4912 54.1495 12.5947 56.5174 13.0083C57.8661 13.2431 57.8661 15.1807 56.5174 15.4111C54.1495 15.8247 51.9563 16.9283 50.2131 18.5835C48.4698 20.2386 47.2541 22.3717 46.7184 24.7151L46.616 25.1461C46.3173 26.4692 44.4309 26.4606 44.1407 25.1376L44.0553 24.7663C43.5415 22.4112 42.3372 20.2629 40.5963 18.5957C38.8554 16.9285 36.657 15.8183 34.2819 15.4069H34.2904Z" stroke="#999999" stroke-width="4.26786" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_8612_14680">
<rect width="59.75" height="59.75" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 10.5994V3.39941M11.6 3.39941V10.5994" stroke="#5D5D5D" stroke-width="0.9" stroke-linecap="round"/>
<path d="M11.6 7C11.6 8.326 9.4508 9.4 6.8 9.4C4.1492 9.4 2 8.326 2 7M11.6 10.6C11.6 11.926 9.4508 13 6.8 13C4.1492 13 2 11.926 2 10.6M6.8 5.8C9.4508 5.8 11.6 4.726 11.6 3.4C11.6 2.074 9.4508 1 6.8 1C4.1492 1 2 2.074 2 3.4C2 4.726 4.1492 5.8 6.8 5.8Z" stroke="#5D5D5D" stroke-width="0.9"/>
</svg>

After

Width:  |  Height:  |  Size: 504 B

View File

@@ -0,0 +1,3 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.04071 11.3081H1.51004C1.33769 11.3088 1.1726 11.3776 1.05073 11.4995C0.928855 11.6213 0.860077 11.7864 0.859375 11.9588V17.4921C0.859375 17.8521 1.15137 18.1428 1.51004 18.1428H7.04337C7.40337 18.1428 7.69404 17.8508 7.69404 17.4921V11.9588C7.69334 11.7864 7.62456 11.6213 7.50269 11.4995C7.38082 11.3776 7.21573 11.3088 7.04337 11.3081H7.04071ZM17.1594 11.3081H11.626C11.4537 11.3088 11.2886 11.3776 11.1667 11.4995C11.0449 11.6213 10.9761 11.7864 10.9754 11.9588V17.4921C10.9754 17.8521 11.266 18.1428 11.626 18.1428H17.1594C17.5194 18.1428 17.81 17.8508 17.81 17.4921V11.9588C17.8093 11.7864 17.7406 11.6213 17.6187 11.4995C17.4968 11.3776 17.3317 11.3088 17.1594 11.3081ZM7.04071 1.19079H1.51004C1.33769 1.19149 1.1726 1.26027 1.05073 1.38214C0.928855 1.50401 0.860077 1.6691 0.859375 1.84145V7.37479C0.859375 7.73479 1.15137 8.02545 1.51004 8.02545H7.04337C7.40337 8.02545 7.69404 7.73345 7.69404 7.37479V1.84012C7.69334 1.66777 7.62456 1.50268 7.50269 1.3808C7.38082 1.25893 7.21573 1.19016 7.04337 1.18945L7.04071 1.19079ZM10.862 5.11212C10.4607 5.04279 10.4607 4.46812 10.862 4.39745C11.5717 4.27403 12.2286 3.94205 12.7489 3.44385C13.2692 2.94565 13.6293 2.3038 13.7834 1.60012L13.8074 1.48945C13.894 1.09345 14.458 1.08945 14.5487 1.48545L14.578 1.61479C14.7379 2.31545 15.1012 2.95326 15.6224 3.4481C16.1436 3.94294 16.7994 4.27276 17.5074 4.39612C17.91 4.46545 17.91 5.04412 17.5074 5.11479C16.7994 5.23815 16.1436 5.56796 15.6224 6.0628C15.1012 6.55765 14.7379 7.19545 14.578 7.89612L14.5487 8.02412C14.458 8.42012 13.8954 8.41745 13.8074 8.02145L13.7834 7.91079C13.6295 7.20686 13.2695 6.56472 12.7492 6.06627C12.2289 5.56781 11.5719 5.23564 10.862 5.11212Z" stroke="#747474" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,6 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.82047 15.07H3.23047C2.40047 15.07 1.73047 14.4 1.73047 13.57V10.98C1.73047 10.58 1.89047 10.2 2.17047 9.91999L4.38047 7.70999C4.58047 7.50999 4.89047 7.50999 5.09047 7.70999C5.29047 7.90999 5.29047 8.21999 5.09047 8.41999L2.88047 10.63C2.78626 10.7223 2.73235 10.8481 2.73047 10.98V13.57C2.73047 13.85 2.95047 14.07 3.23047 14.07H5.82047C5.95047 14.07 6.08047 14.02 6.17047 13.92L8.38047 11.71C8.58047 11.51 8.89047 11.51 9.09047 11.71C9.29047 11.91 9.29047 12.22 9.09047 12.42L6.88047 14.63C6.60047 14.91 6.22047 15.07 5.82047 15.07ZM12.8505 8.44999C12.7849 8.45079 12.7199 8.43786 12.6596 8.41202C12.5993 8.38619 12.5451 8.34803 12.5005 8.29999C12.3005 8.09999 12.3005 7.78999 12.5005 7.58999L14.6005 5.48999C14.7905 5.29999 14.7905 4.97999 14.6005 4.77999L12.0105 2.18999C11.915 2.09844 11.7878 2.04732 11.6555 2.04732C11.5232 2.04732 11.396 2.09844 11.3005 2.18999L9.14047 4.34999C8.94047 4.54999 8.63047 4.54999 8.43047 4.34999C8.23047 4.14999 8.23047 3.83999 8.43047 3.63999L10.5905 1.47999C11.1605 0.90999 12.1405 0.90999 12.7105 1.47999L15.3005 4.06999C15.8805 4.64999 15.8805 5.60999 15.3005 6.18999L13.2005 8.28999C13.1005 8.38999 12.9705 8.43999 12.8505 8.43999V8.44999Z" fill="#5D5D5D"/>
<path d="M11.6305 15.0605C11.2305 15.0605 10.8505 14.9005 10.5705 14.6205L8.38047 12.4305C8.18047 12.2305 8.18047 11.9205 8.38047 11.7205C8.58047 11.5205 8.89047 11.5205 9.09047 11.7205L11.2805 13.9105C11.4705 14.1005 11.8005 14.1005 11.9905 13.9105L14.5805 11.3205C14.7705 11.1305 14.7705 10.8105 14.5805 10.6105L12.3905 8.42055C12.1905 8.22055 12.1905 7.91055 12.3905 7.71055C12.5905 7.51055 12.9005 7.51055 13.1005 7.71055L15.2905 9.90055C15.8705 10.4805 15.8705 11.4405 15.2905 12.0205L12.7005 14.6105C12.4205 14.8905 12.0405 15.0505 11.6405 15.0505L11.6305 15.0605Z" fill="#5D5D5D"/>
<path d="M8.98125 12.8195C8.91567 12.8203 8.85066 12.8073 8.79038 12.7815C8.7301 12.7557 8.6759 12.7175 8.63125 12.6695L2.09125 6.13945C1.51125 5.55945 1.51125 4.59945 2.09125 4.01945L4.68125 1.42945C5.25125 0.859453 6.23125 0.859453 6.80125 1.42945L13.3412 7.96945C13.5412 8.16945 13.5412 8.47945 13.3412 8.67945C13.1412 8.87945 12.8312 8.87945 12.6312 8.67945L6.09125 2.13945C5.99574 2.0479 5.86855 1.99678 5.73625 1.99678C5.60395 1.99678 5.47676 2.0479 5.38125 2.13945L2.79125 4.72945C2.60125 4.91945 2.60125 5.23945 2.79125 5.43945L9.33125 11.9795C9.53125 12.1795 9.53125 12.4895 9.33125 12.6895C9.23125 12.7895 9.10125 12.8395 8.98125 12.8395V12.8195Z" fill="#5D5D5D"/>
<path d="M6.28906 6.01055C6.22349 6.01135 6.15847 5.99841 6.09819 5.97258C6.03791 5.94675 5.98371 5.90858 5.93906 5.86055C5.73906 5.66055 5.73906 5.35055 5.93906 5.15055L7.87906 3.21055C8.07906 3.01055 8.38906 3.01055 8.58906 3.21055C8.78906 3.41055 8.78906 3.72055 8.58906 3.92055L6.64906 5.86055C6.54906 5.96055 6.41906 6.01055 6.29906 6.01055H6.28906ZM8.31906 8.04055C8.25349 8.04135 8.18847 8.02841 8.12819 8.00258C8.06791 7.97675 8.01371 7.93858 7.96906 7.89055C7.76906 7.69055 7.76906 7.38055 7.96906 7.18055L9.90906 5.24055C10.1091 5.04055 10.4191 5.04055 10.6191 5.24055C10.8191 5.44055 10.8191 5.75055 10.6191 5.95055L8.67906 7.89055C8.57906 7.99055 8.44906 8.04055 8.32906 8.04055H8.31906ZM10.3491 10.0705C10.2835 10.0713 10.2185 10.0584 10.1582 10.0326C10.0979 10.0067 10.0437 9.96858 9.99906 9.92055C9.79906 9.72055 9.79906 9.41055 9.99906 9.21055L11.9391 7.27055C12.1391 7.07055 12.4491 7.07055 12.6491 7.27055C12.8491 7.47055 12.8491 7.78055 12.6491 7.98055L10.7091 9.92055C10.6091 10.0205 10.4791 10.0705 10.3591 10.0705H10.3491Z" fill="#5D5D5D"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#747474" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pin-off-icon lucide-pin-off"><path d="M12 17v5"/><path d="M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89"/><path d="m2 2 20 20"/><path d="M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h11"/></svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -0,0 +1,3 @@
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.66574 13.7747C1.66574 14.2168 1.84635 14.6407 2.16784 14.9533C2.48933 15.2658 2.92537 15.4414 3.38002 15.4414H10.2372C10.6918 15.4414 11.1279 15.2658 11.4493 14.9533C11.7708 14.6407 11.9515 14.2168 11.9515 13.7747V3.77474H1.66574V13.7747ZM3.38002 5.44141H10.2372V13.7747H3.38002V5.44141ZM9.80859 1.27474L8.95145 0.441406H4.66574L3.80859 1.27474H0.808594V2.94141H12.8086V1.27474H9.80859Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 520 B

View File

@@ -32,9 +32,9 @@ export default function Accordion({
setIsOpen(!isOpen);
};
return (
<div className={`shadow-sm overflow-hidden ${className}`}>
<div className={`overflow-hidden shadow-sm ${className}`}>
<button
className={`flex items-center justify-between w-full focus:outline-none ${titleClassName}`}
className={`flex w-full items-center justify-between focus:outline-none ${titleClassName}`}
onClick={toggleAccordion}
>
<p className="break-words">{title}</p>

View File

@@ -90,7 +90,7 @@ export default function ContextMenu({
onClick={(e) => e.stopPropagation()}
>
<div
className="flex w-32 flex-col rounded-xl text-sm shadow-xl md:w-36 dark:bg-charleston-green-2 bg-lotion"
className="flex w-32 flex-col rounded-xl bg-lotion text-sm shadow-xl dark:bg-charleston-green-2 md:w-36"
style={{ minWidth: '144px' }}
>
{options.map((option, index) => (
@@ -102,26 +102,22 @@ export default function ContextMenu({
option.onClick(event);
setIsOpen(false);
}}
className={`
flex justify-start items-center gap-4 p-3
transition-colors duration-200 ease-in-out
${index === 0 ? 'rounded-t-xl' : ''}
${index === options.length - 1 ? 'rounded-b-xl' : ''}
${
option.variant === 'danger'
? 'dark:text-red-2000 dark:hover:bg-charcoal-grey text-rosso-corsa hover:bg-bright-gray'
: 'dark:text-bright-gray dark:hover:bg-charcoal-grey text-eerie-black hover:bg-bright-gray'
}
`}
className={`flex items-center justify-start gap-4 p-3 transition-colors duration-200 ease-in-out ${index === 0 ? 'rounded-t-xl' : ''} ${index === options.length - 1 ? 'rounded-b-xl' : ''} ${
option.variant === 'danger'
? 'text-rosso-corsa hover:bg-bright-gray dark:text-red-2000 dark:hover:bg-charcoal-grey/20'
: 'text-eerie-black hover:bg-bright-gray dark:text-bright-gray dark:hover:bg-charcoal-grey/20'
} `}
>
{option.icon && (
<img
width={option.iconWidth || 16}
height={option.iconHeight || 16}
src={option.icon}
alt={option.label}
className={`cursor-pointer hover:opacity-75 ${option.iconClassName || ''}`}
/>
<div className="flex w-4 justify-center">
<img
width={option.iconWidth || 16}
height={option.iconHeight || 16}
src={option.icon}
alt={option.label}
className={`cursor-pointer ${option.iconClassName || ''}`}
/>
</div>
)}
<span>{option.label}</span>
</button>

View File

@@ -1,58 +1,136 @@
import clsx from 'clsx';
import copy from 'copy-to-clipboard';
import { useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import CheckMark from '../assets/checkmark.svg?react';
import Copy from '../assets/copy.svg?react';
import CopyIcon from '../assets/copy.svg?react';
type CopyButtonProps = {
textToCopy: string;
bgColorLight?: string;
bgColorDark?: string;
hoverBgColorLight?: string;
hoverBgColorDark?: string;
iconSize?: string;
padding?: string;
showText?: boolean;
copiedDuration?: number;
className?: string;
iconWrapperClassName?: string;
textClassName?: string;
};
const DEFAULT_ICON_SIZE = 'w-4 h-4';
const DEFAULT_PADDING = 'p-2';
const DEFAULT_COPIED_DURATION = 2000;
const DEFAULT_BG_LIGHT = '#FFFFFF';
const DEFAULT_BG_DARK = 'transparent';
const DEFAULT_HOVER_BG_LIGHT = '#EEEEEE';
const DEFAULT_HOVER_BG_DARK = '#4A4A4A';
export default function CopyButton({
text,
colorLight,
colorDark,
textToCopy,
bgColorLight = DEFAULT_BG_LIGHT,
bgColorDark = DEFAULT_BG_DARK,
hoverBgColorLight = DEFAULT_HOVER_BG_LIGHT,
hoverBgColorDark = DEFAULT_HOVER_BG_DARK,
iconSize = DEFAULT_ICON_SIZE,
padding = DEFAULT_PADDING,
showText = false,
}: {
text: string;
colorLight?: string;
colorDark?: string;
showText?: boolean;
}) {
copiedDuration = DEFAULT_COPIED_DURATION,
className,
iconWrapperClassName,
textClassName,
}: CopyButtonProps) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
const [isCopyHovered, setIsCopyHovered] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const timeoutIdRef = useRef<number | null>(null);
const handleCopyClick = (text: string) => {
copy(text);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 3000);
};
const iconWrapperClasses = clsx(
'flex items-center justify-center rounded-full transition-colors duration-150 ease-in-out',
padding,
`bg-[${bgColorLight}] dark:bg-[${bgColorDark}]`,
`hover:bg-[${hoverBgColorLight}] dark:hover:bg-[${hoverBgColorDark}]`,
{
'bg-green-100 dark:bg-green-900 hover:bg-green-100 dark:hover:bg-green-900':
isCopied,
},
iconWrapperClassName,
);
const rootButtonClasses = clsx(
'flex items-center gap-2 group',
'focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 rounded-full',
className,
);
const textSpanClasses = clsx(
'text-xs text-gray-600 dark:text-gray-400 transition-opacity duration-150 ease-in-out',
{ 'opacity-75': isCopied },
textClassName,
);
const IconComponent = isCopied ? CheckMark : CopyIcon;
const iconClasses = clsx(iconSize, {
'stroke-green-600 dark:stroke-green-400': isCopied,
'fill-none text-gray-700 dark:text-gray-300': !isCopied,
});
const buttonTitle = isCopied
? t('conversation.copied')
: t('conversation.copy');
const displayedText = isCopied
? t('conversation.copied')
: t('conversation.copy');
const handleCopy = useCallback(() => {
if (isCopied) return;
try {
const success = copy(textToCopy);
if (success) {
setIsCopied(true);
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
timeoutIdRef.current = setTimeout(() => {
setIsCopied(false);
timeoutIdRef.current = null;
}, copiedDuration);
} else {
console.warn('Copy command failed.');
}
} catch (error) {
console.error('Failed to copy text:', error);
}
}, [textToCopy, copiedDuration, isCopied]);
useEffect(() => {
return () => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
};
}, []);
return (
<button
onClick={() => handleCopyClick(text)}
onMouseEnter={() => setIsCopyHovered(true)}
onMouseLeave={() => setIsCopyHovered(false)}
className="flex items-center gap-2"
type="button"
onClick={handleCopy}
className={rootButtonClasses}
title={buttonTitle}
aria-label={buttonTitle}
disabled={isCopied}
>
<div
className={`flex items-center justify-center rounded-full p-2 ${
isCopyHovered
? `bg-[#EEEEEE] dark:bg-purple-taupe`
: `bg-[${colorLight ? colorLight : '#FFFFFF'}] dark:bg-[${colorDark ? colorDark : 'transparent'}]`
}`}
>
{copied ? (
<CheckMark className="cursor-pointer stroke-green-2000" />
) : (
<Copy className="w-4 cursor-pointer fill-none" />
)}
<div className={iconWrapperClasses}>
<IconComponent className={iconClasses} aria-hidden="true" />
</div>
{showText && (
<span className="text-xs text-gray-600 dark:text-gray-400">
{copied ? t('conversation.copied') : t('conversation.copy')}
</span>
)}
{showText && <span className={textSpanClasses}>{displayedText}</span>}
<span className="sr-only" aria-live="polite" aria-atomic="true">
{isCopied ? t('conversation.copied', 'Copied to clipboard') : ''}
</span>
</button>
);
}

View File

@@ -52,31 +52,31 @@ const Pagination: React.FC<PaginationProps> = ({
};
return (
<div className="flex items-center text-xs justify-end gap-4 mt-2 p-2 border-gray-200">
<div className="mt-2 flex items-center justify-end gap-4 border-gray-200 p-2 text-xs">
{/* Rows per page dropdown */}
<div className="flex items-center gap-2 relative">
<div className="relative flex items-center gap-2">
<span className="text-gray-900 dark:text-gray-50">
{t('pagination.rowsPerPage')}:
</span>
<div className="relative">
<button
onClick={toggleDropdown}
className="px-3 py-1 border rounded dark:bg-dark-charcoal dark:text-light-gray hover:bg-gray-200 dark:hover:bg-neutral-700"
className="rounded border px-3 py-1 hover:bg-gray-200 dark:bg-dark-charcoal dark:text-light-gray dark:hover:bg-neutral-700"
>
{rowsPerPage}
</button>
<div
className={`absolute z-50 right-0 mt-1 w-28 transform bg-white dark:bg-dark-charcoal shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-200 ease-in-out ${
className={`absolute right-0 z-50 mt-1 w-28 transform bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-200 ease-in-out dark:bg-dark-charcoal ${
isDropdownOpen
? 'scale-100 opacity-100 block'
: 'scale-95 opacity-0 hidden'
? 'block scale-100 opacity-100'
: 'hidden scale-95 opacity-0'
}`}
>
{rowsPerPageOptions.map((option) => (
<div
key={option}
onClick={() => handleSelectRowsPerPage(option)}
className={`cursor-pointer px-4 py-2 text-xs hover:bg-gray-100 dark:hover:bg-neutral-700 ${
className={`cursor-pointer px-4 py-2 text-xs hover:bg-gray-100 dark:hover:bg-neutral-700 ${
rowsPerPage === option
? 'bg-gray-100 dark:bg-neutral-700 dark:text-light-gray'
: 'bg-white dark:bg-dark-charcoal dark:text-light-gray'
@@ -97,45 +97,45 @@ const Pagination: React.FC<PaginationProps> = ({
<button
onClick={handleFirstPage}
disabled={currentPage === 1}
className="px-2 py-1 border rounded disabled:opacity-50"
className="rounded border px-2 py-1 disabled:opacity-50"
>
<img
src={DoubleArrowLeft}
alt={t('pagination.firstPage')}
className="dark:invert dark:sepia dark:brightness-200"
className="dark:brightness-200 dark:invert dark:sepia"
/>
</button>
<button
onClick={handlePreviousPage}
disabled={currentPage === 1}
className="px-2 py-1 border rounded disabled:opacity-50"
className="rounded border px-2 py-1 disabled:opacity-50"
>
<img
src={SingleArrowLeft}
alt={t('pagination.previousPage')}
className="dark:invert dark:sepia dark:brightness-200"
className="dark:brightness-200 dark:invert dark:sepia"
/>
</button>
<button
onClick={handleNextPage}
disabled={currentPage === totalPages}
className="px-2 py-1 border rounded disabled:opacity-50"
className="rounded border px-2 py-1 disabled:opacity-50"
>
<img
src={SingleArrowRight}
alt={t('pagination.nextPage')}
className="dark:invert dark:sepia dark:brightness-200"
className="dark:brightness-200 dark:invert dark:sepia"
/>
</button>
<button
onClick={handleLastPage}
disabled={currentPage === totalPages}
className="px-2 py-1 border rounded disabled:opacity-50"
className="rounded border px-2 py-1 disabled:opacity-50"
>
<img
src={DoubleArrowRight}
alt={t('pagination.lastPage')}
className="dark:invert dark:sepia dark:brightness-200"
className="dark:brightness-200 dark:invert dark:sepia"
/>
</button>
</div>

View File

@@ -1,4 +1,5 @@
import React from 'react';
import Arrow2 from '../assets/dropdown-arrow.svg';
import Edit from '../assets/edit.svg';
import Trash from '../assets/trash.svg';
@@ -9,6 +10,10 @@ function Dropdown({
onSelect,
size = 'w-32',
rounded = 'xl',
buttonBackgroundColor = 'white',
buttonDarkBackgroundColor = 'transparent',
optionsBackgroundColor = 'white',
optionsDarkBackgroundColor = 'dark-charcoal',
border = 'border-2',
borderColor = 'silver',
darkBorderColor = 'dim-gray',
@@ -17,6 +22,8 @@ function Dropdown({
showDelete,
onDelete,
placeholder,
placeholderTextColor = 'gray-500',
darkPlaceholderTextColor = 'gray-400',
contentSize = 'text-base',
}: {
options:
@@ -37,6 +44,10 @@ function Dropdown({
| ((value: { value: number; description: string }) => void);
size?: string;
rounded?: 'xl' | '3xl';
buttonBackgroundColor?: string;
buttonDarkBackgroundColor?: string;
optionsBackgroundColor?: string;
optionsDarkBackgroundColor?: string;
border?: 'border' | 'border-2';
borderColor?: string;
darkBorderColor?: string;
@@ -45,6 +56,8 @@ function Dropdown({
showDelete?: boolean;
onDelete?: (value: string) => void;
placeholder?: string;
placeholderTextColor?: string;
darkPlaceholderTextColor?: string;
contentSize?: string;
}) {
const dropdownRef = React.useRef<HTMLDivElement>(null);
@@ -71,7 +84,7 @@ function Dropdown({
<div
className={[
typeof selectedValue === 'string'
? 'relative mt-2'
? 'relative'
: 'relative align-middle',
size,
].join(' ')}
@@ -79,7 +92,7 @@ function Dropdown({
>
<button
onClick={() => setIsOpen(!isOpen)}
className={`flex w-full cursor-pointer items-center justify-between ${border} border-${borderColor} bg-white px-5 py-3 dark:border-${darkBorderColor} dark:bg-transparent ${
className={`flex w-full cursor-pointer items-center justify-between ${border} border-${borderColor} bg-${buttonBackgroundColor} px-5 py-3 dark:border-${darkBorderColor} dark:bg-${buttonDarkBackgroundColor} ${
isOpen ? `${borderTopRadius}` : `${borderRadius}`
}`}
>
@@ -89,8 +102,9 @@ function Dropdown({
</span>
) : (
<span
className={`truncate dark:text-bright-gray ${
!selectedValue && 'text-silver dark:text-gray-400'
className={`truncate ${selectedValue && `dark:text-bright-gray`} ${
!selectedValue &&
`text-${placeholderTextColor} dark:text-${darkPlaceholderTextColor}`
} ${contentSize}`}
>
{selectedValue && 'label' in selectedValue
@@ -116,7 +130,7 @@ function Dropdown({
</button>
{isOpen && (
<div
className={`absolute left-0 right-0 z-20 -mt-1 max-h-40 overflow-y-auto rounded-b-xl ${border} border-${borderColor} bg-white shadow-lg dark:border-${darkBorderColor} dark:bg-dark-charcoal`}
className={`absolute left-0 right-0 z-20 -mt-1 max-h-40 overflow-y-auto rounded-b-xl ${border} border-${borderColor} bg-${optionsBackgroundColor} shadow-lg dark:border-${darkBorderColor} dark:bg-${optionsDarkBackgroundColor}`}
>
{options.map((option: any, index) => (
<div

View File

@@ -88,7 +88,7 @@ export default function DropdownMenu({
onClick={(e) => e.stopPropagation()}
>
<div
className={`w-28 transform rounded-md bg-white dark:bg-dark-charcoal shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-200 ease-in-out ${className}`}
className={`w-28 transform rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-200 ease-in-out dark:bg-dark-charcoal ${className}`}
>
<div
role="menu"

View File

@@ -36,20 +36,20 @@ const Help = () => {
<button
ref={buttonRef}
onClick={toggleDropdown}
className="my-auto mx-4 w-full flex items-center h-9 gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E]"
className="mx-4 my-auto flex h-9 w-full items-center gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E]"
>
<img src={Info} alt="info" className="ml-2 w-5 filter dark:invert" />
{t('help')}
</button>
{isOpen && (
<div
className={`absolute translate-x-4 -translate-y-28 z-10 w-48 shadow-lg bg-white dark:bg-[#444654] rounded-xl`}
className={`absolute z-10 w-48 -translate-y-28 translate-x-4 rounded-xl bg-white shadow-lg dark:bg-[#444654]`}
>
<a
href="https://docs.docsgpt.cloud/"
target="_blank"
rel="noopener noreferrer"
className="flex items-start gap-4 px-4 py-2 text-black dark:text-white hover:bg-bright-gray dark:hover:bg-[#545561] rounded-t-xl"
className="flex items-start gap-4 rounded-t-xl px-4 py-2 text-black hover:bg-bright-gray dark:text-white dark:hover:bg-[#545561]"
>
<img
src={PageIcon}
@@ -61,12 +61,12 @@ const Help = () => {
</a>
<a
href="mailto:support@docsgpt.cloud"
className="flex items-start gap-4 px-4 py-2 text-black dark:text-white hover:bg-bright-gray dark:hover:bg-[#545561] rounded-b-xl"
className="flex items-start gap-4 rounded-b-xl px-4 py-2 text-black hover:bg-bright-gray dark:text-white dark:hover:bg-[#545561]"
>
<img
src={EmailIcon}
alt="Email Us"
className="filter dark:invert p-0.5"
className="p-0.5 filter dark:invert"
width={20}
/>
{t('emailUs')}

View File

@@ -13,6 +13,7 @@ const Input = ({
className = '',
colorVariant = 'silver',
borderVariant = 'thick',
textSize = 'medium',
children,
labelBgClassName = 'bg-white dark:bg-raisin-black',
onChange,
@@ -28,6 +29,10 @@ const Input = ({
thin: 'border',
thick: 'border-2',
};
const textSizeStyles = {
small: 'text-sm',
medium: 'text-base',
};
const inputRef = useRef<HTMLInputElement>(null);
@@ -35,15 +40,7 @@ const Input = ({
<div className={`relative ${className}`}>
<input
ref={inputRef}
className={`peer h-[42px] w-full rounded-full px-3 py-1
bg-transparent outline-none
text-jet dark:text-bright-gray
placeholder-transparent
${colorStyles[colorVariant]}
${borderStyles[borderVariant]}
[&:-webkit-autofill]:bg-transparent
[&:-webkit-autofill]:appearance-none
[&:-webkit-autofill_selected]:bg-transparent`}
className={`peer h-[42px] w-full rounded-full bg-transparent px-3 py-1 text-jet placeholder-transparent outline-none dark:text-bright-gray ${colorStyles[colorVariant]} ${borderStyles[borderVariant]} ${textSizeStyles[textSize]} [&:-webkit-autofill]:appearance-none [&:-webkit-autofill]:bg-transparent [&:-webkit-autofill_selected]:bg-transparent`}
type={type}
id={id}
name={name}
@@ -61,15 +58,11 @@ const Input = ({
{placeholder && (
<label
htmlFor={id}
className={`absolute left-3 -top-2.5 px-2 text-xs transition-all
peer-placeholder-shown:top-2.5 peer-placeholder-shown:left-3 peer-placeholder-shown:text-base
peer-placeholder-shown:text-gray-4000 peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs
peer-focus:text-gray-4000 dark:text-silver dark:peer-placeholder-shown:text-gray-400
cursor-none pointer-events-none ${labelBgClassName}`}
className={`absolute -top-2.5 left-3 px-2 ${textSizeStyles[textSize]} transition-all peer-placeholder-shown:left-3 peer-placeholder-shown:top-2.5 peer-placeholder-shown:${textSizeStyles[textSize]} pointer-events-none cursor-none peer-placeholder-shown:text-gray-4000 peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs peer-focus:text-gray-4000 dark:text-silver dark:peer-placeholder-shown:text-gray-400 ${labelBgClassName}`}
>
{placeholder}
{required && (
<span className="text-[#D30000] dark:text-[#D42626] ml-0.5">*</span>
<span className="ml-0.5 text-[#D30000] dark:text-[#D42626]">*</span>
)}
</label>
)}

View File

@@ -0,0 +1,424 @@
import React, { useEffect, useRef, useState } from 'react';
import mermaid from 'mermaid';
import CopyButton from './CopyButton';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import {
oneLight,
vscDarkPlus,
} from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { MermaidRendererProps } from './types';
import { useSelector } from 'react-redux';
import { selectStatus } from '../conversation/conversationSlice';
import { useDarkTheme } from '../hooks';
const MermaidRenderer: React.FC<MermaidRendererProps> = ({
code,
isLoading,
}) => {
const [isDarkTheme] = useDarkTheme();
const diagramId = useRef(`mermaid-${crypto.randomUUID()}`);
const status = useSelector(selectStatus);
const [error, setError] = useState<string | null>(null);
const [showCode, setShowCode] = useState<boolean>(false);
const [showDownloadMenu, setShowDownloadMenu] = useState<boolean>(false);
const downloadMenuRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [hoverPosition, setHoverPosition] = useState<{
x: number;
y: number;
} | null>(null);
const [isHovering, setIsHovering] = useState<boolean>(false);
const [zoomFactor, setZoomFactor] = useState<number>(2);
const handleMouseMove = (event: React.MouseEvent) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = (event.clientX - rect.left) / rect.width;
const y = (event.clientY - rect.top) / rect.height;
setHoverPosition({ x, y });
};
const handleMouseEnter = () => setIsHovering(true);
const handleMouseLeave = () => {
setIsHovering(false);
setHoverPosition(null);
};
const handleKeyDown = (event: React.KeyboardEvent) => {
if (!isHovering) return;
if (event.key === '+' || event.key === '=') {
setZoomFactor((prev) => Math.min(6, prev + 0.5)); // Cap at 6x
event.preventDefault();
} else if (event.key === '-') {
setZoomFactor((prev) => Math.max(1, prev - 0.5)); // Minimum 1x
event.preventDefault();
}
};
const handleWheel = (event: React.WheelEvent) => {
if (!isHovering) return;
if (event.ctrlKey || event.metaKey) {
event.preventDefault();
if (event.deltaY < 0) {
setZoomFactor((prev) => Math.min(6, prev + 0.25));
} else {
setZoomFactor((prev) => Math.max(1, prev - 0.25));
}
}
};
const getTransformOrigin = () => {
if (!hoverPosition) return 'center center';
return `${hoverPosition.x * 100}% ${hoverPosition.y * 100}%`;
};
useEffect(() => {
const renderDiagram = async () => {
mermaid.initialize({
startOnLoad: true,
theme: isDarkTheme ? 'dark' : 'default',
securityLevel: 'loose',
suppressErrorRendering: true,
});
const isCurrentlyLoading =
isLoading !== undefined ? isLoading : status === 'loading';
if (!isCurrentlyLoading && code) {
try {
const element = document.getElementById(diagramId.current);
if (element) {
element.removeAttribute('data-processed');
await mermaid.parse(code); //syntax check
mermaid.contentLoaded();
}
} catch (err) {
console.error('Error rendering mermaid diagram:', err);
setError(
`Failed to render diagram: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
};
renderDiagram();
}, [code, isDarkTheme, isLoading]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
downloadMenuRef.current &&
!downloadMenuRef.current.contains(event.target as Node)
) {
setShowDownloadMenu(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showDownloadMenu]);
const downloadSvg = (): void => {
const element = document.getElementById(diagramId.current);
if (!element) return;
const svgElement = element.querySelector('svg');
if (!svgElement) return;
const svgClone = svgElement.cloneNode(true) as SVGElement;
if (!svgClone.hasAttribute('xmlns')) {
svgClone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
}
if (!svgClone.hasAttribute('width') || !svgClone.hasAttribute('height')) {
const viewBox = svgClone.getAttribute('viewBox')?.split(' ') || [];
if (viewBox.length === 4) {
svgClone.setAttribute('width', viewBox[2]);
svgClone.setAttribute('height', viewBox[3]);
}
}
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(svgClone);
const svgBlob = new Blob(
[`<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n${svgString}`],
{ type: 'image/svg+xml' },
);
const url = URL.createObjectURL(svgBlob);
const link = document.createElement('a');
link.href = url;
link.download = 'diagram.svg';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const downloadPng = (): void => {
const element = document.getElementById(diagramId.current);
if (!element) return;
const svgElement = element.querySelector('svg');
if (!svgElement) return;
const svgClone = svgElement.cloneNode(true) as SVGElement;
if (!svgClone.hasAttribute('xmlns')) {
svgClone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
}
let width = parseInt(svgClone.getAttribute('width') || '0');
let height = parseInt(svgClone.getAttribute('height') || '0');
if (!width || !height) {
const viewBox = svgClone.getAttribute('viewBox')?.split(' ') || [];
if (viewBox.length === 4) {
width = parseInt(viewBox[2]);
height = parseInt(viewBox[3]);
svgClone.setAttribute('width', width.toString());
svgClone.setAttribute('height', height.toString());
} else {
width = 800;
height = 600;
svgClone.setAttribute('width', width.toString());
svgClone.setAttribute('height', height.toString());
}
}
const serializer = new XMLSerializer();
const svgString = serializer.serializeToString(svgClone);
const svgBase64 = btoa(unescape(encodeURIComponent(svgString)));
const dataUrl = `data:image/svg+xml;base64,${svgBase64}`;
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function (): void {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Could not get canvas context');
return;
}
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, width, height);
try {
const pngUrl = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'diagram.png';
link.href = pngUrl;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (e) {
console.error('Failed to create PNG:', e);
// Fallback to SVG download if PNG fails
downloadSvg();
}
};
img.src = dataUrl;
};
const downloadMmd = (): void => {
const blob = new Blob([code], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'diagram.mmd';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const downloadOptions = [
{ label: 'Download as SVG', action: downloadSvg },
{ label: 'Download as PNG', action: downloadPng },
{ label: 'Download as MMD', action: downloadMmd },
];
const isCurrentlyLoading =
isLoading !== undefined ? isLoading : status === 'loading';
const showDiagramOptions = !isCurrentlyLoading && !error;
const errorRender = !isCurrentlyLoading && error;
return (
<div className="w-inherit group relative rounded-lg border border-light-silver bg-white dark:border-raisin-black dark:bg-eerie-black">
<div className="flex items-center justify-between bg-platinum px-2 py-1 dark:bg-eerie-black-2">
<span className="text-xs font-medium text-just-black dark:text-chinese-white">
mermaid
</span>
<div className="flex items-center gap-2">
<CopyButton textToCopy={String(code).replace(/\n$/, '')} />
{showDiagramOptions && (
<div className="relative" ref={downloadMenuRef}>
<button
onClick={() => setShowDownloadMenu(!showDownloadMenu)}
className="flex h-full items-center rounded bg-gray-100 px-2 py-1 text-xs dark:bg-gray-700"
title="Download options"
>
Download <span className="ml-1"></span>
</button>
{showDownloadMenu && (
<div className="absolute right-0 z-10 mt-1 w-40 rounded border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800">
<ul>
{downloadOptions.map((option, index) => (
<li key={index}>
<button
onClick={() => {
option.action();
setShowDownloadMenu(false);
}}
className="w-full px-4 py-2 text-left text-xs hover:bg-gray-100 dark:hover:bg-gray-700"
>
{option.label}
</button>
</li>
))}
</ul>
</div>
)}
</div>
)}
{showDiagramOptions && (
<button
onClick={() => setShowCode(!showCode)}
className={`flex h-full items-center rounded px-2 py-1 text-xs ${
showCode
? 'bg-blue-200 dark:bg-blue-800'
: 'bg-gray-100 dark:bg-gray-700'
}`}
title="View Code"
>
Code
</button>
)}
</div>
</div>
{isCurrentlyLoading ? (
<div className="flex items-center justify-center bg-white p-4 dark:bg-eerie-black">
<div className="text-sm text-gray-500 dark:text-gray-400">
Loading diagram...
</div>
</div>
) : errorRender ? (
<div className="m-2 rounded border-2 border-red-400 dark:border-red-700">
<div className="overflow-auto whitespace-normal break-words bg-red-100 px-4 py-2 text-sm text-red-800 dark:bg-red-900/30 dark:text-red-300">
{error}
</div>
</div>
) : (
<>
<div
ref={containerRef}
className="no-scrollbar relative block w-full bg-white p-4 dark:bg-eerie-black"
style={{
overflow: 'auto',
scrollbarWidth: 'none',
msOverflowStyle: 'none',
width: '100%',
}}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onKeyDown={handleKeyDown}
onWheel={handleWheel}
tabIndex={0}
>
{isHovering && (
<>
<div className="absolute right-2 top-2 z-10 flex items-center gap-2 rounded bg-black/70 px-2 py-1 text-xs text-white">
<button
onClick={() =>
setZoomFactor((prev) => Math.max(1, prev - 0.5))
}
className="rounded px-1 hover:bg-gray-600"
title="Decrease zoom"
>
-
</button>
<span
className="cursor-pointer hover:underline"
onClick={() => {
setZoomFactor(2);
}}
title="Reset zoom"
>
{zoomFactor.toFixed(1)}x
</span>
<button
onClick={() =>
setZoomFactor((prev) => Math.min(6, prev + 0.5))
}
className="rounded px-1 hover:bg-gray-600"
title="Increase zoom"
>
+
</button>
</div>
</>
)}
<pre
className="mermaid w-full select-none"
id={diagramId.current}
key={`mermaid-${diagramId.current}`}
style={{
transform: isHovering ? `scale(${zoomFactor})` : `scale(1)`,
transformOrigin: getTransformOrigin(),
transition: 'transform 0.2s ease',
cursor: 'default',
width: '100%',
display: 'flex',
justifyContent: 'center',
}}
>
{code}
</pre>
</div>
{showCode && (
<div className="border-t border-light-silver dark:border-raisin-black">
<div className="bg-platinum p-2 dark:bg-eerie-black-2">
<span className="text-xs font-medium text-just-black dark:text-chinese-white">
Mermaid Code
</span>
</div>
<SyntaxHighlighter
language="mermaid"
style={isDarkTheme ? vscDarkPlus : oneLight}
customStyle={{
margin: 0,
borderRadius: 0,
scrollbarWidth: 'thin',
maxHeight: '300px',
}}
>
{code}
</SyntaxHighlighter>
</div>
)}
</>
)}
</div>
);
};
export default MermaidRenderer;

View File

@@ -1,27 +1,220 @@
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDarkTheme } from '../hooks';
import Send from '../assets/send.svg';
import SendDark from '../assets/send_dark.svg';
import { useDispatch, useSelector } from 'react-redux';
import endpoints from '../api/endpoints';
import userService from '../api/services/userService';
import AlertIcon from '../assets/alert.svg';
import ClipIcon from '../assets/clip.svg';
import ExitIcon from '../assets/exit.svg';
import PaperPlane from '../assets/paper_plane.svg';
import SourceIcon from '../assets/source.svg';
import SpinnerDark from '../assets/spinner-dark.svg';
import Spinner from '../assets/spinner.svg';
import ToolIcon from '../assets/tool.svg';
import {
addAttachment,
removeAttachment,
selectAttachments,
updateAttachment,
} from '../conversation/conversationSlice';
import { useDarkTheme } from '../hooks';
import { ActiveState } from '../models/misc';
import {
selectSelectedDocs,
selectToken,
} from '../preferences/preferenceSlice';
import Upload from '../upload/Upload';
import { getOS, isTouchDevice } from '../utils/browserUtils';
import SourcesPopup from './SourcesPopup';
import ToolsPopup from './ToolsPopup';
interface MessageInputProps {
type MessageInputProps = {
value: string;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
onSubmit: () => void;
loading: boolean;
}
showSourceButton?: boolean;
showToolButton?: boolean;
autoFocus?: boolean;
};
export default function MessageInput({
value,
onChange,
onSubmit,
loading,
showSourceButton = true,
showToolButton = true,
autoFocus = true,
}: MessageInputProps) {
const { t } = useTranslation();
const [isDarkTheme] = useDarkTheme();
const inputRef = useRef<HTMLTextAreaElement>(null);
const sourceButtonRef = useRef<HTMLButtonElement>(null);
const toolButtonRef = useRef<HTMLButtonElement>(null);
const [isSourcesPopupOpen, setIsSourcesPopupOpen] = useState(false);
const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false);
const [uploadModalState, setUploadModalState] =
useState<ActiveState>('INACTIVE');
const selectedDocs = useSelector(selectSelectedDocs);
const token = useSelector(selectToken);
const attachments = useSelector(selectAttachments);
const dispatch = useDispatch();
const browserOS = getOS();
const isTouch = isTouchDevice();
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
((browserOS === 'win' || browserOS === 'linux') &&
event.ctrlKey &&
event.key === 'k') ||
(browserOS === 'mac' && event.metaKey && event.key === 'k')
) {
event.preventDefault();
setIsSourcesPopupOpen(!isSourcesPopupOpen);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [browserOS]);
const handleFileAttachment = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || e.target.files.length === 0) return;
const file = e.target.files[0];
const formData = new FormData();
formData.append('file', file);
const apiHost = import.meta.env.VITE_API_HOST;
const xhr = new XMLHttpRequest();
const newAttachment = {
fileName: file.name,
progress: 0,
status: 'uploading' as const,
taskId: '',
};
dispatch(addAttachment(newAttachment));
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded / event.total) * 100);
dispatch(
updateAttachment({
taskId: newAttachment.taskId,
updates: { progress },
}),
);
}
});
xhr.onload = () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.task_id) {
dispatch(
updateAttachment({
taskId: newAttachment.taskId,
updates: {
taskId: response.task_id,
status: 'processing',
progress: 10,
},
}),
);
}
} else {
dispatch(
updateAttachment({
taskId: newAttachment.taskId,
updates: { status: 'failed' },
}),
);
}
};
xhr.onerror = () => {
dispatch(
updateAttachment({
taskId: newAttachment.taskId,
updates: { status: 'failed' },
}),
);
};
xhr.open('POST', `${apiHost}${endpoints.USER.STORE_ATTACHMENT}`);
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
xhr.send(formData);
e.target.value = '';
};
useEffect(() => {
const checkTaskStatus = () => {
const processingAttachments = attachments.filter(
(att) => att.status === 'processing' && att.taskId,
);
processingAttachments.forEach((attachment) => {
userService
.getTaskStatus(attachment.taskId!, null)
.then((data) => data.json())
.then((data) => {
if (data.status === 'SUCCESS') {
dispatch(
updateAttachment({
taskId: attachment.taskId!,
updates: {
status: 'completed',
progress: 100,
id: data.result?.attachment_id,
token_count: data.result?.token_count,
},
}),
);
} else if (data.status === 'FAILURE') {
dispatch(
updateAttachment({
taskId: attachment.taskId!,
updates: { status: 'failed' },
}),
);
} else if (data.status === 'PROGRESS' && data.result?.current) {
dispatch(
updateAttachment({
taskId: attachment.taskId!,
updates: { progress: data.result.current },
}),
);
}
})
.catch(() => {
dispatch(
updateAttachment({
taskId: attachment.taskId!,
updates: { status: 'failed' },
}),
);
});
});
};
const interval = setInterval(() => {
if (attachments.some((att) => att.status === 'processing')) {
checkTaskStatus();
}
}, 2000);
return () => clearInterval(interval);
}, [attachments, dispatch]);
const handleInput = () => {
if (inputRef.current) {
@@ -34,16 +227,15 @@ export default function MessageInput({
}
};
// Focus the textarea and set initial height on mount.
useEffect(() => {
inputRef.current?.focus();
if (autoFocus) inputRef.current?.focus();
handleInput();
}, []);
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSubmit();
handleSubmit();
if (inputRef.current) {
inputRef.current.value = '';
handleInput();
@@ -51,43 +243,218 @@ export default function MessageInput({
}
};
const handlePostDocumentSelect = (doc: any) => {
console.log('Selected document:', doc);
};
const handleSubmit = () => {
onSubmit();
};
return (
<div className="flex w-full mx-2 items-center rounded-[40px] border dark:border-grey border-dark-gray bg-lotion dark:bg-charleston-green-3">
<label htmlFor="message-input" className="sr-only">
{t('inputPlaceholder')}
</label>
<textarea
id="message-input"
ref={inputRef}
value={value}
onChange={onChange}
tabIndex={1}
placeholder={t('inputPlaceholder')}
className="inputbox-style w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-lotion dark:bg-charleston-green-3 py-5 text-base leading-tight opacity-100 focus:outline-none dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50 px-6"
onInput={handleInput}
onKeyDown={handleKeyDown}
aria-label={t('inputPlaceholder')}
/>
{loading ? (
<img
src={isDarkTheme ? SpinnerDark : Spinner}
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent"
alt={t('loading')}
/>
) : (
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000 dark:hover:bg-dark-charcoal">
<div className="mx-2 flex w-full flex-col">
<div className="relative flex w-full flex-col rounded-[23px] border border-dark-gray bg-lotion dark:border-grey dark:bg-transparent">
<div className="flex flex-wrap gap-1.5 px-4 pb-0 pt-3 sm:gap-2 sm:px-6">
{attachments.map((attachment, index) => (
<div
key={index}
className={`group relative flex items-center rounded-[32px] border border-[#AAAAAA] bg-white px-2 py-1 text-[12px] text-[#5D5D5D] dark:border-purple-taupe dark:bg-[#1F2028] dark:text-bright-gray sm:px-3 sm:py-1.5 sm:text-[14px] ${
attachment.status !== 'completed' ? 'opacity-70' : 'opacity-100'
}`}
title={attachment.fileName}
>
<span className="max-w-[120px] truncate font-medium sm:max-w-[150px]">
{attachment.fileName}
</span>
{attachment.status === 'completed' && (
<button
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full bg-white p-1 opacity-0 transition-opacity hover:bg-white/95 focus:opacity-100 group-hover:opacity-100 dark:bg-[#1F2028] dark:hover:bg-[#1F2028]/95"
onClick={() => {
if (attachment.id) {
dispatch(removeAttachment(attachment.id));
}
}}
aria-label="Remove attachment"
>
<img
src={ExitIcon}
alt="Remove"
className="h-2.5 w-2.5 filter dark:invert"
/>
</button>
)}
{attachment.status === 'failed' && (
<img
src={AlertIcon}
alt="Upload failed"
className="ml-2 h-3.5 w-3.5"
title="Upload failed"
/>
)}
{(attachment.status === 'uploading' ||
attachment.status === 'processing') && (
<div className="relative ml-2 h-4 w-4">
<svg className="h-4 w-4" viewBox="0 0 24 24">
{/* Background circle */}
<circle
className="text-gray-200 dark:text-gray-700"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<circle
className="text-blue-600 dark:text-blue-400"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
strokeDasharray="62.83"
strokeDashoffset={62.83 * (1 - attachment.progress / 100)}
transform="rotate(-90 12 12)"
/>
</svg>
</div>
)}
</div>
))}
</div>
<div className="w-full">
<label htmlFor="message-input" className="sr-only">
{t('inputPlaceholder')}
</label>
<textarea
id="message-input"
ref={inputRef}
value={value}
onChange={onChange}
tabIndex={1}
placeholder={t('inputPlaceholder')}
className="inputbox-style no-scrollbar w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-t-[23px] bg-lotion px-4 py-3 text-base leading-tight opacity-100 focus:outline-none dark:bg-transparent dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50 sm:px-6 sm:py-5"
onInput={handleInput}
onKeyDown={handleKeyDown}
aria-label={t('inputPlaceholder')}
/>
</div>
<div className="flex items-center px-3 py-1.5 sm:px-4 sm:py-2">
<div className="flex flex-grow flex-wrap gap-1 sm:gap-2">
{showSourceButton && (
<button
ref={sourceButtonRef}
className="xs:px-3 xs:py-1.5 flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C] sm:max-w-[150px]"
onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)}
title={
selectedDocs
? selectedDocs.name
: t('conversation.sources.title')
}
>
<img
src={SourceIcon}
alt="Sources"
className="mr-1 h-3.5 w-3.5 flex-shrink-0 sm:mr-1.5 sm:h-4"
/>
<span className="xs:text-[12px] overflow-hidden truncate text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
{selectedDocs
? selectedDocs.name
: t('conversation.sources.title')}
</span>
{!isTouch && (
<span className="ml-1 hidden text-[10px] text-gray-500 dark:text-gray-400 sm:inline-block">
{browserOS === 'mac' ? '(⌘K)' : '(ctrl+K)'}
</span>
)}
</button>
)}
{showToolButton && (
<button
ref={toolButtonRef}
className="xs:px-3 xs:py-1.5 xs:max-w-[150px] flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C]"
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
>
<img
src={ToolIcon}
alt="Tools"
className="mr-1 h-3.5 w-3.5 flex-shrink-0 sm:mr-1.5 sm:h-4 sm:w-4"
/>
<span className="xs:text-[12px] overflow-hidden truncate text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
{t('settings.tools.label')}
</span>
</button>
)}
<label className="xs:px-3 xs:py-1.5 flex cursor-pointer items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 dark:border-purple-taupe dark:hover:bg-[#2C2E3C]">
<img
src={ClipIcon}
alt="Attach"
className="mr-1 h-3.5 w-3.5 sm:mr-1.5 sm:h-4 sm:w-4"
/>
<span className="xs:text-[12px] text-[10px] font-medium text-[#5D5D5D] dark:text-bright-gray sm:text-[14px]">
Attach
</span>
<input
type="file"
className="hidden"
onChange={handleFileAttachment}
/>
</label>
{/* Additional badges can be added here in the future */}
</div>
<button
onClick={onSubmit}
aria-label={t('send')}
className="flex items-center justify-center"
onClick={loading ? undefined : handleSubmit}
aria-label={loading ? t('loading') : t('send')}
className={`flex items-center justify-center rounded-full p-2 sm:p-2.5 ${loading ? 'bg-gray-300 dark:bg-gray-600' : 'bg-black dark:bg-white'} ml-auto flex-shrink-0`}
disabled={loading}
>
<img
className="ml-[4px] h-6 w-6 text-white filter dark:invert-[0.45] invert-[0.35]"
src={isDarkTheme ? SendDark : Send}
alt={t('send')}
/>
{loading ? (
<img
src={isDarkTheme ? SpinnerDark : Spinner}
className="h-3.5 w-3.5 animate-spin sm:h-4 sm:w-4"
alt={t('loading')}
/>
) : (
<img
className={`h-3.5 w-3.5 sm:h-4 sm:w-4 ${isDarkTheme ? 'invert filter' : ''}`}
src={PaperPlane}
alt={t('send')}
/>
)}
</button>
</div>
</div>
<SourcesPopup
isOpen={isSourcesPopupOpen}
onClose={() => setIsSourcesPopupOpen(false)}
anchorRef={sourceButtonRef}
handlePostDocumentSelect={handlePostDocumentSelect}
setUploadModalState={setUploadModalState}
/>
<ToolsPopup
isOpen={isToolsPopupOpen}
onClose={() => setIsToolsPopupOpen(false)}
anchorRef={toolButtonRef}
/>
{uploadModalState === 'ACTIVE' && (
<Upload
receivedFile={[]}
setModalState={setUploadModalState}
isOnboarding={false}
renderTab={null}
close={() => setUploadModalState('INACTIVE')}
/>
)}
</div>
);

View File

@@ -0,0 +1,278 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import CheckmarkIcon from '../assets/checkmark.svg';
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
import NoFilesIcon from '../assets/no-files.svg';
import { useDarkTheme } from '../hooks';
import Input from './Input';
export type OptionType = {
id: string | number;
label: string;
icon?: string | React.ReactNode;
[key: string]: any;
};
type MultiSelectPopupProps = {
isOpen: boolean;
onClose: () => void;
anchorRef: React.RefObject<HTMLElement>;
options: OptionType[];
selectedIds: Set<string | number>;
onSelectionChange: (newSelectedIds: Set<string | number>) => void;
title?: string;
searchPlaceholder?: string;
noOptionsMessage?: string;
loading?: boolean;
footerContent?: React.ReactNode;
showSearch?: boolean;
singleSelect?: boolean;
};
export default function MultiSelectPopup({
isOpen,
onClose,
anchorRef,
options,
selectedIds,
onSelectionChange,
title,
searchPlaceholder,
noOptionsMessage,
loading = false,
footerContent,
showSearch = true,
singleSelect = false,
}: MultiSelectPopupProps) {
const { t } = useTranslation();
const [isDarkTheme] = useDarkTheme();
const popupRef = useRef<HTMLDivElement>(null);
const [searchTerm, setSearchTerm] = useState('');
const [popupPosition, setPopupPosition] = useState({
top: 0,
left: 0,
maxHeight: 0,
showAbove: false,
});
const filteredOptions = options.filter((option) =>
option.label.toLowerCase().includes(searchTerm.toLowerCase()),
);
const handleOptionClick = (optionId: string | number) => {
let newSelectedIds: Set<string | number>;
if (singleSelect) newSelectedIds = new Set<string | number>();
else newSelectedIds = new Set(selectedIds);
if (newSelectedIds.has(optionId)) {
newSelectedIds.delete(optionId);
} else newSelectedIds.add(optionId);
onSelectionChange(newSelectedIds);
};
const renderIcon = (icon: string | React.ReactNode) => {
if (typeof icon === 'string') {
if (icon.startsWith('/') || icon.startsWith('http')) {
return (
<img
src={icon}
alt=""
className="mr-3 h-5 w-5 flex-shrink-0"
aria-hidden="true"
/>
);
}
return (
<span className="mr-3 h-5 w-5 flex-shrink-0" aria-hidden="true">
{icon}
</span>
);
}
return <span className="mr-3 flex-shrink-0">{icon}</span>;
};
useLayoutEffect(() => {
if (!isOpen || !anchorRef.current) return;
const updatePosition = () => {
if (!anchorRef.current) return;
const rect = anchorRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
const popupPadding = 16;
const popupMinWidth = 300;
const popupMaxWidth = 462;
const popupDefaultHeight = 300;
const spaceAbove = rect.top;
const spaceBelow = viewportHeight - rect.bottom;
const showAbove =
spaceBelow < popupDefaultHeight && spaceAbove >= popupDefaultHeight;
const maxHeight = Math.max(
150,
showAbove ? spaceAbove - popupPadding : spaceBelow - popupPadding,
);
const availableWidth = viewportWidth - 20;
const calculatedWidth = Math.min(popupMaxWidth, availableWidth);
let left = rect.left;
if (left + calculatedWidth > viewportWidth - 10) {
left = viewportWidth - calculatedWidth - 10;
}
left = Math.max(10, left);
setPopupPosition({
top: showAbove ? rect.top - 8 : rect.bottom + 8,
left: left,
maxHeight: Math.min(600, maxHeight),
showAbove,
});
};
updatePosition();
window.addEventListener('resize', updatePosition);
window.addEventListener('scroll', updatePosition, true);
return () => {
window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true);
};
}, [isOpen, anchorRef]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
popupRef.current &&
!popupRef.current.contains(event.target as Node) &&
anchorRef.current &&
!anchorRef.current.contains(event.target as Node)
)
onClose();
};
if (isOpen) document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [onClose, anchorRef, isOpen]);
useEffect(() => {
if (!isOpen) setSearchTerm('');
}, [isOpen]);
if (!isOpen) return null;
return (
<div
ref={popupRef}
className="fixed z-[9999] flex flex-col rounded-lg border border-light-silver bg-lotion shadow-[0px_9px_46px_8px_#0000001F,0px_24px_38px_3px_#00000024,0px_11px_15px_-7px_#00000033] dark:border-dim-gray dark:bg-charleston-green-2"
style={{
top: popupPosition.showAbove ? undefined : popupPosition.top,
bottom: popupPosition.showAbove
? window.innerHeight - popupPosition.top + 8
: undefined,
left: popupPosition.left,
maxWidth: `${Math.min(462, window.innerWidth - 20)}px`,
width: '100%',
maxHeight: `${popupPosition.maxHeight}px`,
}}
>
{(title || showSearch) && (
<div className="flex-shrink-0 p-4">
{title && (
<h3 className="mb-4 text-lg font-medium text-gray-900 dark:text-white">
{title}
</h3>
)}
{showSearch && (
<Input
id="multi-select-search"
name="multi-select-search"
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder={
searchPlaceholder ||
t('settings.tools.searchPlaceholder', 'Search...')
}
labelBgClassName="bg-lotion dark:bg-charleston-green-2"
borderVariant="thin"
className="mb-4"
textSize="small"
/>
)}
</div>
)}
<div className="mx-4 mb-4 flex-grow overflow-auto rounded-md border border-[#D9D9D9] dark:border-dim-gray">
{loading ? (
<div className="flex h-full items-center justify-center py-4">
<div className="h-6 w-6 animate-spin rounded-full border-b-2 border-gray-900 dark:border-white"></div>
</div>
) : (
<div className="h-full overflow-y-auto [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-400 dark:[&::-webkit-scrollbar-thumb]:bg-gray-600 [&::-webkit-scrollbar-track]:bg-gray-200 dark:[&::-webkit-scrollbar-track]:bg-[#2C2E3C] [&::-webkit-scrollbar]:w-2">
{filteredOptions.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center px-4 py-8 text-center">
<img
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
alt="No options found"
className="mx-auto mb-3 h-16 w-16"
/>
<p className="text-sm text-gray-500 dark:text-gray-400">
{searchTerm
? 'No results found'
: noOptionsMessage || 'No options available'}
</p>
</div>
) : (
filteredOptions.map((option) => {
const isSelected = selectedIds.has(option.id);
return (
<div
key={option.id}
onClick={() => handleOptionClick(option.id)}
className="flex cursor-pointer items-center justify-between border-b border-[#D9D9D9] p-3 last:border-b-0 hover:bg-gray-100 dark:border-dim-gray dark:hover:bg-charleston-green-3"
role="option"
aria-selected={isSelected}
>
<div className="mr-3 flex flex-grow items-center overflow-hidden">
{option.icon && renderIcon(option.icon)}
<p
className="overflow-hidden overflow-ellipsis whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white"
title={option.label}
>
{option.label}
</p>
</div>
<div className="flex-shrink-0">
<div
className={`flex h-4 w-4 items-center justify-center rounded-sm border border-[#C6C6C6] bg-white dark:border-[#757783] dark:bg-charleston-green-2`}
aria-hidden="true"
>
{isSelected && (
<img
src={CheckmarkIcon}
alt="checkmark"
width={10}
height={10}
/>
)}
</div>
</div>
</div>
);
})
)}
</div>
)}
</div>
{footerContent && (
<div className="flex-shrink-0 border-t border-light-silver p-4 dark:border-dim-gray">
{footerContent}
</div>
)}
</div>
);
}

View File

@@ -1,7 +1,8 @@
import React, { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ArrowLeft from '../assets/arrow-left.svg';
import ArrowRight from '../assets/arrow-right.svg';
import { useTranslation } from 'react-i18next';
type HiddenGradientType = 'left' | 'right' | undefined;
@@ -10,7 +11,6 @@ const useTabs = () => {
const tabs = [
t('settings.general.label'),
t('settings.documents.label'),
t('settings.apiKeys.label'),
t('settings.analytics.label'),
t('settings.logs.label'),
t('settings.tools.label'),
@@ -48,18 +48,18 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
[containerRef.current],
);
return (
<div className="relative mt-6 flex flex-row items-center space-x-1 md:space-x-0 overflow-auto">
<div className="relative mt-6 flex flex-row items-center space-x-1 overflow-auto md:space-x-0">
<div
className={`${hiddenGradient === 'left' ? 'hidden' : ''} md:hidden absolute inset-y-0 left-6 w-14 bg-gradient-to-r from-white dark:from-raisin-black pointer-events-none`}
className={`${hiddenGradient === 'left' ? 'hidden' : ''} pointer-events-none absolute inset-y-0 left-6 w-14 bg-gradient-to-r from-white dark:from-raisin-black md:hidden`}
></div>
<div
className={`${hiddenGradient === 'right' ? 'hidden' : ''} md:hidden absolute inset-y-0 right-6 w-14 bg-gradient-to-l from-white dark:from-raisin-black pointer-events-none`}
className={`${hiddenGradient === 'right' ? 'hidden' : ''} pointer-events-none absolute inset-y-0 right-6 w-14 bg-gradient-to-l from-white dark:from-raisin-black md:hidden`}
></div>
<div className="md:hidden z-10">
<div className="z-10 md:hidden">
<button
onClick={() => scrollTabs(-1)}
className="flex h-6 w-6 items-center rounded-full justify-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
className="flex h-6 w-6 items-center justify-center rounded-full transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
aria-label="Scroll tabs left"
>
<img src={ArrowLeft} alt="left-arrow" className="h-3" />
@@ -67,7 +67,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
</div>
<div
ref={containerRef}
className="flex flex-nowrap overflow-x-auto no-scrollbar md:space-x-4 scroll-smooth snap-x"
className="no-scrollbar flex snap-x flex-nowrap overflow-x-auto scroll-smooth md:space-x-4"
role="tablist"
aria-label="Settings tabs"
>
@@ -75,7 +75,7 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
<button
key={index}
onClick={() => setActiveTab(tab)}
className={`snap-start h-9 rounded-3xl px-4 font-bold transition-colors ${
className={`h-9 snap-start rounded-3xl px-4 font-bold transition-colors ${
activeTab === tab
? 'bg-[#F4F4F5] text-neutral-900 dark:bg-dark-charcoal dark:text-white'
: 'text-neutral-700 hover:text-neutral-900 dark:text-neutral-300 dark:hover:text-white'
@@ -89,10 +89,10 @@ const SettingsBar = ({ setActiveTab, activeTab }: SettingsBarProps) => {
</button>
))}
</div>
<div className="md:hidden z-10">
<div className="z-10 md:hidden">
<button
onClick={() => scrollTabs(1)}
className="flex h-6 w-6 rounded-full items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700"
className="flex h-6 w-6 items-center justify-center rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"
aria-label="Scroll tabs right"
>
<img src={ArrowRight} alt="right-arrow" className="h-3" />

Some files were not shown because too many files have changed in this diff Show More