Compare commits
355 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4025e55b95 | ||
|
|
e1e63ebd64 | ||
|
|
8279df48bf | ||
|
|
d86a06fab0 | ||
|
|
90b24dd915 | ||
|
|
bacd2a6893 | ||
|
|
0f059f247d | ||
|
|
e2b76d9c29 | ||
|
|
1107a2f2bc | ||
|
|
efd43013da | ||
|
|
7b8458b47d | ||
|
|
84eed09a17 | ||
|
|
35b1a40d49 | ||
|
|
81d7fe3fdb | ||
|
|
02187fed4e | ||
|
|
019bf013ac | ||
|
|
d6e59a6a0a | ||
|
|
46aa862943 | ||
|
|
0413cab0d9 | ||
|
|
3357ce8f33 | ||
|
|
1776f6e7fd | ||
|
|
edfe5e1156 | ||
|
|
0768992848 | ||
|
|
1224f94879 | ||
|
|
b58c5344b8 | ||
|
|
7175bc0595 | ||
|
|
b7a6f5696d | ||
|
|
abf5b89c28 | ||
|
|
d554444b0e | ||
|
|
16ae0725e6 | ||
|
|
61feced541 | ||
|
|
a1d4db2f1e | ||
|
|
357e9af627 | ||
|
|
a41519be63 | ||
|
|
870e6b07c8 | ||
|
|
6f41759519 | ||
|
|
6727c42f18 | ||
|
|
90c367842f | ||
|
|
a0bb6e370e | ||
|
|
f2910ab9d1 | ||
|
|
b4bfed2ccb | ||
|
|
2fcde61b6d | ||
|
|
ffddf10de5 | ||
|
|
6e3bd5e6f3 | ||
|
|
b21230c4d6 | ||
|
|
0a533b64e1 | ||
|
|
15b0e321bd | ||
|
|
4d749340a2 | ||
|
|
0ef6ffa452 | ||
|
|
d7b1310ba3 | ||
|
|
7408454a75 | ||
|
|
07b71468cc | ||
|
|
522e966194 | ||
|
|
937c60c9cf | ||
|
|
bbb1e22163 | ||
|
|
a16e83200a | ||
|
|
d437521710 | ||
|
|
5cbf4cf352 | ||
|
|
2985e3b75b | ||
|
|
f34a75fc5b | ||
|
|
5aa88714b8 | ||
|
|
ce56a414e0 | ||
|
|
ba4a7dcd45 | ||
|
|
85c648da6c | ||
|
|
483f8eb690 | ||
|
|
93c868d698 | ||
|
|
a14e70e3f4 | ||
|
|
a6ff606cae | ||
|
|
651eb3374c | ||
|
|
68c71adc5a | ||
|
|
0c4ca9c94d | ||
|
|
8c04f5b3f1 | ||
|
|
35b29a0a1e | ||
|
|
d289f432b1 | ||
|
|
e16e269775 | ||
|
|
4e5d0c2e84 | ||
|
|
c9a2034936 | ||
|
|
b70fc1151d | ||
|
|
c11034edcd | ||
|
|
804d9b42a5 | ||
|
|
b1bb4e6758 | ||
|
|
76ed8f0ba2 | ||
|
|
4dde7eaea1 | ||
|
|
2e2149c110 | ||
|
|
70bb9477c5 | ||
|
|
ec5363e9c1 | ||
|
|
dba3b1c559 | ||
|
|
9606e3f80c | ||
|
|
7bc7b500f5 | ||
|
|
c6e804fa10 | ||
|
|
1cbaf9bd9d | ||
|
|
45145685d5 | ||
|
|
2fbec6f21f | ||
|
|
ad29d2765f | ||
|
|
e47e751142 | ||
|
|
c63d4ccf3e | ||
|
|
e5c30cf841 | ||
|
|
c80678aac5 | ||
|
|
1754570057 | ||
|
|
d87b411193 | ||
|
|
8fc6284317 | ||
|
|
eae49d2367 | ||
|
|
69287c5198 | ||
|
|
e6b3984f78 | ||
|
|
547fe888d4 | ||
|
|
3454309cbc | ||
|
|
544c46cd44 | ||
|
|
2c100825cc | ||
|
|
558ecd84a6 | ||
|
|
df24cfff4f | ||
|
|
bd5d93a964 | ||
|
|
ae2ded119f | ||
|
|
abdb80a6be | ||
|
|
2f9cbe2bf1 | ||
|
|
2cca7d60d5 | ||
|
|
3df745d1d2 | ||
|
|
9862083e0b | ||
|
|
7a4976c470 | ||
|
|
8834a19743 | ||
|
|
6e15403f60 | ||
|
|
7e1cf10cb2 | ||
|
|
ee762c3c68 | ||
|
|
32c06414c5 | ||
|
|
e97e1ba4bc | ||
|
|
2f580f7800 | ||
|
|
1ce1459455 | ||
|
|
c26573482e | ||
|
|
414ec08dee | ||
|
|
1cc78191eb | ||
|
|
75c6c6081a | ||
|
|
8d2ebe9718 | ||
|
|
eed974b883 | ||
|
|
ae846dac4d | ||
|
|
0b09c00b50 | ||
|
|
f7a1874cb3 | ||
|
|
28fb04eb7b | ||
|
|
34310cf420 | ||
|
|
e1d61d7190 | ||
|
|
9c14ac84cb | ||
|
|
1d1ea7b6f2 | ||
|
|
92401f5b7c | ||
|
|
38ac9218ec | ||
|
|
48497c749a | ||
|
|
72a1892058 | ||
|
|
f2c328d212 | ||
|
|
e9eafc40a7 | ||
|
|
933ca1bf81 | ||
|
|
b4fc9aa7eb | ||
|
|
dcc475bbef | ||
|
|
1fe35ad0cd | ||
|
|
f1ed1e0f14 | ||
|
|
fcc746fb98 | ||
|
|
95934a5b7a | ||
|
|
d38b101820 | ||
|
|
91d730a7bc | ||
|
|
0cfa77b628 | ||
|
|
ca4881ad51 | ||
|
|
8c2c064fe2 | ||
|
|
10646b9b86 | ||
|
|
967b195946 | ||
|
|
1ae7771290 | ||
|
|
a585fe4d54 | ||
|
|
fa3a9fe70e | ||
|
|
99952a393f | ||
|
|
920a41e3ca | ||
|
|
e5bec957a1 | ||
|
|
41cb765255 | ||
|
|
2d12a3cd7a | ||
|
|
df4fe0176c | ||
|
|
4fcc80719e | ||
|
|
f6c66f6ee4 | ||
|
|
220d137e66 | ||
|
|
425803a1b6 | ||
|
|
c794ea614a | ||
|
|
9000838aab | ||
|
|
2790bda1e9 | ||
|
|
e13d4daa9a | ||
|
|
2f504a4e03 | ||
|
|
598a50a133 | ||
|
|
1b06a5a3e0 | ||
|
|
9f1d3b0269 | ||
|
|
a09543d38b | ||
|
|
2ab3539925 | ||
|
|
23ddf53abe | ||
|
|
d8720d0849 | ||
|
|
6753b55160 | ||
|
|
7f7f48ad56 | ||
|
|
149ca01029 | ||
|
|
5c8133a810 | ||
|
|
2adccdd1b0 | ||
|
|
b91068d658 | ||
|
|
4534cafd3f | ||
|
|
405e79d729 | ||
|
|
4df2349e9d | ||
|
|
a9b61d3e13 | ||
|
|
3767d14e5c | ||
|
|
889a050f25 | ||
|
|
0701fac807 | ||
|
|
9fba91069a | ||
|
|
4f9ce70ff8 | ||
|
|
5e00d4ded7 | ||
|
|
95cd9ee5bb | ||
|
|
40f16f8ef1 | ||
|
|
3d9288f82f | ||
|
|
c51f12f88b | ||
|
|
0618153390 | ||
|
|
a7c066291b | ||
|
|
a69ac372fa | ||
|
|
16b2a54981 | ||
|
|
3f68e0d66f | ||
|
|
12d483fde6 | ||
|
|
96034a9712 | ||
|
|
d2def4479b | ||
|
|
afbbb913e7 | ||
|
|
ad76f239a3 | ||
|
|
e6b096c9e0 | ||
|
|
6e26b4e6c7 | ||
|
|
ea79494b6d | ||
|
|
afb18a3e4d | ||
|
|
f9c9853102 | ||
|
|
b3eb9fb6fa | ||
|
|
d3b97bf51a | ||
|
|
7a2e491199 | ||
|
|
25efaf08b7 | ||
|
|
f893ea6b98 | ||
|
|
500745b62c | ||
|
|
9ebe5bf1a7 | ||
|
|
4aecb86daa | ||
|
|
6924dd6df6 | ||
|
|
431755144e | ||
|
|
d182f81754 | ||
|
|
de0193fffc | ||
|
|
53e86205ad | ||
|
|
aa670efe3a | ||
|
|
e693fe49a7 | ||
|
|
7eaa32d85f | ||
|
|
ab40d2c37a | ||
|
|
784206b39b | ||
|
|
7c8264e221 | ||
|
|
db7195aa30 | ||
|
|
eb7bbc1612 | ||
|
|
ee3792181d | ||
|
|
9804965a20 | ||
|
|
b84842df3d | ||
|
|
fc170d3033 | ||
|
|
8fa4ec7ad8 | ||
|
|
480825ddd7 | ||
|
|
260e328cc1 | ||
|
|
8873428b4b | ||
|
|
ab43c20b8f | ||
|
|
88d9d4f4a3 | ||
|
|
d4840f85c0 | ||
|
|
6f9ddeaed0 | ||
|
|
af5e73c8cb | ||
|
|
333b6e60e1 | ||
|
|
1b61337b75 | ||
|
|
77991896b4 | ||
|
|
60a670ce29 | ||
|
|
c1c69ed22b | ||
|
|
d71c74c6fb | ||
|
|
590aa8b43f | ||
|
|
607e0166f6 | ||
|
|
130c83ee92 | ||
|
|
fd5e418abf | ||
|
|
262d160314 | ||
|
|
9146827590 | ||
|
|
062b108259 | ||
|
|
ba796b6be1 | ||
|
|
3d763235e1 | ||
|
|
c30c6d9f10 | ||
|
|
311716ed18 | ||
|
|
19bb1b4aa4 | ||
|
|
b8749e36b9 | ||
|
|
00b6639155 | ||
|
|
71d7daaef3 | ||
|
|
8654c5d471 | ||
|
|
02124b3d38 | ||
|
|
340dcfb70d | ||
|
|
a37b92223a | ||
|
|
7d2b8cb4fc | ||
|
|
8d7a134cb4 | ||
|
|
4b849d7201 | ||
|
|
e03e185d30 | ||
|
|
7a02df5588 | ||
|
|
19494685ba | ||
|
|
1e26943c3e | ||
|
|
83fa850142 | ||
|
|
968a116d14 | ||
|
|
fb55b494d7 | ||
|
|
59b6a83d7d | ||
|
|
aabc4f0d7b | ||
|
|
391f686173 | ||
|
|
8e6f6d46ec | ||
|
|
2ba7a55439 | ||
|
|
e07df29ab9 | ||
|
|
abf24fe60f | ||
|
|
fad5f5b81f | ||
|
|
6961f49a0c | ||
|
|
6911f8652a | ||
|
|
6658cec6a0 | ||
|
|
14011b9d84 | ||
|
|
bd2d0b6790 | ||
|
|
d36f58230a | ||
|
|
018f950ca3 | ||
|
|
db8db9fae9 | ||
|
|
79ce8d6563 | ||
|
|
13eaa9a35a | ||
|
|
39f0d76b4b | ||
|
|
0a5832ec75 | ||
|
|
6e147b3ed2 | ||
|
|
c162f79daa | ||
|
|
87585be687 | ||
|
|
ea08d6413c | ||
|
|
879905edf6 | ||
|
|
6fd80a5582 | ||
|
|
0dc7333563 | ||
|
|
f61c3168d2 | ||
|
|
9cadd74a96 | ||
|
|
729fa2352b | ||
|
|
b673aaf9f0 | ||
|
|
3132cc6005 | ||
|
|
ac994d3077 | ||
|
|
02d4f7f2da | ||
|
|
d99569f005 | ||
|
|
ec5166249a | ||
|
|
dadd12adb3 | ||
|
|
88b4fb8c2a | ||
|
|
afecae3786 | ||
|
|
d18598bc33 | ||
|
|
794fc05ada | ||
|
|
5daeb7f876 | ||
|
|
53e71c545e | ||
|
|
959a55e36c | ||
|
|
64572b0024 | ||
|
|
9a0c1caa43 | ||
|
|
eed6723147 | ||
|
|
97fabf51b8 | ||
|
|
5e5e2b8aee | ||
|
|
e01071426f | ||
|
|
eed1bfbe50 | ||
|
|
0c3970a266 | ||
|
|
267cfb621e | ||
|
|
0e90febab2 | ||
|
|
31d947837f | ||
|
|
017b11fbba | ||
|
|
3c492062a9 | ||
|
|
b26b49d0ca | ||
|
|
ed08123550 | ||
|
|
add2db5b7a | ||
|
|
f272d7121a | ||
|
|
577556678c | ||
|
|
e146922367 | ||
|
|
6f1548b7f8 | ||
|
|
9e6fe47b44 | ||
|
|
60cfea1126 |
2
.github/workflows/ci.yml
vendored
@@ -13,7 +13,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -36,7 +35,6 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Build and push Docker images to docker.io and ghcr.io
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
|
||||
4
.github/workflows/cife.yml
vendored
@@ -8,11 +8,11 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository == 'arc53/DocsGPT'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
file: './frontend/Dockerfile'
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64, linux/arm64
|
||||
context: ./frontend
|
||||
push: true
|
||||
tags: |
|
||||
|
||||
1
.gitignore
vendored
@@ -75,6 +75,7 @@ target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
**/*.ipynb
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
|
||||
16
README.md
@@ -7,9 +7,9 @@
|
||||
</p>
|
||||
|
||||
<p align="left">
|
||||
<strong><a href="https://docsgpt.arc53.com/">DocsGPT</a></strong> is a cutting-edge open-source solution that streamlines the process of finding information in the project documentation. With its integration of the powerful <strong>GPT</strong> models, developers can easily ask questions about a project and receive accurate answers.
|
||||
<strong><a href="https://www.docsgpt.cloud/">DocsGPT</a></strong> is a cutting-edge open-source solution that streamlines the process of finding information in the project documentation. With its integration of the powerful <strong>GPT</strong> models, developers can easily ask questions about a project and receive accurate answers.
|
||||
|
||||
Say goodbye to time-consuming manual searches, and let <strong><a href="https://docsgpt.arc53.com/">DocsGPT</a></strong> help you quickly find the information you need. Try it out and see how it revolutionizes your project documentation experience. Contribute to its development and be a part of the future of AI-powered assistance.
|
||||
Say goodbye to time-consuming manual searches, and let <strong><a href="https://www.docsgpt.cloud/">DocsGPT</a></strong> help you quickly find the information you need. Try it out and see how it revolutionizes your project documentation experience. Contribute to its development and be a part of the future of AI-powered assistance.
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
@@ -27,7 +27,7 @@ Say goodbye to time-consuming manual searches, and let <strong><a href="https://
|
||||
|
||||
We're eager to provide personalized assistance when deploying your DocsGPT to a live environment.
|
||||
|
||||
- [Book Demo :wave:](https://airtable.com/appdeaL0F1qV8Bl2C/shrrJF1Ll7btCJRbP)
|
||||
- [Get Enterprise / teams Demo :wave:](https://www.docsgpt.cloud/contact)
|
||||
- [Send Email :email:](mailto:contact@arc53.com?subject=DocsGPT%20support%2Fsolutions)
|
||||
|
||||

|
||||
@@ -52,17 +52,17 @@ If you don't have enough resources to run it, you can use bitsnbytes to quantize
|
||||
|
||||
## Useful Links
|
||||
|
||||
- :mag: :fire: [Live preview](https://docsgpt.arc53.com/)
|
||||
- :mag: :fire: [Cloud Version](https://app.docsgpt.cloud/)
|
||||
|
||||
- :speech_balloon: :tada: [Join our Discord](https://discord.gg/n5BX8dh8rU)
|
||||
|
||||
- :books: :sunglasses: [Guides](https://docs.docsgpt.co.uk/)
|
||||
- :books: :sunglasses: [Guides](https://docs.docsgpt.cloud/)
|
||||
|
||||
- :couple: [Interested in contributing?](https://github.com/arc53/DocsGPT/blob/main/CONTRIBUTING.md)
|
||||
|
||||
- :file_folder: :rocket: [How to use any other documentation](https://docs.docsgpt.co.uk/Guides/How-to-train-on-other-documentation)
|
||||
- :file_folder: :rocket: [How to use any other documentation](https://docs.docsgpt.cloud/Guides/How-to-train-on-other-documentation)
|
||||
|
||||
- :house: :closed_lock_with_key: [How to host it locally (so all data will stay on-premises)](https://docs.docsgpt.co.uk/Guides/How-to-use-different-LLM)
|
||||
- :house: :closed_lock_with_key: [How to host it locally (so all data will stay on-premises)](https://docs.docsgpt.cloud/Guides/How-to-use-different-LLM)
|
||||
|
||||
## Project Structure
|
||||
|
||||
@@ -85,7 +85,7 @@ On Mac OS or Linux, write:
|
||||
|
||||
It will install all the dependencies and allow you to download the local model, use OpenAI or use our LLM API.
|
||||
|
||||
Otherwise, refer to this Guide:
|
||||
Otherwise, refer to this Guide for Windows:
|
||||
|
||||
1. Download and open this repository with `git clone https://github.com/arc53/DocsGPT.git`
|
||||
2. Create a `.env` file in your root directory and set the env variables and `VITE_API_STREAMING` to true or false, depending on whether you want streaming answers or not.
|
||||
|
||||
14
SECURITY.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Supported Versions:
|
||||
|
||||
Currently, we support security patches by committing changes and bumping the version published on Github.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Found a vulnerability? Please email us:
|
||||
|
||||
security@arc53.com
|
||||
|
||||
@@ -1,31 +1,93 @@
|
||||
FROM python:3.11-slim-bullseye as builder
|
||||
# Builder Stage
|
||||
FROM ubuntu:24.04 as builder
|
||||
|
||||
# Tiktoken requires Rust toolchain, so build it in a separate stage
|
||||
RUN apt-get update && apt-get install -y gcc curl
|
||||
RUN apt-get install -y wget unzip
|
||||
RUN wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip
|
||||
RUN unzip mpnet-base-v2.zip -d model
|
||||
RUN rm mpnet-base-v2.zip
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y && apt-get install --reinstall libc6-dev -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
RUN pip install --upgrade pip && pip install tiktoken==0.5.2
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y software-properties-common
|
||||
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa
|
||||
|
||||
# Install necessary packages and Python
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends gcc curl wget unzip libc6-dev python3.11 python3.11-distutils python3.11-venv && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Verify Python installation and setup symlink
|
||||
RUN if [ -f /usr/bin/python3.11 ]; then \
|
||||
ln -s /usr/bin/python3.11 /usr/bin/python; \
|
||||
else \
|
||||
echo "Python 3.11 not found"; exit 1; \
|
||||
fi
|
||||
|
||||
# Download and unzip the model
|
||||
RUN wget https://d3dg1063dc54p9.cloudfront.net/models/embeddings/mpnet-base-v2.zip && \
|
||||
unzip mpnet-base-v2.zip -d model && \
|
||||
rm mpnet-base-v2.zip
|
||||
|
||||
# Install Rust
|
||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
|
||||
# Clean up to reduce container size
|
||||
RUN apt-get remove --purge -y wget unzip && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements.txt
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# Setup Python virtual environment
|
||||
RUN python3.11 -m venv /venv
|
||||
|
||||
# Activate virtual environment and install Python packages
|
||||
ENV PATH="/venv/bin:$PATH"
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
# Install Python packages
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir tiktoken && \
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy pre-built packages and binaries from builder stage
|
||||
COPY --from=builder /usr/local/ /usr/local/
|
||||
# Final Stage
|
||||
FROM ubuntu:24.04 as final
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y software-properties-common
|
||||
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa
|
||||
|
||||
# Install Python
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends python3.11 && \
|
||||
ln -s /usr/bin/python3.11 /usr/bin/python && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Create a non-root user: `appuser` (Feel free to choose a name)
|
||||
RUN groupadd -r appuser && \
|
||||
useradd -r -g appuser -d /app -s /sbin/nologin -c "Docker image user" appuser
|
||||
|
||||
# Copy the virtual environment and model from the builder stage
|
||||
COPY --from=builder /venv /venv
|
||||
COPY --from=builder /model /app/model
|
||||
|
||||
# Copy your application code
|
||||
COPY . /app/application
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_DEBUG=true
|
||||
|
||||
# Change the ownership of the /app directory to the appuser
|
||||
|
||||
RUN mkdir -p /app/application/inputs/local
|
||||
RUN chown -R appuser:appuser /app
|
||||
|
||||
# Set environment variables
|
||||
ENV FLASK_APP=app.py \
|
||||
FLASK_DEBUG=true \
|
||||
PATH="/venv/bin:$PATH"
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 7091
|
||||
|
||||
CMD ["gunicorn", "-w", "2", "--timeout", "120", "--bind", "0.0.0.0:7091", "application.wsgi:app"]
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
|
||||
# Start Gunicorn
|
||||
CMD ["gunicorn", "-w", "2", "--timeout", "120", "--bind", "0.0.0.0:7091", "application.wsgi:app"]
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from flask import Blueprint, request, Response
|
||||
import json
|
||||
import datetime
|
||||
@@ -8,17 +9,12 @@ import traceback
|
||||
|
||||
from pymongo import MongoClient
|
||||
from bson.objectid import ObjectId
|
||||
from transformers import GPT2TokenizerFast
|
||||
|
||||
|
||||
|
||||
from application.core.settings import settings
|
||||
from application.vectorstore.vector_creator import VectorCreator
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.retriever.retriever_creator import RetrieverCreator
|
||||
from application.error import bad_request
|
||||
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
mongo = MongoClient(settings.MONGO_URI)
|
||||
@@ -26,17 +22,23 @@ db = mongo["docsgpt"]
|
||||
conversations_collection = db["conversations"]
|
||||
vectors_collection = db["vectors"]
|
||||
prompts_collection = db["prompts"]
|
||||
answer = Blueprint('answer', __name__)
|
||||
api_key_collection = db["api_keys"]
|
||||
answer = Blueprint("answer", __name__)
|
||||
|
||||
if settings.LLM_NAME == "gpt4":
|
||||
gpt_model = 'gpt-4'
|
||||
gpt_model = ""
|
||||
# to have some kind of default behaviour
|
||||
if settings.LLM_NAME == "openai":
|
||||
gpt_model = "gpt-3.5-turbo"
|
||||
elif settings.LLM_NAME == "anthropic":
|
||||
gpt_model = 'claude-2'
|
||||
else:
|
||||
gpt_model = 'gpt-3.5-turbo'
|
||||
gpt_model = "claude-2"
|
||||
|
||||
if settings.MODEL_NAME: # in case there is particular model name configured
|
||||
gpt_model = settings.MODEL_NAME
|
||||
|
||||
# load the prompts
|
||||
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
with open(os.path.join(current_dir, "prompts", "chat_combine_default.txt"), "r") as f:
|
||||
chat_combine_template = f.read()
|
||||
|
||||
@@ -47,7 +49,7 @@ with open(os.path.join(current_dir, "prompts", "chat_combine_creative.txt"), "r"
|
||||
chat_combine_creative = f.read()
|
||||
|
||||
with open(os.path.join(current_dir, "prompts", "chat_combine_strict.txt"), "r") as f:
|
||||
chat_combine_strict = f.read()
|
||||
chat_combine_strict = f.read()
|
||||
|
||||
api_key_set = settings.API_KEY is not None
|
||||
embeddings_key_set = settings.EMBEDDINGS_KEY is not None
|
||||
@@ -58,11 +60,6 @@ async def async_generate(chain, question, chat_history):
|
||||
return result
|
||||
|
||||
|
||||
def count_tokens(string):
|
||||
tokenizer = GPT2TokenizerFast.from_pretrained('gpt2')
|
||||
return len(tokenizer(string)['input_ids'])
|
||||
|
||||
|
||||
def run_async_chain(chain, question, chat_history):
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
@@ -75,10 +72,19 @@ 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)
|
||||
return data
|
||||
|
||||
|
||||
def get_vectorstore(data):
|
||||
if "active_docs" in data:
|
||||
if data["active_docs"].split("/")[0] == "default":
|
||||
vectorstore = ""
|
||||
vectorstore = ""
|
||||
elif data["active_docs"].split("/")[0] == "local":
|
||||
vectorstore = "indexes/" + data["active_docs"]
|
||||
else:
|
||||
@@ -92,248 +98,291 @@ def get_vectorstore(data):
|
||||
|
||||
|
||||
def is_azure_configured():
|
||||
return settings.OPENAI_API_BASE and settings.OPENAI_API_VERSION and settings.AZURE_DEPLOYMENT_NAME
|
||||
return (
|
||||
settings.OPENAI_API_BASE
|
||||
and settings.OPENAI_API_VERSION
|
||||
and settings.AZURE_DEPLOYMENT_NAME
|
||||
)
|
||||
|
||||
|
||||
def complete_stream(question, docsearch, chat_history, api_key, prompt_id, conversation_id):
|
||||
llm = LLMCreator.create_llm(settings.LLM_NAME, api_key=api_key)
|
||||
|
||||
if prompt_id == 'default':
|
||||
prompt = chat_combine_template
|
||||
elif prompt_id == 'creative':
|
||||
prompt = chat_combine_creative
|
||||
elif prompt_id == 'strict':
|
||||
prompt = chat_combine_strict
|
||||
else:
|
||||
prompt = prompts_collection.find_one({"_id": ObjectId(prompt_id)})["content"]
|
||||
|
||||
docs = docsearch.search(question, k=2)
|
||||
if settings.LLM_NAME == "llama.cpp":
|
||||
docs = [docs[0]]
|
||||
# join all page_content together with a newline
|
||||
docs_together = "\n".join([doc.page_content for doc in docs])
|
||||
p_chat_combine = prompt.replace("{summaries}", docs_together)
|
||||
messages_combine = [{"role": "system", "content": p_chat_combine}]
|
||||
source_log_docs = []
|
||||
for doc in docs:
|
||||
if doc.metadata:
|
||||
source_log_docs.append({"title": doc.metadata['title'].split('/')[-1], "text": doc.page_content})
|
||||
else:
|
||||
source_log_docs.append({"title": doc.page_content, "text": doc.page_content})
|
||||
|
||||
if len(chat_history) > 1:
|
||||
tokens_current_history = 0
|
||||
# count tokens in history
|
||||
chat_history.reverse()
|
||||
for i in chat_history:
|
||||
if "prompt" in i and "response" in i:
|
||||
tokens_batch = count_tokens(i["prompt"]) + count_tokens(i["response"])
|
||||
if tokens_current_history + tokens_batch < settings.TOKENS_MAX_HISTORY:
|
||||
tokens_current_history += tokens_batch
|
||||
messages_combine.append({"role": "user", "content": i["prompt"]})
|
||||
messages_combine.append({"role": "system", "content": i["response"]})
|
||||
messages_combine.append({"role": "user", "content": question})
|
||||
|
||||
response_full = ""
|
||||
completion = llm.gen_stream(model=gpt_model, engine=settings.AZURE_DEPLOYMENT_NAME,
|
||||
messages=messages_combine)
|
||||
for line in completion:
|
||||
data = json.dumps({"answer": str(line)})
|
||||
response_full += str(line)
|
||||
yield f"data: {data}\n\n"
|
||||
|
||||
# save conversation to database
|
||||
if conversation_id is not None:
|
||||
def save_conversation(conversation_id, question, response, source_log_docs, llm):
|
||||
if conversation_id is not None and conversation_id != "None":
|
||||
conversations_collection.update_one(
|
||||
{"_id": ObjectId(conversation_id)},
|
||||
{"$push": {"queries": {"prompt": question, "response": response_full, "sources": source_log_docs}}},
|
||||
{
|
||||
"$push": {
|
||||
"queries": {
|
||||
"prompt": question,
|
||||
"response": response,
|
||||
"sources": source_log_docs,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
else:
|
||||
# create new conversation
|
||||
# generate summary
|
||||
messages_summary = [{"role": "assistant", "content": "Summarise following conversation in no more than 3 "
|
||||
"words, respond ONLY with the summary, use the same "
|
||||
"language as the system \n\nUser: " + question + "\n\n" +
|
||||
"AI: " +
|
||||
response_full},
|
||||
{"role": "user", "content": "Summarise following conversation in no more than 3 words, "
|
||||
"respond ONLY with the summary, use the same language as the "
|
||||
"system"}]
|
||||
messages_summary = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Summarise following conversation in no more than 3 "
|
||||
"words, respond ONLY with the summary, use the same "
|
||||
"language as the system \n\nUser: "
|
||||
+question
|
||||
+"\n\n"
|
||||
+"AI: "
|
||||
+response,
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Summarise following conversation in no more than 3 words, "
|
||||
"respond ONLY with the summary, use the same language as the "
|
||||
"system",
|
||||
},
|
||||
]
|
||||
|
||||
completion = llm.gen(model=gpt_model, engine=settings.AZURE_DEPLOYMENT_NAME,
|
||||
messages=messages_summary, max_tokens=30)
|
||||
completion = llm.gen(model=gpt_model, messages=messages_summary, max_tokens=30)
|
||||
conversation_id = conversations_collection.insert_one(
|
||||
{"user": "local",
|
||||
"date": datetime.datetime.utcnow(),
|
||||
"name": completion,
|
||||
"queries": [{"prompt": question, "response": response_full, "sources": source_log_docs}]}
|
||||
{
|
||||
"user": "local",
|
||||
"date": datetime.datetime.utcnow(),
|
||||
"name": completion,
|
||||
"queries": [
|
||||
{
|
||||
"prompt": question,
|
||||
"response": response,
|
||||
"sources": source_log_docs,
|
||||
}
|
||||
],
|
||||
}
|
||||
).inserted_id
|
||||
return conversation_id
|
||||
|
||||
# send data.type = "end" to indicate that the stream has ended as json
|
||||
data = json.dumps({"type": "id", "id": str(conversation_id)})
|
||||
yield f"data: {data}\n\n"
|
||||
data = json.dumps({"type": "end"})
|
||||
yield f"data: {data}\n\n"
|
||||
|
||||
def get_prompt(prompt_id):
|
||||
if prompt_id == "default":
|
||||
prompt = chat_combine_template
|
||||
elif prompt_id == "creative":
|
||||
prompt = chat_combine_creative
|
||||
elif prompt_id == "strict":
|
||||
prompt = chat_combine_strict
|
||||
else:
|
||||
prompt = prompts_collection.find_one({"_id": ObjectId(prompt_id)})["content"]
|
||||
return prompt
|
||||
|
||||
|
||||
def complete_stream(question, retriever, conversation_id, user_api_key):
|
||||
|
||||
try:
|
||||
response_full = ""
|
||||
source_log_docs = []
|
||||
answer = retriever.gen()
|
||||
for line in answer:
|
||||
if "answer" in line:
|
||||
response_full += str(line["answer"])
|
||||
data = json.dumps(line)
|
||||
yield f"data: {data}\n\n"
|
||||
elif "source" in line:
|
||||
source_log_docs.append(line["source"])
|
||||
|
||||
llm = LLMCreator.create_llm(
|
||||
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
|
||||
)
|
||||
conversation_id = save_conversation(
|
||||
conversation_id, question, response_full, source_log_docs, llm
|
||||
)
|
||||
|
||||
# send data.type = "end" to indicate that the stream has ended as json
|
||||
data = json.dumps({"type": "id", "id": str(conversation_id)})
|
||||
yield f"data: {data}\n\n"
|
||||
data = json.dumps({"type": "end"})
|
||||
yield f"data: {data}\n\n"
|
||||
except Exception as e:
|
||||
print("\033[91merr", str(e), file=sys.stderr)
|
||||
data = json.dumps({"type": "error","error":"Please try again later. We apologize for any inconvenience.",
|
||||
"error_exception": str(e)})
|
||||
yield f"data: {data}\n\n"
|
||||
return
|
||||
|
||||
@answer.route("/stream", methods=["POST"])
|
||||
def stream():
|
||||
try:
|
||||
data = request.get_json()
|
||||
# get parameter from url question
|
||||
question = data["question"]
|
||||
history = data["history"]
|
||||
# history to json object from string
|
||||
history = json.loads(history)
|
||||
conversation_id = data["conversation_id"]
|
||||
if 'prompt_id' in data:
|
||||
if "history" not in data:
|
||||
history = []
|
||||
else:
|
||||
history = data["history"]
|
||||
history = json.loads(history)
|
||||
if "conversation_id" not in data:
|
||||
conversation_id = None
|
||||
else:
|
||||
conversation_id = data["conversation_id"]
|
||||
if "prompt_id" in data:
|
||||
prompt_id = data["prompt_id"]
|
||||
else:
|
||||
prompt_id = 'default'
|
||||
|
||||
# check if active_docs is set
|
||||
|
||||
if not api_key_set:
|
||||
api_key = data["api_key"]
|
||||
prompt_id = "default"
|
||||
if "selectedDocs" in data and data["selectedDocs"] is None:
|
||||
chunks = 0
|
||||
elif "chunks" in data:
|
||||
chunks = int(data["chunks"])
|
||||
else:
|
||||
api_key = settings.API_KEY
|
||||
if not embeddings_key_set:
|
||||
embeddings_key = data["embeddings_key"]
|
||||
chunks = 2
|
||||
if "token_limit" in data:
|
||||
token_limit = data["token_limit"]
|
||||
else:
|
||||
embeddings_key = settings.EMBEDDINGS_KEY
|
||||
if "active_docs" in data:
|
||||
vectorstore = get_vectorstore({"active_docs": data["active_docs"]})
|
||||
else:
|
||||
vectorstore = ""
|
||||
docsearch = VectorCreator.create_vectorstore(settings.VECTOR_STORE, vectorstore, embeddings_key)
|
||||
token_limit = settings.DEFAULT_MAX_HISTORY
|
||||
|
||||
return Response(
|
||||
complete_stream(question, docsearch,
|
||||
chat_history=history, api_key=api_key,
|
||||
prompt_id=prompt_id,
|
||||
conversation_id=conversation_id), mimetype="text/event-stream"
|
||||
# check if active_docs or api_key is set
|
||||
|
||||
if "api_key" in data:
|
||||
data_key = get_data_from_api_key(data["api_key"])
|
||||
chunks = int(data_key["chunks"])
|
||||
prompt_id = data_key["prompt_id"]
|
||||
source = {"active_docs": data_key["source"]}
|
||||
user_api_key = data["api_key"]
|
||||
elif "active_docs" in data:
|
||||
source = {"active_docs": data["active_docs"]}
|
||||
user_api_key = None
|
||||
else:
|
||||
source = {}
|
||||
user_api_key = None
|
||||
|
||||
if (
|
||||
source["active_docs"].split("/")[0] == "default"
|
||||
or source["active_docs"].split("/")[0] == "local"
|
||||
):
|
||||
retriever_name = "classic"
|
||||
else:
|
||||
retriever_name = source["active_docs"]
|
||||
|
||||
prompt = get_prompt(prompt_id)
|
||||
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
retriever_name,
|
||||
question=question,
|
||||
source=source,
|
||||
chat_history=history,
|
||||
prompt=prompt,
|
||||
chunks=chunks,
|
||||
token_limit=token_limit,
|
||||
gpt_model=gpt_model,
|
||||
user_api_key=user_api_key,
|
||||
)
|
||||
|
||||
return Response(
|
||||
complete_stream(
|
||||
question=question,
|
||||
retriever=retriever,
|
||||
conversation_id=conversation_id,
|
||||
user_api_key=user_api_key,
|
||||
),
|
||||
mimetype="text/event-stream",
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
message = "Malformed request body"
|
||||
print("\033[91merr", str(message), file=sys.stderr)
|
||||
return Response(
|
||||
error_stream_generate(message),
|
||||
status=400,
|
||||
mimetype="text/event-stream",
|
||||
)
|
||||
except Exception as e:
|
||||
print("\033[91merr", str(e), file=sys.stderr)
|
||||
message = e.args[0]
|
||||
status_code = 400
|
||||
# # Custom exceptions with two arguments, index 1 as status code
|
||||
if(len(e.args) >= 2):
|
||||
status_code = e.args[1]
|
||||
return Response(
|
||||
error_stream_generate(message),
|
||||
status=status_code,
|
||||
mimetype="text/event-stream",
|
||||
)
|
||||
def error_stream_generate(err_response):
|
||||
data = json.dumps({"type": "error", "error":err_response})
|
||||
yield f"data: {data}\n\n"
|
||||
|
||||
@answer.route("/api/answer", methods=["POST"])
|
||||
def api_answer():
|
||||
data = request.get_json()
|
||||
question = data["question"]
|
||||
history = data["history"]
|
||||
if "history" not in data:
|
||||
history = []
|
||||
else:
|
||||
history = data["history"]
|
||||
if "conversation_id" not in data:
|
||||
conversation_id = None
|
||||
else:
|
||||
conversation_id = data["conversation_id"]
|
||||
print("-" * 5)
|
||||
if not api_key_set:
|
||||
api_key = data["api_key"]
|
||||
else:
|
||||
api_key = settings.API_KEY
|
||||
if not embeddings_key_set:
|
||||
embeddings_key = data["embeddings_key"]
|
||||
else:
|
||||
embeddings_key = settings.EMBEDDINGS_KEY
|
||||
if 'prompt_id' in data:
|
||||
if "prompt_id" in data:
|
||||
prompt_id = data["prompt_id"]
|
||||
else:
|
||||
prompt_id = 'default'
|
||||
|
||||
if prompt_id == 'default':
|
||||
prompt = chat_combine_template
|
||||
elif prompt_id == 'creative':
|
||||
prompt = chat_combine_creative
|
||||
elif prompt_id == 'strict':
|
||||
prompt = chat_combine_strict
|
||||
prompt_id = "default"
|
||||
if "chunks" in data:
|
||||
chunks = int(data["chunks"])
|
||||
else:
|
||||
prompt = prompts_collection.find_one({"_id": ObjectId(prompt_id)})["content"]
|
||||
chunks = 2
|
||||
if "token_limit" in data:
|
||||
token_limit = data["token_limit"]
|
||||
else:
|
||||
token_limit = settings.DEFAULT_MAX_HISTORY
|
||||
|
||||
# use try and except to check for exception
|
||||
try:
|
||||
# check if the vectorstore is set
|
||||
vectorstore = get_vectorstore(data)
|
||||
# loading the index and the store and the prompt template
|
||||
# Note if you have used other embeddings than OpenAI, you need to change the embeddings
|
||||
docsearch = VectorCreator.create_vectorstore(settings.VECTOR_STORE, vectorstore, embeddings_key)
|
||||
|
||||
|
||||
llm = LLMCreator.create_llm(settings.LLM_NAME, api_key=api_key)
|
||||
|
||||
|
||||
|
||||
docs = docsearch.search(question, k=2)
|
||||
# join all page_content together with a newline
|
||||
docs_together = "\n".join([doc.page_content for doc in docs])
|
||||
p_chat_combine = prompt.replace("{summaries}", docs_together)
|
||||
messages_combine = [{"role": "system", "content": p_chat_combine}]
|
||||
source_log_docs = []
|
||||
for doc in docs:
|
||||
if doc.metadata:
|
||||
source_log_docs.append({"title": doc.metadata['title'].split('/')[-1], "text": doc.page_content})
|
||||
else:
|
||||
source_log_docs.append({"title": doc.page_content, "text": doc.page_content})
|
||||
# join all page_content together with a newline
|
||||
|
||||
|
||||
if len(history) > 1:
|
||||
tokens_current_history = 0
|
||||
# count tokens in history
|
||||
history.reverse()
|
||||
for i in history:
|
||||
if "prompt" in i and "response" in i:
|
||||
tokens_batch = count_tokens(i["prompt"]) + count_tokens(i["response"])
|
||||
if tokens_current_history + tokens_batch < settings.TOKENS_MAX_HISTORY:
|
||||
tokens_current_history += tokens_batch
|
||||
messages_combine.append({"role": "user", "content": i["prompt"]})
|
||||
messages_combine.append({"role": "system", "content": i["response"]})
|
||||
messages_combine.append({"role": "user", "content": question})
|
||||
|
||||
|
||||
completion = llm.gen(model=gpt_model, engine=settings.AZURE_DEPLOYMENT_NAME,
|
||||
messages=messages_combine)
|
||||
|
||||
|
||||
result = {"answer": completion, "sources": source_log_docs}
|
||||
logger.debug(result)
|
||||
|
||||
# generate conversationId
|
||||
if conversation_id is not None:
|
||||
conversations_collection.update_one(
|
||||
{"_id": ObjectId(conversation_id)},
|
||||
{"$push": {"queries": {"prompt": question,
|
||||
"response": result["answer"], "sources": result['sources']}}},
|
||||
)
|
||||
|
||||
if "api_key" in data:
|
||||
data_key = get_data_from_api_key(data["api_key"])
|
||||
chunks = int(data_key["chunks"])
|
||||
prompt_id = data_key["prompt_id"]
|
||||
source = {"active_docs": data_key["source"]}
|
||||
user_api_key = data["api_key"]
|
||||
else:
|
||||
# create new conversation
|
||||
# generate summary
|
||||
messages_summary = [
|
||||
{"role": "assistant", "content": "Summarise following conversation in no more than 3 words, "
|
||||
"respond ONLY with the summary, use the same language as the system \n\n"
|
||||
"User: " + question + "\n\n" + "AI: " + result["answer"]},
|
||||
{"role": "user", "content": "Summarise following conversation in no more than 3 words, "
|
||||
"respond ONLY with the summary, use the same language as the system"}
|
||||
]
|
||||
source = data
|
||||
user_api_key = None
|
||||
|
||||
completion = llm.gen(
|
||||
model=gpt_model,
|
||||
engine=settings.AZURE_DEPLOYMENT_NAME,
|
||||
messages=messages_summary,
|
||||
max_tokens=30
|
||||
)
|
||||
conversation_id = conversations_collection.insert_one(
|
||||
{"user": "local",
|
||||
"date": datetime.datetime.utcnow(),
|
||||
"name": completion,
|
||||
"queries": [{"prompt": question, "response": result["answer"], "sources": source_log_docs}]}
|
||||
).inserted_id
|
||||
if (
|
||||
source["active_docs"].split("/")[0] == "default"
|
||||
or source["active_docs"].split("/")[0] == "local"
|
||||
):
|
||||
retriever_name = "classic"
|
||||
else:
|
||||
retriever_name = source["active_docs"]
|
||||
|
||||
result["conversation_id"] = str(conversation_id)
|
||||
prompt = get_prompt(prompt_id)
|
||||
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
retriever_name,
|
||||
question=question,
|
||||
source=source,
|
||||
chat_history=history,
|
||||
prompt=prompt,
|
||||
chunks=chunks,
|
||||
token_limit=token_limit,
|
||||
gpt_model=gpt_model,
|
||||
user_api_key=user_api_key,
|
||||
)
|
||||
source_log_docs = []
|
||||
response_full = ""
|
||||
for line in retriever.gen():
|
||||
if "source" in line:
|
||||
source_log_docs.append(line["source"])
|
||||
elif "answer" in line:
|
||||
response_full += line["answer"]
|
||||
|
||||
llm = LLMCreator.create_llm(
|
||||
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=user_api_key
|
||||
)
|
||||
|
||||
result = {"answer": response_full, "sources": source_log_docs}
|
||||
result["conversation_id"] = save_conversation(
|
||||
conversation_id, question, response_full, source_log_docs, llm
|
||||
)
|
||||
|
||||
# mock result
|
||||
# result = {
|
||||
# "answer": "The answer is 42",
|
||||
# "sources": ["https://en.wikipedia.org/wiki/42_(number)", "https://en.wikipedia.org/wiki/42_(number)"]
|
||||
# }
|
||||
return result
|
||||
except Exception as e:
|
||||
# print whole traceback
|
||||
@@ -347,28 +396,44 @@ def api_search():
|
||||
data = request.get_json()
|
||||
# get parameter from url question
|
||||
question = data["question"]
|
||||
|
||||
if not embeddings_key_set:
|
||||
if "embeddings_key" in data:
|
||||
embeddings_key = data["embeddings_key"]
|
||||
else:
|
||||
embeddings_key = settings.EMBEDDINGS_KEY
|
||||
if "chunks" in data:
|
||||
chunks = int(data["chunks"])
|
||||
else:
|
||||
embeddings_key = settings.EMBEDDINGS_KEY
|
||||
if "active_docs" in data:
|
||||
vectorstore = get_vectorstore({"active_docs": data["active_docs"]})
|
||||
chunks = 2
|
||||
if "api_key" in data:
|
||||
data_key = get_data_from_api_key(data["api_key"])
|
||||
chunks = int(data_key["chunks"])
|
||||
source = {"active_docs": data_key["source"]}
|
||||
user_api_key = data["api_key"]
|
||||
elif "active_docs" in data:
|
||||
source = {"active_docs": data["active_docs"]}
|
||||
user_api_key = None
|
||||
else:
|
||||
vectorstore = ""
|
||||
docsearch = VectorCreator.create_vectorstore(settings.VECTOR_STORE, vectorstore, embeddings_key)
|
||||
source = {}
|
||||
user_api_key = None
|
||||
|
||||
docs = docsearch.search(question, k=2)
|
||||
|
||||
source_log_docs = []
|
||||
for doc in docs:
|
||||
if doc.metadata:
|
||||
source_log_docs.append({"title": doc.metadata['title'].split('/')[-1], "text": doc.page_content})
|
||||
else:
|
||||
source_log_docs.append({"title": doc.page_content, "text": doc.page_content})
|
||||
#yield f"data:{data}\n\n"
|
||||
return source_log_docs
|
||||
if (
|
||||
source["active_docs"].split("/")[0] == "default"
|
||||
or source["active_docs"].split("/")[0] == "local"
|
||||
):
|
||||
retriever_name = "classic"
|
||||
else:
|
||||
retriever_name = source["active_docs"]
|
||||
if "token_limit" in data:
|
||||
token_limit = data["token_limit"]
|
||||
else:
|
||||
token_limit = settings.DEFAULT_MAX_HISTORY
|
||||
|
||||
retriever = RetrieverCreator.create_retriever(
|
||||
retriever_name,
|
||||
question=question,
|
||||
source=source,
|
||||
chat_history=[],
|
||||
prompt="default",
|
||||
chunks=chunks,
|
||||
token_limit=token_limit,
|
||||
gpt_model=gpt_model,
|
||||
user_api_key=user_api_key,
|
||||
)
|
||||
docs = retriever.search()
|
||||
return docs
|
||||
|
||||
2
application/api/internal/routes.py
Normal file → Executable file
@@ -34,6 +34,7 @@ def upload_index_files():
|
||||
if "name" not in request.form:
|
||||
return {"status": "no name"}
|
||||
job_name = secure_filename(request.form["name"])
|
||||
tokens = secure_filename(request.form["tokens"])
|
||||
save_dir = os.path.join(current_dir, "indexes", user, job_name)
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
if "file_faiss" not in request.files:
|
||||
@@ -64,6 +65,7 @@ def upload_index_files():
|
||||
"date": datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S"),
|
||||
"model": settings.EMBEDDINGS_NAME,
|
||||
"type": "local",
|
||||
"tokens": tokens
|
||||
}
|
||||
)
|
||||
return {"status": "ok"}
|
||||
@@ -1,10 +1,14 @@
|
||||
import os
|
||||
import uuid
|
||||
import shutil
|
||||
from flask import Blueprint, request, jsonify
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
from pymongo import MongoClient
|
||||
from bson.objectid import ObjectId
|
||||
from bson.binary import Binary, UuidRepresentation
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from bson.dbref import DBRef
|
||||
from application.api.user.tasks import ingest, ingest_remote
|
||||
|
||||
from application.core.settings import settings
|
||||
@@ -16,9 +20,15 @@ conversations_collection = db["conversations"]
|
||||
vectors_collection = db["vectors"]
|
||||
prompts_collection = db["prompts"]
|
||||
feedback_collection = db["feedback"]
|
||||
user = Blueprint('user', __name__)
|
||||
api_key_collection = db["api_keys"]
|
||||
shared_conversations_collections = db["shared_conversations"]
|
||||
|
||||
user = Blueprint("user", __name__)
|
||||
|
||||
current_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
|
||||
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
@user.route("/api/delete_conversation", methods=["POST"])
|
||||
def delete_conversation():
|
||||
@@ -33,15 +43,25 @@ def delete_conversation():
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@user.route("/api/delete_all_conversations", methods=["POST"])
|
||||
def delete_all_conversations():
|
||||
user_id = "local"
|
||||
conversations_collection.delete_many({"user": user_id})
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@user.route("/api/get_conversations", methods=["get"])
|
||||
def get_conversations():
|
||||
# provides a list of conversations
|
||||
conversations = conversations_collection.find().sort("date", -1).limit(30)
|
||||
list_conversations = []
|
||||
for conversation in conversations:
|
||||
list_conversations.append({"id": str(conversation["_id"]), "name": conversation["name"]})
|
||||
list_conversations.append(
|
||||
{"id": str(conversation["_id"]), "name": conversation["name"]}
|
||||
)
|
||||
|
||||
#list_conversations = [{"id": "default", "name": "default"}, {"id": "jeff", "name": "jeff"}]
|
||||
# list_conversations = [{"id": "default", "name": "default"}, {"id": "jeff", "name": "jeff"}]
|
||||
|
||||
return jsonify(list_conversations)
|
||||
|
||||
@@ -51,7 +71,8 @@ def get_single_conversation():
|
||||
# provides data for a conversation
|
||||
conversation_id = request.args.get("id")
|
||||
conversation = conversations_collection.find_one({"_id": ObjectId(conversation_id)})
|
||||
return jsonify(conversation['queries'])
|
||||
return jsonify(conversation["queries"])
|
||||
|
||||
|
||||
@user.route("/api/update_conversation_name", methods=["POST"])
|
||||
def update_conversation_name():
|
||||
@@ -59,7 +80,7 @@ def update_conversation_name():
|
||||
data = request.get_json()
|
||||
id = data["id"]
|
||||
name = data["name"]
|
||||
conversations_collection.update_one({"_id": ObjectId(id)},{"$set":{"name":name}})
|
||||
conversations_collection.update_one({"_id": ObjectId(id)}, {"$set": {"name": name}})
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -70,7 +91,6 @@ def api_feedback():
|
||||
answer = data["answer"]
|
||||
feedback = data["feedback"]
|
||||
|
||||
|
||||
feedback_collection.insert_one(
|
||||
{
|
||||
"question": question,
|
||||
@@ -80,6 +100,7 @@ def api_feedback():
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@user.route("/api/delete_by_ids", methods=["get"])
|
||||
def delete_by_ids():
|
||||
"""Delete by ID. These are the IDs in the vectorstore"""
|
||||
@@ -94,6 +115,7 @@ def delete_by_ids():
|
||||
return {"status": "ok"}
|
||||
return {"status": "error"}
|
||||
|
||||
|
||||
@user.route("/api/delete_old", methods=["get"])
|
||||
def delete_old():
|
||||
"""Delete old indexes."""
|
||||
@@ -109,7 +131,7 @@ def delete_old():
|
||||
if dirs_clean[0] not in ["indexes", "vectors"]:
|
||||
return {"status": "error"}
|
||||
path_clean = "/".join(dirs_clean)
|
||||
vectors_collection.delete_one({"name": dirs_clean[-1], 'user': dirs_clean[-2]})
|
||||
vectors_collection.delete_one({"name": dirs_clean[-1], "user": dirs_clean[-2]})
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
try:
|
||||
shutil.rmtree(os.path.join(current_dir, path_clean))
|
||||
@@ -120,9 +142,10 @@ def delete_old():
|
||||
settings.VECTOR_STORE, path=os.path.join(current_dir, path_clean)
|
||||
)
|
||||
vetorstore.delete_index()
|
||||
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@user.route("/api/upload", methods=["POST"])
|
||||
def upload_file():
|
||||
"""Upload a file to get vectorized and indexed."""
|
||||
@@ -133,31 +156,51 @@ def upload_file():
|
||||
return {"status": "no name"}
|
||||
job_name = secure_filename(request.form["name"])
|
||||
# check if the post request has the file part
|
||||
if "file" not in request.files:
|
||||
print("No file part")
|
||||
return {"status": "no file"}
|
||||
file = request.files["file"]
|
||||
if file.filename == "":
|
||||
files = request.files.getlist("file")
|
||||
|
||||
if not files or all(file.filename == "" for file in files):
|
||||
return {"status": "no file name"}
|
||||
|
||||
if file:
|
||||
filename = secure_filename(file.filename)
|
||||
# save dir
|
||||
save_dir = os.path.join(current_dir, settings.UPLOAD_FOLDER, user, job_name)
|
||||
# create dir if not exists
|
||||
if not os.path.exists(save_dir):
|
||||
os.makedirs(save_dir)
|
||||
# Directory where files will be saved
|
||||
save_dir = os.path.join(current_dir, settings.UPLOAD_FOLDER, user, job_name)
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
file.save(os.path.join(save_dir, filename))
|
||||
task = ingest.delay(settings.UPLOAD_FOLDER, [".rst", ".md", ".pdf", ".txt", ".docx",
|
||||
".csv", ".epub", ".html", ".mdx"],
|
||||
job_name, filename, user)
|
||||
# task id
|
||||
task_id = task.id
|
||||
return {"status": "ok", "task_id": task_id}
|
||||
if len(files) > 1:
|
||||
# Multiple files; prepare them for zip
|
||||
temp_dir = os.path.join(save_dir, "temp")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
for file in files:
|
||||
filename = secure_filename(file.filename)
|
||||
file.save(os.path.join(temp_dir, filename))
|
||||
|
||||
# Use shutil.make_archive to zip the temp directory
|
||||
zip_path = shutil.make_archive(
|
||||
base_name=os.path.join(save_dir, job_name), format="zip", root_dir=temp_dir
|
||||
)
|
||||
final_filename = os.path.basename(zip_path)
|
||||
|
||||
# Clean up the temporary directory after zipping
|
||||
shutil.rmtree(temp_dir)
|
||||
else:
|
||||
return {"status": "error"}
|
||||
|
||||
# Single file
|
||||
file = files[0]
|
||||
final_filename = secure_filename(file.filename)
|
||||
file_path = os.path.join(save_dir, final_filename)
|
||||
file.save(file_path)
|
||||
|
||||
# Call ingest with the single file or zipped file
|
||||
task = ingest.delay(
|
||||
settings.UPLOAD_FOLDER,
|
||||
[".rst", ".md", ".pdf", ".txt", ".docx", ".csv", ".epub", ".html", ".mdx"],
|
||||
job_name,
|
||||
final_filename,
|
||||
user,
|
||||
)
|
||||
|
||||
return {"status": "ok", "task_id": task.id}
|
||||
|
||||
|
||||
@user.route("/api/remote", methods=["POST"])
|
||||
def upload_remote():
|
||||
"""Upload a remote source to get vectorized and indexed."""
|
||||
@@ -170,25 +213,27 @@ def upload_remote():
|
||||
if "name" not in request.form:
|
||||
return {"status": "no name"}
|
||||
job_name = secure_filename(request.form["name"])
|
||||
# check if the post request has the file part
|
||||
if "data" not in request.form:
|
||||
print("No data")
|
||||
return {"status": "no data"}
|
||||
source_data = request.form["data"]
|
||||
|
||||
if source_data:
|
||||
task = ingest_remote.delay(source_data=source_data, job_name=job_name, user=user, loader=source)
|
||||
# task id
|
||||
task = ingest_remote.delay(
|
||||
source_data=source_data, job_name=job_name, user=user, loader=source
|
||||
)
|
||||
task_id = task.id
|
||||
return {"status": "ok", "task_id": task_id}
|
||||
else:
|
||||
return {"status": "error"}
|
||||
|
||||
|
||||
@user.route("/api/task_status", methods=["GET"])
|
||||
def task_status():
|
||||
"""Get celery job status."""
|
||||
task_id = request.args.get("task_id")
|
||||
from application.celery import celery
|
||||
from application.celery_init import celery
|
||||
|
||||
task = celery.AsyncResult(task_id)
|
||||
task_meta = task.info
|
||||
return {"status": task.status, "result": task_meta}
|
||||
@@ -211,11 +256,12 @@ def combined_json():
|
||||
"docLink": "default",
|
||||
"model": settings.EMBEDDINGS_NAME,
|
||||
"location": "remote",
|
||||
"tokens":""
|
||||
}
|
||||
]
|
||||
# structure: name, language, version, description, fullName, date, docLink
|
||||
# append data from vectors_collection
|
||||
for index in vectors_collection.find({"user": user}):
|
||||
# append data from vectors_collection in sorted order in descending order of date
|
||||
for index in vectors_collection.find({"user": user}).sort("date", -1):
|
||||
data.append(
|
||||
{
|
||||
"name": index["name"],
|
||||
@@ -227,13 +273,46 @@ def combined_json():
|
||||
"docLink": index["location"],
|
||||
"model": settings.EMBEDDINGS_NAME,
|
||||
"location": "local",
|
||||
"tokens" : index["tokens"] if ("tokens" in index.keys()) else ""
|
||||
}
|
||||
)
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
data_remote = requests.get("https://d3dg1063dc54p9.cloudfront.net/combined.json").json()
|
||||
data_remote = requests.get(
|
||||
"https://d3dg1063dc54p9.cloudfront.net/combined.json"
|
||||
).json()
|
||||
for index in data_remote:
|
||||
index["location"] = "remote"
|
||||
data.append(index)
|
||||
if "duckduck_search" in settings.RETRIEVERS_ENABLED:
|
||||
data.append(
|
||||
{
|
||||
"name": "DuckDuckGo Search",
|
||||
"language": "en",
|
||||
"version": "",
|
||||
"description": "duckduck_search",
|
||||
"fullName": "DuckDuckGo Search",
|
||||
"date": "duckduck_search",
|
||||
"docLink": "duckduck_search",
|
||||
"model": settings.EMBEDDINGS_NAME,
|
||||
"location": "custom",
|
||||
"tokens":""
|
||||
}
|
||||
)
|
||||
if "brave_search" in settings.RETRIEVERS_ENABLED:
|
||||
data.append(
|
||||
{
|
||||
"name": "Brave Search",
|
||||
"language": "en",
|
||||
"version": "",
|
||||
"description": "brave_search",
|
||||
"fullName": "Brave Search",
|
||||
"date": "brave_search",
|
||||
"docLink": "brave_search",
|
||||
"model": settings.EMBEDDINGS_NAME,
|
||||
"location": "custom",
|
||||
"tokens":""
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
@@ -245,28 +324,36 @@ def check_docs():
|
||||
# split docs on / and take first part
|
||||
if data["docs"].split("/")[0] == "local":
|
||||
return {"status": "exists"}
|
||||
vectorstore = "vectors/" + data["docs"]
|
||||
vectorstore = "vectors/" + secure_filename(data["docs"])
|
||||
base_path = "https://raw.githubusercontent.com/arc53/DocsHUB/main/"
|
||||
if os.path.exists(vectorstore) or data["docs"] == "default":
|
||||
return {"status": "exists"}
|
||||
else:
|
||||
r = requests.get(base_path + vectorstore + "index.faiss")
|
||||
file_url = urlparse(base_path + vectorstore + "index.faiss")
|
||||
|
||||
if r.status_code != 200:
|
||||
return {"status": "null"}
|
||||
if (
|
||||
file_url.scheme in ["https"]
|
||||
and file_url.netloc == "raw.githubusercontent.com"
|
||||
and file_url.path.startswith("/arc53/DocsHUB/main/")
|
||||
):
|
||||
r = requests.get(file_url.geturl())
|
||||
if r.status_code != 200:
|
||||
return {"status": "null"}
|
||||
else:
|
||||
if not os.path.exists(vectorstore):
|
||||
os.makedirs(vectorstore)
|
||||
with open(vectorstore + "index.faiss", "wb") as f:
|
||||
f.write(r.content)
|
||||
|
||||
r = requests.get(base_path + vectorstore + "index.pkl")
|
||||
with open(vectorstore + "index.pkl", "wb") as f:
|
||||
f.write(r.content)
|
||||
else:
|
||||
if not os.path.exists(vectorstore):
|
||||
os.makedirs(vectorstore)
|
||||
with open(vectorstore + "index.faiss", "wb") as f:
|
||||
f.write(r.content)
|
||||
|
||||
# download the store
|
||||
r = requests.get(base_path + vectorstore + "index.pkl")
|
||||
with open(vectorstore + "index.pkl", "wb") as f:
|
||||
f.write(r.content)
|
||||
return {"status": "null"}
|
||||
|
||||
return {"status": "loaded"}
|
||||
|
||||
|
||||
@user.route("/api/create_prompt", methods=["POST"])
|
||||
def create_prompt():
|
||||
data = request.get_json()
|
||||
@@ -285,6 +372,7 @@ def create_prompt():
|
||||
new_id = str(resp.inserted_id)
|
||||
return {"id": new_id}
|
||||
|
||||
|
||||
@user.route("/api/get_prompts", methods=["GET"])
|
||||
def get_prompts():
|
||||
user = "local"
|
||||
@@ -294,30 +382,39 @@ def get_prompts():
|
||||
list_prompts.append({"id": "creative", "name": "creative", "type": "public"})
|
||||
list_prompts.append({"id": "strict", "name": "strict", "type": "public"})
|
||||
for prompt in prompts:
|
||||
list_prompts.append({"id": str(prompt["_id"]), "name": prompt["name"], "type": "private"})
|
||||
list_prompts.append(
|
||||
{"id": str(prompt["_id"]), "name": prompt["name"], "type": "private"}
|
||||
)
|
||||
|
||||
return jsonify(list_prompts)
|
||||
|
||||
|
||||
@user.route("/api/get_single_prompt", methods=["GET"])
|
||||
def get_single_prompt():
|
||||
prompt_id = request.args.get("id")
|
||||
if prompt_id == 'default':
|
||||
with open(os.path.join(current_dir, "prompts", "chat_combine_default.txt"), "r") as f:
|
||||
if prompt_id == "default":
|
||||
with open(
|
||||
os.path.join(current_dir, "prompts", "chat_combine_default.txt"), "r"
|
||||
) as f:
|
||||
chat_combine_template = f.read()
|
||||
return jsonify({"content": chat_combine_template})
|
||||
elif prompt_id == 'creative':
|
||||
with open(os.path.join(current_dir, "prompts", "chat_combine_creative.txt"), "r") as f:
|
||||
elif prompt_id == "creative":
|
||||
with open(
|
||||
os.path.join(current_dir, "prompts", "chat_combine_creative.txt"), "r"
|
||||
) as f:
|
||||
chat_reduce_creative = f.read()
|
||||
return jsonify({"content": chat_reduce_creative})
|
||||
elif prompt_id == 'strict':
|
||||
with open(os.path.join(current_dir, "prompts", "chat_combine_strict.txt"), "r") as f:
|
||||
chat_reduce_strict = f.read()
|
||||
elif prompt_id == "strict":
|
||||
with open(
|
||||
os.path.join(current_dir, "prompts", "chat_combine_strict.txt"), "r"
|
||||
) as f:
|
||||
chat_reduce_strict = f.read()
|
||||
return jsonify({"content": chat_reduce_strict})
|
||||
|
||||
|
||||
prompt = prompts_collection.find_one({"_id": ObjectId(prompt_id)})
|
||||
return jsonify({"content": prompt["content"]})
|
||||
|
||||
|
||||
@user.route("/api/delete_prompt", methods=["POST"])
|
||||
def delete_prompt():
|
||||
data = request.get_json()
|
||||
@@ -329,6 +426,7 @@ def delete_prompt():
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@user.route("/api/update_prompt", methods=["POST"])
|
||||
def update_prompt_name():
|
||||
data = request.get_json()
|
||||
@@ -338,10 +436,131 @@ def update_prompt_name():
|
||||
# check if name is null
|
||||
if name == "":
|
||||
return {"status": "error"}
|
||||
prompts_collection.update_one({"_id": ObjectId(id)},{"$set":{"name":name, "content": content}})
|
||||
prompts_collection.update_one(
|
||||
{"_id": ObjectId(id)}, {"$set": {"name": name, "content": content}}
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@user.route("/api/get_api_keys", methods=["GET"])
|
||||
def get_api_keys():
|
||||
user = "local"
|
||||
keys = api_key_collection.find({"user": user})
|
||||
list_keys = []
|
||||
for key in keys:
|
||||
list_keys.append(
|
||||
{
|
||||
"id": str(key["_id"]),
|
||||
"name": key["name"],
|
||||
"key": key["key"][:4] + "..." + key["key"][-4:],
|
||||
"source": key["source"],
|
||||
"prompt_id": key["prompt_id"],
|
||||
"chunks": key["chunks"],
|
||||
}
|
||||
)
|
||||
return jsonify(list_keys)
|
||||
|
||||
|
||||
@user.route("/api/create_api_key", methods=["POST"])
|
||||
def create_api_key():
|
||||
data = request.get_json()
|
||||
name = data["name"]
|
||||
source = data["source"]
|
||||
prompt_id = data["prompt_id"]
|
||||
chunks = data["chunks"]
|
||||
key = str(uuid.uuid4())
|
||||
user = "local"
|
||||
resp = api_key_collection.insert_one(
|
||||
{
|
||||
"name": name,
|
||||
"key": key,
|
||||
"source": source,
|
||||
"user": user,
|
||||
"prompt_id": prompt_id,
|
||||
"chunks": chunks,
|
||||
}
|
||||
)
|
||||
new_id = str(resp.inserted_id)
|
||||
return {"id": new_id, "key": key}
|
||||
|
||||
|
||||
@user.route("/api/delete_api_key", methods=["POST"])
|
||||
def delete_api_key():
|
||||
data = request.get_json()
|
||||
id = data["id"]
|
||||
api_key_collection.delete_one(
|
||||
{
|
||||
"_id": ObjectId(id),
|
||||
}
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
#route to share conversation
|
||||
##isPromptable should be passed through queries
|
||||
@user.route("/api/share",methods=["POST"])
|
||||
def share_conversation():
|
||||
try:
|
||||
data = request.get_json()
|
||||
user = "local"
|
||||
if(hasattr(data,"user")):
|
||||
user = data["user"]
|
||||
conversation_id = data["conversation_id"]
|
||||
isPromptable = request.args.get("isPromptable").lower() == "true"
|
||||
conversation = conversations_collection.find_one({"_id": ObjectId(conversation_id)})
|
||||
current_n_queries = len(conversation["queries"])
|
||||
pre_existing = shared_conversations_collections.find_one({
|
||||
"conversation_id":DBRef("conversations",ObjectId(conversation_id)),
|
||||
"isPromptable":isPromptable,
|
||||
"first_n_queries":current_n_queries
|
||||
})
|
||||
print("pre_existing",pre_existing)
|
||||
if(pre_existing is not None):
|
||||
explicit_binary = pre_existing["uuid"]
|
||||
return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),200
|
||||
else:
|
||||
explicit_binary = Binary.from_uuid(uuid.uuid4(), UuidRepresentation.STANDARD)
|
||||
shared_conversations_collections.insert_one({
|
||||
"uuid":explicit_binary,
|
||||
"conversation_id": {
|
||||
"$ref":"conversations",
|
||||
"$id":ObjectId(conversation_id)
|
||||
} ,
|
||||
"isPromptable":isPromptable,
|
||||
"first_n_queries":current_n_queries,
|
||||
"user":user
|
||||
})
|
||||
## Identifier as route parameter in frontend
|
||||
return jsonify({"success":True, "identifier":str(explicit_binary.as_uuid())}),201
|
||||
except Exception as err:
|
||||
return jsonify({"success":False,"error":str(err)}),400
|
||||
|
||||
#route to get publicly shared conversations
|
||||
@user.route("/api/shared_conversation/<string:identifier>",methods=["GET"])
|
||||
def get_publicly_shared_conversations(identifier : str):
|
||||
try:
|
||||
query_uuid = Binary.from_uuid(uuid.UUID(identifier), UuidRepresentation.STANDARD)
|
||||
shared = shared_conversations_collections.find_one({"uuid":query_uuid})
|
||||
conversation_queries=[]
|
||||
if shared and 'conversation_id' in shared and isinstance(shared['conversation_id'], DBRef):
|
||||
# Resolve the DBRef
|
||||
conversation_ref = shared['conversation_id']
|
||||
conversation = db.dereference(conversation_ref)
|
||||
if(conversation is None):
|
||||
return jsonify({"sucess":False,"error":"might have broken url or the conversation does not exist"}),404
|
||||
conversation_queries = conversation['queries'][:(shared["first_n_queries"])]
|
||||
for query in conversation_queries:
|
||||
query.pop("sources") ## avoid exposing sources
|
||||
else:
|
||||
return jsonify({"sucess":False,"error":"might have broken url or the conversation does not exist"}),404
|
||||
date = conversation["_id"].generation_time.isoformat()
|
||||
return jsonify({
|
||||
"success":True,
|
||||
"queries":conversation_queries,
|
||||
"title":conversation["name"],
|
||||
"timestamp":date
|
||||
}), 200
|
||||
except Exception as err:
|
||||
print (err)
|
||||
return jsonify({"success":False,"error":str(err)}),400
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from application.worker import ingest_worker, remote_worker
|
||||
from application.celery import celery
|
||||
from application.celery_init import celery
|
||||
|
||||
@celery.task(bind=True)
|
||||
def ingest(self, directory, formats, name_job, filename, user):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import platform
|
||||
import dotenv
|
||||
from application.celery import celery
|
||||
from application.celery_init import celery
|
||||
from flask import Flask, request, redirect
|
||||
from application.core.settings import settings
|
||||
from application.api.user.routes import user
|
||||
@@ -40,5 +40,5 @@ def after_request(response):
|
||||
return response
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, port=7091)
|
||||
app.run(debug=settings.FLASK_DEBUG_MODE, port=7091)
|
||||
|
||||
|
||||
@@ -9,14 +9,17 @@ current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__
|
||||
|
||||
class Settings(BaseSettings):
|
||||
LLM_NAME: str = "docsgpt"
|
||||
MODEL_NAME: Optional[str] = None # if LLM_NAME is openai, MODEL_NAME can be gpt-4 or gpt-3.5-turbo
|
||||
EMBEDDINGS_NAME: str = "huggingface_sentence-transformers/all-mpnet-base-v2"
|
||||
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
|
||||
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/1"
|
||||
MONGO_URI: str = "mongodb://localhost:27017/docsgpt"
|
||||
MODEL_PATH: str = os.path.join(current_dir, "models/docsgpt-7b-f16.gguf")
|
||||
TOKENS_MAX_HISTORY: int = 150
|
||||
DEFAULT_MAX_HISTORY: int = 150
|
||||
MODEL_TOKEN_LIMITS: dict = {"gpt-3.5-turbo": 4096, "claude-2": 1e5}
|
||||
UPLOAD_FOLDER: str = "inputs"
|
||||
VECTOR_STORE: str = "faiss" # "faiss" or "elasticsearch" or "qdrant"
|
||||
RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search
|
||||
|
||||
API_URL: str = "http://localhost:7091" # backend url for celery worker
|
||||
|
||||
@@ -58,6 +61,10 @@ class Settings(BaseSettings):
|
||||
QDRANT_PATH: Optional[str] = None
|
||||
QDRANT_DISTANCE_FUNC: str = "Cosine"
|
||||
|
||||
BRAVE_SEARCH_API_KEY: Optional[str] = None
|
||||
|
||||
FLASK_DEBUG_MODE: bool = False
|
||||
|
||||
|
||||
path = Path(__file__).parent.parent.absolute()
|
||||
settings = Settings(_env_file=path.joinpath(".env"), _env_file_encoding="utf-8")
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
from application.llm.base import BaseLLM
|
||||
from application.core.settings import settings
|
||||
|
||||
|
||||
class AnthropicLLM(BaseLLM):
|
||||
|
||||
def __init__(self, api_key=None):
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
from anthropic import Anthropic, HUMAN_PROMPT, AI_PROMPT
|
||||
self.api_key = api_key or settings.ANTHROPIC_API_KEY # If not provided, use a default from settings
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_key = (
|
||||
api_key or settings.ANTHROPIC_API_KEY
|
||||
) # If not provided, use a default from settings
|
||||
self.user_api_key = user_api_key
|
||||
self.anthropic = Anthropic(api_key=self.api_key)
|
||||
self.HUMAN_PROMPT = HUMAN_PROMPT
|
||||
self.AI_PROMPT = AI_PROMPT
|
||||
|
||||
def gen(self, model, messages, engine=None, max_tokens=300, stream=False, **kwargs):
|
||||
context = messages[0]['content']
|
||||
user_question = messages[-1]['content']
|
||||
def _raw_gen(
|
||||
self, baseself, model, messages, stream=False, max_tokens=300, **kwargs
|
||||
):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Context \n {context} \n ### Question \n {user_question}"
|
||||
if stream:
|
||||
return self.gen_stream(model, prompt, max_tokens, **kwargs)
|
||||
return self.gen_stream(model, prompt, stream, max_tokens, **kwargs)
|
||||
|
||||
completion = self.anthropic.completions.create(
|
||||
model=model,
|
||||
@@ -25,9 +33,11 @@ class AnthropicLLM(BaseLLM):
|
||||
)
|
||||
return completion.completion
|
||||
|
||||
def gen_stream(self, model, messages, engine=None, max_tokens=300, **kwargs):
|
||||
context = messages[0]['content']
|
||||
user_question = messages[-1]['content']
|
||||
def _raw_gen_stream(
|
||||
self, baseself, model, messages, stream=True, max_tokens=300, **kwargs
|
||||
):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Context \n {context} \n ### Question \n {user_question}"
|
||||
stream_response = self.anthropic.completions.create(
|
||||
model=model,
|
||||
@@ -37,4 +47,4 @@ class AnthropicLLM(BaseLLM):
|
||||
)
|
||||
|
||||
for completion in stream_response:
|
||||
yield completion.completion
|
||||
yield completion.completion
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from application.usage import gen_token_usage, stream_token_usage
|
||||
|
||||
|
||||
class BaseLLM(ABC):
|
||||
def __init__(self):
|
||||
pass
|
||||
self.token_usage = {"prompt_tokens": 0, "generated_tokens": 0}
|
||||
|
||||
def _apply_decorator(self, method, decorator, *args, **kwargs):
|
||||
return decorator(method, *args, **kwargs)
|
||||
|
||||
@abstractmethod
|
||||
def gen(self, *args, **kwargs):
|
||||
def _raw_gen(self, model, messages, stream, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def gen(self, model, messages, stream=False, *args, **kwargs):
|
||||
return self._apply_decorator(self._raw_gen, gen_token_usage)(
|
||||
self, model=model, messages=messages, stream=stream, *args, **kwargs
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def gen_stream(self, *args, **kwargs):
|
||||
def _raw_gen_stream(self, model, messages, stream, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def gen_stream(self, model, messages, stream=True, *args, **kwargs):
|
||||
return self._apply_decorator(self._raw_gen_stream, stream_token_usage)(
|
||||
self, model=model, messages=messages, stream=stream, *args, **kwargs
|
||||
)
|
||||
|
||||
@@ -2,48 +2,43 @@ from application.llm.base import BaseLLM
|
||||
import json
|
||||
import requests
|
||||
|
||||
|
||||
class DocsGPTAPILLM(BaseLLM):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.endpoint = "https://llm.docsgpt.co.uk"
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
self.endpoint = "https://llm.docsgpt.co.uk"
|
||||
|
||||
|
||||
def gen(self, model, engine, messages, stream=False, **kwargs):
|
||||
context = messages[0]['content']
|
||||
user_question = messages[-1]['content']
|
||||
def _raw_gen(self, baseself, model, messages, stream=False, *args, **kwargs):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||
|
||||
response = requests.post(
|
||||
f"{self.endpoint}/answer",
|
||||
json={
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": 30
|
||||
}
|
||||
f"{self.endpoint}/answer", json={"prompt": prompt, "max_new_tokens": 30}
|
||||
)
|
||||
response_clean = response.json()['a'].replace("###", "")
|
||||
response_clean = response.json()["a"].replace("###", "")
|
||||
|
||||
return response_clean
|
||||
|
||||
def gen_stream(self, model, engine, messages, stream=True, **kwargs):
|
||||
context = messages[0]['content']
|
||||
user_question = messages[-1]['content']
|
||||
def _raw_gen_stream(self, baseself, model, messages, stream=True, *args, **kwargs):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||
|
||||
# send prompt to endpoint /stream
|
||||
response = requests.post(
|
||||
f"{self.endpoint}/stream",
|
||||
json={
|
||||
"prompt": prompt,
|
||||
"max_new_tokens": 256
|
||||
},
|
||||
stream=True
|
||||
json={"prompt": prompt, "max_new_tokens": 256},
|
||||
stream=True,
|
||||
)
|
||||
|
||||
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
#data = json.loads(line)
|
||||
data_str = line.decode('utf-8')
|
||||
# data = json.loads(line)
|
||||
data_str = line.decode("utf-8")
|
||||
if data_str.startswith("data: "):
|
||||
data = json.loads(data_str[6:])
|
||||
yield data['a']
|
||||
|
||||
yield data["a"]
|
||||
|
||||
@@ -1,44 +1,68 @@
|
||||
from application.llm.base import BaseLLM
|
||||
|
||||
|
||||
class HuggingFaceLLM(BaseLLM):
|
||||
|
||||
def __init__(self, api_key, llm_name='Arc53/DocsGPT-7B',q=False):
|
||||
def __init__(
|
||||
self,
|
||||
api_key=None,
|
||||
user_api_key=None,
|
||||
llm_name="Arc53/DocsGPT-7B",
|
||||
q=False,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
global hf
|
||||
|
||||
|
||||
from langchain.llms import HuggingFacePipeline
|
||||
|
||||
if q:
|
||||
import torch
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, BitsAndBytesConfig
|
||||
from transformers import (
|
||||
AutoModelForCausalLM,
|
||||
AutoTokenizer,
|
||||
pipeline,
|
||||
BitsAndBytesConfig,
|
||||
)
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(llm_name)
|
||||
bnb_config = BitsAndBytesConfig(
|
||||
load_in_4bit=True,
|
||||
bnb_4bit_use_double_quant=True,
|
||||
bnb_4bit_quant_type="nf4",
|
||||
bnb_4bit_compute_dtype=torch.bfloat16
|
||||
)
|
||||
model = AutoModelForCausalLM.from_pretrained(llm_name,quantization_config=bnb_config)
|
||||
load_in_4bit=True,
|
||||
bnb_4bit_use_double_quant=True,
|
||||
bnb_4bit_quant_type="nf4",
|
||||
bnb_4bit_compute_dtype=torch.bfloat16,
|
||||
)
|
||||
model = AutoModelForCausalLM.from_pretrained(
|
||||
llm_name, quantization_config=bnb_config
|
||||
)
|
||||
else:
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(llm_name)
|
||||
model = AutoModelForCausalLM.from_pretrained(llm_name)
|
||||
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
pipe = pipeline(
|
||||
"text-generation", model=model,
|
||||
tokenizer=tokenizer, max_new_tokens=2000,
|
||||
device_map="auto", eos_token_id=tokenizer.eos_token_id
|
||||
"text-generation",
|
||||
model=model,
|
||||
tokenizer=tokenizer,
|
||||
max_new_tokens=2000,
|
||||
device_map="auto",
|
||||
eos_token_id=tokenizer.eos_token_id,
|
||||
)
|
||||
hf = HuggingFacePipeline(pipeline=pipe)
|
||||
|
||||
def gen(self, model, engine, messages, stream=False, **kwargs):
|
||||
context = messages[0]['content']
|
||||
user_question = messages[-1]['content']
|
||||
def _raw_gen(self, baseself, model, messages, stream=False, **kwargs):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||
|
||||
result = hf(prompt)
|
||||
|
||||
return result.content
|
||||
|
||||
def gen_stream(self, model, engine, messages, stream=True, **kwargs):
|
||||
def _raw_gen_stream(self, baseself, model, messages, stream=True, **kwargs):
|
||||
|
||||
raise NotImplementedError("HuggingFaceLLM Streaming is not implemented yet.")
|
||||
|
||||
|
||||
@@ -1,39 +1,55 @@
|
||||
from application.llm.base import BaseLLM
|
||||
from application.core.settings import settings
|
||||
import threading
|
||||
|
||||
class LlamaSingleton:
|
||||
_instances = {}
|
||||
_lock = threading.Lock() # Add a lock for thread synchronization
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, llm_name):
|
||||
if llm_name not in cls._instances:
|
||||
try:
|
||||
from llama_cpp import Llama
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Please install llama_cpp using pip install llama-cpp-python"
|
||||
)
|
||||
cls._instances[llm_name] = Llama(model_path=llm_name, n_ctx=2048)
|
||||
return cls._instances[llm_name]
|
||||
|
||||
@classmethod
|
||||
def query_model(cls, llm, prompt, **kwargs):
|
||||
with cls._lock:
|
||||
return llm(prompt, **kwargs)
|
||||
|
||||
|
||||
class LlamaCpp(BaseLLM):
|
||||
def __init__(
|
||||
self,
|
||||
api_key=None,
|
||||
user_api_key=None,
|
||||
llm_name=settings.MODEL_PATH,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
self.llama = LlamaSingleton.get_instance(llm_name)
|
||||
|
||||
def __init__(self, api_key, llm_name=settings.MODEL_PATH, **kwargs):
|
||||
global llama
|
||||
try:
|
||||
from llama_cpp import Llama
|
||||
except ImportError:
|
||||
raise ImportError("Please install llama_cpp using pip install llama-cpp-python")
|
||||
|
||||
llama = Llama(model_path=llm_name, n_ctx=2048)
|
||||
|
||||
def gen(self, model, engine, messages, stream=False, **kwargs):
|
||||
context = messages[0]['content']
|
||||
user_question = messages[-1]['content']
|
||||
def _raw_gen(self, baseself, model, messages, stream=False, **kwargs):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||
result = LlamaSingleton.query_model(self.llama, prompt, max_tokens=150, echo=False)
|
||||
return result["choices"][0]["text"].split("### Answer \n")[-1]
|
||||
|
||||
result = llama(prompt, max_tokens=150, echo=False)
|
||||
|
||||
# import sys
|
||||
# print(result['choices'][0]['text'].split('### Answer \n')[-1], file=sys.stderr)
|
||||
|
||||
return result['choices'][0]['text'].split('### Answer \n')[-1]
|
||||
|
||||
def gen_stream(self, model, engine, messages, stream=True, **kwargs):
|
||||
context = messages[0]['content']
|
||||
user_question = messages[-1]['content']
|
||||
def _raw_gen_stream(self, baseself, model, messages, stream=True, **kwargs):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||
|
||||
result = llama(prompt, max_tokens=150, echo=False, stream=stream)
|
||||
|
||||
# import sys
|
||||
# print(list(result), file=sys.stderr)
|
||||
|
||||
result = LlamaSingleton.query_model(self.llama, prompt, max_tokens=150, echo=False, stream=stream)
|
||||
for item in result:
|
||||
for choice in item['choices']:
|
||||
yield choice['text']
|
||||
for choice in item["choices"]:
|
||||
yield choice["text"]
|
||||
@@ -7,22 +7,21 @@ from application.llm.docsgpt_provider import DocsGPTAPILLM
|
||||
from application.llm.premai import PremAILLM
|
||||
|
||||
|
||||
|
||||
class LLMCreator:
|
||||
llms = {
|
||||
'openai': OpenAILLM,
|
||||
'azure_openai': AzureOpenAILLM,
|
||||
'sagemaker': SagemakerAPILLM,
|
||||
'huggingface': HuggingFaceLLM,
|
||||
'llama.cpp': LlamaCpp,
|
||||
'anthropic': AnthropicLLM,
|
||||
'docsgpt': DocsGPTAPILLM,
|
||||
'premai': PremAILLM,
|
||||
"openai": OpenAILLM,
|
||||
"azure_openai": AzureOpenAILLM,
|
||||
"sagemaker": SagemakerAPILLM,
|
||||
"huggingface": HuggingFaceLLM,
|
||||
"llama.cpp": LlamaCpp,
|
||||
"anthropic": AnthropicLLM,
|
||||
"docsgpt": DocsGPTAPILLM,
|
||||
"premai": PremAILLM,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_llm(cls, type, *args, **kwargs):
|
||||
def create_llm(cls, type, api_key, user_api_key, *args, **kwargs):
|
||||
llm_class = cls.llms.get(type.lower())
|
||||
if not llm_class:
|
||||
raise ValueError(f"No LLM class found for type {type}")
|
||||
return llm_class(*args, **kwargs)
|
||||
return llm_class(api_key, user_api_key, *args, **kwargs)
|
||||
|
||||
@@ -1,36 +1,53 @@
|
||||
from application.llm.base import BaseLLM
|
||||
from application.core.settings import settings
|
||||
|
||||
|
||||
class OpenAILLM(BaseLLM):
|
||||
|
||||
def __init__(self, api_key):
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
global openai
|
||||
from openai import OpenAI
|
||||
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.client = OpenAI(
|
||||
api_key=api_key,
|
||||
)
|
||||
api_key=api_key,
|
||||
)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
|
||||
def _get_openai(self):
|
||||
# Import openai when needed
|
||||
import openai
|
||||
|
||||
|
||||
return openai
|
||||
|
||||
def gen(self, model, engine, messages, stream=False, **kwargs):
|
||||
response = self.client.chat.completions.create(model=model,
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
**kwargs)
|
||||
def _raw_gen(
|
||||
self,
|
||||
baseself,
|
||||
model,
|
||||
messages,
|
||||
stream=False,
|
||||
engine=settings.AZURE_DEPLOYMENT_NAME,
|
||||
**kwargs
|
||||
):
|
||||
response = self.client.chat.completions.create(
|
||||
model=model, messages=messages, stream=stream, **kwargs
|
||||
)
|
||||
|
||||
return response.choices[0].message.content
|
||||
|
||||
def gen_stream(self, model, engine, messages, stream=True, **kwargs):
|
||||
response = self.client.chat.completions.create(model=model,
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
**kwargs)
|
||||
def _raw_gen_stream(
|
||||
self,
|
||||
baseself,
|
||||
model,
|
||||
messages,
|
||||
stream=True,
|
||||
engine=settings.AZURE_DEPLOYMENT_NAME,
|
||||
**kwargs
|
||||
):
|
||||
response = self.client.chat.completions.create(
|
||||
model=model, messages=messages, stream=stream, **kwargs
|
||||
)
|
||||
|
||||
for line in response:
|
||||
# import sys
|
||||
@@ -41,14 +58,17 @@ class OpenAILLM(BaseLLM):
|
||||
|
||||
class AzureOpenAILLM(OpenAILLM):
|
||||
|
||||
def __init__(self, openai_api_key, openai_api_base, openai_api_version, deployment_name):
|
||||
def __init__(
|
||||
self, openai_api_key, openai_api_base, openai_api_version, deployment_name
|
||||
):
|
||||
super().__init__(openai_api_key)
|
||||
self.api_base = settings.OPENAI_API_BASE,
|
||||
self.api_version = settings.OPENAI_API_VERSION,
|
||||
self.deployment_name = settings.AZURE_DEPLOYMENT_NAME,
|
||||
self.api_base = (settings.OPENAI_API_BASE,)
|
||||
self.api_version = (settings.OPENAI_API_VERSION,)
|
||||
self.deployment_name = (settings.AZURE_DEPLOYMENT_NAME,)
|
||||
from openai import AzureOpenAI
|
||||
|
||||
self.client = AzureOpenAI(
|
||||
api_key=openai_api_key,
|
||||
api_key=openai_api_key,
|
||||
api_version=settings.OPENAI_API_VERSION,
|
||||
api_base=settings.OPENAI_API_BASE,
|
||||
deployment_name=settings.AZURE_DEPLOYMENT_NAME,
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
from application.llm.base import BaseLLM
|
||||
from application.core.settings import settings
|
||||
|
||||
|
||||
class PremAILLM(BaseLLM):
|
||||
|
||||
def __init__(self, api_key):
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
from premai import Prem
|
||||
|
||||
self.client = Prem(
|
||||
api_key=api_key
|
||||
)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.client = Prem(api_key=api_key)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
self.project_id = settings.PREMAI_PROJECT_ID
|
||||
|
||||
def gen(self, model, engine, messages, stream=False, **kwargs):
|
||||
response = self.client.chat.completions.create(model=model,
|
||||
def _raw_gen(self, baseself, model, messages, stream=False, **kwargs):
|
||||
response = self.client.chat.completions.create(
|
||||
model=model,
|
||||
project_id=self.project_id,
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
**kwargs)
|
||||
**kwargs
|
||||
)
|
||||
|
||||
return response.choices[0].message["content"]
|
||||
|
||||
def gen_stream(self, model, engine, messages, stream=True, **kwargs):
|
||||
response = self.client.chat.completions.create(model=model,
|
||||
def _raw_gen_stream(self, baseself, model, messages, stream=True, **kwargs):
|
||||
response = self.client.chat.completions.create(
|
||||
model=model,
|
||||
project_id=self.project_id,
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
**kwargs)
|
||||
**kwargs
|
||||
)
|
||||
|
||||
for line in response:
|
||||
if line.choices[0].delta["content"] is not None:
|
||||
|
||||
@@ -4,11 +4,10 @@ import json
|
||||
import io
|
||||
|
||||
|
||||
|
||||
class LineIterator:
|
||||
"""
|
||||
A helper class for parsing the byte stream input.
|
||||
|
||||
A helper class for parsing the byte stream input.
|
||||
|
||||
The output of the model will be in the following format:
|
||||
```
|
||||
b'{"outputs": [" a"]}\n'
|
||||
@@ -16,21 +15,21 @@ class LineIterator:
|
||||
b'{"outputs": [" problem"]}\n'
|
||||
...
|
||||
```
|
||||
|
||||
While usually each PayloadPart event from the event stream will contain a byte array
|
||||
|
||||
While usually each PayloadPart event from the event stream will contain a byte array
|
||||
with a full json, this is not guaranteed and some of the json objects may be split across
|
||||
PayloadPart events. For example:
|
||||
```
|
||||
{'PayloadPart': {'Bytes': b'{"outputs": '}}
|
||||
{'PayloadPart': {'Bytes': b'[" problem"]}\n'}}
|
||||
```
|
||||
|
||||
|
||||
This class accounts for this by concatenating bytes written via the 'write' function
|
||||
and then exposing a method which will return lines (ending with a '\n' character) within
|
||||
the buffer via the 'scan_lines' function. It maintains the position of the last read
|
||||
position to ensure that previous bytes are not exposed again.
|
||||
the buffer via the 'scan_lines' function. It maintains the position of the last read
|
||||
position to ensure that previous bytes are not exposed again.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, stream):
|
||||
self.byte_iterator = iter(stream)
|
||||
self.buffer = io.BytesIO()
|
||||
@@ -43,7 +42,7 @@ class LineIterator:
|
||||
while True:
|
||||
self.buffer.seek(self.read_pos)
|
||||
line = self.buffer.readline()
|
||||
if line and line[-1] == ord('\n'):
|
||||
if line and line[-1] == ord("\n"):
|
||||
self.read_pos += len(line)
|
||||
return line[:-1]
|
||||
try:
|
||||
@@ -52,33 +51,35 @@ class LineIterator:
|
||||
if self.read_pos < self.buffer.getbuffer().nbytes:
|
||||
continue
|
||||
raise
|
||||
if 'PayloadPart' not in chunk:
|
||||
print('Unknown event type:' + chunk)
|
||||
if "PayloadPart" not in chunk:
|
||||
print("Unknown event type:" + chunk)
|
||||
continue
|
||||
self.buffer.seek(0, io.SEEK_END)
|
||||
self.buffer.write(chunk['PayloadPart']['Bytes'])
|
||||
self.buffer.write(chunk["PayloadPart"]["Bytes"])
|
||||
|
||||
|
||||
class SagemakerAPILLM(BaseLLM):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, api_key=None, user_api_key=None, *args, **kwargs):
|
||||
import boto3
|
||||
|
||||
runtime = boto3.client(
|
||||
'runtime.sagemaker',
|
||||
aws_access_key_id='xxx',
|
||||
aws_secret_access_key='xxx',
|
||||
region_name='us-west-2'
|
||||
"runtime.sagemaker",
|
||||
aws_access_key_id="xxx",
|
||||
aws_secret_access_key="xxx",
|
||||
region_name="us-west-2",
|
||||
)
|
||||
|
||||
|
||||
self.endpoint = settings.SAGEMAKER_ENDPOINT
|
||||
super().__init__(*args, **kwargs)
|
||||
self.api_key = api_key
|
||||
self.user_api_key = user_api_key
|
||||
self.endpoint = settings.SAGEMAKER_ENDPOINT
|
||||
self.runtime = runtime
|
||||
|
||||
|
||||
def gen(self, model, engine, messages, stream=False, **kwargs):
|
||||
context = messages[0]['content']
|
||||
user_question = messages[-1]['content']
|
||||
def _raw_gen(self, baseself, model, messages, stream=False, **kwargs):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||
|
||||
|
||||
# Construct payload for endpoint
|
||||
payload = {
|
||||
@@ -89,25 +90,25 @@ class SagemakerAPILLM(BaseLLM):
|
||||
"temperature": 0.1,
|
||||
"max_new_tokens": 30,
|
||||
"repetition_penalty": 1.03,
|
||||
"stop": ["</s>", "###"]
|
||||
}
|
||||
"stop": ["</s>", "###"],
|
||||
},
|
||||
}
|
||||
body_bytes = json.dumps(payload).encode('utf-8')
|
||||
body_bytes = json.dumps(payload).encode("utf-8")
|
||||
|
||||
# Invoke the endpoint
|
||||
response = self.runtime.invoke_endpoint(EndpointName=self.endpoint,
|
||||
ContentType='application/json',
|
||||
Body=body_bytes)
|
||||
result = json.loads(response['Body'].read().decode())
|
||||
response = self.runtime.invoke_endpoint(
|
||||
EndpointName=self.endpoint, ContentType="application/json", Body=body_bytes
|
||||
)
|
||||
result = json.loads(response["Body"].read().decode())
|
||||
import sys
|
||||
print(result[0]['generated_text'], file=sys.stderr)
|
||||
return result[0]['generated_text'][len(prompt):]
|
||||
|
||||
def gen_stream(self, model, engine, messages, stream=True, **kwargs):
|
||||
context = messages[0]['content']
|
||||
user_question = messages[-1]['content']
|
||||
print(result[0]["generated_text"], file=sys.stderr)
|
||||
return result[0]["generated_text"][len(prompt) :]
|
||||
|
||||
def _raw_gen_stream(self, baseself, model, messages, stream=True, **kwargs):
|
||||
context = messages[0]["content"]
|
||||
user_question = messages[-1]["content"]
|
||||
prompt = f"### Instruction \n {user_question} \n ### Context \n {context} \n ### Answer \n"
|
||||
|
||||
|
||||
# Construct payload for endpoint
|
||||
payload = {
|
||||
@@ -118,22 +119,22 @@ class SagemakerAPILLM(BaseLLM):
|
||||
"temperature": 0.1,
|
||||
"max_new_tokens": 512,
|
||||
"repetition_penalty": 1.03,
|
||||
"stop": ["</s>", "###"]
|
||||
}
|
||||
"stop": ["</s>", "###"],
|
||||
},
|
||||
}
|
||||
body_bytes = json.dumps(payload).encode('utf-8')
|
||||
body_bytes = json.dumps(payload).encode("utf-8")
|
||||
|
||||
# Invoke the endpoint
|
||||
response = self.runtime.invoke_endpoint_with_response_stream(EndpointName=self.endpoint,
|
||||
ContentType='application/json',
|
||||
Body=body_bytes)
|
||||
#result = json.loads(response['Body'].read().decode())
|
||||
event_stream = response['Body']
|
||||
start_json = b'{'
|
||||
response = self.runtime.invoke_endpoint_with_response_stream(
|
||||
EndpointName=self.endpoint, ContentType="application/json", Body=body_bytes
|
||||
)
|
||||
# result = json.loads(response['Body'].read().decode())
|
||||
event_stream = response["Body"]
|
||||
start_json = b"{"
|
||||
for line in LineIterator(event_stream):
|
||||
if line != b'' and start_json in line:
|
||||
#print(line)
|
||||
data = json.loads(line[line.find(start_json):].decode('utf-8'))
|
||||
if data['token']['text'] not in ["</s>", "###"]:
|
||||
print(data['token']['text'],end='')
|
||||
yield data['token']['text']
|
||||
if line != b"" and start_json in line:
|
||||
# print(line)
|
||||
data = json.loads(line[line.find(start_json) :].decode("utf-8"))
|
||||
if data["token"]["text"] not in ["</s>", "###"]:
|
||||
print(data["token"]["text"], end="")
|
||||
yield data["token"]["text"]
|
||||
|
||||
52
application/parser/open_ai_func.py
Normal file → Executable file
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
|
||||
import tiktoken
|
||||
from application.vectorstore.vector_creator import VectorCreator
|
||||
from application.core.settings import settings
|
||||
from retry import retry
|
||||
@@ -11,14 +10,6 @@ from retry import retry
|
||||
# from langchain_community.embeddings import CohereEmbeddings
|
||||
|
||||
|
||||
def num_tokens_from_string(string: str, encoding_name: str) -> int:
|
||||
# Function to convert string to tokens and estimate user cost.
|
||||
encoding = tiktoken.get_encoding(encoding_name)
|
||||
num_tokens = len(encoding.encode(string))
|
||||
total_price = ((num_tokens / 1000) * 0.0004)
|
||||
return num_tokens, total_price
|
||||
|
||||
|
||||
@retry(tries=10, delay=60)
|
||||
def store_add_texts_with_retry(store, i):
|
||||
store.add_texts([i.page_content], metadatas=[i.metadata])
|
||||
@@ -26,13 +17,13 @@ def store_add_texts_with_retry(store, i):
|
||||
|
||||
|
||||
def call_openai_api(docs, folder_name, task_status):
|
||||
# Function to create a vector store from the documents and save it to disk.
|
||||
# Function to create a vector store from the documents and save it to disk
|
||||
|
||||
# create output folder if it doesn't exist
|
||||
if not os.path.exists(f"{folder_name}"):
|
||||
os.makedirs(f"{folder_name}")
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
c1 = 0
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
docs_init = [docs[0]]
|
||||
@@ -40,25 +31,32 @@ def call_openai_api(docs, folder_name, task_status):
|
||||
|
||||
store = VectorCreator.create_vectorstore(
|
||||
settings.VECTOR_STORE,
|
||||
docs_init = docs_init,
|
||||
docs_init=docs_init,
|
||||
path=f"{folder_name}",
|
||||
embeddings_key=os.getenv("EMBEDDINGS_KEY")
|
||||
embeddings_key=os.getenv("EMBEDDINGS_KEY"),
|
||||
)
|
||||
else:
|
||||
store = VectorCreator.create_vectorstore(
|
||||
settings.VECTOR_STORE,
|
||||
path=f"{folder_name}",
|
||||
embeddings_key=os.getenv("EMBEDDINGS_KEY")
|
||||
embeddings_key=os.getenv("EMBEDDINGS_KEY"),
|
||||
)
|
||||
# Uncomment for MPNet embeddings
|
||||
# model_name = "sentence-transformers/all-mpnet-base-v2"
|
||||
# hf = HuggingFaceEmbeddings(model_name=model_name)
|
||||
# store = FAISS.from_documents(docs_test, hf)
|
||||
s1 = len(docs)
|
||||
for i in tqdm(docs, desc="Embedding 🦖", unit="docs", total=len(docs),
|
||||
bar_format='{l_bar}{bar}| Time Left: {remaining}'):
|
||||
for i in tqdm(
|
||||
docs,
|
||||
desc="Embedding 🦖",
|
||||
unit="docs",
|
||||
total=len(docs),
|
||||
bar_format="{l_bar}{bar}| Time Left: {remaining}",
|
||||
):
|
||||
try:
|
||||
task_status.update_state(state='PROGRESS', meta={'current': int((c1 / s1) * 100)})
|
||||
task_status.update_state(
|
||||
state="PROGRESS", meta={"current": int((c1 / s1) * 100)}
|
||||
)
|
||||
store_add_texts_with_retry(store, i)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
@@ -72,23 +70,3 @@ def call_openai_api(docs, folder_name, task_status):
|
||||
store.save_local(f"{folder_name}")
|
||||
|
||||
|
||||
def get_user_permission(docs, folder_name):
|
||||
# Function to ask user permission to call the OpenAI api and spend their OpenAI funds.
|
||||
# Here we convert the docs list to a string and calculate the number of OpenAI tokens the string represents.
|
||||
# docs_content = (" ".join(docs))
|
||||
docs_content = ""
|
||||
for doc in docs:
|
||||
docs_content += doc.page_content
|
||||
|
||||
tokens, total_price = num_tokens_from_string(string=docs_content, encoding_name="cl100k_base")
|
||||
# Here we print the number of tokens and the approx user cost with some visually appealing formatting.
|
||||
print(f"Number of Tokens = {format(tokens, ',d')}")
|
||||
print(f"Approx Cost = ${format(total_price, ',.2f')}")
|
||||
# Here we check for user permission before calling the API.
|
||||
user_input = input("Price Okay? (Y/N) \n").lower()
|
||||
if user_input == "y":
|
||||
call_openai_api(docs, folder_name)
|
||||
elif user_input == "":
|
||||
call_openai_api(docs, folder_name)
|
||||
else:
|
||||
print("The API was not called. No money was spent.")
|
||||
|
||||
26
application/parser/remote/reddit_loader.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from application.parser.remote.base import BaseRemote
|
||||
from langchain_community.document_loaders import RedditPostsLoader
|
||||
|
||||
|
||||
class RedditPostsLoaderRemote(BaseRemote):
|
||||
def load_data(self, inputs):
|
||||
data = eval(inputs)
|
||||
client_id = data.get("client_id")
|
||||
client_secret = data.get("client_secret")
|
||||
user_agent = data.get("user_agent")
|
||||
categories = data.get("categories", ["new", "hot"])
|
||||
mode = data.get("mode", "subreddit")
|
||||
search_queries = data.get("search_queries")
|
||||
number_posts = data.get("number_posts", 10)
|
||||
self.loader = RedditPostsLoader(
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
user_agent=user_agent,
|
||||
categories=categories,
|
||||
mode=mode,
|
||||
search_queries=search_queries,
|
||||
number_posts=number_posts,
|
||||
)
|
||||
documents = self.loader.load()
|
||||
print(f"Loaded {len(documents)} documents from Reddit")
|
||||
return documents
|
||||
@@ -1,13 +1,15 @@
|
||||
from application.parser.remote.sitemap_loader import SitemapLoader
|
||||
from application.parser.remote.crawler_loader import CrawlerLoader
|
||||
from application.parser.remote.web_loader import WebLoader
|
||||
from application.parser.remote.reddit_loader import RedditPostsLoaderRemote
|
||||
|
||||
|
||||
class RemoteCreator:
|
||||
loaders = {
|
||||
'url': WebLoader,
|
||||
'sitemap': SitemapLoader,
|
||||
'crawler': CrawlerLoader
|
||||
"url": WebLoader,
|
||||
"sitemap": SitemapLoader,
|
||||
"crawler": CrawlerLoader,
|
||||
"reddit": RedditPostsLoaderRemote,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -15,4 +17,4 @@ class RemoteCreator:
|
||||
loader_class = cls.loaders.get(type.lower())
|
||||
if not loader_class:
|
||||
raise ValueError(f"No LLM class found for type {type}")
|
||||
return loader_class(*args, **kwargs)
|
||||
return loader_class(*args, **kwargs)
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
from application.parser.remote.base import BaseRemote
|
||||
from langchain_community.document_loaders import WebBaseLoader
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*"
|
||||
";q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Referer": "https://www.google.com/",
|
||||
"DNT": "1",
|
||||
"Connection": "keep-alive",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
}
|
||||
|
||||
|
||||
class WebLoader(BaseRemote):
|
||||
def __init__(self):
|
||||
from langchain.document_loaders import WebBaseLoader
|
||||
self.loader = WebBaseLoader
|
||||
|
||||
def load_data(self, inputs):
|
||||
urls = inputs
|
||||
|
||||
if isinstance(urls, str):
|
||||
urls = [urls] # Convert string to list if a single URL is passed
|
||||
|
||||
urls = [urls]
|
||||
documents = []
|
||||
for url in urls:
|
||||
try:
|
||||
loader = self.loader([url]) # Process URLs one by one
|
||||
loader = self.loader([url], header_template=headers)
|
||||
documents.extend(loader.load())
|
||||
except Exception as e:
|
||||
print(f"Error processing URL {url}: {e}")
|
||||
continue # Continue with the next URL if an error occurs
|
||||
return documents
|
||||
continue
|
||||
return documents
|
||||
|
||||
@@ -22,7 +22,10 @@ def group_documents(documents: List[Document], min_tokens: int, max_tokens: int)
|
||||
doc_len = len(tiktoken.get_encoding("cl100k_base").encode(doc.text))
|
||||
|
||||
# Check if current group is empty or if the document can be added based on token count and matching metadata
|
||||
if current_group is None or (len(tiktoken.get_encoding("cl100k_base").encode(current_group.text)) + doc_len < max_tokens and doc_len < min_tokens and current_group.extra_info == doc.extra_info):
|
||||
if (current_group is None or
|
||||
(len(tiktoken.get_encoding("cl100k_base").encode(current_group.text)) + doc_len < max_tokens and
|
||||
doc_len < min_tokens and
|
||||
current_group.extra_info == doc.extra_info)):
|
||||
if current_group is None:
|
||||
current_group = doc # Use the document directly to retain its metadata
|
||||
else:
|
||||
|
||||
@@ -3,32 +3,32 @@ boto3==1.34.6
|
||||
celery==5.3.6
|
||||
dataclasses_json==0.6.3
|
||||
docx2txt==0.8
|
||||
duckduckgo-search==5.3.0
|
||||
EbookLib==0.18
|
||||
elasticsearch==8.12.0
|
||||
escodegen==1.0.11
|
||||
esprima==4.0.1
|
||||
faiss-cpu==1.7.4
|
||||
Flask==3.0.1
|
||||
gunicorn==21.2.0
|
||||
gunicorn==22.0.0
|
||||
html2text==2020.1.16
|
||||
javalang==0.13.0
|
||||
langchain==0.1.4
|
||||
langchain-openai==0.0.5
|
||||
nltk==3.8.1
|
||||
openapi3_parser==1.1.16
|
||||
pandas==2.2.0
|
||||
pydantic_settings==2.1.0
|
||||
pymongo==4.6.1
|
||||
pymongo==4.6.3
|
||||
PyPDF2==3.0.1
|
||||
python-dotenv==1.0.1
|
||||
qdrant-client==1.7.3
|
||||
qdrant-client==1.9.0
|
||||
redis==5.0.1
|
||||
Requests==2.31.0
|
||||
Requests==2.32.0
|
||||
retry==0.9.2
|
||||
sentence-transformers
|
||||
tiktoken==0.5.2
|
||||
torch==2.1.2
|
||||
tqdm==4.66.1
|
||||
tiktoken
|
||||
torch
|
||||
tqdm==4.66.3
|
||||
transformers==4.36.2
|
||||
unstructured==0.12.2
|
||||
Werkzeug==3.0.1
|
||||
Werkzeug==3.0.3
|
||||
|
||||
0
application/retriever/__init__.py
Normal file
14
application/retriever/base.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class BaseRetriever(ABC):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def gen(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def search(self, *args, **kwargs):
|
||||
pass
|
||||
103
application/retriever/brave_search.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import json
|
||||
from application.retriever.base import BaseRetriever
|
||||
from application.core.settings import settings
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.utils import count_tokens
|
||||
from langchain_community.tools import BraveSearch
|
||||
|
||||
|
||||
class BraveRetSearch(BaseRetriever):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
question,
|
||||
source,
|
||||
chat_history,
|
||||
prompt,
|
||||
chunks=2,
|
||||
token_limit=150,
|
||||
gpt_model="docsgpt",
|
||||
user_api_key=None,
|
||||
):
|
||||
self.question = question
|
||||
self.source = source
|
||||
self.chat_history = chat_history
|
||||
self.prompt = prompt
|
||||
self.chunks = chunks
|
||||
self.gpt_model = gpt_model
|
||||
self.token_limit = (
|
||||
token_limit
|
||||
if token_limit
|
||||
< settings.MODEL_TOKEN_LIMITS.get(
|
||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||
)
|
||||
else settings.MODEL_TOKEN_LIMITS.get(
|
||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||
)
|
||||
)
|
||||
self.user_api_key = user_api_key
|
||||
|
||||
def _get_data(self):
|
||||
if self.chunks == 0:
|
||||
docs = []
|
||||
else:
|
||||
search = BraveSearch.from_api_key(
|
||||
api_key=settings.BRAVE_SEARCH_API_KEY,
|
||||
search_kwargs={"count": int(self.chunks)},
|
||||
)
|
||||
results = search.run(self.question)
|
||||
results = json.loads(results)
|
||||
|
||||
docs = []
|
||||
for i in results:
|
||||
try:
|
||||
title = i["title"]
|
||||
link = i["link"]
|
||||
snippet = i["snippet"]
|
||||
docs.append({"text": snippet, "title": title, "link": link})
|
||||
except IndexError:
|
||||
pass
|
||||
if settings.LLM_NAME == "llama.cpp":
|
||||
docs = [docs[0]]
|
||||
|
||||
return docs
|
||||
|
||||
def gen(self):
|
||||
docs = self._get_data()
|
||||
|
||||
# join all page_content together with a newline
|
||||
docs_together = "\n".join([doc["text"] for doc in docs])
|
||||
p_chat_combine = self.prompt.replace("{summaries}", docs_together)
|
||||
messages_combine = [{"role": "system", "content": p_chat_combine}]
|
||||
for doc in docs:
|
||||
yield {"source": doc}
|
||||
|
||||
if len(self.chat_history) > 1:
|
||||
tokens_current_history = 0
|
||||
# count tokens in history
|
||||
self.chat_history.reverse()
|
||||
for i in self.chat_history:
|
||||
if "prompt" in i and "response" in i:
|
||||
tokens_batch = count_tokens(i["prompt"]) + count_tokens(
|
||||
i["response"]
|
||||
)
|
||||
if tokens_current_history + tokens_batch < self.token_limit:
|
||||
tokens_current_history += tokens_batch
|
||||
messages_combine.append(
|
||||
{"role": "user", "content": i["prompt"]}
|
||||
)
|
||||
messages_combine.append(
|
||||
{"role": "system", "content": i["response"]}
|
||||
)
|
||||
messages_combine.append({"role": "user", "content": self.question})
|
||||
|
||||
llm = LLMCreator.create_llm(
|
||||
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=self.user_api_key
|
||||
)
|
||||
|
||||
completion = llm.gen_stream(model=self.gpt_model, messages=messages_combine)
|
||||
for line in completion:
|
||||
yield {"answer": str(line)}
|
||||
|
||||
def search(self):
|
||||
return self._get_data()
|
||||
123
application/retriever/classic_rag.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import os
|
||||
from application.retriever.base import BaseRetriever
|
||||
from application.core.settings import settings
|
||||
from application.vectorstore.vector_creator import VectorCreator
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
|
||||
from application.utils import count_tokens
|
||||
|
||||
|
||||
class ClassicRAG(BaseRetriever):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
question,
|
||||
source,
|
||||
chat_history,
|
||||
prompt,
|
||||
chunks=2,
|
||||
token_limit=150,
|
||||
gpt_model="docsgpt",
|
||||
user_api_key=None,
|
||||
):
|
||||
self.question = question
|
||||
self.vectorstore = self._get_vectorstore(source=source)
|
||||
self.chat_history = chat_history
|
||||
self.prompt = prompt
|
||||
self.chunks = chunks
|
||||
self.gpt_model = gpt_model
|
||||
self.token_limit = (
|
||||
token_limit
|
||||
if token_limit
|
||||
< settings.MODEL_TOKEN_LIMITS.get(
|
||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||
)
|
||||
else settings.MODEL_TOKEN_LIMITS.get(
|
||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||
)
|
||||
)
|
||||
self.user_api_key = user_api_key
|
||||
|
||||
def _get_vectorstore(self, source):
|
||||
if "active_docs" in source:
|
||||
if source["active_docs"].split("/")[0] == "default":
|
||||
vectorstore = ""
|
||||
elif source["active_docs"].split("/")[0] == "local":
|
||||
vectorstore = "indexes/" + source["active_docs"]
|
||||
else:
|
||||
vectorstore = "vectors/" + source["active_docs"]
|
||||
if source["active_docs"] == "default":
|
||||
vectorstore = ""
|
||||
else:
|
||||
vectorstore = ""
|
||||
vectorstore = os.path.join("application", vectorstore)
|
||||
return vectorstore
|
||||
|
||||
def _get_data(self):
|
||||
if self.chunks == 0:
|
||||
docs = []
|
||||
else:
|
||||
docsearch = VectorCreator.create_vectorstore(
|
||||
settings.VECTOR_STORE, self.vectorstore, settings.EMBEDDINGS_KEY
|
||||
)
|
||||
docs_temp = docsearch.search(self.question, k=self.chunks)
|
||||
docs = [
|
||||
{
|
||||
"title": (
|
||||
i.metadata["title"].split("/")[-1]
|
||||
if i.metadata
|
||||
else i.page_content
|
||||
),
|
||||
"text": i.page_content,
|
||||
"source": (
|
||||
i.metadata.get("source")
|
||||
if i.metadata.get("source")
|
||||
else "local"
|
||||
),
|
||||
}
|
||||
for i in docs_temp
|
||||
]
|
||||
if settings.LLM_NAME == "llama.cpp":
|
||||
docs = [docs[0]]
|
||||
|
||||
return docs
|
||||
|
||||
def gen(self):
|
||||
docs = self._get_data()
|
||||
|
||||
# join all page_content together with a newline
|
||||
docs_together = "\n".join([doc["text"] for doc in docs])
|
||||
p_chat_combine = self.prompt.replace("{summaries}", docs_together)
|
||||
messages_combine = [{"role": "system", "content": p_chat_combine}]
|
||||
for doc in docs:
|
||||
yield {"source": doc}
|
||||
|
||||
if len(self.chat_history) > 1:
|
||||
tokens_current_history = 0
|
||||
# count tokens in history
|
||||
self.chat_history.reverse()
|
||||
for i in self.chat_history:
|
||||
if "prompt" in i and "response" in i:
|
||||
tokens_batch = count_tokens(i["prompt"]) + count_tokens(
|
||||
i["response"]
|
||||
)
|
||||
if tokens_current_history + tokens_batch < self.token_limit:
|
||||
tokens_current_history += tokens_batch
|
||||
messages_combine.append(
|
||||
{"role": "user", "content": i["prompt"]}
|
||||
)
|
||||
messages_combine.append(
|
||||
{"role": "system", "content": i["response"]}
|
||||
)
|
||||
messages_combine.append({"role": "user", "content": self.question})
|
||||
|
||||
llm = LLMCreator.create_llm(
|
||||
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=self.user_api_key
|
||||
)
|
||||
|
||||
completion = llm.gen_stream(model=self.gpt_model, messages=messages_combine)
|
||||
for line in completion:
|
||||
yield {"answer": str(line)}
|
||||
|
||||
def search(self):
|
||||
return self._get_data()
|
||||
120
application/retriever/duckduck_search.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from application.retriever.base import BaseRetriever
|
||||
from application.core.settings import settings
|
||||
from application.llm.llm_creator import LLMCreator
|
||||
from application.utils import count_tokens
|
||||
from langchain_community.tools import DuckDuckGoSearchResults
|
||||
from langchain_community.utilities import DuckDuckGoSearchAPIWrapper
|
||||
|
||||
|
||||
class DuckDuckSearch(BaseRetriever):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
question,
|
||||
source,
|
||||
chat_history,
|
||||
prompt,
|
||||
chunks=2,
|
||||
token_limit=150,
|
||||
gpt_model="docsgpt",
|
||||
user_api_key=None,
|
||||
):
|
||||
self.question = question
|
||||
self.source = source
|
||||
self.chat_history = chat_history
|
||||
self.prompt = prompt
|
||||
self.chunks = chunks
|
||||
self.gpt_model = gpt_model
|
||||
self.token_limit = (
|
||||
token_limit
|
||||
if token_limit
|
||||
< settings.MODEL_TOKEN_LIMITS.get(
|
||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||
)
|
||||
else settings.MODEL_TOKEN_LIMITS.get(
|
||||
self.gpt_model, settings.DEFAULT_MAX_HISTORY
|
||||
)
|
||||
)
|
||||
self.user_api_key = user_api_key
|
||||
|
||||
def _parse_lang_string(self, input_string):
|
||||
result = []
|
||||
current_item = ""
|
||||
inside_brackets = False
|
||||
for char in input_string:
|
||||
if char == "[":
|
||||
inside_brackets = True
|
||||
elif char == "]":
|
||||
inside_brackets = False
|
||||
result.append(current_item)
|
||||
current_item = ""
|
||||
elif inside_brackets:
|
||||
current_item += char
|
||||
|
||||
if inside_brackets:
|
||||
result.append(current_item)
|
||||
|
||||
return result
|
||||
|
||||
def _get_data(self):
|
||||
if self.chunks == 0:
|
||||
docs = []
|
||||
else:
|
||||
wrapper = DuckDuckGoSearchAPIWrapper(max_results=self.chunks)
|
||||
search = DuckDuckGoSearchResults(api_wrapper=wrapper)
|
||||
results = search.run(self.question)
|
||||
results = self._parse_lang_string(results)
|
||||
|
||||
docs = []
|
||||
for i in results:
|
||||
try:
|
||||
text = i.split("title:")[0]
|
||||
title = i.split("title:")[1].split("link:")[0]
|
||||
link = i.split("link:")[1]
|
||||
docs.append({"text": text, "title": title, "link": link})
|
||||
except IndexError:
|
||||
pass
|
||||
if settings.LLM_NAME == "llama.cpp":
|
||||
docs = [docs[0]]
|
||||
|
||||
return docs
|
||||
|
||||
def gen(self):
|
||||
docs = self._get_data()
|
||||
|
||||
# join all page_content together with a newline
|
||||
docs_together = "\n".join([doc["text"] for doc in docs])
|
||||
p_chat_combine = self.prompt.replace("{summaries}", docs_together)
|
||||
messages_combine = [{"role": "system", "content": p_chat_combine}]
|
||||
for doc in docs:
|
||||
yield {"source": doc}
|
||||
|
||||
if len(self.chat_history) > 1:
|
||||
tokens_current_history = 0
|
||||
# count tokens in history
|
||||
self.chat_history.reverse()
|
||||
for i in self.chat_history:
|
||||
if "prompt" in i and "response" in i:
|
||||
tokens_batch = count_tokens(i["prompt"]) + count_tokens(
|
||||
i["response"]
|
||||
)
|
||||
if tokens_current_history + tokens_batch < self.token_limit:
|
||||
tokens_current_history += tokens_batch
|
||||
messages_combine.append(
|
||||
{"role": "user", "content": i["prompt"]}
|
||||
)
|
||||
messages_combine.append(
|
||||
{"role": "system", "content": i["response"]}
|
||||
)
|
||||
messages_combine.append({"role": "user", "content": self.question})
|
||||
|
||||
llm = LLMCreator.create_llm(
|
||||
settings.LLM_NAME, api_key=settings.API_KEY, user_api_key=self.user_api_key
|
||||
)
|
||||
|
||||
completion = llm.gen_stream(model=self.gpt_model, messages=messages_combine)
|
||||
for line in completion:
|
||||
yield {"answer": str(line)}
|
||||
|
||||
def search(self):
|
||||
return self._get_data()
|
||||
19
application/retriever/retriever_creator.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from application.retriever.classic_rag import ClassicRAG
|
||||
from application.retriever.duckduck_search import DuckDuckSearch
|
||||
from application.retriever.brave_search import BraveRetSearch
|
||||
|
||||
|
||||
|
||||
class RetrieverCreator:
|
||||
retievers = {
|
||||
'classic': ClassicRAG,
|
||||
'duckduck_search': DuckDuckSearch,
|
||||
'brave_search': BraveRetSearch
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_retriever(cls, type, *args, **kwargs):
|
||||
retiever_class = cls.retievers.get(type.lower())
|
||||
if not retiever_class:
|
||||
raise ValueError(f"No retievers class found for type {type}")
|
||||
return retiever_class(*args, **kwargs)
|
||||
49
application/usage.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import sys
|
||||
from pymongo import MongoClient
|
||||
from datetime import datetime
|
||||
from application.core.settings import settings
|
||||
from application.utils import count_tokens
|
||||
|
||||
mongo = MongoClient(settings.MONGO_URI)
|
||||
db = mongo["docsgpt"]
|
||||
usage_collection = db["token_usage"]
|
||||
|
||||
|
||||
def update_token_usage(user_api_key, token_usage):
|
||||
if "pytest" in sys.modules:
|
||||
return
|
||||
usage_data = {
|
||||
"api_key": user_api_key,
|
||||
"prompt_tokens": token_usage["prompt_tokens"],
|
||||
"generated_tokens": token_usage["generated_tokens"],
|
||||
"timestamp": datetime.now(),
|
||||
}
|
||||
usage_collection.insert_one(usage_data)
|
||||
|
||||
|
||||
def gen_token_usage(func):
|
||||
def wrapper(self, model, messages, stream, **kwargs):
|
||||
for message in messages:
|
||||
self.token_usage["prompt_tokens"] += count_tokens(message["content"])
|
||||
result = func(self, model, messages, stream, **kwargs)
|
||||
self.token_usage["generated_tokens"] += count_tokens(result)
|
||||
update_token_usage(self.user_api_key, self.token_usage)
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def stream_token_usage(func):
|
||||
def wrapper(self, model, messages, stream, **kwargs):
|
||||
for message in messages:
|
||||
self.token_usage["prompt_tokens"] += count_tokens(message["content"])
|
||||
batch = []
|
||||
result = func(self, model, messages, stream, **kwargs)
|
||||
for r in result:
|
||||
batch.append(r)
|
||||
yield r
|
||||
for line in batch:
|
||||
self.token_usage["generated_tokens"] += count_tokens(line)
|
||||
update_token_usage(self.user_api_key, self.token_usage)
|
||||
|
||||
return wrapper
|
||||
6
application/utils.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from transformers import GPT2TokenizerFast
|
||||
|
||||
tokenizer = GPT2TokenizerFast.from_pretrained('gpt2')
|
||||
tokenizer.model_max_length = 100000
|
||||
def count_tokens(string):
|
||||
return len(tokenizer(string)['input_ids'])
|
||||
@@ -8,6 +8,32 @@ from langchain_community.embeddings import (
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
from application.core.settings import settings
|
||||
|
||||
class EmbeddingsSingleton:
|
||||
_instances = {}
|
||||
|
||||
@staticmethod
|
||||
def get_instance(embeddings_name, *args, **kwargs):
|
||||
if embeddings_name not in EmbeddingsSingleton._instances:
|
||||
EmbeddingsSingleton._instances[embeddings_name] = EmbeddingsSingleton._create_instance(
|
||||
embeddings_name, *args, **kwargs
|
||||
)
|
||||
return EmbeddingsSingleton._instances[embeddings_name]
|
||||
|
||||
@staticmethod
|
||||
def _create_instance(embeddings_name, *args, **kwargs):
|
||||
embeddings_factory = {
|
||||
"openai_text-embedding-ada-002": OpenAIEmbeddings,
|
||||
"huggingface_sentence-transformers/all-mpnet-base-v2": HuggingFaceEmbeddings,
|
||||
"huggingface_sentence-transformers-all-mpnet-base-v2": HuggingFaceEmbeddings,
|
||||
"huggingface_hkunlp/instructor-large": HuggingFaceInstructEmbeddings,
|
||||
"cohere_medium": CohereEmbeddings
|
||||
}
|
||||
|
||||
if embeddings_name not in embeddings_factory:
|
||||
raise ValueError(f"Invalid embeddings_name: {embeddings_name}")
|
||||
|
||||
return embeddings_factory[embeddings_name](*args, **kwargs)
|
||||
|
||||
class BaseVectorStore(ABC):
|
||||
def __init__(self):
|
||||
pass
|
||||
@@ -20,37 +46,36 @@ class BaseVectorStore(ABC):
|
||||
return settings.OPENAI_API_BASE and settings.OPENAI_API_VERSION and settings.AZURE_DEPLOYMENT_NAME
|
||||
|
||||
def _get_embeddings(self, embeddings_name, embeddings_key=None):
|
||||
embeddings_factory = {
|
||||
"openai_text-embedding-ada-002": OpenAIEmbeddings,
|
||||
"huggingface_sentence-transformers/all-mpnet-base-v2": HuggingFaceEmbeddings,
|
||||
"huggingface_hkunlp/instructor-large": HuggingFaceInstructEmbeddings,
|
||||
"cohere_medium": CohereEmbeddings
|
||||
}
|
||||
|
||||
if embeddings_name not in embeddings_factory:
|
||||
raise ValueError(f"Invalid embeddings_name: {embeddings_name}")
|
||||
|
||||
if embeddings_name == "openai_text-embedding-ada-002":
|
||||
if self.is_azure_configured():
|
||||
os.environ["OPENAI_API_TYPE"] = "azure"
|
||||
embedding_instance = embeddings_factory[embeddings_name](
|
||||
embedding_instance = EmbeddingsSingleton.get_instance(
|
||||
embeddings_name,
|
||||
model=settings.AZURE_EMBEDDINGS_DEPLOYMENT_NAME
|
||||
)
|
||||
else:
|
||||
embedding_instance = embeddings_factory[embeddings_name](
|
||||
embedding_instance = EmbeddingsSingleton.get_instance(
|
||||
embeddings_name,
|
||||
openai_api_key=embeddings_key
|
||||
)
|
||||
elif embeddings_name == "cohere_medium":
|
||||
embedding_instance = embeddings_factory[embeddings_name](
|
||||
embedding_instance = EmbeddingsSingleton.get_instance(
|
||||
embeddings_name,
|
||||
cohere_api_key=embeddings_key
|
||||
)
|
||||
elif embeddings_name == "huggingface_sentence-transformers/all-mpnet-base-v2":
|
||||
embedding_instance = embeddings_factory[embeddings_name](
|
||||
#model_name="./model/all-mpnet-base-v2",
|
||||
model_kwargs={"device": "cpu"},
|
||||
)
|
||||
if os.path.exists("./model/all-mpnet-base-v2"):
|
||||
embedding_instance = EmbeddingsSingleton.get_instance(
|
||||
embeddings_name,
|
||||
model_name="./model/all-mpnet-base-v2",
|
||||
model_kwargs={"device": "cpu"}
|
||||
)
|
||||
else:
|
||||
embedding_instance = EmbeddingsSingleton.get_instance(
|
||||
embeddings_name,
|
||||
model_kwargs={"device": "cpu"}
|
||||
)
|
||||
else:
|
||||
embedding_instance = embeddings_factory[embeddings_name]()
|
||||
|
||||
return embedding_instance
|
||||
embedding_instance = EmbeddingsSingleton.get_instance(embeddings_name)
|
||||
|
||||
return embedding_instance
|
||||
195
application/worker.py
Normal file → Executable file
@@ -2,9 +2,9 @@ import os
|
||||
import shutil
|
||||
import string
|
||||
import zipfile
|
||||
import tiktoken
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import nltk
|
||||
import requests
|
||||
|
||||
from application.core.settings import settings
|
||||
@@ -14,24 +14,48 @@ from application.parser.open_ai_func import call_openai_api
|
||||
from application.parser.schema.base import Document
|
||||
from application.parser.token_func import group_split
|
||||
|
||||
try:
|
||||
nltk.download('punkt', quiet=True)
|
||||
nltk.download('averaged_perceptron_tagger', quiet=True)
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
|
||||
# Define a function to extract metadata from a given filename.
|
||||
def metadata_from_filename(title):
|
||||
store = '/'.join(title.split('/')[1:3])
|
||||
return {'title': title, 'store': store}
|
||||
store = "/".join(title.split("/")[1:3])
|
||||
return {"title": title, "store": store}
|
||||
|
||||
|
||||
# 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)])
|
||||
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.
|
||||
|
||||
Args:
|
||||
zip_path (str): Path to the zip file to be extracted.
|
||||
extract_to (str): Destination path for extracted files.
|
||||
current_depth (int): Current depth of recursion.
|
||||
max_depth (int): Maximum allowed depth of recursion to prevent infinite loops.
|
||||
"""
|
||||
if current_depth > max_depth:
|
||||
print(f"Reached maximum recursion depth of {max_depth}")
|
||||
return
|
||||
|
||||
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
||||
zip_ref.extractall(extract_to)
|
||||
os.remove(zip_path) # Remove the zip file after extracting
|
||||
|
||||
# Check for nested zip files and extract them
|
||||
for root, dirs, files in os.walk(extract_to):
|
||||
for file in files:
|
||||
if file.endswith(".zip"):
|
||||
# If a nested zip file is found, extract it recursively
|
||||
file_path = os.path.join(root, file)
|
||||
extract_zip_recursive(file_path, root, current_depth + 1, max_depth)
|
||||
|
||||
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# Define the main function for ingesting and processing documents.
|
||||
def ingest_worker(self, directory, formats, name_job, filename, user):
|
||||
@@ -62,38 +86,54 @@ def ingest_worker(self, directory, formats, name_job, filename, user):
|
||||
token_check = True
|
||||
min_tokens = 150
|
||||
max_tokens = 1250
|
||||
full_path = directory + '/' + user + '/' + name_job
|
||||
recursion_depth = 2
|
||||
full_path = os.path.join(directory, user, name_job)
|
||||
import sys
|
||||
|
||||
print(full_path, file=sys.stderr)
|
||||
# check if API_URL env variable is set
|
||||
file_data = {'name': name_job, 'file': filename, 'user': user}
|
||||
response = requests.get(urljoin(settings.API_URL, "/api/download"), params=file_data)
|
||||
file_data = {"name": name_job, "file": filename, "user": user}
|
||||
response = requests.get(
|
||||
urljoin(settings.API_URL, "/api/download"), params=file_data
|
||||
)
|
||||
# check if file is in the response
|
||||
print(response, file=sys.stderr)
|
||||
file = response.content
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
os.makedirs(full_path)
|
||||
with open(full_path + '/' + filename, 'wb') as f:
|
||||
with open(os.path.join(full_path, filename), "wb") as f:
|
||||
f.write(file)
|
||||
|
||||
# check if file is .zip and extract it
|
||||
if filename.endswith('.zip'):
|
||||
with zipfile.ZipFile(full_path + '/' + filename, 'r') as zip_ref:
|
||||
zip_ref.extractall(full_path)
|
||||
os.remove(full_path + '/' + filename)
|
||||
if filename.endswith(".zip"):
|
||||
extract_zip_recursive(
|
||||
os.path.join(full_path, filename), full_path, 0, recursion_depth
|
||||
)
|
||||
|
||||
self.update_state(state='PROGRESS', meta={'current': 1})
|
||||
self.update_state(state="PROGRESS", meta={"current": 1})
|
||||
|
||||
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()
|
||||
raw_docs = group_split(documents=raw_docs, min_tokens=min_tokens, max_tokens=max_tokens, token_check=token_check)
|
||||
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()
|
||||
raw_docs = group_split(
|
||||
documents=raw_docs,
|
||||
min_tokens=min_tokens,
|
||||
max_tokens=max_tokens,
|
||||
token_check=token_check,
|
||||
)
|
||||
|
||||
docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
|
||||
|
||||
call_openai_api(docs, full_path, self)
|
||||
self.update_state(state='PROGRESS', meta={'current': 100})
|
||||
tokens = count_tokens_docs(docs)
|
||||
self.update_state(state="PROGRESS", meta={"current": 100})
|
||||
|
||||
if sample:
|
||||
for i in range(min(5, len(raw_docs))):
|
||||
@@ -101,70 +141,97 @@ def ingest_worker(self, directory, formats, name_job, filename, user):
|
||||
|
||||
# get files from outputs/inputs/index.faiss and outputs/inputs/index.pkl
|
||||
# and send them to the server (provide user and name in form)
|
||||
file_data = {'name': name_job, 'user': user}
|
||||
file_data = {"name": name_job, "user": user, "tokens":tokens}
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
files = {'file_faiss': open(full_path + '/index.faiss', 'rb'),
|
||||
'file_pkl': open(full_path + '/index.pkl', 'rb')}
|
||||
response = requests.post(urljoin(settings.API_URL, "/api/upload_index"), files=files, data=file_data)
|
||||
response = requests.get(urljoin(settings.API_URL, "/api/delete_old?path=" + full_path))
|
||||
files = {
|
||||
"file_faiss": open(full_path + "/index.faiss", "rb"),
|
||||
"file_pkl": open(full_path + "/index.pkl", "rb"),
|
||||
}
|
||||
response = requests.post(
|
||||
urljoin(settings.API_URL, "/api/upload_index"), files=files, data=file_data
|
||||
)
|
||||
response = requests.get(
|
||||
urljoin(settings.API_URL, "/api/delete_old?path=" + full_path)
|
||||
)
|
||||
else:
|
||||
response = requests.post(urljoin(settings.API_URL, "/api/upload_index"), data=file_data)
|
||||
response = requests.post(
|
||||
urljoin(settings.API_URL, "/api/upload_index"), data=file_data
|
||||
)
|
||||
|
||||
|
||||
# delete local
|
||||
shutil.rmtree(full_path)
|
||||
|
||||
return {
|
||||
'directory': directory,
|
||||
'formats': formats,
|
||||
'name_job': name_job,
|
||||
'filename': filename,
|
||||
'user': user,
|
||||
'limited': False
|
||||
"directory": directory,
|
||||
"formats": formats,
|
||||
"name_job": name_job,
|
||||
"filename": filename,
|
||||
"user": user,
|
||||
"limited": False,
|
||||
}
|
||||
|
||||
def remote_worker(self, source_data, name_job, user, directory = 'temp', loader = 'url'):
|
||||
# sample = False
|
||||
|
||||
def remote_worker(self, source_data, name_job, user, loader, directory="temp"):
|
||||
token_check = True
|
||||
min_tokens = 150
|
||||
max_tokens = 1250
|
||||
full_path = directory + '/' + user + '/' + name_job
|
||||
full_path = directory + "/" + user + "/" + name_job
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
os.makedirs(full_path)
|
||||
self.update_state(state="PROGRESS", meta={"current": 1})
|
||||
|
||||
self.update_state(state='PROGRESS', meta={'current': 1})
|
||||
|
||||
# source_data {"data": [url]} for url type task just urls
|
||||
|
||||
# Use RemoteCreator to load data from URL
|
||||
remote_loader = RemoteCreator.create_loader(loader)
|
||||
raw_docs = remote_loader.load_data(source_data)
|
||||
|
||||
docs = group_split(documents=raw_docs, min_tokens=min_tokens, max_tokens=max_tokens, token_check=token_check)
|
||||
|
||||
#docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
|
||||
|
||||
docs = group_split(
|
||||
documents=raw_docs,
|
||||
min_tokens=min_tokens,
|
||||
max_tokens=max_tokens,
|
||||
token_check=token_check,
|
||||
)
|
||||
# docs = [Document.to_langchain_format(raw_doc) for raw_doc in raw_docs]
|
||||
call_openai_api(docs, full_path, self)
|
||||
self.update_state(state='PROGRESS', meta={'current': 100})
|
||||
|
||||
|
||||
tokens = count_tokens_docs(docs)
|
||||
self.update_state(state="PROGRESS", meta={"current": 100})
|
||||
|
||||
# Proceed with uploading and cleaning as in the original function
|
||||
file_data = {'name': name_job, 'user': user}
|
||||
file_data = {"name": name_job, "user": user, "tokens":tokens}
|
||||
if settings.VECTOR_STORE == "faiss":
|
||||
files = {'file_faiss': open(full_path + '/index.faiss', 'rb'),
|
||||
'file_pkl': open(full_path + '/index.pkl', 'rb')}
|
||||
requests.post(urljoin(settings.API_URL, "/api/upload_index"), files=files, data=file_data)
|
||||
files = {
|
||||
"file_faiss": open(full_path + "/index.faiss", "rb"),
|
||||
"file_pkl": open(full_path + "/index.pkl", "rb"),
|
||||
}
|
||||
|
||||
requests.post(
|
||||
urljoin(settings.API_URL, "/api/upload_index"), files=files, data=file_data
|
||||
)
|
||||
requests.get(urljoin(settings.API_URL, "/api/delete_old?path=" + full_path))
|
||||
else:
|
||||
requests.post(urljoin(settings.API_URL, "/api/upload_index"), data=file_data)
|
||||
|
||||
shutil.rmtree(full_path)
|
||||
|
||||
return {
|
||||
'urls': source_data,
|
||||
'name_job': name_job,
|
||||
'user': user,
|
||||
'limited': False
|
||||
}
|
||||
return {"urls": source_data, "name_job": name_job, "user": user, "limited": False}
|
||||
|
||||
|
||||
def count_tokens_docs(docs):
|
||||
# Here we convert the docs list to a string and calculate the number of tokens the string represents.
|
||||
# docs_content = (" ".join(docs))
|
||||
docs_content = ""
|
||||
for doc in docs:
|
||||
docs_content += doc.page_content
|
||||
|
||||
tokens, total_price = num_tokens_from_string(
|
||||
string=docs_content, encoding_name="cl100k_base"
|
||||
)
|
||||
# Here we print the number of tokens and the approx user cost with some visually appealing formatting.
|
||||
return tokens
|
||||
|
||||
|
||||
def num_tokens_from_string(string: str, encoding_name: str) -> int:
|
||||
# Function to convert string to tokens and estimate user cost.
|
||||
encoding = tiktoken.get_encoding(encoding_name)
|
||||
num_tokens = len(encoding.encode(string))
|
||||
total_price = (num_tokens / 1000) * 0.0004
|
||||
return num_tokens, total_price
|
||||
@@ -1,4 +1,5 @@
|
||||
from application.app import app
|
||||
from application.core.settings import settings
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, port=7091)
|
||||
app.run(debug=settings.FLASK_DEBUG_MODE, port=7091)
|
||||
|
||||
527
docs/package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"dependencies": {
|
||||
"@vercel/analytics": "^1.1.1",
|
||||
"docsgpt": "^0.3.7",
|
||||
"next": "^14.0.4",
|
||||
"next": "^14.1.1",
|
||||
"nextra": "^2.13.2",
|
||||
"nextra-theme-docs": "^2.13.2",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -227,3 +227,124 @@ JSON response indicating the status of the operation:
|
||||
```json
|
||||
{ "status": "ok" }
|
||||
```
|
||||
|
||||
### 7. /api/get_api_keys
|
||||
**Description:**
|
||||
|
||||
The endpoint retrieves a list of API keys for the user.
|
||||
|
||||
**Request:**
|
||||
|
||||
**Method**: `GET`
|
||||
|
||||
**Sample JavaScript Fetch Request:**
|
||||
```js
|
||||
// get_api_keys (GET http://127.0.0.1:5000/api/get_api_keys)
|
||||
fetch("http://localhost:5001/api/get_api_keys", {
|
||||
"method": "GET",
|
||||
"headers": {
|
||||
"Content-Type": "application/json; charset=utf-8"
|
||||
},
|
||||
})
|
||||
.then((res) => res.text())
|
||||
.then(console.log.bind(console))
|
||||
|
||||
```
|
||||
**Response:**
|
||||
|
||||
JSON response with a list of created API keys:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"key": "string",
|
||||
"source": "string"
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
### 8. /api/create_api_key
|
||||
|
||||
**Description:**
|
||||
|
||||
Create a new API key for the user.
|
||||
|
||||
**Request:**
|
||||
|
||||
**Method**: `POST`
|
||||
|
||||
**Headers**: Content-Type should be set to `application/json; charset=utf-8`
|
||||
|
||||
**Request Body**: JSON object with the following fields:
|
||||
* `name` — A name for the API key.
|
||||
* `source` — The source documents that will be used.
|
||||
* `prompt_id` — The prompt ID.
|
||||
* `chunks` — The number of chunks used to process an answer.
|
||||
|
||||
Here is a JavaScript Fetch Request example:
|
||||
```js
|
||||
// create_api_key (POST http://127.0.0.1:5000/api/create_api_key)
|
||||
fetch("http://127.0.0.1:5000/api/create_api_key", {
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Content-Type": "application/json; charset=utf-8"
|
||||
},
|
||||
"body": JSON.stringify({"name":"Example Key Name",
|
||||
"source":"Example Source",
|
||||
"prompt_id":"creative",
|
||||
"chunks":"2"})
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(console.log.bind(console))
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
In response, you will get a JSON document containing the `id` and `key`:
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"key": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### 9. /api/delete_api_key
|
||||
|
||||
**Description:**
|
||||
|
||||
Delete an API key for the user.
|
||||
|
||||
**Request:**
|
||||
|
||||
**Method**: `POST`
|
||||
|
||||
**Headers**: Content-Type should be set to `application/json; charset=utf-8`
|
||||
|
||||
**Request Body**: JSON object with the field:
|
||||
* `id` — The unique identifier of the API key to be deleted.
|
||||
|
||||
Here is a JavaScript Fetch Request example:
|
||||
```js
|
||||
// delete_api_key (POST http://127.0.0.1:5000/api/delete_api_key)
|
||||
fetch("http://127.0.0.1:5000/api/delete_api_key", {
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Content-Type": "application/json; charset=utf-8"
|
||||
},
|
||||
"body": JSON.stringify({"id":"API_KEY_ID"})
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(console.log.bind(console))
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
In response, you will get a JSON document indicating the status of the operation:
|
||||
```json
|
||||
{
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
10
docs/pages/API/_meta.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"API-docs": {
|
||||
"title": "🗂️️ API-docs",
|
||||
"href": "/API/API-docs"
|
||||
},
|
||||
"api-key-guide": {
|
||||
"title": "🔐 API Keys guide",
|
||||
"href": "/API/api-key-guide"
|
||||
}
|
||||
}
|
||||
30
docs/pages/API/api-key-guide.md
Normal file
@@ -0,0 +1,30 @@
|
||||
## Guide to DocsGPT API Keys
|
||||
|
||||
DocsGPT API keys are essential for developers and users who wish to integrate the DocsGPT models into external applications, such as the our widget. This guide will walk you through the steps of obtaining an API key, starting from uploading your document to understanding the key variables associated with API keys.
|
||||
|
||||
### Uploading Your Document
|
||||
|
||||
Before creating your first API key, you must upload the document that will be linked to this key. You can upload your document through two methods:
|
||||
|
||||
- **GUI Web App Upload:** A user-friendly graphical interface that allows for easy upload and management of documents.
|
||||
- **Using `/api/upload` Method:** For users comfortable with API calls, this method provides a direct way to upload documents.
|
||||
|
||||
### Obtaining Your API Key
|
||||
|
||||
After uploading your document, you can obtain an API key either through the graphical user interface or via an API call:
|
||||
|
||||
- **Graphical User Interface:** Navigate to the Settings section of the DocsGPT web app, find the API Keys option, and press 'Create New' to generate your key.
|
||||
- **API Call:** Alternatively, you can use the `/api/create_api_key` endpoint to create a new API key. For detailed instructions, visit [DocsGPT API Documentation](https://docs.docsgpt.cloud/API/API-docs#8-apicreate_api_key).
|
||||
|
||||
### Understanding Key Variables
|
||||
|
||||
Upon creating your API key, you will encounter several key variables. Each serves a specific purpose:
|
||||
|
||||
- **Name:** Assign a name to your API key for easy identification.
|
||||
- **Source:** Indicates the source document(s) linked to your API key, which DocsGPT will use to generate responses.
|
||||
- **ID:** A unique identifier for your API key. You can view this by making a call to `/api/get_api_keys`.
|
||||
- **Key:** The API key itself, which will be used in your application to authenticate API requests.
|
||||
|
||||
With your API key ready, you can now integrate DocsGPT into your application, such as the DocsGPT Widget or any other software, via `/api/answer` or `/stream` endpoints. The source document is preset with the API key, allowing you to bypass fields like `selectDocs` and `active_docs` during implementation.
|
||||
|
||||
Congratulations on taking the first step towards enhancing your applications with DocsGPT! With this guide, you're now equipped to navigate the process of obtaining and understanding DocsGPT API keys.
|
||||
100
docs/pages/Deploying/Kubernetes-Deploying.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Self-hosting DocsGPT on Kubernetes
|
||||
|
||||
This guide will walk you through deploying DocsGPT on Kubernetes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure you have the following installed before proceeding:
|
||||
|
||||
- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||
- Access to a Kubernetes cluster
|
||||
|
||||
## Folder Structure
|
||||
|
||||
The `k8s` folder contains the necessary deployment and service configuration files:
|
||||
|
||||
- `deployments/`
|
||||
- `services/`
|
||||
- `docsgpt-secrets.yaml`
|
||||
|
||||
## Deployment Instructions
|
||||
|
||||
1. **Clone the Repository**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/arc53/DocsGPT.git
|
||||
cd docsgpt/k8s
|
||||
```
|
||||
|
||||
2. **Configure Secrets (optional)**
|
||||
|
||||
Ensure that you have all the necessary secrets in `docsgpt-secrets.yaml`. Update it with your secrets before applying if you want. By default we will use qdrant as a vectorstore and public docsgpt llm as llm for inference.
|
||||
|
||||
3. **Apply Kubernetes Deployments**
|
||||
|
||||
Deploy your DocsGPT resources using the following commands:
|
||||
|
||||
```sh
|
||||
kubectl apply -f deployments/
|
||||
```
|
||||
|
||||
4. **Apply Kubernetes Services**
|
||||
|
||||
Set up your services using the following commands:
|
||||
|
||||
```sh
|
||||
kubectl apply -f services/
|
||||
```
|
||||
|
||||
5. **Apply Secrets**
|
||||
|
||||
Apply the secret configurations:
|
||||
|
||||
```sh
|
||||
kubectl apply -f docsgpt-secrets.yaml
|
||||
```
|
||||
|
||||
6. **Substitute API URL**
|
||||
|
||||
After deploying the services, you need to update the environment variable `VITE_API_HOST` in your deployment file `deployments/docsgpt-deploy.yaml` with the actual endpoint URL created by your `docsgpt-api-service`.
|
||||
|
||||
```sh
|
||||
kubectl get services/docsgpt-api-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}' | xargs -I {} sed -i "s|<your-api-endpoint>|{}|g" deployments/docsgpt-deploy.yaml
|
||||
```
|
||||
|
||||
7. **Rerun Deployment**
|
||||
|
||||
After making the changes, reapply the deployment configuration to update the environment variables:
|
||||
|
||||
```sh
|
||||
kubectl apply -f deployments/
|
||||
```
|
||||
|
||||
## Verifying the Deployment
|
||||
|
||||
To verify if everything is set up correctly, you can run the following:
|
||||
|
||||
```sh
|
||||
kubectl get pods
|
||||
kubectl get services
|
||||
```
|
||||
|
||||
Ensure that the pods are running and the services are available.
|
||||
|
||||
## Accessing DocsGPT
|
||||
|
||||
To access DocsGPT, you need to find the external IP address of the frontend service. You can do this by running:
|
||||
|
||||
```sh
|
||||
kubectl get services/docsgpt-frontend-service | awk 'NR>1 {print "http://" $4}'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any issues, you can check the logs of the pods for more details:
|
||||
|
||||
```sh
|
||||
kubectl logs <pod-name>
|
||||
```
|
||||
|
||||
Replace `<pod-name>` with the actual name of your DocsGPT pod.
|
||||
@@ -110,19 +110,3 @@ Option 2: Using Git Bash or Command Prompt (CMD):
|
||||
6. To stop the setup, just press **Ctrl + C** in the Git Bash or Command Prompt terminal.
|
||||
|
||||
These steps should help you set up and run the project on Windows using either WSL or Git Bash/Command Prompt. Make sure you have Docker installed and properly configured on your Windows system for this to work.
|
||||
|
||||
|
||||
### Chrome Extension
|
||||
|
||||
#### Installing the Chrome extension:
|
||||
To enhance your DocsGPT experience, you can install the DocsGPT Chrome extension. Here's how:
|
||||
|
||||
1. In the DocsGPT GitHub repository, click on the **Code** button and select **Download ZIP**.
|
||||
2. Unzip the downloaded file to a location you can easily access.
|
||||
3. Open the Google Chrome browser and click on the three dots menu (upper right corner).
|
||||
4. Select **More Tools** and then **Extensions**.
|
||||
5. Turn on the **Developer mode** switch in the top right corner of the **Extensions page**.
|
||||
6. Click on the **Load unpacked** button.
|
||||
7. Select the **Chrome** folder where the DocsGPT files have been unzipped (docsgpt-main > extensions > chrome).
|
||||
8. The extension should now be added to Google Chrome and can be managed on the Extensions page.
|
||||
9. To disable or remove the extension, simply turn off the toggle switch on the extension card or click the **Remove** button.
|
||||
|
||||
@@ -10,5 +10,9 @@
|
||||
"Railway-Deploying": {
|
||||
"title": "🚂Deploying on Railway",
|
||||
"href": "/Deploying/Railway-Deploying"
|
||||
},
|
||||
"Kubernetes-Deploying": {
|
||||
"title": "☸️Deploying on Kubernetes",
|
||||
"href": "/Deploying/Kubernetes-Deploying"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"API-docs": {
|
||||
"title": "🗂️️ API-docs",
|
||||
"href": "/Developing/API-docs"
|
||||
}
|
||||
}
|
||||
34
docs/pages/Extensions/Chrome-extension.mdx
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
import {Steps} from 'nextra/components'
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
|
||||
## Chrome Extension Setup Guide
|
||||
|
||||
To enhance your DocsGPT experience, you can install the DocsGPT Chrome extension. Here's how:
|
||||
<Steps >
|
||||
### Step 1
|
||||
|
||||
|
||||
|
||||
In the DocsGPT GitHub repository, click on the **Code** button and select **Download ZIP**.
|
||||
### Step 2
|
||||
Unzip the downloaded file to a location you can easily access.
|
||||
### Step 3
|
||||
Open the Google Chrome browser and click on the three dots menu (upper right corner).
|
||||
### Step 4
|
||||
Select **More Tools** and then **Extensions**.
|
||||
### Step 5
|
||||
Turn on the **Developer mode** switch in the top right corner of the **Extensions page**.
|
||||
### Step 6
|
||||
Click on the **Load unpacked** button.
|
||||
### Step 7
|
||||
7. Select the **Chrome** folder where the DocsGPT files have been unzipped (docsgpt-main > extensions > chrome).
|
||||
### Step 8
|
||||
The extension should now be added to Google Chrome and can be managed on the Extensions page.
|
||||
### Step 9
|
||||
To disable or remove the extension, simply turn off the toggle switch on the extension card or click the **Remove** button.
|
||||
</Steps>
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
"href": "/Extensions/Chatwoot-extension"
|
||||
},
|
||||
"react-widget": {
|
||||
"title": "🏗️ Widget setup",
|
||||
"href": "/Extensions/react-widget"
|
||||
}
|
||||
"title": "🏗️ Widget setup",
|
||||
"href": "/Extensions/react-widget"
|
||||
},
|
||||
"Chrome-extension": {
|
||||
"title": "🌐 Chrome Extension",
|
||||
"href": "/Extensions/Chrome-extension"
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,59 @@ export default function MyApp({ Component, pageProps }) {
|
||||
)
|
||||
}
|
||||
```
|
||||
### How to use DocsGPTWidget with HTML
|
||||
```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>
|
||||
</head>
|
||||
<body>
|
||||
<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');
|
||||
}
|
||||
</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>
|
||||
</head>
|
||||
<body>
|
||||
<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', {
|
||||
apiHost: 'http://localhost:7001',
|
||||
selectDocs: 'default',
|
||||
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.'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
For more information about React, refer to this [link here](https://react.dev/learn)
|
||||
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
# Customizing the Main Prompt
|
||||
|
||||
Customizing the main prompt for DocsGPT gives you the ability to tailor the AI's responses to your specific requirements. By modifying the prompt text, you can achieve more accurate and relevant answers. Here's how you can do it:
|
||||
|
||||
1. Navigate to `/application/prompts/combine_prompt.txt`.
|
||||
1. Navigate to `SideBar -> Settings`.
|
||||
|
||||
|
||||
|
||||
|
||||
2.In Settings select the `Active Prompt` now you will be able to see various prompts style.x
|
||||
|
||||
|
||||
|
||||
|
||||
3.Click on the `edit icon` on the prompt of your choice and you will be able to see the current prompt for it,you can now customise the prompt as per your choice.
|
||||
|
||||
### Video Demo
|
||||
<Image src="/prompts.gif" alt="prompts" width={800} height={500} />
|
||||
|
||||
|
||||
2. Open the `combine_prompt.txt` file and modify the prompt text to suit your needs. You can experiment with different phrasings and structures to observe how the model responds. The main prompt serves as guidance to the AI model on how to generate responses.
|
||||
|
||||
## Example Prompt Modification
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
## How to train on other documentation
|
||||
|
||||
This AI can utilize any documentation, but it requires preparation for similarity search. Follow these steps to get your documentation ready:
|
||||
|
||||
**Step 1: Prepare Your Documentation**
|
||||

|
||||
|
||||
Start by going to `/scripts/` folder.
|
||||
|
||||
If you open this file, you will see that it uses RST files from the folder to create a `index.faiss` and `index.pkl`.
|
||||
|
||||
It currently uses OPENAI to create the vector store, so make sure your documentation is not too large. Using Pandas cost me around $3-$4.
|
||||
|
||||
You can typically find documentation on GitHub in the `docs/` folder for most open-source projects.
|
||||
|
||||
### 1. Find documentation in .rst/.md format and create a folder with it in your scripts directory.
|
||||
- Name it `inputs/`.
|
||||
- Put all your .rst/.md files in there.
|
||||
- The search is recursive, so you don't need to flatten them.
|
||||
|
||||
If there are no .rst/.md files, convert whatever you find to a .txt file and feed it. (Don't forget to change the extension in the script).
|
||||
|
||||
### Step 2: Configure Your OpenAI API Key
|
||||
1. Create a .env file in the scripts/ folder.
|
||||
- Add your OpenAI API key inside: OPENAI_API_KEY=<your-api-key>.
|
||||
|
||||
### Step 3: Run the Ingestion Script
|
||||
|
||||
`python ingest.py ingest`
|
||||
|
||||
It will provide you with the estimated cost.
|
||||
|
||||
### Step 4: Move `index.faiss` and `index.pkl` generated in `scripts/output` to `application/` folder.
|
||||
|
||||
|
||||
### Step 5: Run the Web App
|
||||
Once you run it, it will use new context relevant to your documentation.Make sure you select default in the dropdown in the UI.
|
||||
|
||||
## Customization
|
||||
You can learn more about options while running ingest.py by running:
|
||||
- Make sure you select 'default' from the dropdown in the UI.
|
||||
|
||||
## Customization
|
||||
You can learn more about options while running ingest.py by executing:
|
||||
`python ingest.py --help`
|
||||
| Options | |
|
||||
|:--------------------------------:|:------------------------------------------------------------------------------------------------------------------------------:|
|
||||
| **ingest** | Runs 'ingest' function, converting documentation to Faiss plus Index format |
|
||||
| --dir TEXT | List of paths to directory for index creation. E.g. --dir inputs --dir inputs2 [default: inputs] |
|
||||
| --file TEXT | File paths to use (Optional; overrides directory) E.g. --files inputs/1.md --files inputs/2.md |
|
||||
| --recursive / --no-recursive | Whether to recursively search in subdirectories [default: recursive] |
|
||||
| --limit INTEGER | Maximum number of files to read |
|
||||
| --formats TEXT | List of required extensions (list with .) Currently supported: .rst, .md, .pdf, .docx, .csv, .epub, .html [default: .rst, .md] |
|
||||
| --exclude / --no-exclude | Whether to exclude hidden files (dotfiles) [default: exclude] |
|
||||
| -y, --yes | Whether to skip price confirmation |
|
||||
| --sample / --no-sample | Whether to output sample of the first 5 split documents. [default: no-sample] |
|
||||
| --token-check / --no-token-check | Whether to group small documents and split large. Improves semantics. [default: token-check] |
|
||||
| --min_tokens INTEGER | Minimum number of tokens to not group. [default: 150] |
|
||||
| --max_tokens INTEGER | Maximum number of tokens to not split. [default: 2000] |
|
||||
| | |
|
||||
| **convert** | Creates documentation in .md format from source code |
|
||||
| --dir TEXT | Path to a directory with source code. E.g. --dir inputs [default: inputs] |
|
||||
| --formats TEXT | Source code language from which to create documentation. Supports py, js and java. E.g. --formats py [default: py] |
|
||||
44
docs/pages/Guides/How-to-train-on-other-documentation.mdx
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
import Image from 'next/image'
|
||||
import { Steps } from 'nextra/components'
|
||||
|
||||
## How to train on other documentation
|
||||
|
||||
Training on other documentation sources can greatly enhance the versatility and depth of DocsGPT's knowledge. By incorporating diverse materials, you can broaden the AI's understanding and improve its ability to generate insightful responses across a range of topics. Here's a step-by-step guide on how to effectively train DocsGPT on additional documentation sources:
|
||||
|
||||
**Get your document ready**:
|
||||
|
||||
Make sure you have the document on which you want to train on ready with you on the device which you are using .You can also use links to the documentation to train on.
|
||||
|
||||
<Callout type="warning" emoji="⚠️">
|
||||
Note: The document should be either of the given file formats .pdf, .txt, .rst, .docx, .md, .zip and limited to 25mb.You can also train using the link of the documentation.
|
||||
|
||||
</Callout>
|
||||
|
||||
### Video Demo
|
||||
|
||||
<Image src="/docs.gif" alt="prompts" width={800} height={500} />
|
||||
|
||||
|
||||
|
||||
<Steps>
|
||||
### Step1
|
||||
Navigate to the sidebar where you will find `Source Docs` option,here you will find 3 options built in which are default,Web Search and None.
|
||||
|
||||
|
||||
### Step 2
|
||||
Click on the `Upload icon` just beside the source docs options,now borwse and upload the document which you want to train on or select the `remote` option if you have to insert the link of the documentation.
|
||||
|
||||
|
||||
### Step 3
|
||||
Now you will be able to see the name of the file uploaded under the Uploaded Files ,now click on `Train`,once you click on train it might take some time to train on the document. You will be able to see the `Training progress` and once the training is completed you can click the `finish` button and there you go your docuemnt is uploaded.
|
||||
|
||||
|
||||
### Step 4
|
||||
Go to `New chat` and from the side bar select the document you uploaded under the `Source Docs` and go ahead with your chat, now you can ask qestions regarding the document you uploaded and you will get the effective answer based on it.
|
||||
|
||||
</Steps>
|
||||
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Setting Up Local Language Models for Your App
|
||||
|
||||
Your app relies on two essential models: Embeddings and Text Generation. While OpenAI's default models work seamlessly, you have the flexibility to switch providers or even run the models locally.
|
||||
|
||||
## Step 1: Configure Environment Variables
|
||||
|
||||
Navigate to the `.env` file or set the following environment variables:
|
||||
|
||||
```env
|
||||
LLM_NAME=<your Text Generation model>
|
||||
API_KEY=<API key for Text Generation>
|
||||
EMBEDDINGS_NAME=<LLM for Embeddings>
|
||||
EMBEDDINGS_KEY=<API key for Embeddings>
|
||||
VITE_API_STREAMING=<true or false>
|
||||
```
|
||||
|
||||
You can omit the keys if users provide their own. Ensure you set `LLM_NAME` and `EMBEDDINGS_NAME`.
|
||||
|
||||
## Step 2: Choose Your Models
|
||||
|
||||
**Options for `LLM_NAME`:**
|
||||
- openai ([More details](https://platform.openai.com/docs/models))
|
||||
- anthropic ([More details](https://docs.anthropic.com/claude/reference/selecting-a-model))
|
||||
- manifest ([More details](https://python.langchain.com/docs/integrations/llms/manifest))
|
||||
- cohere ([More details](https://docs.cohere.com/docs/llmu))
|
||||
- llama.cpp ([More details](https://python.langchain.com/docs/integrations/llms/llamacpp))
|
||||
- huggingface (Arc53/DocsGPT-7B by default)
|
||||
- sagemaker ([Mode details](https://aws.amazon.com/sagemaker/))
|
||||
|
||||
|
||||
Note: for huggingface you can choose any model inside application/llm/huggingface.py or pass llm_name on init, loads
|
||||
|
||||
**Options for `EMBEDDINGS_NAME`:**
|
||||
- openai_text-embedding-ada-002
|
||||
- huggingface_sentence-transformers/all-mpnet-base-v2
|
||||
- huggingface_hkunlp/instructor-large
|
||||
- cohere_medium
|
||||
|
||||
If you want to be completely local, set `EMBEDDINGS_NAME` to `huggingface_sentence-transformers/all-mpnet-base-v2`.
|
||||
|
||||
For llama.cpp Download the required model and place it in the `models/` folder.
|
||||
|
||||
Alternatively, for local Llama setup, run `setup.sh` and choose option 1. The script handles the DocsGPT model addition.
|
||||
|
||||
## Step 3: Local Hosting for Privacy
|
||||
|
||||
If working with sensitive data, host everything locally by setting `LLM_NAME`, llama.cpp or huggingface, use any model available on Hugging Face, for llama.cpp you need to convert it into gguf format.
|
||||
That's it! Your app is now configured for local and private hosting, ensuring optimal security for critical data.
|
||||
41
docs/pages/Guides/How-to-use-different-LLM.mdx
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
import Image from 'next/image'
|
||||
import { Steps } from 'nextra/components'
|
||||
|
||||
# Setting Up Local Language Models for Your App
|
||||
|
||||
Setting up local language models for your app can significantly enhance its capabilities, enabling it to understand and generate text in multiple languages without relying on external APIs. By integrating local language models, you can improve privacy, reduce latency, and ensure continuous functionality even in offline environments. Here's a comprehensive guide on how to set up local language models for your application:
|
||||
|
||||
## Steps:
|
||||
### For cloud version LLM change:
|
||||
<Steps >
|
||||
### Step 1
|
||||
Visit the chat screen and you will be to see the default LLM selected.
|
||||
### Step 2
|
||||
Click on it and you will get a drop down of various LLM's available to choose.
|
||||
### Step 3
|
||||
Choose the LLM of your choice.
|
||||
|
||||
</Steps>
|
||||
|
||||
|
||||
|
||||
|
||||
### Video Demo
|
||||
<Image src="/llms.gif" alt="prompts" width={800} height={500} />
|
||||
|
||||
### For Open source llm change:
|
||||
<Steps >
|
||||
### Step 1
|
||||
For open source you have to edit .env file with LLM_NAME with their desired LLM name.
|
||||
### Step 2
|
||||
All the supported LLM providers are here application/llm and you can check what env variable are needed for each
|
||||
List of latest supported LLMs are https://github.com/arc53/DocsGPT/blob/main/application/llm/llm_creator.py
|
||||
### Step 3
|
||||
Visit application/llm and select the file of your selected llm and there you will find the speicifc requirements needed to be filled in order to use it,i.e API key of that llm.
|
||||
</Steps>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"Customising-prompts": {
|
||||
"title": "🏗️️ Customising Prompts",
|
||||
"title": "️💻 Customising Prompts",
|
||||
"href": "/Guides/Customising-prompts"
|
||||
},
|
||||
"How-to-train-on-other-documentation": {
|
||||
@@ -8,7 +8,7 @@
|
||||
"href": "/Guides/How-to-train-on-other-documentation"
|
||||
},
|
||||
"How-to-use-different-LLM": {
|
||||
"title": "⚙️️ How to use different LLM's",
|
||||
"title": "️🤖 How to use different LLM's",
|
||||
"href": "/Guides/How-to-use-different-LLM"
|
||||
},
|
||||
"My-AI-answers-questions-using-external-knowledge": {
|
||||
|
||||
@@ -4,7 +4,7 @@ export default function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<>
|
||||
<Component {...pageProps} />
|
||||
<DocsGPTWidget selectDocs="local/docsgpt-sep.zip/"/>
|
||||
<DocsGPTWidget apiKey="d61a020c-ac8f-4f23-bb98-458e4da3c240" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -2,14 +2,16 @@
|
||||
title: 'Home'
|
||||
---
|
||||
import { Cards, Card } from 'nextra/components'
|
||||
import Image from 'next/image'
|
||||
import deployingGuides from './Deploying/_meta.json';
|
||||
import developingGuides from './Developing/_meta.json';
|
||||
import developingGuides from './API/_meta.json';
|
||||
import extensionGuides from './Extensions/_meta.json';
|
||||
import mainGuides from './Guides/_meta.json';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const allGuides = {
|
||||
...deployingGuides,
|
||||
...developingGuides,
|
||||
@@ -21,9 +23,12 @@ export const allGuides = {
|
||||
|
||||
DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖. Eliminate lengthy manual searches 🔍 and enhance your documentation experience with DocsGPT, and consider contributing to its AI-powered future 🚀.
|
||||
|
||||

|
||||
|
||||
Try it yourself: [https://docsgpt.arc53.com/](https://docsgpt.arc53.com/)
|
||||
|
||||
<Image src="/homevideo.gif" alt="homedemo" width={800} height={500}/>
|
||||
|
||||
|
||||
Try it yourself: [https://www.docsgpt.cloud/](https://www.docsgpt.cloud/)
|
||||
|
||||
<Cards
|
||||
num={3}
|
||||
|
||||
BIN
docs/public/docs.gif
Normal file
|
After Width: | Height: | Size: 839 KiB |
BIN
docs/public/homevideo.gif
Normal file
|
After Width: | Height: | Size: 23 MiB |
BIN
docs/public/llms.gif
Normal file
|
After Width: | Height: | Size: 500 KiB |
BIN
docs/public/prompts.gif
Normal file
|
After Width: | Height: | Size: 974 KiB |
@@ -1,6 +1,5 @@
|
||||
# DocsGPT react widget
|
||||
|
||||
|
||||
This widget will allow you to embed a DocsGPT assistant in your React app.
|
||||
|
||||
## Installation
|
||||
@@ -11,6 +10,8 @@ npm install docsgpt
|
||||
|
||||
## Usage
|
||||
|
||||
### React
|
||||
|
||||
```javascript
|
||||
import { DocsGPTWidget } from "docsgpt";
|
||||
|
||||
@@ -25,9 +26,9 @@ To link the widget to your api and your documents you can pass parameters to the
|
||||
import { DocsGPTWidget } from "docsgpt";
|
||||
|
||||
const App = () => {
|
||||
return <DocsGPTWidget
|
||||
return <DocsGPTWidget
|
||||
apiHost = 'http://localhost:7001',
|
||||
selectDocs = 'default',
|
||||
selectDocs = 'default',
|
||||
apiKey = '',
|
||||
avatar = 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png',
|
||||
title = 'Get AI assistance',
|
||||
@@ -38,10 +39,65 @@ To link the widget to your api and your documents you can pass parameters to the
|
||||
};
|
||||
```
|
||||
|
||||
### Html
|
||||
|
||||
```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>
|
||||
</head>
|
||||
<body>
|
||||
<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');
|
||||
}
|
||||
</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>
|
||||
</head>
|
||||
<body>
|
||||
<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', , {
|
||||
apiHost: 'http://localhost:7001',
|
||||
selectDocs: 'default',
|
||||
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.'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Our github
|
||||
|
||||
[DocsGPT](https://github.com/arc53/DocsGPT)
|
||||
|
||||
You can find the source code in the extensions/react-widget folder.
|
||||
|
||||
|
||||
6340
extensions/react-widget/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docsgpt",
|
||||
"version": "0.3.7",
|
||||
"version": "0.3.9",
|
||||
"private": false,
|
||||
"description": "DocsGPT 🦖 is an innovative open-source tool designed to simplify the retrieval of information from project documentation using advanced GPT models 🤖.",
|
||||
"source": "./src/index.html",
|
||||
@@ -11,6 +11,18 @@
|
||||
"dist",
|
||||
"package.json"
|
||||
],
|
||||
"targets": {
|
||||
"modern": {
|
||||
"engines": {
|
||||
"browsers": "Chrome 80"
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"engines": {
|
||||
"browsers": "> 0.5%, last 2 versions, not dead"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@parcel/resolver-default": {
|
||||
"packageExports": true
|
||||
},
|
||||
@@ -18,8 +30,8 @@
|
||||
"styled-components": "^5"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "parcel build src/index.ts",
|
||||
"dev": "parcel",
|
||||
"build": "parcel build src/main.tsx --public-url ./",
|
||||
"dev": "parcel src/index.html -p 3000",
|
||||
"test": "jest",
|
||||
"lint": "eslint",
|
||||
"check": "tsc --noEmit",
|
||||
@@ -33,16 +45,13 @@
|
||||
"@parcel/transformer-typescript-tsc": "^2.12.0",
|
||||
"@parcel/validator-typescript": "^2.12.0",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@types/react": "^18.2.61",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"dompurify": "^3.0.9",
|
||||
"dompurify": "^3.1.5",
|
||||
"flow-bin": "^0.229.2",
|
||||
"i": "^0.3.7",
|
||||
"install": "^0.13.0",
|
||||
"npm": "^10.5.0",
|
||||
"parcel": "^2.12.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"styled-components": "^6.1.8"
|
||||
@@ -54,7 +63,10 @@
|
||||
"@parcel/packager-ts": "^2.12.0",
|
||||
"@parcel/transformer-typescript-types": "^2.12.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"babel-loader": "^8.0.4",
|
||||
"parcel": "^2.12.0",
|
||||
"process": "^0.11.10",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
import { Fragment, useEffect, useRef, useState } from 'react'
|
||||
import { PaperPlaneIcon, RocketIcon, ExclamationTriangleIcon, Cross2Icon } from '@radix-ui/react-icons';
|
||||
import { MESSAGE_TYPE } from '../models/types';
|
||||
import { Query, Status } from '../models/types';
|
||||
import MessageIcon from '../assets/message.svg'
|
||||
import { fetchAnswerStreaming } from '../requests/streamingApi';
|
||||
import styled, { keyframes, createGlobalStyle } from 'styled-components';
|
||||
import React from 'react'
|
||||
import DOMPurify from 'dompurify';
|
||||
import snarkdown from '@bpmn-io/snarkdown';
|
||||
import { sanitize } from 'dompurify';
|
||||
import styled, { keyframes, createGlobalStyle } from 'styled-components';
|
||||
import { PaperPlaneIcon, RocketIcon, ExclamationTriangleIcon, Cross2Icon } from '@radix-ui/react-icons';
|
||||
import MessageIcon from '../assets/message.svg';
|
||||
import { MESSAGE_TYPE, Query, Status } from '../types/index';
|
||||
import { fetchAnswerStreaming } from '../requests/streamingApi';
|
||||
|
||||
const GlobalStyles = createGlobalStyle`
|
||||
.response pre {
|
||||
padding: 8px;
|
||||
@@ -285,7 +285,7 @@ const Hero = ({ title, description }: { title: string, description: string }) =>
|
||||
export const DocsGPTWidget = ({
|
||||
apiHost = 'https://gptcloud.arc53.com',
|
||||
selectDocs = 'default',
|
||||
apiKey = 'docsgpt-public',
|
||||
apiKey = '82962c9a-aa77-4152-94e5-a4f84fd44c6a',
|
||||
avatar = 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png',
|
||||
title = 'Get AI assistance',
|
||||
description = 'DocsGPT\'s AI Chatbot is here to help',
|
||||
@@ -293,13 +293,13 @@ export const DocsGPTWidget = ({
|
||||
heroDescription = 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.'
|
||||
}) => {
|
||||
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [status, setStatus] = useState<Status>('idle');
|
||||
const [queries, setQueries] = useState<Query[]>([])
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
const [eventInterrupt, setEventInterrupt] = useState<boolean>(false); //click or scroll by user while autoScrolling
|
||||
const endMessageRef = useRef<HTMLDivElement | null>(null);
|
||||
const [prompt, setPrompt] = React.useState('');
|
||||
const [status, setStatus] = React.useState<Status>('idle');
|
||||
const [queries, setQueries] = React.useState<Query[]>([])
|
||||
const [conversationId, setConversationId] = React.useState<string | null>(null)
|
||||
const [open, setOpen] = React.useState<boolean>(false)
|
||||
const [eventInterrupt, setEventInterrupt] = React.useState<boolean>(false); //click or scroll by user while autoScrolling
|
||||
const endMessageRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const handleUserInterrupt = () => {
|
||||
(status === 'loading') && setEventInterrupt(true);
|
||||
}
|
||||
@@ -317,7 +317,7 @@ export const DocsGPTWidget = ({
|
||||
lastChild && scrollToBottom(lastChild)
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
!eventInterrupt && scrollToBottom(endMessageRef.current);
|
||||
}, [queries.length, queries[queries.length - 1]?.response]);
|
||||
|
||||
@@ -397,7 +397,7 @@ export const DocsGPTWidget = ({
|
||||
{
|
||||
queries.length > 0 ? queries?.map((query, index) => {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<React.Fragment key={index}>
|
||||
{
|
||||
query.prompt && <MessageBubble type='QUESTION'>
|
||||
<Message
|
||||
@@ -413,7 +413,7 @@ export const DocsGPTWidget = ({
|
||||
type='ANSWER'
|
||||
ref={(index === queries.length - 1) ? endMessageRef : null}
|
||||
>
|
||||
<div className="response" dangerouslySetInnerHTML={{ __html: sanitize(snarkdown(query.response)) }} />
|
||||
<div className="response" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(snarkdown(query.response)) }} />
|
||||
</Message>
|
||||
</MessageBubble>
|
||||
: <div>
|
||||
@@ -437,7 +437,7 @@ export const DocsGPTWidget = ({
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Fragment>)
|
||||
</React.Fragment>)
|
||||
})
|
||||
: <Hero title={heroTitle} description={heroDescription} />
|
||||
}
|
||||
|
||||
@@ -9,5 +9,11 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="main.tsx"></script>
|
||||
<script type="module" src="../dist/main.js"></script>
|
||||
<script type="module">
|
||||
window.onload = function() {
|
||||
renderDocsGPTWidget('app');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import React from 'react';
|
||||
const root = createRoot(document.getElementById('app') as HTMLElement);
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { DocsGPTWidget } from './components/DocsGPTWidget';
|
||||
|
||||
root.render(<App />);
|
||||
const renderWidget = (elementId: string, props = {}) => {
|
||||
const root = createRoot(document.getElementById(elementId) as HTMLElement);
|
||||
root.render(<DocsGPTWidget {...props} />);
|
||||
};
|
||||
|
||||
(window as any).renderDocsGPTWidget = renderWidget;
|
||||
export { DocsGPTWidget };
|
||||
13
extensions/react-widget/src/types/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export type MESSAGE_TYPE = 'QUESTION' | 'ANSWER' | 'ERROR';
|
||||
export type Status = 'idle' | 'loading' | 'failed';
|
||||
export type FEEDBACK = 'LIKE' | 'DISLIKE';
|
||||
|
||||
export interface Query {
|
||||
prompt: string;
|
||||
response?: string;
|
||||
feedback?: FEEDBACK;
|
||||
error?: string;
|
||||
sources?: { title: string; text: string }[];
|
||||
conversationId?: string | null;
|
||||
title?: string | null;
|
||||
}
|
||||
14
extensions/web-widget/package-lock.json
generated
@@ -152,12 +152,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -294,9 +294,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DocsGPT 🦖</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="h-screen"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0,viewport-fit=cover" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>DocsGPT 🦖</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root" class="h-screen"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
117
frontend/package-lock.json
generated
@@ -10,10 +10,14 @@
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.9.2",
|
||||
"@vercel/analytics": "^0.1.10",
|
||||
"i18next": "^23.11.5",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.8.1",
|
||||
@@ -44,7 +48,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^5.0.12",
|
||||
"vite": "^5.0.13",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
}
|
||||
},
|
||||
@@ -354,11 +358,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.20.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz",
|
||||
"integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==",
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz",
|
||||
"integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1485,7 +1490,7 @@
|
||||
"version": "18.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz",
|
||||
"integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
@@ -2151,12 +2156,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -3699,9 +3704,9 @@
|
||||
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -4134,6 +4139,15 @@
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz",
|
||||
@@ -4158,6 +4172,38 @@
|
||||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "23.11.5",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz",
|
||||
"integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
|
||||
"integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||
@@ -6571,6 +6617,7 @@
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -6678,6 +6725,28 @@
|
||||
"react": ">= 16.8 || 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.2.tgz",
|
||||
"integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.9",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -6875,9 +6944,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.4.3",
|
||||
@@ -7855,9 +7925,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
|
||||
"integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
|
||||
"version": "5.0.13",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.13.tgz",
|
||||
"integrity": "sha512-/9ovhv2M2dGTuA+dY93B9trfyWMDRQw2jdVBhHNP6wr0oF34wG2i/N55801iZIpgUpnHDm4F/FabGQLyc+eOgg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.19.3",
|
||||
@@ -7923,6 +7993,15 @@
|
||||
"vite": "^2.6.0 || 3 || 4 || 5"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -21,10 +21,14 @@
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.9.2",
|
||||
"@vercel/analytics": "^0.1.10",
|
||||
"i18next": "^23.11.5",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.8.1",
|
||||
@@ -55,7 +59,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^5.0.12",
|
||||
"vite": "^5.0.13",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
frontend/public/fonts/Inter-Variable.ttf
Normal file
@@ -1,4 +1,5 @@
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import Navigation from './Navigation';
|
||||
import Conversation from './conversation/Conversation';
|
||||
import About from './About';
|
||||
@@ -6,30 +7,55 @@ import PageNotFound from './PageNotFound';
|
||||
import { inject } from '@vercel/analytics';
|
||||
import { useMediaQuery } from './hooks';
|
||||
import { useState } from 'react';
|
||||
import Setting from './Setting';
|
||||
|
||||
import Setting from './settings';
|
||||
import './locale/i18n';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import SharedConversation from './conversation/SharedConversation';
|
||||
import { useDarkTheme } from './hooks';
|
||||
inject();
|
||||
|
||||
export default function App() {
|
||||
function MainLayout() {
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [navOpen, setNavOpen] = useState(!isMobile);
|
||||
return (
|
||||
<div className="min-h-full min-w-full dark:bg-raisin-black">
|
||||
<div className="dark:bg-raisin-black">
|
||||
<Navigation navOpen={navOpen} setNavOpen={setNavOpen} />
|
||||
<div
|
||||
className={`transition-all duration-200 ${
|
||||
className={`min-h-screen ${
|
||||
!isMobile
|
||||
? `ml-0 ${!navOpen ? '-mt-5 md:mx-auto lg:mx-auto' : 'md:ml-72'}`
|
||||
? `ml-0 ${!navOpen ? 'md:mx-auto lg:mx-auto' : 'md:ml-72'}`
|
||||
: 'ml-0 md:ml-16'
|
||||
}`}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<Conversation />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="*" element={<PageNotFound />} />
|
||||
<Route path="/settings" element={<Setting />} />
|
||||
</Routes>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
useEffect(() => {
|
||||
localStorage.setItem('selectedTheme', isDarkTheme ? 'Dark' : 'Light');
|
||||
if (isDarkTheme) {
|
||||
document
|
||||
.getElementById('root')
|
||||
?.classList.add('dark', 'dark:bg-raisin-black');
|
||||
} else {
|
||||
document.getElementById('root')?.classList.remove('dark');
|
||||
}
|
||||
}, [isDarkTheme]);
|
||||
return (
|
||||
<>
|
||||
<Routes>
|
||||
<Route element={<MainLayout />}>
|
||||
<Route index element={<Conversation />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/settings" element={<Setting />} />
|
||||
</Route>
|
||||
<Route path="/share/:identifier" element={<SharedConversation />} />
|
||||
<Route path="/*" element={<PageNotFound />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,195 +1,52 @@
|
||||
import { useDarkTheme, useMediaQuery } from './hooks';
|
||||
import { Fragment } from 'react';
|
||||
import DocsGPT3 from './assets/cute_docsgpt3.svg';
|
||||
|
||||
export default function Hero({ className = '' }: { className?: string }) {
|
||||
// const isMobile = window.innerWidth <= 768;
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export default function Hero({
|
||||
handleQuestion,
|
||||
}: {
|
||||
handleQuestion: ({
|
||||
question,
|
||||
isRetry,
|
||||
}: {
|
||||
question: string;
|
||||
isRetry?: boolean;
|
||||
}) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const demos = t('demo', { returnObjects: true }) as Array<{
|
||||
header: string;
|
||||
query: string;
|
||||
}>;
|
||||
return (
|
||||
<div
|
||||
className={`mt-14 ${
|
||||
isMobile ? 'mb-2' : 'mb-12'
|
||||
} flex flex-col text-black-1000 dark:text-bright-gray`}
|
||||
className={`mt-16 mb-4 flex w-full flex-col justify-end text-black-1000 dark:text-bright-gray sm:w-full lg:mt-6`}
|
||||
>
|
||||
<div className=" mb-2 flex items-center justify-center sm:mb-10">
|
||||
<p className="mr-2 text-4xl font-semibold">DocsGPT</p>
|
||||
<img className="mb-2 h-14" src={DocsGPT3} alt="DocsGPT" />
|
||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||
<div className="flex items-center">
|
||||
<span className="p-0 text-4xl font-semibold">DocsGPT</span>
|
||||
<img className="mb-1 inline w-14 p-0" src={DocsGPT3} alt="docsgpt" />
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex flex-col items-center justify-center dark:text-white"></div>
|
||||
</div>
|
||||
{isMobile ? (
|
||||
<p className="mb-3 text-center leading-6">
|
||||
Welcome to <span className="font-bold">DocsGPT</span>, your technical
|
||||
documentation assistant! Start by entering your query in the input
|
||||
field below, and we'll provide you with the most relevant
|
||||
answers.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-3 text-center leading-6">
|
||||
Welcome to DocsGPT, your technical documentation assistant!
|
||||
</p>
|
||||
<p className="mb-3 text-center leading-6">
|
||||
Enter a query related to the information in the documentation you
|
||||
selected to receive
|
||||
<br /> and we will provide you with the most relevant answers.
|
||||
</p>
|
||||
<p className="mb-3 text-center leading-6">
|
||||
Start by entering your query in the input field below and we will do
|
||||
the rest!
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={`sections ${
|
||||
isMobile ? '' : 'mt-1'
|
||||
} flex flex-wrap items-center justify-center gap-2 sm:gap-1 md:gap-0`}
|
||||
>
|
||||
{/* first */}
|
||||
<div className="h-auto rounded-[50px] bg-gradient-to-l from-[#6EE7B7]/70 via-[#3B82F6] to-[#9333EA]/50 p-1 dark:from-[#D16FF8] dark:via-[#48E6E0] dark:to-[#C85EF6] md:h-60 md:rounded-tr-none md:rounded-br-none">
|
||||
<div
|
||||
className={`h-full rounded-[45px] bg-white dark:bg-dark-charcoal p-${
|
||||
isMobile ? '3.5' : '6 py-8'
|
||||
} md:rounded-tr-none md:rounded-br-none`}
|
||||
>
|
||||
{/* Add Mobile check here */}
|
||||
{isMobile ? (
|
||||
<div className="flex justify-center">
|
||||
<img
|
||||
src={
|
||||
isDarkTheme ? '/message-text-dark.svg' : '/message-text.svg'
|
||||
}
|
||||
alt="lock"
|
||||
className="h-[24px] w-[24px] "
|
||||
/>
|
||||
<h2 className="mb-0 pl-1 text-lg font-bold">
|
||||
Chat with Your Data
|
||||
</h2>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<img
|
||||
src={
|
||||
isDarkTheme ? '/message-text-dark.svg' : '/message-text.svg'
|
||||
}
|
||||
alt="lock"
|
||||
className="h-[24px] w-[24px]"
|
||||
/>
|
||||
<h2 className="mt-2 mb-3 text-lg font-bold">
|
||||
Chat with Your Data
|
||||
</h2>
|
||||
</>
|
||||
)}
|
||||
<p
|
||||
className={
|
||||
isMobile
|
||||
? `w-[250px] text-center text-xs text-gray-500 dark:text-bright-gray`
|
||||
: `w-[250px] text-xs text-gray-500 dark:text-bright-gray`
|
||||
}
|
||||
>
|
||||
DocsGPT will use your data to answer questions. Whether its
|
||||
documentation, source code, or Microsoft files, DocsGPT allows you
|
||||
to have interactive conversations and find answers based on the
|
||||
provided data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* second */}
|
||||
<div className="h-auto rounded-[50px] bg-gradient-to-r from-[#6EE7B7]/70 via-[#3B82F6] to-[#9333EA]/50 p-1 dark:from-[#D16FF8] dark:via-[#48E6E0] dark:to-[#C85EF6] md:h-60 md:rounded-none md:py-1 md:px-0">
|
||||
<div
|
||||
className={`h-full rounded-[45px] bg-white dark:bg-dark-charcoal p-${
|
||||
isMobile ? '3.5' : '6 py-6'
|
||||
} md:rounded-none`}
|
||||
>
|
||||
{/* Add Mobile check here */}
|
||||
{isMobile ? (
|
||||
<div className="flex justify-center ">
|
||||
<img
|
||||
src={isDarkTheme ? '/lock-dark.svg' : '/lock.svg'}
|
||||
alt="lock"
|
||||
className="h-[24px] w-[24px]"
|
||||
/>
|
||||
<h2 className="mb-0 pl-1 text-lg font-bold">
|
||||
Secure Data Storage
|
||||
</h2>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<img
|
||||
src={isDarkTheme ? '/lock-dark.svg' : '/lock.svg'}
|
||||
alt="lock"
|
||||
className="h-[24px] w-[24px]"
|
||||
/>
|
||||
<h2 className="mt-2 mb-3 text-lg font-bold">
|
||||
Secure Data Storage
|
||||
</h2>
|
||||
</>
|
||||
)}
|
||||
<p
|
||||
className={
|
||||
isMobile
|
||||
? `w-[250px] text-center text-xs text-gray-500 dark:text-bright-gray`
|
||||
: `w-[250px] text-xs text-gray-500 dark:text-bright-gray`
|
||||
}
|
||||
>
|
||||
The security of your data is our top priority. DocsGPT ensures the
|
||||
utmost protection for your sensitive information. With secure data
|
||||
storage and privacy measures in place, you can trust that your
|
||||
data is kept safe and confidential.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* third */}
|
||||
<div className="h-auto rounded-[50px] bg-gradient-to-l from-[#6EE7B7]/70 via-[#3B82F6] to-[#9333EA]/50 p-1 dark:from-[#D16FF8] dark:via-[#48E6E0] dark:to-[#C85EF6] md:h-60 md:rounded-tl-none md:rounded-bl-none ">
|
||||
<div
|
||||
className={`firefox h-full rounded-[45px] bg-white dark:bg-dark-charcoal p-${
|
||||
isMobile ? '3.5' : '6 px-6 '
|
||||
} lg:rounded-tl-none lg:rounded-bl-none`}
|
||||
>
|
||||
{/* Add Mobile check here */}
|
||||
{isMobile ? (
|
||||
<div className="flex justify-center">
|
||||
<img
|
||||
src={
|
||||
isDarkTheme
|
||||
? 'message-programming-dark.svg'
|
||||
: '/message-programming.svg'
|
||||
}
|
||||
alt="lock"
|
||||
className="h-[24px] w-[24px]"
|
||||
/>
|
||||
<h2 className="mb-0 pl-1 text-lg font-bold">
|
||||
Open Source Code
|
||||
</h2>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<img
|
||||
src={
|
||||
isDarkTheme
|
||||
? '/message-programming-dark.svg'
|
||||
: '/message-programming.svg'
|
||||
}
|
||||
alt="lock"
|
||||
className="h-[24px] w-[24px]"
|
||||
/>
|
||||
<h2 className="mt-2 mb-3 text-lg font-bold">
|
||||
Open Source Code
|
||||
</h2>
|
||||
</>
|
||||
)}
|
||||
<p
|
||||
className={
|
||||
isMobile
|
||||
? `w-[250px] text-center text-xs text-gray-500 dark:text-bright-gray`
|
||||
: `w-[250px] text-xs text-gray-500 dark:text-bright-gray`
|
||||
}
|
||||
>
|
||||
DocsGPT is built on open source principles, promoting transparency
|
||||
and collaboration. The source code is freely available, enabling
|
||||
developers to contribute, enhance, and customize the app to meet
|
||||
their specific needs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-16 grid w-full grid-cols-1 items-center gap-4 self-center text-xs sm:w-auto sm:gap-6 md:mb-0 md:text-sm lg:grid-cols-2">
|
||||
{demos?.map(
|
||||
(demo: { header: string; query: string }, key: number) =>
|
||||
demo.header &&
|
||||
demo.query && (
|
||||
<Fragment key={key}>
|
||||
<button
|
||||
onClick={() => handleQuestion({ question: demo.query })}
|
||||
className="w-full rounded-full border-2 border-silver px-6 py-4 text-left hover:border-gray-4000 dark:hover:border-gray-3000 xl:min-w-[24vw]"
|
||||
>
|
||||
<p className="mb-1 font-semibold text-black dark:text-silver">
|
||||
{demo.header}
|
||||
</p>
|
||||
<span className="text-gray-400">{demo.query}</span>
|
||||
</button>
|
||||
</Fragment>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import DocsGPT3 from './assets/cute_docsgpt3.svg';
|
||||
import Documentation from './assets/documentation.svg';
|
||||
import DocumentationDark from './assets/documentation-dark.svg';
|
||||
import Discord from './assets/discord.svg';
|
||||
import DiscordDark from './assets/discord-dark.svg';
|
||||
import Expand from './assets/expand.svg';
|
||||
import Github from './assets/github.svg';
|
||||
import GithubDark from './assets/github-dark.svg';
|
||||
import Hamburger from './assets/hamburger.svg';
|
||||
import HamburgerDark from './assets/hamburger-dark.svg';
|
||||
import Info from './assets/info.svg';
|
||||
import InfoDark from './assets/info-dark.svg';
|
||||
import SettingGear from './assets/settingGear.svg';
|
||||
import SettingGearDark from './assets/settingGear-dark.svg';
|
||||
import Twitter from './assets/TwitterX.svg';
|
||||
import Add from './assets/add.svg';
|
||||
import UploadIcon from './assets/upload.svg';
|
||||
import { ActiveState } from './models/misc';
|
||||
import APIKeyModal from './preferences/APIKeyModal';
|
||||
import DeleteConvModal from './modals/DeleteConvModal';
|
||||
|
||||
import {
|
||||
selectApiKeyStatus,
|
||||
selectSelectedDocs,
|
||||
@@ -29,6 +25,9 @@ import {
|
||||
selectConversations,
|
||||
setConversations,
|
||||
selectConversationId,
|
||||
selectModalStateDeleteConv,
|
||||
setModalStateDeleteConv,
|
||||
setSourceDocs,
|
||||
} from './preferences/preferenceSlice';
|
||||
import {
|
||||
setConversation,
|
||||
@@ -36,40 +35,43 @@ import {
|
||||
} from './conversation/conversationSlice';
|
||||
import { useMediaQuery, useOutsideAlerter } from './hooks';
|
||||
import Upload from './upload/Upload';
|
||||
import { Doc, getConversations } from './preferences/preferenceApi';
|
||||
import { Doc, getConversations, getDocs } from './preferences/preferenceApi';
|
||||
import SelectDocsModal from './preferences/SelectDocsModal';
|
||||
import ConversationTile from './conversation/ConversationTile';
|
||||
import { useDarkTheme } from './hooks';
|
||||
import SourceDropdown from './components/SourceDropdown';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
interface NavigationProps {
|
||||
navOpen: boolean;
|
||||
setNavOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
const NavImage: React.FC<{
|
||||
/* const NavImage: React.FC<{
|
||||
Light: string | undefined;
|
||||
Dark: string | undefined;
|
||||
}> = ({ Light, Dark }) => {
|
||||
return (
|
||||
<>
|
||||
<img src={Dark} alt="icon" className="ml-2 hidden w-5 dark:block " />
|
||||
<img src={Light} alt="icon" className="ml-2 w-5 dark:hidden " />
|
||||
<img src={Light} alt="icon" className="ml-2 w-5 dark:hidden filter dark:invert" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
NavImage.propTypes = {
|
||||
Light: PropTypes.string,
|
||||
Dark: PropTypes.string,
|
||||
};
|
||||
}; */
|
||||
export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
const dispatch = useDispatch();
|
||||
const docs = useSelector(selectSourceDocs);
|
||||
const selectedDocs = useSelector(selectSelectedDocs);
|
||||
const conversations = useSelector(selectConversations);
|
||||
const modalStateDeleteConv = useSelector(selectModalStateDeleteConv);
|
||||
const conversationId = useSelector(selectConversationId);
|
||||
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const [isDocsListOpen, setIsDocsListOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isApiKeySet = useSelector(selectApiKeyStatus);
|
||||
const [apiKeyModalState, setApiKeyModalState] =
|
||||
@@ -92,6 +94,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
fetchConversations();
|
||||
}
|
||||
}, [conversations, dispatch]);
|
||||
|
||||
async function fetchConversations() {
|
||||
return await getConversations()
|
||||
.then((fetchedConversations) => {
|
||||
@@ -102,6 +105,16 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
});
|
||||
}
|
||||
|
||||
const handleDeleteAllConversations = () => {
|
||||
fetch(`${apiHost}/api/delete_all_conversations`, {
|
||||
method: 'POST',
|
||||
})
|
||||
.then(() => {
|
||||
fetchConversations();
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
};
|
||||
|
||||
const handleDeleteConversation = (id: string) => {
|
||||
fetch(`${apiHost}/api/delete_conversation?id=${id}`, {
|
||||
method: 'POST',
|
||||
@@ -112,19 +125,29 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
.catch((error) => console.error(error));
|
||||
};
|
||||
|
||||
const handleDeleteClick = (index: number, doc: Doc) => {
|
||||
const docPath = 'indexes/' + 'local' + '/' + doc.name;
|
||||
const handleDeleteClick = (doc: Doc) => {
|
||||
const docPath = `indexes/local/${doc.name}`;
|
||||
|
||||
fetch(`${apiHost}/api/delete_old?path=${docPath}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
.then(() => {
|
||||
// remove the image element from the DOM
|
||||
const imageElement = document.querySelector(
|
||||
`#img-${index}`,
|
||||
) as HTMLElement;
|
||||
const parentElement = imageElement.parentNode as HTMLElement;
|
||||
parentElement.parentNode?.removeChild(parentElement);
|
||||
// const imageElement = document.querySelector(
|
||||
// `#img-${index}`,
|
||||
// ) as HTMLElement;
|
||||
// const parentElement = imageElement.parentNode as HTMLElement;
|
||||
// parentElement.parentNode?.removeChild(parentElement);
|
||||
|
||||
return getDocs();
|
||||
})
|
||||
.then((updatedDocs) => {
|
||||
dispatch(setSourceDocs(updatedDocs));
|
||||
dispatch(
|
||||
setSelectedDocs(
|
||||
updatedDocs?.find((doc) => doc.name.toLowerCase() === 'default'),
|
||||
),
|
||||
);
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
};
|
||||
@@ -254,13 +277,15 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
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">
|
||||
New Chat
|
||||
{t('newChat')}
|
||||
</p>
|
||||
</NavLink>
|
||||
<div className="mb-auto h-[56vh] overflow-x-hidden overflow-y-scroll dark:text-white">
|
||||
{conversations && (
|
||||
<div className="mb-auto h-[78vh] overflow-y-auto overflow-x-hidden dark:text-white">
|
||||
{conversations && conversations.length > 0 ? (
|
||||
<div>
|
||||
<p className="ml-6 mt-3 text-sm font-semibold">Chats</p>
|
||||
<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>
|
||||
<div className="conversations-container">
|
||||
{conversations?.map((conversation) => (
|
||||
<ConversationTile
|
||||
@@ -275,12 +300,14 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</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 flex gap-2 px-2">
|
||||
<div className="relative my-4 mx-4 flex gap-2">
|
||||
<SourceDropdown
|
||||
options={docs}
|
||||
selectedDocs={selectedDocs}
|
||||
@@ -295,66 +322,84 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
onClick={() => setUploadModalState('ACTIVE')}
|
||||
></img>
|
||||
</div>
|
||||
<p className="ml-6 mt-3 text-sm font-semibold">Source Docs</p>
|
||||
<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
|
||||
to="/settings"
|
||||
className={({ isActive }) =>
|
||||
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe ${
|
||||
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
|
||||
isActive ? 'bg-gray-3000 dark:bg-transparent' : ''
|
||||
}`
|
||||
}
|
||||
>
|
||||
<NavImage Light={SettingGear} Dark={SettingGearDark} />
|
||||
<img
|
||||
src={SettingGear}
|
||||
alt="icon"
|
||||
className="ml-2 w-5 filter dark:invert"
|
||||
/>
|
||||
<p className="my-auto text-sm text-eerie-black dark:text-white">
|
||||
Settings
|
||||
{t('settings.label')}
|
||||
</p>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 border-b-[1.5px] py-2 dark:border-b-purple-taupe">
|
||||
<div className="flex justify-between gap-2 border-b-[1.5px] py-2 dark:border-b-purple-taupe">
|
||||
<NavLink
|
||||
to="/about"
|
||||
className={({ isActive }) =>
|
||||
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe ${
|
||||
isActive ? 'bg-gray-3000 dark:bg-purple-taupe' : ''
|
||||
`my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
|
||||
isActive ? 'bg-gray-3000 dark:bg-[#28292E]' : ''
|
||||
}`
|
||||
}
|
||||
>
|
||||
<NavImage Light={Info} Dark={InfoDark} />
|
||||
<p className="my-auto text-sm">About</p>
|
||||
<img
|
||||
src={Info}
|
||||
alt="icon"
|
||||
className="ml-2 w-5 filter dark:invert"
|
||||
/>
|
||||
<p className="my-auto pr-1 text-sm">{t('about')}</p>
|
||||
</NavLink>
|
||||
|
||||
<a
|
||||
href="https://docs.docsgpt.co.uk/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe"
|
||||
>
|
||||
<NavImage Light={Documentation} Dark={DocumentationDark} />
|
||||
<p className="my-auto text-sm ">Documentation</p>
|
||||
</a>
|
||||
<a
|
||||
href="https://discord.gg/WHJdfbQDR4"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="my-auto mx-4 flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe"
|
||||
>
|
||||
<NavImage Light={Discord} Dark={DiscordDark} />
|
||||
{/* <img src={isDarkTheme ? DiscordDark : Discord} alt="discord-link" className="ml-2 w-5" /> */}
|
||||
<p className="my-auto text-sm">Visit our Discord</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com/arc53/DocsGPT"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mx-4 mt-auto flex h-9 cursor-pointer gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe"
|
||||
>
|
||||
<NavImage Light={Github} Dark={GithubDark} />
|
||||
<p className="my-auto text-sm">Visit our Github</p>
|
||||
</a>
|
||||
<div className="flex items-center justify-evenly gap-1 px-1">
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://discord.gg/WHJdfbQDR4'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={Discord}
|
||||
alt="discord"
|
||||
className="m-2 w-6 self-center filter dark:invert"
|
||||
/>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://twitter.com/docsgptai'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={Twitter}
|
||||
alt="x"
|
||||
className="m-2 w-5 self-center filter dark:invert"
|
||||
/>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
target="_blank"
|
||||
to={'https://github.com/arc53/docsgpt'}
|
||||
className={
|
||||
'rounded-full hover:bg-gray-100 dark:hover:bg-[#28292E]'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={Github}
|
||||
alt="github"
|
||||
className="m-2 w-6 self-center filter dark:invert"
|
||||
/>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -370,6 +415,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<SelectDocsModal
|
||||
modalState={selectedDocsModalState}
|
||||
setModalState={setSelectedDocsModalState}
|
||||
@@ -380,6 +426,11 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
setModalState={setApiKeyModalState}
|
||||
isCancellable={isApiKeySet}
|
||||
/>
|
||||
<DeleteConvModal
|
||||
modalState={modalStateDeleteConv}
|
||||
setModalState={setModalStateDeleteConv}
|
||||
handleDeleteAllConv={handleDeleteAllConversations}
|
||||
/>
|
||||
<Upload
|
||||
modalState={uploadModalState}
|
||||
setModalState={setUploadModalState}
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
export default function PageNotFound() {
|
||||
return (
|
||||
<div className="mx-5 grid min-h-screen md:mx-36">
|
||||
<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 lg:p-10 xl:p-16">
|
||||
<div className="grid min-h-screen dark:bg-raisin-black">
|
||||
<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 hover:bg-blue-3000">
|
||||
<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">
|
||||
<Link to="/">Go Back Home</Link>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@@ -1,725 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import ArrowLeft from './assets/arrow-left.svg';
|
||||
import ArrowRight from './assets/arrow-right.svg';
|
||||
import Trash from './assets/trash.svg';
|
||||
import {
|
||||
selectPrompt,
|
||||
setPrompt,
|
||||
selectSourceDocs,
|
||||
setSourceDocs,
|
||||
} from './preferences/preferenceSlice';
|
||||
import { Doc } from './preferences/preferenceApi';
|
||||
import { useDarkTheme } from './hooks';
|
||||
import Dropdown from './components/Dropdown';
|
||||
type PromptProps = {
|
||||
prompts: { name: string; id: string; type: string }[];
|
||||
selectedPrompt: { name: string; id: string; type: string };
|
||||
onSelectPrompt: (name: string, id: string, type: string) => void;
|
||||
setPrompts: (prompts: { name: string; id: string; type: string }[]) => void;
|
||||
apiHost: string;
|
||||
};
|
||||
|
||||
const Setting: React.FC = () => {
|
||||
const tabs = ['General', 'Prompts', 'Documents'];
|
||||
//const tabs = ['General', 'Prompts', 'Documents', 'Widgets'];
|
||||
|
||||
const [activeTab, setActiveTab] = useState('General');
|
||||
const [prompts, setPrompts] = useState<
|
||||
{ name: string; id: string; type: string }[]
|
||||
>([]);
|
||||
const selectedPrompt = useSelector(selectPrompt);
|
||||
const [isAddPromptModalOpen, setAddPromptModalOpen] = useState(false);
|
||||
const documents = useSelector(selectSourceDocs);
|
||||
const [isAddDocumentModalOpen, setAddDocumentModalOpen] = useState(false);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
||||
const [widgetScreenshot, setWidgetScreenshot] = useState<File | null>(null);
|
||||
|
||||
const updateWidgetScreenshot = (screenshot: File | null) => {
|
||||
setWidgetScreenshot(screenshot);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPrompts = async () => {
|
||||
try {
|
||||
const response = await fetch(`${apiHost}/api/get_prompts`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch prompts');
|
||||
}
|
||||
const promptsData = await response.json();
|
||||
setPrompts(promptsData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPrompts();
|
||||
}, []);
|
||||
|
||||
const onDeletePrompt = (name: string, id: string) => {
|
||||
setPrompts(prompts.filter((prompt) => prompt.id !== id));
|
||||
|
||||
fetch(`${apiHost}/api/delete_prompt`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
// send id in body only
|
||||
body: JSON.stringify({ id: id }),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete prompt');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteClick = (index: number, doc: Doc) => {
|
||||
const docPath = 'indexes/' + 'local' + '/' + doc.name;
|
||||
|
||||
fetch(`${apiHost}/api/delete_old?path=${docPath}`, {
|
||||
method: 'GET',
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok && documents) {
|
||||
const updatedDocuments = [
|
||||
...documents.slice(0, index),
|
||||
...documents.slice(index + 1),
|
||||
];
|
||||
dispatch(setSourceDocs(updatedDocuments));
|
||||
}
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="wa p-4 pt-20 md:p-12">
|
||||
<p className="text-2xl font-bold text-eerie-black dark:text-bright-gray">
|
||||
Settings
|
||||
</p>
|
||||
<div className="mt-6 flex flex-row items-center space-x-4 overflow-x-auto md:space-x-8 ">
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
onClick={() => scrollTabs(-1)}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-purple-30 transition-all hover:bg-gray-100"
|
||||
>
|
||||
<img src={ArrowLeft} alt="left-arrow" className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-nowrap space-x-4 overflow-x-auto md:space-x-8">
|
||||
{tabs.map((tab, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`h-9 rounded-3xl px-4 font-bold ${
|
||||
activeTab === tab
|
||||
? 'bg-purple-3000 text-purple-30 dark:bg-dark-charcoal'
|
||||
: 'text-gray-6000'
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
onClick={() => scrollTabs(1)}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-purple-30 hover:bg-gray-100"
|
||||
>
|
||||
<img src={ArrowRight} alt="right-arrow" className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{renderActiveTab()}
|
||||
|
||||
{/* {activeTab === 'Widgets' && (
|
||||
<Widgets
|
||||
widgetScreenshot={widgetScreenshot}
|
||||
onWidgetScreenshotChange={updateWidgetScreenshot}
|
||||
/>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
|
||||
function scrollTabs(direction: number) {
|
||||
const container = document.querySelector('.flex-nowrap');
|
||||
if (container) {
|
||||
container.scrollLeft += direction * 100; // Adjust the scroll amount as needed
|
||||
}
|
||||
}
|
||||
|
||||
function renderActiveTab() {
|
||||
switch (activeTab) {
|
||||
case 'General':
|
||||
return <General />;
|
||||
case 'Prompts':
|
||||
return (
|
||||
<Prompts
|
||||
prompts={prompts}
|
||||
selectedPrompt={selectedPrompt}
|
||||
onSelectPrompt={(name, id, type) =>
|
||||
dispatch(setPrompt({ name: name, id: id, type: type }))
|
||||
}
|
||||
setPrompts={setPrompts}
|
||||
apiHost={apiHost}
|
||||
/>
|
||||
);
|
||||
case 'Documents':
|
||||
return (
|
||||
<Documents
|
||||
documents={documents}
|
||||
handleDeleteDocument={handleDeleteClick}
|
||||
/>
|
||||
);
|
||||
case 'Widgets':
|
||||
return (
|
||||
<Widgets
|
||||
widgetScreenshot={widgetScreenshot} // Add this line
|
||||
onWidgetScreenshotChange={updateWidgetScreenshot} // Add this line
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const General: React.FC = () => {
|
||||
const themes = ['Light', 'Dark'];
|
||||
const languages = ['English'];
|
||||
const [isDarkTheme, toggleTheme] = useDarkTheme();
|
||||
const [selectedTheme, setSelectedTheme] = useState(
|
||||
isDarkTheme ? 'Dark' : 'Light',
|
||||
);
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
|
||||
return (
|
||||
<div className="mt-[59px]">
|
||||
<div className="mb-4">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">Select Theme</p>
|
||||
<Dropdown
|
||||
options={themes}
|
||||
selectedValue={selectedTheme}
|
||||
onSelect={(option: string) => {
|
||||
setSelectedTheme(option);
|
||||
option !== selectedTheme && toggleTheme();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
Select Language
|
||||
</p>
|
||||
<Dropdown
|
||||
options={languages}
|
||||
selectedValue={selectedLanguage}
|
||||
onSelect={setSelectedLanguage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Setting;
|
||||
|
||||
const Prompts: React.FC<PromptProps> = ({
|
||||
prompts,
|
||||
selectedPrompt,
|
||||
onSelectPrompt,
|
||||
setPrompts,
|
||||
apiHost,
|
||||
}) => {
|
||||
const handleSelectPrompt = ({
|
||||
name,
|
||||
id,
|
||||
type,
|
||||
}: {
|
||||
name: string;
|
||||
id: string;
|
||||
type: string;
|
||||
}) => {
|
||||
setNewPromptName(name);
|
||||
onSelectPrompt(name, id, type);
|
||||
};
|
||||
const [newPromptName, setNewPromptName] = useState(selectedPrompt.name);
|
||||
const [newPromptContent, setNewPromptContent] = useState('');
|
||||
|
||||
const handleAddPrompt = async () => {
|
||||
try {
|
||||
const response = await fetch(`${apiHost}/api/create_prompt`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: newPromptName,
|
||||
content: newPromptContent,
|
||||
}),
|
||||
});
|
||||
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' },
|
||||
]);
|
||||
}
|
||||
onSelectPrompt(newPromptName, newPrompt.id, newPromptContent);
|
||||
setNewPromptName(newPromptName);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePrompt = () => {
|
||||
setPrompts(prompts.filter((prompt) => prompt.id !== selectedPrompt.id));
|
||||
console.log('selectedPrompt.id', selectedPrompt.id);
|
||||
|
||||
fetch(`${apiHost}/api/delete_prompt`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ id: selectedPrompt.id }),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete prompt');
|
||||
}
|
||||
// get 1st prompt and set it as selected
|
||||
if (prompts.length > 0) {
|
||||
onSelectPrompt(prompts[0].name, prompts[0].id, prompts[0].type);
|
||||
setNewPromptName(prompts[0].name);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPromptContent = async () => {
|
||||
console.log('fetching prompt content');
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${apiHost}/api/get_single_prompt?id=${selectedPrompt.id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch prompt content');
|
||||
}
|
||||
const promptContent = await response.json();
|
||||
setNewPromptContent(promptContent.content);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPromptContent();
|
||||
}, [selectedPrompt]);
|
||||
|
||||
const handleSaveChanges = () => {
|
||||
fetch(`${apiHost}/api/update_prompt`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: selectedPrompt.id,
|
||||
name: newPromptName,
|
||||
content: newPromptContent,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update prompt');
|
||||
}
|
||||
onSelectPrompt(newPromptName, selectedPrompt.id, selectedPrompt.type);
|
||||
setNewPromptName(newPromptName);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-[59px]">
|
||||
<div className="mb-4">
|
||||
<p className="font-semibold dark:text-bright-gray">Active Prompt</p>
|
||||
<Dropdown
|
||||
options={prompts}
|
||||
selectedValue={selectedPrompt.name}
|
||||
onSelect={handleSelectPrompt}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="dark:text-bright-gray">Prompt name </p>{' '}
|
||||
<p className="mb-2 text-xs italic text-eerie-black dark:text-bright-gray">
|
||||
start by editing name
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={newPromptName}
|
||||
placeholder="Active Prompt Name"
|
||||
className="w-full rounded-lg border-2 p-2 dark:border-chinese-silver dark:bg-transparent dark:text-white"
|
||||
onChange={(e) => setNewPromptName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="mb-2 dark:text-bright-gray">Prompt content</p>
|
||||
<textarea
|
||||
className="h-32 w-full rounded-lg border-2 p-2 dark:border-chinese-silver dark:bg-transparent dark:text-white"
|
||||
value={newPromptContent}
|
||||
onChange={(e) => setNewPromptContent(e.target.value)}
|
||||
placeholder="Active prompt contents"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
className={`rounded-lg bg-green-500 px-4 py-2 font-bold text-white transition-all hover:bg-green-700 ${
|
||||
newPromptName === selectedPrompt.name
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}`}
|
||||
onClick={handleAddPrompt}
|
||||
disabled={newPromptName === selectedPrompt.name}
|
||||
>
|
||||
Add New Prompt
|
||||
</button>
|
||||
<button
|
||||
className={`rounded-lg bg-red-500 px-4 py-2 font-bold text-white transition-all hover:bg-red-700 ${
|
||||
selectedPrompt.type === 'public'
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}`}
|
||||
onClick={handleDeletePrompt}
|
||||
disabled={selectedPrompt.type === 'public'}
|
||||
>
|
||||
Delete Prompt
|
||||
</button>
|
||||
<button
|
||||
className={`rounded-lg bg-blue-500 px-4 py-2 font-bold text-white transition-all hover:bg-blue-700 ${
|
||||
selectedPrompt.type === 'public'
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}`}
|
||||
onClick={handleSaveChanges}
|
||||
disabled={selectedPrompt.type === 'public'}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type AddPromptModalProps = {
|
||||
newPromptName: string;
|
||||
onNewPromptNameChange: (name: string) => void;
|
||||
onAddPrompt: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const AddPromptModal: React.FC<AddPromptModalProps> = ({
|
||||
newPromptName,
|
||||
onNewPromptNameChange,
|
||||
onAddPrompt,
|
||||
onClose,
|
||||
}) => {
|
||||
return (
|
||||
<div className="fixed top-0 left-0 flex h-screen w-screen items-center justify-center bg-gray-900 bg-opacity-50">
|
||||
<div className="rounded-3xl bg-white p-4">
|
||||
<p className="mb-2 text-2xl font-bold text-jet">Add New Prompt</p>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter Prompt Name"
|
||||
value={newPromptName}
|
||||
onChange={(e) => onNewPromptNameChange(e.target.value)}
|
||||
className="mb-4 w-full rounded-3xl border-2 p-2 dark:border-chinese-silver"
|
||||
/>
|
||||
<button
|
||||
onClick={onAddPrompt}
|
||||
className="rounded-3xl bg-purple-300 px-4 py-2 font-bold text-white transition-all hover:bg-purple-600"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="mt-4 rounded-3xl px-4 py-2 font-bold text-red-500"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type DocumentsProps = {
|
||||
documents: Doc[] | null;
|
||||
handleDeleteDocument: (index: number, document: Doc) => void;
|
||||
};
|
||||
|
||||
const Documents: React.FC<DocumentsProps> = ({
|
||||
documents,
|
||||
handleDeleteDocument,
|
||||
}) => {
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<div className="flex flex-col">
|
||||
{/* <h2 className="text-xl font-semibold">Documents</h2> */}
|
||||
|
||||
<div className="mt-[27px] w-max overflow-x-auto rounded-xl border dark:border-chinese-silver">
|
||||
<table className="block w-full table-auto content-center justify-center text-center dark:text-bright-gray">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border-r p-4 md:w-[244px]">Document Name</th>
|
||||
<th className="w-[244px] border-r px-4 py-2">Vector Date</th>
|
||||
<th className="w-[244px] border-r px-4 py-2">Type</th>
|
||||
<th className="px-4 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{documents &&
|
||||
documents.map((document, index) => (
|
||||
<tr key={index}>
|
||||
<td className="border-r border-t px-4 py-2">
|
||||
{document.name}
|
||||
</td>
|
||||
<td className="border-r border-t px-4 py-2">
|
||||
{document.date}
|
||||
</td>
|
||||
<td className="border-r border-t px-4 py-2">
|
||||
{document.location === 'remote'
|
||||
? 'Pre-loaded'
|
||||
: 'Private'}
|
||||
</td>
|
||||
<td className="border-t px-4 py-2">
|
||||
{document.location !== 'remote' && (
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className="h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
id={`img-${index}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteDocument(index, document);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* <button
|
||||
onClick={toggleAddDocumentModal}
|
||||
className="mt-10 w-32 rounded-lg bg-purple-300 px-4 py-2 font-bold text-white transition-all hover:bg-purple-600"
|
||||
>
|
||||
Add New
|
||||
</button> */}
|
||||
</div>
|
||||
|
||||
{/* {isAddDocumentModalOpen && (
|
||||
<AddDocumentModal
|
||||
newDocument={newDocument}
|
||||
onNewDocumentChange={setNewDocument}
|
||||
onAddDocument={addDocument}
|
||||
onClose={toggleAddDocumentModal}
|
||||
/>
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type Document = {
|
||||
name: string;
|
||||
vectorDate: string;
|
||||
vectorLocation: string;
|
||||
};
|
||||
|
||||
// Modal for adding a new document
|
||||
type AddDocumentModalProps = {
|
||||
newDocument: Document;
|
||||
onNewDocumentChange: (document: Document) => void;
|
||||
onAddDocument: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const AddDocumentModal: React.FC<AddDocumentModalProps> = ({
|
||||
newDocument,
|
||||
onNewDocumentChange,
|
||||
onAddDocument,
|
||||
onClose,
|
||||
}) => {
|
||||
return (
|
||||
<div className="fixed top-0 left-0 flex h-screen w-screen items-center justify-center bg-gray-900 bg-opacity-50">
|
||||
<div className="w-[50%] rounded-lg bg-white p-4">
|
||||
<p className="mb-2 text-2xl font-bold text-jet">Add New Document</p>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Document Name"
|
||||
value={newDocument.name}
|
||||
onChange={(e) =>
|
||||
onNewDocumentChange({ ...newDocument, name: e.target.value })
|
||||
}
|
||||
className="mb-4 w-full rounded-lg border-2 p-2"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Vector Date"
|
||||
value={newDocument.vectorDate}
|
||||
onChange={(e) =>
|
||||
onNewDocumentChange({ ...newDocument, vectorDate: e.target.value })
|
||||
}
|
||||
className="mb-4 w-full rounded-lg border-2 p-2"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Vector Location"
|
||||
value={newDocument.vectorLocation}
|
||||
onChange={(e) =>
|
||||
onNewDocumentChange({
|
||||
...newDocument,
|
||||
vectorLocation: e.target.value,
|
||||
})
|
||||
}
|
||||
className="mb-4 w-full rounded-lg border-2 p-2"
|
||||
/>
|
||||
<button
|
||||
onClick={onAddDocument}
|
||||
className="rounded-lg bg-purple-300 px-4 py-2 font-bold text-white transition-all hover:bg-purple-600"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="mt-4 rounded-lg px-4 py-2 font-bold text-red-500"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Widgets: React.FC<{
|
||||
widgetScreenshot: File | null;
|
||||
onWidgetScreenshotChange: (screenshot: File | null) => void;
|
||||
}> = ({ widgetScreenshot, onWidgetScreenshotChange }) => {
|
||||
const widgetSources = ['Source 1', 'Source 2', 'Source 3'];
|
||||
const widgetMethods = ['Method 1', 'Method 2', 'Method 3'];
|
||||
const widgetTypes = ['Type 1', 'Type 2', 'Type 3'];
|
||||
|
||||
const [selectedWidgetSource, setSelectedWidgetSource] = useState(
|
||||
widgetSources[0],
|
||||
);
|
||||
const [selectedWidgetMethod, setSelectedWidgetMethod] = useState(
|
||||
widgetMethods[0],
|
||||
);
|
||||
const [selectedWidgetType, setSelectedWidgetType] = useState(widgetTypes[0]);
|
||||
|
||||
// const [widgetScreenshot, setWidgetScreenshot] = useState<File | null>(null);
|
||||
const [widgetCode, setWidgetCode] = useState<string>(''); // Your widget code state
|
||||
|
||||
const handleScreenshotChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const files = event.target.files;
|
||||
|
||||
if (files && files.length > 0) {
|
||||
const selectedScreenshot = files[0];
|
||||
onWidgetScreenshotChange(selectedScreenshot); // Update the screenshot in the parent component
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
// Create a new textarea element to select the text
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = widgetCode;
|
||||
document.body.appendChild(textArea);
|
||||
|
||||
// Select and copy the text
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
|
||||
// Clean up the textarea element
|
||||
document.body.removeChild(textArea);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-[59px]">
|
||||
<p className="font-bold text-jet">Widget Source</p>
|
||||
<Dropdown
|
||||
options={widgetSources}
|
||||
selectedValue={selectedWidgetSource}
|
||||
onSelect={setSelectedWidgetSource}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<p className="font-bold text-jet">Widget Method</p>
|
||||
<Dropdown
|
||||
options={widgetMethods}
|
||||
selectedValue={selectedWidgetMethod}
|
||||
onSelect={setSelectedWidgetMethod}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<p className="font-bold text-jet">Widget Type</p>
|
||||
<Dropdown
|
||||
options={widgetTypes}
|
||||
selectedValue={selectedWidgetType}
|
||||
onSelect={setSelectedWidgetType}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<p className="font-bold text-jet">Widget Code Snippet</p>
|
||||
<textarea
|
||||
rows={4}
|
||||
value={widgetCode}
|
||||
onChange={(e) => setWidgetCode(e.target.value)}
|
||||
className="mt-3 w-full rounded-lg border-2 p-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<button
|
||||
onClick={handleCopyToClipboard}
|
||||
className="rounded-lg bg-blue-400 px-2 py-2 font-bold text-white transition-all hover:bg-blue-600"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<p className="text-lg font-semibold">Widget Screenshot</p>
|
||||
<input type="file" accept="image/*" onChange={handleScreenshotChange} />
|
||||
</div>
|
||||
|
||||
{widgetScreenshot && (
|
||||
<div className="mt-4">
|
||||
<img
|
||||
src={URL.createObjectURL(widgetScreenshot)}
|
||||
alt="Widget Screenshot"
|
||||
className="max-w-full rounded-lg border border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
3
frontend/src/assets/TwitterX.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.175 0.843262H16.9354L10.9054 7.75269L18 17.1564H12.4457L8.09229 11.4543L3.11657 17.1564H0.353571L6.80271 9.76355L0 0.844547H5.69571L9.62486 6.05555L14.175 0.843262ZM13.2043 15.5004H14.7343L4.86 2.41312H3.21943L13.2043 15.5004Z" fill="#747474"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 361 B |
3
frontend/src/assets/red-trash.svg
Normal file
@@ -0,0 +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>
|
||||
|
After Width: | Height: | Size: 511 B |
3
frontend/src/assets/share.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="17" viewBox="0 0 14 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.04167 7.2997C1.96431 7.2997 1.89013 7.32976 1.83543 7.38326C1.78073 7.43677 1.75 7.50934 1.75 7.585V15.0029C1.75 15.1604 1.88067 15.2882 2.04167 15.2882H11.9583C12.0357 15.2882 12.1099 15.2581 12.1646 15.2046C12.2193 15.1511 12.25 15.0785 12.25 15.0029V7.585C12.25 7.50934 12.2193 7.43677 12.1646 7.38326C12.1099 7.32976 12.0357 7.2997 11.9583 7.2997H10.7917C10.5596 7.2997 10.337 7.20952 10.1729 7.04901C10.0089 6.8885 9.91667 6.67079 9.91667 6.44379C9.91667 6.21679 10.0089 5.99909 10.1729 5.83857C10.337 5.67806 10.5596 5.58788 10.7917 5.58788H11.9583C13.0853 5.58788 14 6.48259 14 7.585V15.0029C14 15.5325 13.7849 16.0405 13.402 16.4151C13.0191 16.7896 12.4998 17 11.9583 17H2.04167C1.50018 17 0.980877 16.7896 0.59799 16.4151C0.215104 16.0405 0 15.5325 0 15.0029V7.585C0 6.48259 0.914667 5.58788 2.04167 5.58788H3.20833C3.4404 5.58788 3.66296 5.67806 3.82705 5.83857C3.99115 5.99909 4.08333 6.21679 4.08333 6.44379C4.08333 6.67079 3.99115 6.8885 3.82705 7.04901C3.66296 7.20952 3.4404 7.2997 3.20833 7.2997H2.04167ZM6.7935 0.0838185C6.82059 0.0572492 6.85278 0.0361694 6.88821 0.0217864C6.92365 0.0074035 6.96164 0 7 0C7.03836 0 7.07635 0.0074035 7.11179 0.0217864C7.14722 0.0361694 7.17941 0.0572492 7.2065 0.0838185L10.5852 3.38877C10.6261 3.42867 10.6539 3.47955 10.6652 3.53496C10.6765 3.59037 10.6707 3.64782 10.6486 3.70001C10.6265 3.75221 10.589 3.7968 10.541 3.82815C10.4929 3.85949 10.4364 3.87617 10.3787 3.87607H7.875V10.438C7.875 10.665 7.78281 10.8827 7.61872 11.0433C7.45462 11.2038 7.23206 11.2939 7 11.2939C6.76794 11.2939 6.54538 11.2038 6.38128 11.0433C6.21719 10.8827 6.125 10.665 6.125 10.438V3.87607H3.62133C3.56357 3.87617 3.50708 3.85949 3.45902 3.82815C3.41096 3.7968 3.37349 3.75221 3.35138 3.70001C3.32926 3.64782 3.32348 3.59037 3.33478 3.53496C3.34607 3.47955 3.37394 3.42867 3.41483 3.38877L6.7935 0.0838185Z" fill="#747474"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
frontend/src/assets/spinner-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="white" d="M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z"><animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></svg>
|
||||
|
After Width: | Height: | Size: 454 B |
@@ -1,9 +1 @@
|
||||
<svg width="30" height="33" viewBox="0 0 30 33" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect width="30" height="33" fill="none"/>
|
||||
<defs>
|
||||
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_1_917" transform="scale(0.0166667 0.0151515)"/>
|
||||
</pattern>
|
||||
<image id="image0_1_917" width="60" height="66" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAABCCAYAAAAL1LXDAAAGuElEQVRoge2aSXPaSheG3wYJBJiSQAx2mCInhqLiSjZJKllml1+cn5BVUnE5k4sEDAZjZgkHMUiIvgtf9FkxYIwxn+H63bXULZ1H3eo+ffoQSinFf0i2/7cBq9YD8KbrAXjT9QC86WJW9SJKKTqdDlRVhc1mg9frhcvlQq/XA8MwYFl2JXbcGbAsy1AUBZVKBaqqQtd1MAwDTdNACIGmaWAYBjzPY2trC06nE6FQCD6fD5RS2Gw22GzLH4BkGZ7WaDRCu92GqqpotVqoVqtQFAW6rkPXdQAXPfz3qwgh5jWO4+B0OuH1ehGNRuH1euHz+eDxeG5rnvWdywCu1Wr49esXFEVBp9NBr9e7Aje3Qf9+BI/Hg1gshng8jkgkAkopCCG3NfV2wM1mE1+/fkWpVDJ7cpY4joPL5QKlFL1eD4PBACzLglKK4XB41ThCwPM8kskktre3wfM87Hb7rcBvDGwYBlqtFsrlMkqlEprNJgzDmAgnCAICgQAEQTAnJZZlYRiG+T+rqmqODEVRJo4OlmXh9Xqxv7+PaDQKp9O5OmBZlvH582dUKpUrver1ehEMBuHxeOD3+81/0GazgRACu91uqT8ajcyJqdlsQtd1yLKMTCZjTnSj0cisHwwG8fjxY8RiMfA8vxrgDx8+oFarWXqBYRiIoghJkrC9vQ2PxwOHw7GQQYZhoNFooFQqodFooF6vWz6s2+1GJBJBKpVCIBC48fCeG/jPnz/49OkTTk5OLF+d4zjs7u4imUyC5/mlLSWGYUBRFOTzeWQyGfT7ffMewzDw+Xx49+7djWfxuYBbrRZ+/PiBfD5vfm2Hw4FEIoF0Og1RFG+IM780TUOhUMDBwQE6nY7lniRJePXqFdxu99w9fW139Pt9ZLNZFItFE5ZlWUSjUaTTafh8vgUw5pfD4YAkSXj9+jUCgQAY5n++Uj6fRzabhaqqcz/vWuCzszPkcjlzSBFCIIoiUqkURFG8E2/obzEMg3g8jhcvXiAcDpuTH6UUP3/+xPHx8cRlbeKzZt0cDAY4PDxEt9s1r1FK8fLlSwQCgVsg3FyEEMTjcXNZq1QqAIBut4vj42OEQiGEw+FrnzOzew4ODtBut80yz/N4//49RFFciteziILBIPb398FxnHlNlmVks9m52k8FrtfrqFarFqciFoutbBhPE8MwiMVi2N3dNf/n0WiEYrGIer1uWUEmaaLlg8EAhUIBsiybPenz+RCJRBZeX5etRCIBQRBM+8ZD+7oJbCKw3W5HNBqFJEngeR4ulwuSJCEYDC7f8gUliiISiYSlAyqVChqNxsx2EycthmEQDofh9/vR7XZxdHSEp0+frmyTPo9YlsXOzg7Ozs5QLpcBAJ1OB7VaDZIkTW039WckhMDhcEAQBLx582bp+9JlSBAERKNRs6xpGhRFmdlmrWNaLMsiGAzC5XIBuFgyz8/PIcvy1DZrDQxcQF/eOXW7XdRqtan11x7Y4/FYnKDRaIRmszm1/toD22w2CIJgKV/2DK/UX4VRdylCiGX1oJRC1/WpMbW1Bx5vJMauJqUUDMNc2UqOtfbAwMXENZ6pAZixsknaCODhcGjx+QkhU/39jQC22Ww4Pz83y4ZhIBQKTa67KqPuSpRSy1aREIJerze1/toDE0LQ6XTMTQSlFOFweCr02gMDQLlcvhJJ3dhlqd1uo9/vmzEth8MBp9MJt9s9sf7aA6uqitPTU7PMcRx2dnam1l9rYMMwIMuyuSQRQsBx3MwA41oD9/t95HI5s0wpRSwWm3lUu7bAuq7j9+/flvV3fJg3K8i4tsCtVgsnJyfQNA3AxXBOJBLw+/2bBzwajVCtVq/EzB89enTt2fHKsniWpeFwiFwuh+/fv5tnXeM9cSQSubb9WgFTSiHLMj5+/GiZmARBwNu3b+d6xloBV6tVfPnyxXLN7Xbj+fPnFn96ltYCWNM0NBoNHB0dodlsmr3rdruxt7c309H4W2sBfHp6im/fvlmcDIZhEIlE8OTJk7l7F7jHwONkt0wmYzmfHsvn8yGVSt04ueXeAY/DM8Vi0UyLunzYTQjBs2fPkE6nFzoNuRfAlFIoigJZltFqtaCqqpnsdnk2drvdSKfTSCaTNxrGl3UvgIfDIQ4PD1Eul68MXeAiMikIAiRJgiRJt0pMuxfAhmHAMIyJsJcTaERRtCS1LKJ7ATz2lAqFAgghcLlc2NragiAI2NvbmxqQW0T3Anh8Hs2yLMLhsHk27fF4Fk4xnKalpA8vQ5RStFqtO01yA+4R8Kq0ltvD2+gBeNP1ALzp+s8B/wPYvPrTcSgesQAAAABJRU5ErkJggg=="/>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="black" d="M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z"><animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 454 B |
3
frontend/src/assets/three-dots.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="25" viewBox="0 0 10 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 3.5C3.9 3.5 3 4.4 3 5.5C3 6.6 3.9 7.5 5 7.5C6.1 7.5 7 6.6 7 5.5C7 4.4 6.1 3.5 5 3.5ZM5 17.5C3.9 17.5 3 18.4 3 19.5C3 20.6 3.9 21.5 5 21.5C6.1 21.5 7 20.6 7 19.5C7 18.4 6.1 17.5 5 17.5ZM5 10.5C3.9 10.5 3 11.4 3 12.5C3 13.6 3.9 14.5 5 14.5C6.1 14.5 7 13.6 7 12.5C7 11.4 6.1 10.5 5 10.5Z" fill="#747474"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 418 B |
45
frontend/src/components/CopyButton.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useState } from 'react';
|
||||
import Copy from './../assets/copy.svg?react';
|
||||
import CheckMark from './../assets/checkmark.svg?react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
|
||||
export default function CoppyButton({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isCopyHovered, setIsCopyHovered] = useState(false);
|
||||
|
||||
const handleCopyClick = (text: string) => {
|
||||
copy(text);
|
||||
setCopied(true);
|
||||
// Reset copied to false after a few seconds
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full p-2 ${
|
||||
isCopyHovered
|
||||
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
|
||||
: 'bg-[#ffffff] dark:bg-transparent'
|
||||
}`}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckMark
|
||||
className="cursor-pointer stroke-green-2000"
|
||||
onMouseEnter={() => setIsCopyHovered(true)}
|
||||
onMouseLeave={() => setIsCopyHovered(false)}
|
||||
/>
|
||||
) : (
|
||||
<Copy
|
||||
className="cursor-pointer fill-none"
|
||||
onClick={() => {
|
||||
handleCopyClick(text);
|
||||
}}
|
||||
onMouseEnter={() => setIsCopyHovered(true)}
|
||||
onMouseLeave={() => setIsCopyHovered(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,57 +1,104 @@
|
||||
import { useState } from 'react';
|
||||
import React from 'react';
|
||||
import Arrow2 from '../assets/dropdown-arrow.svg';
|
||||
import Edit from '../assets/edit.svg';
|
||||
import Trash from '../assets/trash.svg';
|
||||
|
||||
function Dropdown({
|
||||
options,
|
||||
selectedValue,
|
||||
onSelect,
|
||||
size = 'w-32',
|
||||
rounded = 'xl',
|
||||
border = 'border-2',
|
||||
borderColor = 'silver',
|
||||
showEdit,
|
||||
onEdit,
|
||||
showDelete,
|
||||
onDelete,
|
||||
placeholder,
|
||||
}: {
|
||||
options:
|
||||
| string[]
|
||||
| { name: string; id: string; type: string }[]
|
||||
| { label: string; value: string }[];
|
||||
selectedValue: string | { label: string; value: string };
|
||||
| { label: string; value: string }[]
|
||||
| { value: number; description: string }[];
|
||||
selectedValue:
|
||||
| string
|
||||
| { label: string; value: string }
|
||||
| { value: number; description: string }
|
||||
| null;
|
||||
onSelect:
|
||||
| ((value: string) => void)
|
||||
| ((value: { name: string; id: string; type: string }) => void)
|
||||
| ((value: { label: string; value: string }) => void);
|
||||
| ((value: { label: string; value: string }) => void)
|
||||
| ((value: { value: number; description: string }) => void);
|
||||
size?: string;
|
||||
rounded?: 'xl' | '3xl';
|
||||
border?: 'border' | 'border-2';
|
||||
borderColor?: string;
|
||||
showEdit?: boolean;
|
||||
onEdit?: (value: { name: string; id: string; type: string }) => void;
|
||||
showDelete?: boolean;
|
||||
onDelete?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const borderRadius = rounded === 'xl' ? 'rounded-xl' : 'rounded-3xl';
|
||||
const borderTopRadius = rounded === 'xl' ? 'rounded-t-xl' : 'rounded-t-3xl';
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
className={[
|
||||
typeof selectedValue === 'string'
|
||||
? 'relative mt-2 w-32'
|
||||
: 'relative w-full align-middle'
|
||||
}
|
||||
? 'relative mt-2'
|
||||
: 'relative align-middle',
|
||||
size,
|
||||
].join(' ')}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`flex w-full cursor-pointer items-center justify-between border-2 bg-white p-3 dark:border-chinese-silver dark:bg-transparent ${
|
||||
isOpen
|
||||
? typeof selectedValue === 'string'
|
||||
? 'rounded-t-xl'
|
||||
: 'rounded-t-2xl'
|
||||
: typeof selectedValue === 'string'
|
||||
? 'rounded-xl'
|
||||
: 'rounded-full'
|
||||
className={`flex w-full cursor-pointer items-center justify-between ${border} border-${borderColor} bg-white px-5 py-3 dark:border-${borderColor}/40 dark:bg-transparent ${
|
||||
isOpen ? `${borderTopRadius}` : `${borderRadius}`
|
||||
}`}
|
||||
>
|
||||
{typeof selectedValue === 'string' ? (
|
||||
<span className="flex-1 overflow-hidden text-ellipsis dark:text-bright-gray">
|
||||
<span className="overflow-hidden text-ellipsis dark:text-bright-gray">
|
||||
{selectedValue}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`overflow-hidden text-ellipsis dark:text-bright-gray ${
|
||||
!selectedValue && 'text-silver'
|
||||
!selectedValue && 'text-silver dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{selectedValue ? selectedValue.label : 'From URL'}
|
||||
{selectedValue && 'label' in selectedValue
|
||||
? selectedValue.label
|
||||
: selectedValue && 'description' in selectedValue
|
||||
? `${
|
||||
selectedValue.value < 1e9
|
||||
? selectedValue.value + ` (${selectedValue.description})`
|
||||
: selectedValue.description
|
||||
}`
|
||||
: placeholder
|
||||
? placeholder
|
||||
: 'From URL'}
|
||||
</span>
|
||||
)}
|
||||
<img
|
||||
@@ -63,7 +110,9 @@ function Dropdown({
|
||||
/>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="absolute left-0 right-0 z-50 -mt-1 overflow-y-auto rounded-b-xl border-2 bg-white shadow-lg dark:border-chinese-silver dark:bg-dark-charcoal">
|
||||
<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-${borderColor}/40 dark:bg-dark-charcoal`}
|
||||
>
|
||||
{options.map((option: any, index) => (
|
||||
<div
|
||||
key={index}
|
||||
@@ -74,17 +123,49 @@ function Dropdown({
|
||||
onSelect(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className="ml-2 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3 dark:text-light-gray"
|
||||
className="ml-5 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3 dark:text-light-gray"
|
||||
>
|
||||
{typeof option === 'string'
|
||||
? option
|
||||
: option.name
|
||||
? option.name
|
||||
: option.label}
|
||||
: option.label
|
||||
? option.label
|
||||
: `${
|
||||
option.value < 1e9
|
||||
? option.value + ` (${option.description})`
|
||||
: option.description
|
||||
}`}
|
||||
</span>
|
||||
{showEdit && onEdit && (
|
||||
<img
|
||||
src={Edit}
|
||||
alt="Edit"
|
||||
className="mr-4 h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
onClick={() => {
|
||||
onEdit({
|
||||
id: option.id,
|
||||
name: option.name,
|
||||
type: option.type,
|
||||
});
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showDelete && onDelete && (
|
||||
<button onClick={() => onDelete(option)} className="p-2">
|
||||
Delete
|
||||
<button
|
||||
onClick={() => onDelete(option.id)}
|
||||
disabled={option.type === 'public'}
|
||||
>
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className={`mr-2 h-4 w-4 cursor-pointer hover:opacity-50 ${
|
||||
option.type === 'public'
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
43
frontend/src/components/Input.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { InputProps } from './types';
|
||||
|
||||
const Input = ({
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
value,
|
||||
isAutoFocused = false,
|
||||
placeholder,
|
||||
maxLength,
|
||||
className,
|
||||
colorVariant = 'silver',
|
||||
children,
|
||||
onChange,
|
||||
onPaste,
|
||||
onKeyDown,
|
||||
}: InputProps) => {
|
||||
const colorStyles = {
|
||||
silver: 'border-silver dark:border-silver/40',
|
||||
jet: 'border-jet',
|
||||
gray: 'border-gray-5000 dark:text-silver',
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
className={`h-[42px] w-full rounded-full border-2 px-3 outline-none dark:bg-transparent dark:text-white ${className} ${colorStyles[colorVariant]}`}
|
||||
type={type}
|
||||
id={id}
|
||||
name={name}
|
||||
autoFocus={isAutoFocused}
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
{children}
|
||||
</input>
|
||||
);
|
||||
};
|
||||
|
||||
export default Input;
|
||||
17
frontend/src/components/RetryIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { SVGProps } from 'react';
|
||||
const RetryIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlSpace="preserve"
|
||||
width={16}
|
||||
height={16}
|
||||
fill={props.fill}
|
||||
stroke={props.stroke}
|
||||
viewBox="0 0 383.748 383.748"
|
||||
{...props}
|
||||
>
|
||||
<path d="M62.772 95.042C90.904 54.899 137.496 30 187.343 30c83.743 0 151.874 68.13 151.874 151.874h30C369.217 81.588 287.629 0 187.343 0c-35.038 0-69.061 9.989-98.391 28.888a182.423 182.423 0 0 0-47.731 44.705L2.081 34.641v113.365h113.91L62.772 95.042zM381.667 235.742h-113.91l53.219 52.965c-28.132 40.142-74.724 65.042-124.571 65.042-83.744 0-151.874-68.13-151.874-151.874h-30c0 100.286 81.588 181.874 181.874 181.874 35.038 0 69.062-9.989 98.391-28.888a182.443 182.443 0 0 0 47.731-44.706l39.139 38.952V235.742z" />
|
||||
</svg>
|
||||
);
|
||||
export default RetryIcon;
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import Trash from '../assets/trash.svg';
|
||||
import Arrow2 from '../assets/dropdown-arrow.svg';
|
||||
import { Doc } from '../preferences/preferenceApi';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
type Props = {
|
||||
options: Doc[] | null;
|
||||
selectedDocs: Doc | null;
|
||||
@@ -21,21 +22,46 @@ function SourceDropdown({
|
||||
handleDeleteClick,
|
||||
}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||
const embeddingsName =
|
||||
import.meta.env.VITE_EMBEDDINGS_NAME ||
|
||||
'huggingface_sentence-transformers/all-mpnet-base-v2';
|
||||
|
||||
const handleEmptyDocumentSelect = () => {
|
||||
dispatch(setSelectedDocs(null));
|
||||
setIsDocsListOpen(false);
|
||||
};
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsDocsListOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className="relative w-5/6 rounded-3xl">
|
||||
<div className="relative w-5/6 rounded-3xl" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsDocsListOpen(!isDocsListOpen)}
|
||||
className={`flex w-full cursor-pointer items-center border-2 bg-white p-3 dark:border-chinese-silver dark:bg-transparent ${
|
||||
isDocsListOpen ? 'rounded-t-3xl' : 'rounded-3xl'
|
||||
className={`flex w-full cursor-pointer items-center border border-silver bg-white p-[14px] dark:bg-transparent ${
|
||||
isDocsListOpen
|
||||
? 'rounded-t-3xl dark:border-silver/40'
|
||||
: 'rounded-3xl dark:border-purple-taupe'
|
||||
}`}
|
||||
>
|
||||
<span className="ml-1 mr-2 flex-1 overflow-hidden text-ellipsis text-left dark:text-bright-gray">
|
||||
<div className="flex flex-row gap-2">
|
||||
<p className="max-w-3/4 truncate whitespace-nowrap">
|
||||
{selectedDocs?.name}
|
||||
{selectedDocs?.name || 'None'}
|
||||
</p>
|
||||
<p className="flex flex-col items-center justify-center">
|
||||
{selectedDocs?.version}
|
||||
@@ -51,7 +77,7 @@ function SourceDropdown({
|
||||
/>
|
||||
</button>
|
||||
{isDocsListOpen && (
|
||||
<div className="absolute left-0 right-0 z-50 -mt-1 max-h-40 overflow-y-auto rounded-b-xl border-2 bg-white shadow-lg dark:border-chinese-silver dark:bg-dark-charcoal">
|
||||
<div className="absolute left-0 right-0 z-50 -mt-1 max-h-40 overflow-y-auto rounded-b-xl border border-silver bg-white shadow-lg dark:border-silver/40 dark:bg-dark-charcoal">
|
||||
{options ? (
|
||||
options.map((option: any, index: number) => {
|
||||
if (option.model === embeddingsName) {
|
||||
@@ -80,7 +106,7 @@ function SourceDropdown({
|
||||
id={`img-${index}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteClick(index, option);
|
||||
handleDeleteClick(option);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -89,10 +115,16 @@ function SourceDropdown({
|
||||
}
|
||||
})
|
||||
) : (
|
||||
<div className="h-10 w-full cursor-pointer border-b-[1px] hover:bg-gray-100 dark:border-b-purple-taupe dark:hover:bg-purple-taupe">
|
||||
<p className="ml-5 py-3">No default documentation.</p>
|
||||
</div>
|
||||
<></>
|
||||
)}
|
||||
<div
|
||||
className="flex cursor-pointer items-center justify-between hover:bg-gray-100 dark:text-bright-gray dark:hover:bg-purple-taupe"
|
||||
onClick={handleEmptyDocumentSelect}
|
||||
>
|
||||
<span className="ml-4 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap py-3">
|
||||
{t('none')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
21
frontend/src/components/types/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export type InputProps = {
|
||||
type: 'text' | 'number';
|
||||
value: string | string[] | number;
|
||||
colorVariant?: 'silver' | 'jet' | 'gray';
|
||||
isAutoFocused?: boolean;
|
||||
id?: string;
|
||||
maxLength?: number;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
children?: React.ReactElement;
|
||||
onChange: (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
|
||||
) => void;
|
||||
onPaste?: (
|
||||
e: React.ClipboardEvent<HTMLTextAreaElement | HTMLInputElement>,
|
||||
) => void;
|
||||
onKeyDown?: (
|
||||
e: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>,
|
||||
) => void;
|
||||
};
|
||||
@@ -11,15 +11,23 @@ import {
|
||||
selectStatus,
|
||||
updateQuery,
|
||||
} from './conversationSlice';
|
||||
import { selectConversationId } from '../preferences/preferenceSlice';
|
||||
import Send from './../assets/send.svg';
|
||||
import SendDark from './../assets/send_dark.svg';
|
||||
import Spinner from './../assets/spinner.svg';
|
||||
import SpinnerDark from './../assets/spinner-dark.svg';
|
||||
import { FEEDBACK, Query } from './conversationModels';
|
||||
import { sendFeedback } from './conversationApi';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ArrowDown from './../assets/arrow-down.svg';
|
||||
import RetryIcon from '../components/RetryIcon';
|
||||
import ShareIcon from '../assets/share.svg';
|
||||
import { ShareConversationModal } from '../modals/ShareConversationModal';
|
||||
|
||||
export default function Conversation() {
|
||||
const queries = useSelector(selectQueries);
|
||||
const status = useSelector(selectStatus);
|
||||
const conversationId = useSelector(selectConversationId);
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const endMessageRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
@@ -27,6 +35,9 @@ export default function Conversation() {
|
||||
const [hasScrolledToLast, setHasScrolledToLast] = useState(true);
|
||||
const fetchStream = useRef<any>(null);
|
||||
const [eventInterrupt, setEventInterrupt] = useState(false);
|
||||
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
|
||||
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleUserInterruption = () => {
|
||||
if (!eventInterrupt && status === 'loading') setEventInterrupt(true);
|
||||
@@ -70,6 +81,13 @@ export default function Conversation() {
|
||||
};
|
||||
}, [endMessageRef.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (queries.length) {
|
||||
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
|
||||
queries[queries.length - 1].response && setLastQueryReturnedErr(false); //considering a query that initially returned error can later include a response property on retry
|
||||
}
|
||||
}, [queries[queries.length - 1]]);
|
||||
|
||||
const scrollIntoView = () => {
|
||||
endMessageRef?.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
@@ -77,13 +95,20 @@ export default function Conversation() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleQuestion = (question: string) => {
|
||||
const handleQuestion = ({
|
||||
question,
|
||||
isRetry = false,
|
||||
}: {
|
||||
question: string;
|
||||
isRetry?: boolean;
|
||||
}) => {
|
||||
question = question.trim();
|
||||
if (question === '') return;
|
||||
setEventInterrupt(false);
|
||||
dispatch(addQuery({ prompt: question }));
|
||||
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
|
||||
fetchStream.current = dispatch(fetchAnswer({ question }));
|
||||
};
|
||||
|
||||
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
|
||||
const prevFeedback = query.feedback;
|
||||
dispatch(updateQuery({ index, query: { feedback } }));
|
||||
@@ -92,19 +117,32 @@ export default function Conversation() {
|
||||
);
|
||||
};
|
||||
|
||||
const handleQuestionSubmission = () => {
|
||||
if (inputRef.current?.textContent && status !== 'loading') {
|
||||
if (lastQueryReturnedErr) {
|
||||
// update last failed query with new prompt
|
||||
dispatch(
|
||||
updateQuery({
|
||||
index: queries.length - 1,
|
||||
query: {
|
||||
prompt: inputRef.current.textContent,
|
||||
},
|
||||
}),
|
||||
);
|
||||
handleQuestion({
|
||||
question: queries[queries.length - 1].prompt,
|
||||
isRetry: true,
|
||||
});
|
||||
} else {
|
||||
handleQuestion({ question: inputRef.current.textContent });
|
||||
}
|
||||
inputRef.current.textContent = '';
|
||||
}
|
||||
};
|
||||
|
||||
const prepResponseView = (query: Query, index: number) => {
|
||||
let responseView;
|
||||
if (query.error) {
|
||||
responseView = (
|
||||
<ConversationBubble
|
||||
ref={endMessageRef}
|
||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`}
|
||||
key={`${index}ERROR`}
|
||||
message={query.error}
|
||||
type="ERROR"
|
||||
></ConversationBubble>
|
||||
);
|
||||
} else if (query.response) {
|
||||
if (query.response) {
|
||||
responseView = (
|
||||
<ConversationBubble
|
||||
ref={endMessageRef}
|
||||
@@ -119,6 +157,35 @@ export default function Conversation() {
|
||||
}
|
||||
></ConversationBubble>
|
||||
);
|
||||
} else if (query.error) {
|
||||
const retryBtn = (
|
||||
<button
|
||||
className="flex items-center justify-center gap-3 self-center rounded-full border border-silver py-3 px-5 text-lg text-gray-500 transition-colors delay-100 hover:border-gray-500 disabled:cursor-not-allowed dark:text-bright-gray"
|
||||
disabled={status === 'loading'}
|
||||
onClick={() => {
|
||||
handleQuestion({
|
||||
question: queries[queries.length - 1].prompt,
|
||||
isRetry: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<RetryIcon
|
||||
fill={isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)'}
|
||||
stroke={isDarkTheme ? 'rgb(236 236 241)' : 'rgb(107 114 120)'}
|
||||
/>
|
||||
Retry
|
||||
</button>
|
||||
);
|
||||
responseView = (
|
||||
<ConversationBubble
|
||||
ref={endMessageRef}
|
||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'} `}
|
||||
key={`${index}ERROR`}
|
||||
message={query.error}
|
||||
type="ERROR"
|
||||
retryBtn={retryBtn}
|
||||
></ConversationBubble>
|
||||
);
|
||||
}
|
||||
return responseView;
|
||||
};
|
||||
@@ -130,86 +197,109 @@ export default function Conversation() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onWheel={handleUserInterruption}
|
||||
onTouchMove={handleUserInterruption}
|
||||
className="flex w-full flex-col justify-center p-4 md:flex-row"
|
||||
>
|
||||
{queries.length > 0 && !hasScrolledToLast && (
|
||||
<button
|
||||
onClick={scrollIntoView}
|
||||
aria-label="scroll to bottom"
|
||||
className="fixed bottom-32 right-14 z-10 flex h-7 w-7 items-center justify-center rounded-full border-[0.5px] border-gray-alpha bg-gray-100 bg-opacity-50 dark:bg-purple-taupe md:h-9 md:w-9 md:bg-opacity-100 "
|
||||
>
|
||||
<img
|
||||
src={ArrowDown}
|
||||
alt="arrow down"
|
||||
className="h4- w-4 opacity-50 md:h-5 md:w-5"
|
||||
/>
|
||||
</button>
|
||||
<div className="flex h-screen flex-col gap-7 pb-2">
|
||||
{conversationId && (
|
||||
<>
|
||||
<button
|
||||
title="Share"
|
||||
onClick={() => {
|
||||
setShareModalState(true);
|
||||
}}
|
||||
className="fixed top-4 right-20 z-30 rounded-full hover:bg-bright-gray dark:hover:bg-[#28292E]"
|
||||
>
|
||||
<img
|
||||
className="m-2 h-5 w-5 filter dark:invert"
|
||||
alt="share"
|
||||
src={ShareIcon}
|
||||
/>
|
||||
</button>
|
||||
{isShareModalOpen && (
|
||||
<ShareConversationModal
|
||||
close={() => {
|
||||
setShareModalState(false);
|
||||
}}
|
||||
conversationId={conversationId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
onWheel={handleUserInterruption}
|
||||
onTouchMove={handleUserInterruption}
|
||||
className="flex h-[90%] w-full flex-1 justify-center overflow-y-auto p-4 md:h-[83vh]"
|
||||
>
|
||||
{queries.length > 0 && !hasScrolledToLast && (
|
||||
<button
|
||||
onClick={scrollIntoView}
|
||||
aria-label="scroll to bottom"
|
||||
className="fixed bottom-40 right-14 z-10 flex h-7 w-7 items-center justify-center rounded-full border-[0.5px] border-gray-alpha bg-gray-100 bg-opacity-50 dark:bg-purple-taupe md:h-9 md:w-9 md:bg-opacity-100 "
|
||||
>
|
||||
<img
|
||||
src={ArrowDown}
|
||||
alt="arrow down"
|
||||
className="h-4 w-4 opacity-50 md:h-5 md:w-5"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{queries.length > 0 && (
|
||||
<div className="mt-20 mb-9 flex flex-col transition-all md:w-3/4">
|
||||
{queries.map((query, index) => {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<ConversationBubble
|
||||
className={'mb-7 last:mb-28'}
|
||||
key={`${index}QUESTION`}
|
||||
message={query.prompt}
|
||||
type="QUESTION"
|
||||
sources={query.sources}
|
||||
></ConversationBubble>
|
||||
{prepResponseView(query, index)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{queries.length === 0 && <Hero className="mt-24 md:mt-52"></Hero>}
|
||||
<div className="absolute bottom-0 flex w-11/12 flex-col items-end self-center bg-white pt-4 dark:bg-raisin-black md:fixed md:w-[65%]">
|
||||
<div className="flex h-full w-full">
|
||||
{queries.length > 0 && (
|
||||
<div className="mt-16 w-full md:w-8/12">
|
||||
{queries.map((query, index) => {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<ConversationBubble
|
||||
className={'mb-1 last:mb-28 md:mb-7'}
|
||||
key={`${index}QUESTION`}
|
||||
message={query.prompt}
|
||||
type="QUESTION"
|
||||
sources={query.sources}
|
||||
></ConversationBubble>
|
||||
|
||||
{prepResponseView(query, index)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{queries.length === 0 && <Hero handleQuestion={handleQuestion} />}
|
||||
</div>
|
||||
|
||||
<div className="flex w-11/12 flex-col items-end self-center rounded-2xl bg-opacity-0 pb-1 sm:w-6/12">
|
||||
<div className="flex h-full w-full items-center rounded-[40px] border border-silver bg-white py-1 dark:bg-raisin-black">
|
||||
<div
|
||||
id="inputbox"
|
||||
ref={inputRef}
|
||||
tabIndex={1}
|
||||
placeholder="Type your message here..."
|
||||
placeholder={t('inputPlaceholder')}
|
||||
contentEditable
|
||||
onPaste={handlePaste}
|
||||
className={`border-000000 max-h-24 min-h-[2.6rem] w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-3xl border bg-white py-2 pl-4 pr-9 text-base leading-7 opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
|
||||
className={`inputbox-style max-h-24 w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-white pt-5 pb-[22px] text-base leading-tight opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (inputRef.current?.textContent && status !== 'loading') {
|
||||
handleQuestion(inputRef.current.textContent);
|
||||
inputRef.current.textContent = '';
|
||||
}
|
||||
handleQuestionSubmission();
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
{status === 'loading' ? (
|
||||
<img
|
||||
src={Spinner}
|
||||
className="relative right-[38px] bottom-[7px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent"
|
||||
src={isDarkTheme ? SpinnerDark : Spinner}
|
||||
className="relative right-[38px] bottom-[24px] -mr-[30px] animate-spin cursor-pointer self-end bg-transparent"
|
||||
></img>
|
||||
) : (
|
||||
<div className="relative right-[43px] bottom-[7px] -mr-[35px] h-[35px] w-[35px] cursor-pointer self-end rounded-full hover:bg-gray-3000">
|
||||
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000">
|
||||
<img
|
||||
className="ml-[9px] mt-[9px] text-white"
|
||||
onClick={() => {
|
||||
if (inputRef.current?.textContent) {
|
||||
handleQuestion(inputRef.current.textContent);
|
||||
inputRef.current.textContent = '';
|
||||
}
|
||||
}}
|
||||
className="ml-[4px] h-6 w-6 text-white "
|
||||
onClick={handleQuestionSubmission}
|
||||
src={isDarkTheme ? SendDark : Send}
|
||||
></img>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-595959 w-[100vw] self-center bg-white bg-transparent p-5 text-center text-xs dark:bg-raisin-black dark:text-bright-gray md:w-full">
|
||||
DocsGPT uses GenAI, please review critial information using sources.
|
||||
|
||||
<p className="text-gray-595959 hidden w-[100vw] self-center bg-white bg-transparent py-2 text-center text-xs dark:bg-raisin-black dark:text-bright-gray md:inline md:w-full">
|
||||
{t('tagline')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { forwardRef, useState } from 'react';
|
||||
import Avatar from '../components/Avatar';
|
||||
import CopyButton from '../components/CopyButton';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
|
||||
import classes from './ConversationBubble.module.css';
|
||||
import Alert from './../assets/alert.svg';
|
||||
import Like from './../assets/like.svg?react';
|
||||
import Dislike from './../assets/dislike.svg?react';
|
||||
import Copy from './../assets/copy.svg?react';
|
||||
import CheckMark from './../assets/checkmark.svg?react';
|
||||
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
||||
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
|
||||
@@ -23,24 +22,15 @@ const ConversationBubble = forwardRef<
|
||||
className?: string;
|
||||
feedback?: FEEDBACK;
|
||||
handleFeedback?: (feedback: FEEDBACK) => void;
|
||||
sources?: { title: string; text: string }[];
|
||||
sources?: { title: string; text: string; source: string }[];
|
||||
retryBtn?: React.ReactElement;
|
||||
}
|
||||
>(function ConversationBubble(
|
||||
{ message, type, className, feedback, handleFeedback, sources },
|
||||
{ message, type, className, feedback, handleFeedback, sources, retryBtn },
|
||||
ref,
|
||||
) {
|
||||
const [openSource, setOpenSource] = useState<number | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopyClick = (text: string) => {
|
||||
copy(text);
|
||||
setCopied(true);
|
||||
// Reset copied to false after a few seconds
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
};
|
||||
const [isCopyHovered, setIsCopyHovered] = useState(false);
|
||||
const [isLikeHovered, setIsLikeHovered] = useState(false);
|
||||
const [isDislikeHovered, setIsDislikeHovered] = useState(false);
|
||||
const [isLikeClicked, setIsLikeClicked] = useState(false);
|
||||
@@ -52,8 +42,8 @@ const ConversationBubble = forwardRef<
|
||||
bubble = (
|
||||
<div ref={ref} className={`flex flex-row-reverse self-end ${className}`}>
|
||||
<Avatar className="mt-2 text-2xl" avatar="🧑💻"></Avatar>
|
||||
<div className="mr-2 ml-10 flex items-center rounded-3xl bg-purple-30 p-3.5 text-white">
|
||||
<ReactMarkdown className="whitespace-pre-wrap break-all">
|
||||
<div className="ml-10 mr-2 flex items-center rounded-[28px] bg-purple-30 py-[14px] px-[19px] text-white">
|
||||
<ReactMarkdown className="whitespace-pre-wrap break-normal leading-normal">
|
||||
{message}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
@@ -63,7 +53,7 @@ const ConversationBubble = forwardRef<
|
||||
bubble = (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`flex flex-wrap self-start ${className} group flex-col pr-20 dark:text-bright-gray`}
|
||||
className={`flex flex-wrap self-start ${className} group flex-col dark:text-bright-gray`}
|
||||
>
|
||||
<div className="flex flex-wrap self-start lg:flex-nowrap">
|
||||
<Avatar
|
||||
@@ -78,31 +68,46 @@ const ConversationBubble = forwardRef<
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`ml-2 mr-5 flex max-w-[90vw] rounded-3xl bg-gray-1000 p-3.5 dark:bg-gun-metal md:max-w-[70vw] lg:max-w-[50vw] ${
|
||||
className={`ml-2 mr-5 flex max-w-[90vw] rounded-[28px] bg-gray-1000 py-[14px] px-7 dark:bg-gun-metal md:max-w-[70vw] lg:max-w-[50vw] ${
|
||||
type === 'ERROR'
|
||||
? 'flex-row items-center rounded-full border border-transparent bg-[#FFE7E7] p-2 py-5 text-sm font-normal text-red-3000 dark:border-red-2000 dark:text-white'
|
||||
? 'relative flex-row items-center rounded-full border border-transparent bg-[#FFE7E7] p-2 py-5 text-sm font-normal text-red-3000 dark:border-red-2000 dark:text-white'
|
||||
: 'flex-col rounded-3xl'
|
||||
}`}
|
||||
>
|
||||
{type === 'ERROR' && (
|
||||
<img src={Alert} alt="alert" className="mr-2 inline" />
|
||||
<>
|
||||
<img src={Alert} alt="alert" className="mr-2 inline" />
|
||||
<div className="absolute -right-32 top-1/2 -translate-y-1/2">
|
||||
{retryBtn}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<ReactMarkdown
|
||||
className="whitespace-pre-wrap break-words"
|
||||
className="whitespace-pre-wrap break-normal leading-normal"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
PreTag="div"
|
||||
language={match[1]}
|
||||
{...props}
|
||||
style={vscDarkPlus}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
<div className="group relative">
|
||||
<SyntaxHighlighter
|
||||
PreTag="div"
|
||||
language={match[1]}
|
||||
{...props}
|
||||
style={vscDarkPlus}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
<div
|
||||
className={`absolute right-3 top-3 lg:invisible
|
||||
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''} `}
|
||||
>
|
||||
<CopyButton
|
||||
text={String(children).replace(/\n$/, '')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<code className={className ? className : ''} {...props}>
|
||||
{children}
|
||||
@@ -160,7 +165,10 @@ const ConversationBubble = forwardRef<
|
||||
>
|
||||
{message}
|
||||
</ReactMarkdown>
|
||||
{DisableSourceFE || type === 'ERROR' ? null : (
|
||||
{DisableSourceFE ||
|
||||
type === 'ERROR' ||
|
||||
!sources ||
|
||||
sources.length === 0 ? null : (
|
||||
<>
|
||||
<span className="mt-3 h-px w-full bg-[#DEDEDE]"></span>
|
||||
<div className="mt-3 flex w-full flex-row flex-wrap items-center justify-start gap-2">
|
||||
@@ -169,13 +177,19 @@ const ConversationBubble = forwardRef<
|
||||
{sources?.map((source, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`max-w-fit cursor-pointer rounded-[28px] py-1 px-4 ${
|
||||
className={`max-w-xs cursor-pointer rounded-[28px] px-4 py-1 sm:max-w-sm md:max-w-md ${
|
||||
openSource === index
|
||||
? 'bg-[#007DFF]'
|
||||
: 'bg-[#D7EBFD] hover:bg-[#BFE1FF]'
|
||||
}`}
|
||||
onClick={() =>
|
||||
setOpenSource(openSource === index ? null : index)
|
||||
source.source !== 'local'
|
||||
? window.open(
|
||||
source.source,
|
||||
'_blank',
|
||||
'noopener, noreferrer',
|
||||
)
|
||||
: setOpenSource(openSource === index ? null : index)
|
||||
}
|
||||
>
|
||||
<p
|
||||
@@ -194,109 +208,89 @@ const ConversationBubble = forwardRef<
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={`relative mr-5 block items-center justify-center lg:invisible
|
||||
</div>
|
||||
<div className="my-2 flex justify-start lg:ml-12">
|
||||
<div
|
||||
className={`relative mr-5 block items-center justify-center lg:invisible
|
||||
${type !== 'ERROR' ? 'group-hover:lg:visible' : ''}`}
|
||||
>
|
||||
<div className="absolute left-2 top-4">
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full p-2
|
||||
${
|
||||
isCopyHovered
|
||||
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
|
||||
: 'bg-[#ffffff] dark:bg-transparent'
|
||||
}`}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckMark
|
||||
className="cursor-pointer stroke-green-2000"
|
||||
onMouseEnter={() => setIsCopyHovered(true)}
|
||||
onMouseLeave={() => setIsCopyHovered(false)}
|
||||
/>
|
||||
) : (
|
||||
<Copy
|
||||
className={`cursor-pointer fill-none`}
|
||||
onClick={() => {
|
||||
handleCopyClick(message);
|
||||
}}
|
||||
onMouseEnter={() => setIsCopyHovered(true)}
|
||||
onMouseLeave={() => setIsCopyHovered(false)}
|
||||
></Copy>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
>
|
||||
<div>
|
||||
<CopyButton text={message} />
|
||||
</div>
|
||||
<div
|
||||
className={`relative mr-5 flex items-center justify-center ${
|
||||
!isLikeClicked ? 'lg:invisible' : ''
|
||||
} ${
|
||||
feedback === 'LIKE' || type !== 'ERROR'
|
||||
? 'group-hover:lg:visible'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<div className="absolute left-6 top-4">
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full p-2 dark:bg-transparent ${
|
||||
isLikeHovered
|
||||
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
|
||||
: 'bg-[#ffffff] dark:bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<Like
|
||||
className={`cursor-pointer
|
||||
</div>
|
||||
{handleFeedback && (
|
||||
<>
|
||||
<div
|
||||
className={`relative mr-5 flex items-center justify-center ${
|
||||
!isLikeClicked ? 'lg:invisible' : ''
|
||||
} ${
|
||||
feedback === 'LIKE' || type !== 'ERROR'
|
||||
? 'group-hover:lg:visible'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full p-2 dark:bg-transparent ${
|
||||
isLikeHovered
|
||||
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
|
||||
: 'bg-[#ffffff] dark:bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<Like
|
||||
className={`cursor-pointer
|
||||
${
|
||||
isLikeClicked || feedback === 'LIKE'
|
||||
? 'fill-white-3000 stroke-purple-30 dark:fill-transparent'
|
||||
: 'fill-none stroke-gray-4000'
|
||||
}`}
|
||||
onClick={() => {
|
||||
handleFeedback?.('LIKE');
|
||||
setIsLikeClicked(true);
|
||||
setIsDislikeClicked(false);
|
||||
}}
|
||||
onMouseEnter={() => setIsLikeHovered(true)}
|
||||
onMouseLeave={() => setIsLikeHovered(false)}
|
||||
></Like>
|
||||
onClick={() => {
|
||||
handleFeedback?.('LIKE');
|
||||
setIsLikeClicked(true);
|
||||
setIsDislikeClicked(false);
|
||||
}}
|
||||
onMouseEnter={() => setIsLikeHovered(true)}
|
||||
onMouseLeave={() => setIsLikeHovered(false)}
|
||||
></Like>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`mr-13 relative flex items-center justify-center ${
|
||||
!isDislikeClicked ? 'lg:invisible' : ''
|
||||
} ${
|
||||
feedback === 'DISLIKE' || type !== 'ERROR'
|
||||
? 'group-hover:lg:visible'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<div className="absolute left-10 top-4">
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full p-2 ${
|
||||
isDislikeHovered
|
||||
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
|
||||
: 'bg-[#ffffff] dark:bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<Dislike
|
||||
className={`cursor-pointer ${
|
||||
isDislikeClicked || feedback === 'DISLIKE'
|
||||
? 'fill-white-3000 stroke-red-2000 dark:fill-transparent'
|
||||
: 'fill-none stroke-gray-4000'
|
||||
<div
|
||||
className={`mr-13 relative flex items-center justify-center ${
|
||||
!isDislikeClicked ? 'lg:invisible' : ''
|
||||
} ${
|
||||
feedback === 'DISLIKE' || type !== 'ERROR'
|
||||
? 'group-hover:lg:visible'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-full p-2 ${
|
||||
isDislikeHovered
|
||||
? 'bg-[#EEEEEE] dark:bg-purple-taupe'
|
||||
: 'bg-[#ffffff] dark:bg-transparent'
|
||||
}`}
|
||||
onClick={() => {
|
||||
handleFeedback?.('DISLIKE');
|
||||
setIsDislikeClicked(true);
|
||||
setIsLikeClicked(false);
|
||||
}}
|
||||
onMouseEnter={() => setIsDislikeHovered(true)}
|
||||
onMouseLeave={() => setIsDislikeHovered(false)}
|
||||
></Dislike>
|
||||
>
|
||||
<Dislike
|
||||
className={`cursor-pointer ${
|
||||
isDislikeClicked || feedback === 'DISLIKE'
|
||||
? 'fill-white-3000 stroke-red-2000 dark:fill-transparent'
|
||||
: 'fill-none stroke-gray-4000'
|
||||
}`}
|
||||
onClick={() => {
|
||||
handleFeedback?.('DISLIKE');
|
||||
setIsDislikeClicked(true);
|
||||
setIsLikeClicked(false);
|
||||
}}
|
||||
onMouseEnter={() => setIsDislikeHovered(true)}
|
||||
onMouseLeave={() => setIsDislikeHovered(false)}
|
||||
></Dislike>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{sources && openSource !== null && sources[openSource] && (
|
||||
|
||||
@@ -5,11 +5,15 @@ import Exit from '../assets/exit.svg';
|
||||
import Message from '../assets/message.svg';
|
||||
import MessageDark from '../assets/message-dark.svg';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import CheckMark2 from '../assets/checkMark2.svg';
|
||||
import Trash from '../assets/trash.svg';
|
||||
|
||||
import Trash from '../assets/red-trash.svg';
|
||||
import Share from '../assets/share.svg';
|
||||
import threeDots from '../assets/three-dots.svg';
|
||||
import { selectConversationId } from '../preferences/preferenceSlice';
|
||||
|
||||
import { ActiveState } from '../models/misc';
|
||||
import { ShareConversationModal } from '../modals/ShareConversationModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
interface ConversationProps {
|
||||
name: string;
|
||||
id: string;
|
||||
@@ -32,22 +36,19 @@ export default function ConversationTile({
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [conversationName, setConversationsName] = useState('');
|
||||
// useOutsideAlerter(
|
||||
// tileRef,
|
||||
// () =>
|
||||
// handleSaveConversation({
|
||||
// id: conversationId || conversation.id,
|
||||
// name: conversationName,
|
||||
// }),
|
||||
// [conversationName],
|
||||
// );
|
||||
|
||||
const [isOpen, setOpen] = useState<boolean>(false);
|
||||
const [isShareModalOpen, setShareModalState] = useState<boolean>(false);
|
||||
const [deleteModalState, setDeleteModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
setConversationsName(conversation.name);
|
||||
}, [conversation.name]);
|
||||
|
||||
function handleEditConversation() {
|
||||
setIsEdit(true);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
function handleSaveConversation(changedConversation: ConversationProps) {
|
||||
@@ -59,6 +60,18 @@ export default function ConversationTile({
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
function onClear() {
|
||||
setConversationsName(conversation.name);
|
||||
setIsEdit(false);
|
||||
@@ -69,9 +82,9 @@ export default function ConversationTile({
|
||||
onClick={() => {
|
||||
selectConversation(conversation.id);
|
||||
}}
|
||||
className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-purple-taupe ${
|
||||
className={`my-auto mx-4 mt-4 flex h-9 cursor-pointer items-center justify-between gap-4 rounded-3xl hover:bg-gray-100 dark:hover:bg-[#28292E] ${
|
||||
conversationId === conversation.id
|
||||
? 'bg-gray-100 dark:bg-purple-taupe'
|
||||
? 'bg-gray-100 dark:bg-[#28292E]'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
@@ -88,7 +101,7 @@ export default function ConversationTile({
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
className="h-6 w-full px-1 text-sm font-normal leading-6 outline-[#0075FF] focus:outline-1"
|
||||
className="h-6 w-full bg-transparent px-1 text-sm font-normal leading-6 focus:outline-[#0075FF]"
|
||||
value={conversationName}
|
||||
onChange={(e) => setConversationsName(e.target.value)}
|
||||
/>
|
||||
@@ -99,36 +112,108 @@ export default function ConversationTile({
|
||||
)}
|
||||
</div>
|
||||
{conversationId === conversation.id && (
|
||||
<div className="flex text-white dark:text-[#949494]">
|
||||
<img
|
||||
src={isEdit ? CheckMark2 : Edit}
|
||||
alt="Edit"
|
||||
className="mr-2 h-4 w-4 cursor-pointer text-white hover:opacity-50"
|
||||
id={`img-${conversation.id}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
isEdit
|
||||
? handleSaveConversation({
|
||||
<div className="flex text-white dark:text-[#949494]" ref={menuRef}>
|
||||
{isEdit ? (
|
||||
<div className="flex gap-1">
|
||||
<img
|
||||
src={CheckMark2}
|
||||
alt="Edit"
|
||||
className="mr-2 h-4 w-4 cursor-pointer text-white hover:opacity-50"
|
||||
id={`img-${conversation.id}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleSaveConversation({
|
||||
id: conversationId,
|
||||
name: conversationName,
|
||||
})
|
||||
: handleEditConversation();
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
src={isEdit ? Exit : Trash}
|
||||
alt="Exit"
|
||||
className={`mr-4 ${
|
||||
isEdit ? 'h-3 w-3' : 'h-4 w-4'
|
||||
}mt-px cursor-pointer hover:opacity-50`}
|
||||
id={`img-${conversation.id}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
isEdit ? onClear() : onDeleteConversation(conversation.id);
|
||||
}}
|
||||
/>
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<img
|
||||
src={isEdit ? Exit : Trash}
|
||||
alt="Exit"
|
||||
className={`mr-4 mt-px h-3 w-3 cursor-pointer hover:opacity-50`}
|
||||
id={`img-${conversation.id}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={() => setOpen(!isOpen)}>
|
||||
<img src={threeDots} className="mr-4 w-2" />
|
||||
</button>
|
||||
)}
|
||||
{isOpen && (
|
||||
<div className="flex-start absolute flex w-32 translate-x-1 translate-y-5 flex-col rounded-xl bg-stone-100 text-sm text-black shadow-xl dark:bg-chinese-black dark:text-chinese-silver md:w-36">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShareModalState(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
className="flex-start flex items-center gap-4 rounded-t-xl p-3 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
||||
>
|
||||
<img
|
||||
src={Share}
|
||||
alt="Share"
|
||||
width={14}
|
||||
height={14}
|
||||
className="cursor-pointer hover:opacity-50"
|
||||
id={`img-${conversation.id}`}
|
||||
/>
|
||||
<span>{t('convTile.share')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={(event) => {
|
||||
handleEditConversation();
|
||||
}}
|
||||
className="flex-start flex items-center gap-4 p-3 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
||||
>
|
||||
<img
|
||||
src={Edit}
|
||||
alt="Edit"
|
||||
width={16}
|
||||
height={16}
|
||||
className="cursor-pointer hover:opacity-50"
|
||||
id={`img-${conversation.id}`}
|
||||
/>
|
||||
<span>{t('convTile.rename')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={(event) => {
|
||||
setDeleteModalState('ACTIVE');
|
||||
setOpen(false);
|
||||
}}
|
||||
className="flex-start flex items-center gap-3 rounded-b-xl p-2 text-red-700 hover:bg-bright-gray dark:hover:bg-dark-charcoal"
|
||||
>
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Edit"
|
||||
width={24}
|
||||
height={24}
|
||||
className="cursor-pointer hover:opacity-50"
|
||||
/>
|
||||
<span>{t('convTile.delete')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ConfirmationModal
|
||||
message={t('convTile.deleteWarning')}
|
||||
modalState={deleteModalState}
|
||||
setModalState={setDeleteModalState}
|
||||
handleSubmit={() => onDeleteConversation(conversation.id)}
|
||||
submitLabel={t('convTile.delete')}
|
||||
/>
|
||||
{isShareModalOpen && conversationId && (
|
||||
<ShareConversationModal
|
||||
close={() => {
|
||||
setShareModalState(false);
|
||||
}}
|
||||
conversationId={conversationId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
144
frontend/src/conversation/SharedConversation.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Query } from './conversationModels';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ConversationBubble from './ConversationBubble';
|
||||
import { Fragment } from 'react';
|
||||
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
||||
const SharedConversation = () => {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { identifier } = params; //identifier is a uuid, not conversationId
|
||||
const [queries, setQueries] = useState<Query[]>([]);
|
||||
const [title, setTitle] = useState('');
|
||||
const [date, setDate] = useState('');
|
||||
const { t } = useTranslation();
|
||||
function formatISODate(isoDateStr: string) {
|
||||
const date = new Date(isoDateStr);
|
||||
|
||||
const monthNames = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'Aug',
|
||||
'Sept',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
|
||||
const month = monthNames[date.getMonth()];
|
||||
const day = date.getDate();
|
||||
const year = date.getFullYear();
|
||||
|
||||
let hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
|
||||
hours = hours % 12;
|
||||
hours = hours ? hours : 12;
|
||||
const minutesStr = minutes < 10 ? '0' + minutes : minutes;
|
||||
const formattedDate = `Published ${month} ${day}, ${year} at ${hours}:${minutesStr} ${ampm}`;
|
||||
return formattedDate;
|
||||
}
|
||||
const fetchQueris = () => {
|
||||
fetch(`${apiHost}/api/shared_conversation/${identifier}`)
|
||||
.then((res) => {
|
||||
if (res.status === 404 || res.status === 400) navigate('/pagenotfound');
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
setQueries(data.queries);
|
||||
setTitle(data.title);
|
||||
setDate(formatISODate(data.timestamp));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const prepResponseView = (query: Query, index: number) => {
|
||||
let responseView;
|
||||
if (query.response) {
|
||||
responseView = (
|
||||
<ConversationBubble
|
||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'}`}
|
||||
key={`${index}ANSWER`}
|
||||
message={query.response}
|
||||
type={'ANSWER'}
|
||||
></ConversationBubble>
|
||||
);
|
||||
} else if (query.error) {
|
||||
responseView = (
|
||||
<ConversationBubble
|
||||
className={`${index === queries.length - 1 ? 'mb-32' : 'mb-7'} `}
|
||||
key={`${index}ERROR`}
|
||||
message={query.error}
|
||||
type="ERROR"
|
||||
></ConversationBubble>
|
||||
);
|
||||
}
|
||||
return responseView;
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchQueris();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-between gap-2 overflow-y-hidden dark:bg-raisin-black">
|
||||
<div className="flex w-full justify-center overflow-auto">
|
||||
<div className="mt-0 w-11/12 md:w-10/12 lg:w-6/12">
|
||||
<div className="mb-2 w-full border-b pb-2">
|
||||
<h1 className="font-semi-bold text-4xl text-chinese-black dark:text-chinese-silver">
|
||||
{title}
|
||||
</h1>
|
||||
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
|
||||
{t('sharedConv.subtitle')}{' '}
|
||||
<a href="/" className="text-[#007DFF]">
|
||||
DocsGPT
|
||||
</a>
|
||||
</h2>
|
||||
<h2 className="font-semi-bold text-base text-chinese-black dark:text-chinese-silver">
|
||||
{date}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="">
|
||||
{queries?.map((query, index) => {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<ConversationBubble
|
||||
className={'mb-1 last:mb-28 md:mb-7'}
|
||||
key={`${index}QUESTION`}
|
||||
message={query.prompt}
|
||||
type="QUESTION"
|
||||
sources={query.sources}
|
||||
></ConversationBubble>
|
||||
|
||||
{prepResponseView(query, index)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className=" flex flex-col items-center gap-4 pb-2">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="w-fit rounded-full bg-purple-30 p-4 text-white shadow-xl transition-colors duration-200 hover:bg-purple-taupe"
|
||||
>
|
||||
{t('sharedConv.button')}
|
||||
</button>
|
||||
<span className="hidden text-xs text-dark-charcoal dark:text-silver sm:inline">
|
||||
{t('sharedConv.meta')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SharedConversation;
|
||||
@@ -3,14 +3,42 @@ import { Doc } from '../preferences/preferenceApi';
|
||||
|
||||
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
||||
|
||||
function getDocPath(selectedDocs: Doc | null): string {
|
||||
let docPath = 'default';
|
||||
|
||||
if (selectedDocs) {
|
||||
let namePath = selectedDocs.name;
|
||||
if (selectedDocs.language === namePath) {
|
||||
namePath = '.project';
|
||||
}
|
||||
if (selectedDocs.location === 'local') {
|
||||
docPath = 'local' + '/' + selectedDocs.name + '/';
|
||||
} else if (selectedDocs.location === 'remote') {
|
||||
docPath =
|
||||
selectedDocs.language +
|
||||
'/' +
|
||||
namePath +
|
||||
'/' +
|
||||
selectedDocs.version +
|
||||
'/' +
|
||||
selectedDocs.model +
|
||||
'/';
|
||||
} else if (selectedDocs.location === 'custom') {
|
||||
docPath = selectedDocs.docLink;
|
||||
}
|
||||
}
|
||||
|
||||
return docPath;
|
||||
}
|
||||
export function fetchAnswerApi(
|
||||
question: string,
|
||||
signal: AbortSignal,
|
||||
apiKey: string,
|
||||
selectedDocs: Doc,
|
||||
selectedDocs: Doc | null,
|
||||
history: Array<any> = [],
|
||||
conversationId: string | null,
|
||||
promptId: string | null,
|
||||
chunks: string,
|
||||
token_limit: number,
|
||||
): Promise<
|
||||
| {
|
||||
result: any;
|
||||
@@ -28,25 +56,7 @@ export function fetchAnswerApi(
|
||||
title: any;
|
||||
}
|
||||
> {
|
||||
let namePath = selectedDocs.name;
|
||||
if (selectedDocs.language === namePath) {
|
||||
namePath = '.project';
|
||||
}
|
||||
|
||||
let docPath = 'default';
|
||||
if (selectedDocs.location === 'local') {
|
||||
docPath = 'local' + '/' + selectedDocs.name + '/';
|
||||
} else if (selectedDocs.location === 'remote') {
|
||||
docPath =
|
||||
selectedDocs.language +
|
||||
'/' +
|
||||
namePath +
|
||||
'/' +
|
||||
selectedDocs.version +
|
||||
'/' +
|
||||
selectedDocs.model +
|
||||
'/';
|
||||
}
|
||||
const docPath = getDocPath(selectedDocs);
|
||||
//in history array remove all keys except prompt and response
|
||||
history = history.map((item) => {
|
||||
return { prompt: item.prompt, response: item.response };
|
||||
@@ -59,12 +69,12 @@ export function fetchAnswerApi(
|
||||
},
|
||||
body: JSON.stringify({
|
||||
question: question,
|
||||
api_key: apiKey,
|
||||
embeddings_key: apiKey,
|
||||
history: history,
|
||||
active_docs: docPath,
|
||||
conversation_id: conversationId,
|
||||
prompt_id: promptId,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
}),
|
||||
signal,
|
||||
})
|
||||
@@ -90,32 +100,15 @@ export function fetchAnswerApi(
|
||||
export function fetchAnswerSteaming(
|
||||
question: string,
|
||||
signal: AbortSignal,
|
||||
apiKey: string,
|
||||
selectedDocs: Doc,
|
||||
selectedDocs: Doc | null,
|
||||
history: Array<any> = [],
|
||||
conversationId: string | null,
|
||||
promptId: string | null,
|
||||
chunks: string,
|
||||
token_limit: number,
|
||||
onEvent: (event: MessageEvent) => void,
|
||||
): Promise<Answer> {
|
||||
let namePath = selectedDocs.name;
|
||||
if (selectedDocs.language === namePath) {
|
||||
namePath = '.project';
|
||||
}
|
||||
|
||||
let docPath = 'default';
|
||||
if (selectedDocs.location === 'local') {
|
||||
docPath = 'local' + '/' + selectedDocs.name + '/';
|
||||
} else if (selectedDocs.location === 'remote') {
|
||||
docPath =
|
||||
selectedDocs.language +
|
||||
'/' +
|
||||
namePath +
|
||||
'/' +
|
||||
selectedDocs.version +
|
||||
'/' +
|
||||
selectedDocs.model +
|
||||
'/';
|
||||
}
|
||||
const docPath = getDocPath(selectedDocs);
|
||||
|
||||
history = history.map((item) => {
|
||||
return { prompt: item.prompt, response: item.response };
|
||||
@@ -124,12 +117,12 @@ export function fetchAnswerSteaming(
|
||||
return new Promise<Answer>((resolve, reject) => {
|
||||
const body = {
|
||||
question: question,
|
||||
api_key: apiKey,
|
||||
embeddings_key: apiKey,
|
||||
active_docs: docPath,
|
||||
history: JSON.stringify(history),
|
||||
conversation_id: conversationId,
|
||||
prompt_id: promptId,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
};
|
||||
fetch(apiHost + '/stream', {
|
||||
method: 'POST',
|
||||
@@ -188,41 +181,21 @@ export function fetchAnswerSteaming(
|
||||
}
|
||||
export function searchEndpoint(
|
||||
question: string,
|
||||
apiKey: string,
|
||||
selectedDocs: Doc,
|
||||
selectedDocs: Doc | null,
|
||||
conversation_id: string | null,
|
||||
history: Array<any> = [],
|
||||
chunks: string,
|
||||
token_limit: number,
|
||||
) {
|
||||
/*
|
||||
"active_docs": "default",
|
||||
"question": "Summarise",
|
||||
"conversation_id": null,
|
||||
"history": "[]" */
|
||||
let namePath = selectedDocs.name;
|
||||
if (selectedDocs.language === namePath) {
|
||||
namePath = '.project';
|
||||
}
|
||||
|
||||
let docPath = 'default';
|
||||
if (selectedDocs.location === 'local') {
|
||||
docPath = 'local' + '/' + selectedDocs.name + '/';
|
||||
} else if (selectedDocs.location === 'remote') {
|
||||
docPath =
|
||||
selectedDocs.language +
|
||||
'/' +
|
||||
namePath +
|
||||
'/' +
|
||||
selectedDocs.version +
|
||||
'/' +
|
||||
selectedDocs.model +
|
||||
'/';
|
||||
}
|
||||
const docPath = getDocPath(selectedDocs);
|
||||
|
||||
const body = {
|
||||
question: question,
|
||||
active_docs: docPath,
|
||||
conversation_id,
|
||||
history,
|
||||
chunks: chunks,
|
||||
token_limit: token_limit,
|
||||
};
|
||||
return fetch(`${apiHost}/api/search`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface Answer {
|
||||
answer: string;
|
||||
query: string;
|
||||
result: string;
|
||||
sources: { title: string; text: string }[];
|
||||
sources: { title: string; text: string; source: string }[];
|
||||
conversationId: string | null;
|
||||
title: string | null;
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export interface Query {
|
||||
response?: string;
|
||||
feedback?: FEEDBACK;
|
||||
error?: string;
|
||||
sources?: { title: string; text: string }[];
|
||||
sources?: { title: string; text: string; source: string }[];
|
||||
conversationId?: string | null;
|
||||
title?: string | null;
|
||||
}
|
||||
|
||||
@@ -23,11 +23,12 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
|
||||
await fetchAnswerSteaming(
|
||||
question,
|
||||
signal,
|
||||
state.preference.apiKey,
|
||||
state.preference.selectedDocs!,
|
||||
state.conversation.queries,
|
||||
state.conversation.conversationId,
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
|
||||
(event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
@@ -47,10 +48,11 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
|
||||
searchEndpoint(
|
||||
//search for sources post streaming
|
||||
question,
|
||||
state.preference.apiKey,
|
||||
state.preference.selectedDocs!,
|
||||
state.conversation.conversationId,
|
||||
state.conversation.queries,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
).then((sources) => {
|
||||
//dispatch streaming sources
|
||||
dispatch(
|
||||
@@ -66,6 +68,15 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
|
||||
query: { conversationId: data.id },
|
||||
}),
|
||||
);
|
||||
} else if (data.type === 'error') {
|
||||
// set status to 'failed'
|
||||
dispatch(conversationSlice.actions.setStatus('failed'));
|
||||
dispatch(
|
||||
conversationSlice.actions.raiseError({
|
||||
index: state.conversation.queries.length - 1,
|
||||
message: data.error,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const result = data.answer;
|
||||
dispatch(
|
||||
@@ -81,11 +92,12 @@ export const fetchAnswer = createAsyncThunk<Answer, { question: string }>(
|
||||
const answer = await fetchAnswerApi(
|
||||
question,
|
||||
signal,
|
||||
state.preference.apiKey,
|
||||
state.preference.selectedDocs!,
|
||||
state.conversation.queries,
|
||||
state.conversation.conversationId,
|
||||
state.preference.prompt.id,
|
||||
state.preference.chunks,
|
||||
state.preference.token_limit,
|
||||
);
|
||||
if (answer) {
|
||||
let sourcesPrepped = [];
|
||||
@@ -148,7 +160,7 @@ export const conversationSlice = createSlice({
|
||||
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
||||
) {
|
||||
const { index, query } = action.payload;
|
||||
if (query.response) {
|
||||
if (query.response != undefined) {
|
||||
state.queries[index].response =
|
||||
(state.queries[index].response || '') + query.response;
|
||||
} else {
|
||||
@@ -188,6 +200,13 @@ export const conversationSlice = createSlice({
|
||||
setStatus(state, action: PayloadAction<Status>) {
|
||||
state.status = action.payload;
|
||||
},
|
||||
raiseError(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; message: string }>,
|
||||
) {
|
||||
const { index, message } = action.payload;
|
||||
state.queries[index].error = message;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder
|
||||
@@ -201,7 +220,7 @@ export const conversationSlice = createSlice({
|
||||
}
|
||||
state.status = 'failed';
|
||||
state.queries[state.queries.length - 1].error =
|
||||
'Something went wrong. Please try again later.';
|
||||
'Something went wrong. Please check your internet connection.';
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -77,21 +77,23 @@ export function useDarkTheme() {
|
||||
// Set dark mode based on local storage preference
|
||||
if (savedMode === 'Dark') {
|
||||
setIsDarkTheme(true);
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.add('dark:bg-raisin-black');
|
||||
document
|
||||
.getElementById('root')
|
||||
?.classList.add('dark', 'dark:bg-raisin-black');
|
||||
} else {
|
||||
// If no preference found, set to default (light mode)
|
||||
setIsDarkTheme(false);
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.getElementById('root')?.classList.remove('dark');
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
localStorage.setItem('selectedTheme', isDarkTheme ? 'Dark' : 'Light');
|
||||
if (isDarkTheme) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.add('dark:bg-raisin-black');
|
||||
document
|
||||
.getElementById('root')
|
||||
?.classList.add('dark', 'dark:bg-raisin-black');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.getElementById('root')?.classList.remove('dark');
|
||||
}
|
||||
}, [isDarkTheme]);
|
||||
//method to toggle theme
|
||||
|
||||