chore: import upstream snapshot with attribution
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
docsgpt_url=<docsgpt_api_url>
|
||||
chatwoot_url=<chatwoot_url>
|
||||
docsgpt_key=<openai_api_key or other llm>
|
||||
chatwoot_token=xxxxx
|
||||
account_id=(optional) 1
|
||||
assignee_id=(optional) 1
|
||||
@@ -0,0 +1,115 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import pprint
|
||||
|
||||
import dotenv
|
||||
import requests
|
||||
from flask import Flask, request
|
||||
|
||||
dotenv.load_dotenv()
|
||||
docsgpt_url = os.getenv("docsgpt_url")
|
||||
chatwoot_url = os.getenv("chatwoot_url")
|
||||
docsgpt_key = os.getenv("docsgpt_key")
|
||||
chatwoot_token = os.getenv("chatwoot_token")
|
||||
chatwoot_webhook_secret = os.getenv("chatwoot_webhook_secret", "")
|
||||
# account_id = os.getenv("account_id")
|
||||
# assignee_id = os.getenv("assignee_id")
|
||||
label_stop = "human-requested"
|
||||
|
||||
|
||||
def send_to_bot(sender, message):
|
||||
data = {
|
||||
'sender': sender,
|
||||
'question': message,
|
||||
'api_key': docsgpt_key,
|
||||
'embeddings_key': docsgpt_key,
|
||||
'history': ''
|
||||
}
|
||||
headers = {"Content-Type": "application/json",
|
||||
"Accept": "application/json"}
|
||||
|
||||
r = requests.post(f'{docsgpt_url}/api/answer',
|
||||
json=data, headers=headers)
|
||||
return r.json()['answer']
|
||||
|
||||
|
||||
def send_to_chatwoot(account, conversation, message):
|
||||
data = {
|
||||
'content': message
|
||||
}
|
||||
url = f"{chatwoot_url}/api/v1/accounts/{account}/conversations/{conversation}/messages"
|
||||
headers = {"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"api_access_token": f"{chatwoot_token}"}
|
||||
|
||||
r = requests.post(url,
|
||||
json=data, headers=headers)
|
||||
return r.json()
|
||||
|
||||
|
||||
def is_valid_chatwoot_signature(raw_body: bytes, signature_header: str | None) -> bool:
|
||||
"""Validate Chatwoot webhook signature using shared secret."""
|
||||
if not chatwoot_webhook_secret or not signature_header:
|
||||
return False
|
||||
|
||||
expected = hmac.new(
|
||||
chatwoot_webhook_secret.encode("utf-8"), raw_body, hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
provided = signature_header.strip()
|
||||
if provided.startswith("sha256="):
|
||||
provided = provided.split("=", maxsplit=1)[1]
|
||||
|
||||
return hmac.compare_digest(provided, expected)
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/docsgpt', methods=['POST'])
|
||||
def docsgpt():
|
||||
raw_body = request.get_data()
|
||||
signature = request.headers.get("X-Chatwoot-Signature")
|
||||
if not is_valid_chatwoot_signature(raw_body, signature):
|
||||
return "Unauthorized", 401
|
||||
|
||||
data = request.get_json(silent=True)
|
||||
if not isinstance(data, dict):
|
||||
return "Invalid payload", 400
|
||||
pp = pprint.PrettyPrinter(indent=4)
|
||||
pp.pprint(data)
|
||||
try:
|
||||
message_type = data['message_type']
|
||||
except KeyError:
|
||||
return "Not a message"
|
||||
message = data['content']
|
||||
conversation = data['conversation']['id']
|
||||
contact = data['sender']['id']
|
||||
account = data['account']['id']
|
||||
assignee = data['conversation']['meta']['assignee']['id']
|
||||
print(account)
|
||||
print(label_stop)
|
||||
print(data['conversation']['labels'])
|
||||
print(assignee)
|
||||
|
||||
if label_stop in data['conversation']['labels']:
|
||||
return "Label stop"
|
||||
# elif str(account) != str(account_id):
|
||||
# return "Not the right account"
|
||||
|
||||
# elif str(assignee) != str(assignee_id):
|
||||
# return "Not the right assignee"
|
||||
|
||||
if (message_type == "incoming"):
|
||||
bot_response = send_to_bot(contact, message)
|
||||
create_message = send_to_chatwoot(
|
||||
account, conversation, bot_response)
|
||||
else:
|
||||
return "Not an incoming message"
|
||||
|
||||
return create_message
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=80)
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
.parcel-cache
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@parcel/config-default",
|
||||
"resolvers": ["@parcel/resolver-glob","..."],
|
||||
"transformers": {
|
||||
"*.svg": ["...", "@parcel/transformer-svg-react", "@parcel/transformer-typescript-tsc"]
|
||||
},
|
||||
"validators": {
|
||||
"*.{ts,tsx}": ["@parcel/validator-typescript"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
# DocsGPT react widget
|
||||
|
||||
This widget will allow you to embed a DocsGPT assistant in your React app.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install docsgpt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### React
|
||||
|
||||
```javascript
|
||||
import { DocsGPTWidget } from "docsgpt-react";
|
||||
|
||||
const App = () => {
|
||||
return <DocsGPTWidget />;
|
||||
};
|
||||
```
|
||||
|
||||
To link the widget to your api and your documents you can pass parameters to the <DocsGPTWidget /> component.
|
||||
|
||||
```javascript
|
||||
import { DocsGPTWidget } from "docsgpt-react";
|
||||
|
||||
const App = () => {
|
||||
return <DocsGPTWidget
|
||||
apiHost="https://gptcloud.arc53.com"
|
||||
apiKey=""
|
||||
avatar = "https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"
|
||||
title = "Get AI assistance"
|
||||
description = "DocsGPT's AI Chatbot is here to help"
|
||||
heroTitle = "Welcome to DocsGPT !"
|
||||
heroDescription="This chatbot is built with DocsGPT and utilises GenAI,
|
||||
please review important information using sources."
|
||||
theme = "dark"
|
||||
buttonIcon = "https://your-icon"
|
||||
buttonBg = "#222327"
|
||||
/>;
|
||||
};
|
||||
```
|
||||
|
||||
### 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',
|
||||
apiKey:"",
|
||||
avatar: 'https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png',
|
||||
title: 'Get AI assistance',
|
||||
description: "DocsGPT's AI Chatbot is here to help",
|
||||
heroTitle: 'Welcome to DocsGPT!',
|
||||
heroDescription: 'This chatbot is built with DocsGPT and utilises GenAI, please review important information using sources.',
|
||||
theme:"dark",
|
||||
buttonIcon:"https://your-icon.svg",
|
||||
buttonBg:"#222327"
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
# SearchBar
|
||||
|
||||
The `SearchBar` component is an interactive search bar designed to provide search results based on **vector similarity search**. It also includes the capability to open the AI Chatbot, enabling users to query.
|
||||
|
||||
---
|
||||
|
||||
### Importing the Component
|
||||
```tsx
|
||||
import { SearchBar } from "docsgpt-react";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Usage Example
|
||||
```tsx
|
||||
<SearchBar
|
||||
apiKey="your-api-key"
|
||||
apiHost="https://gptcloud.arc53.com"
|
||||
theme="light"
|
||||
placeholder="Search or Ask AI..."
|
||||
width="300px"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTML embedding for Search bar
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SearchBar Embedding</title>
|
||||
<script src="https://unpkg.com/docsgpt/dist/modern/main.js"></script> <!-- The bundled JavaScript file -->
|
||||
</head>
|
||||
<body>
|
||||
<!-- Element where the SearchBar will render -->
|
||||
<div id="search-bar-container"></div>
|
||||
|
||||
<script>
|
||||
// Render the SearchBar into the specified element
|
||||
renderSearchBar('search-bar-container', {
|
||||
apiKey: 'your-api-key-here',
|
||||
apiHost: 'https://your-api-host.com',
|
||||
theme: 'light',
|
||||
placeholder: 'Search here...',
|
||||
width: '300px'
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| **Prop** | **Type** | **Default Value** | **Description** |
|
||||
|-----------------|-----------|-------------------------------------|--------------------------------------------------------------------------------------------------|
|
||||
| **`apiKey`** | `string` | `"74039c6d-bff7-44ce-ae55-2973cbf13837"` | Your API key generated from the app. Used for authenticating requests. |
|
||||
| **`apiHost`** | `string` | `"https://gptcloud.arc53.com"` | The base URL of the server hosting the vector similarity search and chatbot services. |
|
||||
| **`theme`** | `"dark" \| "light"` | `"dark"` | The theme of the search bar. Accepts `"dark"` or `"light"`. |
|
||||
| **`placeholder`** | `string` | `"Search or Ask AI..."` | Placeholder text displayed in the search input field. |
|
||||
| **`width`** | `string` | `"256px"` | Width of the search bar. Accepts any valid CSS width value (e.g., `"300px"`, `"100%"`, `"20rem"`). |
|
||||
|
||||
|
||||
Feel free to reach out if you need help customizing or extending the `SearchBar`!
|
||||
|
||||
## Our github
|
||||
|
||||
[DocsGPT](https://github.com/arc53/DocsGPT)
|
||||
|
||||
You can find the source code in the extensions/react-widget folder.
|
||||
@@ -0,0 +1,72 @@
|
||||
import js from '@eslint/js'
|
||||
import tsParser from '@typescript-eslint/parser'
|
||||
import tsPlugin from '@typescript-eslint/eslint-plugin'
|
||||
import react from 'eslint-plugin-react'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
import prettier from 'eslint-plugin-prettier'
|
||||
import globals from 'globals'
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'prettier.config.cjs',
|
||||
'custom.d.ts',
|
||||
'package-lock.json',
|
||||
'package.json',
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.es2021,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tsPlugin,
|
||||
react,
|
||||
'unused-imports': unusedImports,
|
||||
prettier,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...tsPlugin.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...prettier.configs.recommended.rules,
|
||||
'react/prop-types': 'off',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'no-undef': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-unused-expressions': 'warn',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
endOfLine: 'auto',
|
||||
},
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
+9525
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"name": "docsgpt",
|
||||
"version": "0.6.3",
|
||||
"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",
|
||||
"main": "dist/main.js",
|
||||
"module": "dist/module.js",
|
||||
"types": "dist/types.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"package.json"
|
||||
],
|
||||
"targets": {
|
||||
"modern": {
|
||||
"engines": {
|
||||
"browsers": "Chrome 80"
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"engines": {
|
||||
"browsers": "> 0.5%, last 2 versions, not dead"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@parcel/resolver-default": {
|
||||
"packageExports": true
|
||||
},
|
||||
"resolution": {
|
||||
"styled-components": "^5"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "parcel build src/browser.tsx --public-url ./",
|
||||
"build:react": "parcel build src/index.ts",
|
||||
"serve": "parcel serve -p 3000",
|
||||
"dev": "parcel -p 3000",
|
||||
"test": "jest",
|
||||
"lint": "eslint ./src --ext .jsx,.js,.ts,.tsx",
|
||||
"lint-fix": "eslint ./src --ext .jsx,.js,.ts,.tsx --fix",
|
||||
"format": "prettier ./src --write",
|
||||
"check": "tsc --noEmit",
|
||||
"ci": "yarn build && yarn test && yarn lint && yarn check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.23.3",
|
||||
"@bpmn-io/snarkdown": "^2.2.0",
|
||||
"@parcel/resolver-glob": "^2.16.4",
|
||||
"@parcel/transformer-svg-react": "^2.16.4",
|
||||
"@parcel/transformer-typescript-tsc": "^2.16.4",
|
||||
"@parcel/validator-typescript": "^2.16.4",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"dompurify": "^3.1.5",
|
||||
"flow-bin": "^0.311.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"styled-components": "^6.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/preset-env": "^7.24.0",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@parcel/packager-ts": "^2.16.4",
|
||||
"@parcel/transformer-typescript-types": "^2.16.4",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
||||
"@typescript-eslint/parser": "^8.59.0",
|
||||
"babel-loader": "^10.1.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-unused-imports": "^4.4.1",
|
||||
"globals": "^17.5.0",
|
||||
"parcel": "^2.16.4",
|
||||
"prettier": "^3.8.1",
|
||||
"process": "^0.11.10",
|
||||
"svgo": "^4.0.1",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/arc53/DocsGPT.git"
|
||||
},
|
||||
"keywords": [
|
||||
"docsgpt",
|
||||
"chatbot",
|
||||
"assistant",
|
||||
"ai",
|
||||
"chatdocs",
|
||||
"widget"
|
||||
],
|
||||
"author": "Arc53",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/arc53/DocsGPT/issues"
|
||||
},
|
||||
"homepage": "https://github.com/arc53/DocsGPT#readme"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
trailingComma: 'all',
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
printWidth: 80,
|
||||
};
|
||||
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Create backup of original files
|
||||
cp package.json package_original.json
|
||||
cp package-lock.json package-lock_original.json
|
||||
|
||||
# Store the latest version after publishing
|
||||
LATEST_VERSION=""
|
||||
|
||||
# Check if a specific version was provided
|
||||
if [ "$1" ]; then
|
||||
VERSION_UPDATE_TYPE="$1"
|
||||
echo "Using custom version update: $VERSION_UPDATE_TYPE"
|
||||
else
|
||||
VERSION_UPDATE_TYPE="patch"
|
||||
echo "No version specified, defaulting to patch update"
|
||||
fi
|
||||
|
||||
publish_package() {
|
||||
PACKAGE_NAME=$1
|
||||
BUILD_COMMAND=$2
|
||||
IS_REACT=$3
|
||||
|
||||
echo "Preparing to publish ${PACKAGE_NAME}..."
|
||||
|
||||
# Restore original package.json state before each publish
|
||||
cp package_original.json package.json
|
||||
cp package-lock_original.json package-lock.json
|
||||
|
||||
# Update package name in package.json
|
||||
jq --arg name "$PACKAGE_NAME" '.name=$name' package.json > temp.json && mv temp.json package.json
|
||||
|
||||
# Handle targets based on package type
|
||||
if [ "$IS_REACT" = "true" ]; then
|
||||
echo "Removing targets for React library build..."
|
||||
jq 'del(.targets)' package.json > temp.json && mv temp.json package.json
|
||||
fi
|
||||
|
||||
# Clean dist directory
|
||||
if [ -d "dist" ]; then
|
||||
echo "Cleaning dist directory..."
|
||||
rm -rf dist
|
||||
fi
|
||||
|
||||
# Update version based on input parameter or default to patch
|
||||
if [[ "$VERSION_UPDATE_TYPE" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
# If full version number is provided (e.g., 0.5.0)
|
||||
LATEST_VERSION=$(npm version "$VERSION_UPDATE_TYPE" --no-git-tag-version)
|
||||
else
|
||||
# If update type is provided (patch, minor, major)
|
||||
LATEST_VERSION=$(npm version "$VERSION_UPDATE_TYPE" --no-git-tag-version)
|
||||
fi
|
||||
|
||||
echo "New version: ${LATEST_VERSION}"
|
||||
|
||||
# Build package
|
||||
npm run "$BUILD_COMMAND"
|
||||
|
||||
# Publish package
|
||||
npm publish
|
||||
|
||||
echo "Successfully published ${PACKAGE_NAME} version ${LATEST_VERSION}"
|
||||
}
|
||||
|
||||
# First publish docsgpt (HTML bundle)
|
||||
publish_package "docsgpt" "build" "false"
|
||||
|
||||
# Then publish docsgpt-react (React library)
|
||||
publish_package "docsgpt-react" "build:react" "true"
|
||||
|
||||
# Restore original state but keep the updated version
|
||||
cp package_original.json package.json
|
||||
cp package-lock_original.json package-lock.json
|
||||
|
||||
# Update the version in the final package.json
|
||||
jq --arg version "${LATEST_VERSION#v}" '.version=$version' package.json > temp.json && mv temp.json package.json
|
||||
|
||||
# Run npm install to update package-lock-only
|
||||
npm install --package-lock-only
|
||||
|
||||
# Cleanup backup files
|
||||
rm -f package_original.json
|
||||
rm -f package-lock_original.json
|
||||
rm -f temp.json
|
||||
|
||||
echo "---Process completed---"
|
||||
echo "Final version in package.json: $(jq -r '.version' package.json)"
|
||||
echo "Final version in package-lock.json: $(jq -r '.version' package-lock.json)"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { DocsGPTWidget } from './components/DocsGPTWidget';
|
||||
import { SearchBar } from './components/SearchBar';
|
||||
export const App = () => {
|
||||
return (
|
||||
<div>
|
||||
<SearchBar />
|
||||
<DocsGPTWidget />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
//exports browser ready methods
|
||||
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { DocsGPTWidget } from './components/DocsGPTWidget';
|
||||
import { SearchBar } from './components/SearchBar';
|
||||
import React from 'react';
|
||||
if (typeof window !== 'undefined') {
|
||||
const renderWidget = (elementId: string, props = {}) => {
|
||||
const root = createRoot(document.getElementById(elementId) as HTMLElement);
|
||||
root.render(<DocsGPTWidget {...props} />);
|
||||
};
|
||||
const renderSearchBar = (elementId: string, props = {}) => {
|
||||
const root = createRoot(document.getElementById(elementId) as HTMLElement);
|
||||
root.render(<SearchBar {...props} />);
|
||||
};
|
||||
(window as unknown as Record<string, unknown>).renderDocsGPTWidget =
|
||||
renderWidget;
|
||||
|
||||
(window as unknown as Record<string, unknown>).renderSearchBar =
|
||||
renderSearchBar;
|
||||
}
|
||||
|
||||
export { DocsGPTWidget, SearchBar };
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,611 @@
|
||||
import React from 'react';
|
||||
import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
|
||||
import { WidgetCore } from './DocsGPTWidget';
|
||||
import { SearchBarProps } from '@/types';
|
||||
import { getSearchResults } from '../requests/searchAPI';
|
||||
import { Result } from '@/types';
|
||||
import { getOS, processMarkdownString } from '../utils/helper';
|
||||
import DOMPurify from 'dompurify';
|
||||
import {
|
||||
CodeIcon,
|
||||
TextAlignLeftIcon,
|
||||
HeadingIcon,
|
||||
ReaderIcon,
|
||||
ListBulletIcon,
|
||||
QuoteIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
const themes = {
|
||||
dark: {
|
||||
name: 'dark',
|
||||
bg: '#202124',
|
||||
text: '#EDEDED',
|
||||
primary: {
|
||||
text: '#FAFAFA',
|
||||
bg: '#111111',
|
||||
},
|
||||
secondary: {
|
||||
text: '#A1A1AA',
|
||||
bg: '#38383b',
|
||||
},
|
||||
},
|
||||
light: {
|
||||
name: 'light',
|
||||
bg: '#EAEAEA',
|
||||
text: '#171717',
|
||||
primary: {
|
||||
text: '#222327',
|
||||
bg: '#fff',
|
||||
},
|
||||
secondary: {
|
||||
text: '#A1A1AA',
|
||||
bg: '#F6F6F6',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
.highlight {
|
||||
color: ${(props) => (props.theme.name === 'dark' ? '#4B9EFF' : '#0066CC')};
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
const loadGeistFont = () => {
|
||||
const link = document.createElement('link');
|
||||
link.href =
|
||||
'https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap';
|
||||
link.rel = 'stylesheet';
|
||||
document.head.appendChild(link);
|
||||
};
|
||||
|
||||
const Main = styled.div`
|
||||
all: initial;
|
||||
font-family: 'Geist', sans-serif;
|
||||
`;
|
||||
const SearchButton = styled.button<{ $inputWidth: string }>`
|
||||
padding: 6px 6px;
|
||||
font-family: inherit;
|
||||
width: ${({ $inputWidth }) => $inputWidth};
|
||||
border-radius: 8px;
|
||||
display: inline;
|
||||
color: ${(props) => props.theme.secondary.text};
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: ${(props) => props.theme.secondary.bg};
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
transition: background-color 128ms linear;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
`;
|
||||
const SearchOverlay = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #0000001a;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
z-index: 99;
|
||||
`;
|
||||
|
||||
const SearchResults = styled.div`
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: ${(props) =>
|
||||
props.theme.name === 'dark'
|
||||
? 'rgba(0, 0, 0, 0.15)'
|
||||
: 'rgba(255, 255, 255, 0.4)'};
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 15px;
|
||||
padding: 8px 0px 8px 0px;
|
||||
width: 792px;
|
||||
max-width: 90vw;
|
||||
height: 396px;
|
||||
z-index: 100;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
backdrop-filter: blur(82px);
|
||||
-webkit-backdrop-filter: blur(82px);
|
||||
border-radius: 10px;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
height: 80vh;
|
||||
width: 90vw;
|
||||
}
|
||||
`;
|
||||
|
||||
const SearchResultsScroll = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #383838 transparent;
|
||||
padding: 0 16px;
|
||||
`;
|
||||
|
||||
const IconTitleWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.element-icon {
|
||||
margin: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled.h3`
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
margin: 0;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
const ContentWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
`;
|
||||
|
||||
const ResultWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
font-family: 'Geist', sans-serif;
|
||||
border-radius: 8px;
|
||||
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
display: flex;
|
||||
margin-left: 8px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 4px 0px 0px 12px;
|
||||
font-size: 15px;
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
line-height: 1.6;
|
||||
border-left: 2px solid ${(props) => props.theme.primary.text}CC;
|
||||
overflow: hidden;
|
||||
`;
|
||||
const ContentSegment = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding-right: 16px;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const Toolkit = styled.kbd`
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background-color: ${(props) => props.theme.primary.bg};
|
||||
color: ${(props) => props.theme.secondary.text};
|
||||
font-weight: 600;
|
||||
font-size: 10px;
|
||||
padding: 3px 6px;
|
||||
border: 1px solid ${(props) => props.theme.secondary.text};
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
`;
|
||||
const Loader = styled.div`
|
||||
margin: 2rem auto;
|
||||
border: 4px solid
|
||||
${(props) =>
|
||||
props.theme.name === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.2)'
|
||||
: 'rgba(0, 0, 0, 0.1)'};
|
||||
border-top: 4px solid
|
||||
${(props) =>
|
||||
props.theme.name === 'dark' ? '#FFFFFF' : props.theme.primary.bg};
|
||||
border-radius: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
animation: spin 1s linear infinite;
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const NoResults = styled.div`
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: ${(props) => (props.theme.name === 'dark' ? '#E0E0E0' : '#505050')};
|
||||
font-weight: 500;
|
||||
`;
|
||||
const AskAIButton = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
width: calc(100% - 32px);
|
||||
margin: 0 16px 16px 16px;
|
||||
box-sizing: border-box;
|
||||
height: 50px;
|
||||
padding: 8px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
background-color: ${(props) =>
|
||||
props.theme.name === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.03)'};
|
||||
|
||||
&:hover {
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
background-color: ${(props) =>
|
||||
props.theme.name === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.06)'};
|
||||
}
|
||||
`;
|
||||
|
||||
const SearchHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid
|
||||
${(props) =>
|
||||
props.theme.name === 'dark' ? '#FFFFFF24' : 'rgba(0, 0, 0, 0.14)'};
|
||||
`;
|
||||
|
||||
const TextField = styled.input`
|
||||
width: calc(100% - 32px);
|
||||
margin: 0 16px;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) =>
|
||||
props.theme.name === 'dark'
|
||||
? 'rgba(255, 255, 255, 0.6)'
|
||||
: 'rgba(0, 0, 0, 0.5)'} !important;
|
||||
opacity: 100%; /* Force opacity to ensure placeholder is visible */
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
const EscapeInstruction = styled.kbd`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 12px 16px 0;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: transparent;
|
||||
border: 1px solid
|
||||
${(props) =>
|
||||
props.theme.name === 'dark'
|
||||
? 'rgba(237, 237, 237, 0.6)'
|
||||
: 'rgba(23, 23, 23, 0.6)'};
|
||||
color: ${(props) => (props.theme.name === 'dark' ? '#EDEDED' : '#171717')};
|
||||
font-size: 12px;
|
||||
font-family: 'Geist', sans-serif;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
`;
|
||||
|
||||
export const SearchBar = ({
|
||||
apiKey = '74039c6d-bff7-44ce-ae55-2973cbf13837',
|
||||
apiHost = 'https://gptcloud.arc53.com',
|
||||
theme = 'dark',
|
||||
placeholder = 'Search or Ask AI...',
|
||||
width = '256px',
|
||||
buttonText = 'Search here',
|
||||
}: SearchBarProps) => {
|
||||
const [input, setInput] = React.useState<string>('');
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
const [isWidgetOpen, setIsWidgetOpen] = React.useState<boolean>(false);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const containerRef = React.useRef<HTMLInputElement>(null);
|
||||
const [isResultVisible, setIsResultVisible] = React.useState<boolean>(false);
|
||||
const [results, setResults] = React.useState<Result[]>([]);
|
||||
const debounceTimeout = React.useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null,
|
||||
);
|
||||
const abortControllerRef = React.useRef<AbortController | null>(null);
|
||||
const browserOS = getOS();
|
||||
const isTouch = 'ontouchstart' in window;
|
||||
|
||||
const getKeyboardInstruction = () => {
|
||||
if (isResultVisible) return 'Enter';
|
||||
return browserOS === 'mac' ? '⌘ + K' : 'Ctrl + K';
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
loadGeistFont();
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsResultVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
((browserOS === 'win' || browserOS === 'linux') &&
|
||||
event.ctrlKey &&
|
||||
event.key === 'k') ||
|
||||
(browserOS === 'mac' && event.metaKey && event.key === 'k')
|
||||
) {
|
||||
event.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
setIsResultVisible(true);
|
||||
} else if (event.key === 'Escape') {
|
||||
setIsResultVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!input) {
|
||||
setResults([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
if (debounceTimeout.current) {
|
||||
clearTimeout(debounceTimeout.current);
|
||||
}
|
||||
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
debounceTimeout.current = setTimeout(() => {
|
||||
getSearchResults(input, apiKey, apiHost, abortController.signal)
|
||||
.then((data) => setResults(data))
|
||||
.catch((err) => !abortController.signal.aborted && console.log(err))
|
||||
.finally(() => setLoading(false));
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
clearTimeout(debounceTimeout.current ?? undefined);
|
||||
};
|
||||
}, [input]);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
openWidget();
|
||||
}
|
||||
};
|
||||
|
||||
const openWidget = () => {
|
||||
setIsWidgetOpen(true);
|
||||
setIsResultVisible(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsWidgetOpen(false);
|
||||
setIsResultVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={{ ...themes[theme] }}>
|
||||
<Main>
|
||||
<GlobalStyle />
|
||||
<Container ref={containerRef}>
|
||||
<SearchButton
|
||||
onClick={() => setIsResultVisible(true)}
|
||||
$inputWidth={width}
|
||||
>
|
||||
{buttonText}
|
||||
</SearchButton>
|
||||
{isResultVisible && (
|
||||
<>
|
||||
<SearchOverlay onClick={() => setIsResultVisible(false)} />
|
||||
<SearchResults>
|
||||
<SearchHeader>
|
||||
<TextField
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
/>
|
||||
<EscapeInstruction onClick={() => setIsResultVisible(false)}>
|
||||
Esc
|
||||
</EscapeInstruction>
|
||||
</SearchHeader>
|
||||
<AskAIButton onClick={openWidget}>
|
||||
<img
|
||||
src="https://d3dg1063dc54p9.cloudfront.net/cute-docsgpt.png"
|
||||
alt="DocsGPT"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>Ask the AI</span>
|
||||
</AskAIButton>
|
||||
<SearchResultsScroll>
|
||||
{!loading ? (
|
||||
results.length > 0 ? (
|
||||
results.map((res, key) => {
|
||||
const containsSource = res.source !== 'local';
|
||||
const processedResults = processMarkdownString(
|
||||
res.text,
|
||||
input,
|
||||
);
|
||||
if (processedResults)
|
||||
return (
|
||||
<ResultWrapper
|
||||
key={key}
|
||||
onClick={() => {
|
||||
if (!containsSource) return;
|
||||
window.open(
|
||||
res.source,
|
||||
'_blank',
|
||||
'noopener, noreferrer',
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
<ContentWrapper>
|
||||
<IconTitleWrapper>
|
||||
<ReaderIcon className="title-icon" />
|
||||
<Title>{res.title}</Title>
|
||||
</IconTitleWrapper>
|
||||
<Content>
|
||||
{processedResults.map((element, index) => (
|
||||
<ContentSegment key={index}>
|
||||
<IconTitleWrapper>
|
||||
{element.tag === 'code' && (
|
||||
<CodeIcon className="element-icon" />
|
||||
)}
|
||||
{(element.tag === 'bulletList' ||
|
||||
element.tag === 'numberedList') && (
|
||||
<ListBulletIcon className="element-icon" />
|
||||
)}
|
||||
{element.tag === 'text' && (
|
||||
<TextAlignLeftIcon className="element-icon" />
|
||||
)}
|
||||
{element.tag === 'heading' && (
|
||||
<HeadingIcon className="element-icon" />
|
||||
)}
|
||||
{element.tag === 'blockquote' && (
|
||||
<QuoteIcon className="element-icon" />
|
||||
)}
|
||||
</IconTitleWrapper>
|
||||
<div
|
||||
style={{ flex: 1 }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(
|
||||
element.content,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ContentSegment>
|
||||
))}
|
||||
</Content>
|
||||
</ContentWrapper>
|
||||
</div>
|
||||
</ResultWrapper>
|
||||
);
|
||||
return null;
|
||||
})
|
||||
) : (
|
||||
<NoResults>No results found</NoResults>
|
||||
)
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</SearchResultsScroll>
|
||||
</SearchResults>
|
||||
</>
|
||||
)}
|
||||
{isTouch ? (
|
||||
<Toolkit
|
||||
onClick={() => {
|
||||
setIsWidgetOpen(true);
|
||||
}}
|
||||
title={'Tap to Ask the AI'}
|
||||
>
|
||||
Tap
|
||||
</Toolkit>
|
||||
) : (
|
||||
<Toolkit
|
||||
title={
|
||||
getKeyboardInstruction() === 'Enter'
|
||||
? 'Press Enter to Ask AI'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{getKeyboardInstruction()}
|
||||
</Toolkit>
|
||||
)}
|
||||
</Container>
|
||||
<WidgetCore
|
||||
theme={theme}
|
||||
apiHost={apiHost}
|
||||
apiKey={apiKey}
|
||||
prefilledQuery={input}
|
||||
isOpen={isWidgetOpen}
|
||||
handleClose={handleClose}
|
||||
size={'large'}
|
||||
/>
|
||||
</Main>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
<!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>
|
||||
<script type="module" src="main.tsx"></script>
|
||||
<!-- <script type="module">
|
||||
window.onload = function() {
|
||||
renderDocsGPTWidget('app');
|
||||
renderSearchBar('app')
|
||||
}
|
||||
</script> -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
//exports methods for React
|
||||
export { SearchBar } from './components/SearchBar';
|
||||
export { DocsGPTWidget } from './components/DocsGPTWidget';
|
||||
@@ -0,0 +1,7 @@
|
||||
//development
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
import React from 'react';
|
||||
const container = document.getElementById('app') as HTMLElement;
|
||||
const root = createRoot(container);
|
||||
root.render(<App />);
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Result } from '@/types';
|
||||
|
||||
async function getSearchResults(
|
||||
question: string,
|
||||
apiKey: string,
|
||||
apiHost: string,
|
||||
signal: AbortSignal,
|
||||
): Promise<Result[]> {
|
||||
const payload = {
|
||||
question,
|
||||
api_key: apiKey,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiHost}/api/search`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: Result[] = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (!(error instanceof DOMException && error.name == 'AbortError')) {
|
||||
console.error('Failed to fetch documents:', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export { getSearchResults };
|
||||
@@ -0,0 +1,116 @@
|
||||
interface HistoryItem {
|
||||
prompt: string;
|
||||
response?: string;
|
||||
}
|
||||
|
||||
interface FetchAnswerStreamingProps {
|
||||
question?: string;
|
||||
apiKey?: string;
|
||||
selectedDocs?: string;
|
||||
history?: HistoryItem[];
|
||||
conversationId?: string | null;
|
||||
apiHost?: string;
|
||||
onEvent?: (event: MessageEvent) => void;
|
||||
}
|
||||
|
||||
export interface FeedbackPayload {
|
||||
question?: string;
|
||||
answer?: string;
|
||||
feedback: string | null;
|
||||
apikey?: string;
|
||||
conversation_id: string;
|
||||
question_index: number;
|
||||
}
|
||||
|
||||
export function fetchAnswerStreaming({
|
||||
question = '',
|
||||
apiKey = '',
|
||||
history = [],
|
||||
conversationId = null,
|
||||
apiHost = '',
|
||||
onEvent = () => {
|
||||
console.log('Event triggered, but no handler provided.');
|
||||
},
|
||||
}: FetchAnswerStreamingProps): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const body = {
|
||||
question: question,
|
||||
history: JSON.stringify(history),
|
||||
conversation_id: conversationId,
|
||||
model: 'default',
|
||||
api_key: apiKey,
|
||||
};
|
||||
fetch(apiHost + '/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.body) throw Error('No response body');
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let counter = 0; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
const processStream = ({
|
||||
done,
|
||||
value,
|
||||
}: ReadableStreamReadResult<Uint8Array>) => {
|
||||
if (done) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
counter += 1;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.trim() == '') {
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('data:')) {
|
||||
line = line.substring(5);
|
||||
}
|
||||
|
||||
const messageEvent = new MessageEvent('message', {
|
||||
data: line,
|
||||
});
|
||||
|
||||
onEvent(messageEvent); // handle each message
|
||||
}
|
||||
|
||||
reader.read().then(processStream).catch(reject);
|
||||
};
|
||||
|
||||
reader.read().then(processStream).catch(reject);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Connection failed:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const sendFeedback = (
|
||||
payload: FeedbackPayload,
|
||||
apiHost: string,
|
||||
): Promise<Response> => {
|
||||
return fetch(`${apiHost}/api/feedback`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
question: payload.question,
|
||||
answer: payload.answer,
|
||||
feedback: payload.feedback,
|
||||
api_key: payload.apikey,
|
||||
conversation_id: payload.conversation_id,
|
||||
question_index: payload.question_index,
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'styled-components';
|
||||
|
||||
declare module 'styled-components' {
|
||||
export interface DefaultTheme {
|
||||
bg: string;
|
||||
text: string;
|
||||
primary: {
|
||||
text: string;
|
||||
bg: string;
|
||||
};
|
||||
secondary: {
|
||||
text: string;
|
||||
bg: string;
|
||||
};
|
||||
/** Present only in SearchBar theme */
|
||||
name?: string;
|
||||
/** Present only in DocsGPTWidget theme (always provided when these styled components render) */
|
||||
dimensions?: {
|
||||
size: string;
|
||||
width: string;
|
||||
height: string;
|
||||
maxWidth?: string;
|
||||
maxHeight?: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type MESSAGE_TYPE = 'QUESTION' | 'ANSWER' | 'ERROR';
|
||||
|
||||
export type Status = 'idle' | 'loading' | 'failed';
|
||||
|
||||
export type FEEDBACK = 'LIKE' | 'DISLIKE';
|
||||
|
||||
export type THEME = 'light' | 'dark';
|
||||
|
||||
export interface Query {
|
||||
prompt: string;
|
||||
response?: string;
|
||||
feedback?: FEEDBACK;
|
||||
error?: string;
|
||||
sources?: { title: string; text: string; source: string }[];
|
||||
conversationId?: string | null;
|
||||
title?: string | null;
|
||||
}
|
||||
|
||||
export interface WidgetProps {
|
||||
apiHost?: string;
|
||||
apiKey?: string;
|
||||
avatar?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
heroTitle?: string;
|
||||
heroDescription?: string;
|
||||
size?:
|
||||
| 'small'
|
||||
| 'medium'
|
||||
| 'large'
|
||||
| {
|
||||
custom: {
|
||||
width: string;
|
||||
height: string;
|
||||
maxWidth?: string;
|
||||
maxHeight?: string;
|
||||
};
|
||||
};
|
||||
theme?: THEME;
|
||||
buttonIcon?: string;
|
||||
buttonText?: string;
|
||||
buttonBg?: string;
|
||||
collectFeedback?: boolean;
|
||||
showSources?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
export interface WidgetCoreProps extends WidgetProps {
|
||||
widgetRef?: React.RefObject<HTMLDivElement> | null;
|
||||
handleClose?: React.MouseEventHandler | undefined;
|
||||
isOpen: boolean;
|
||||
prefilledQuery?: string;
|
||||
}
|
||||
|
||||
export interface SearchBarProps {
|
||||
apiHost?: string;
|
||||
apiKey?: string;
|
||||
theme?: THEME;
|
||||
placeholder?: string;
|
||||
width?: string;
|
||||
buttonText?: string;
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
text: string;
|
||||
title: string;
|
||||
source: string;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
export const getOS = () => {
|
||||
const platform = window.navigator.platform;
|
||||
const userAgent = window.navigator.userAgent || window.navigator.vendor;
|
||||
|
||||
if (/Mac/i.test(platform)) {
|
||||
return 'mac';
|
||||
}
|
||||
|
||||
if (/Win/i.test(platform)) {
|
||||
return 'win';
|
||||
}
|
||||
|
||||
if (/Linux/i.test(platform) && !/Android/i.test(userAgent)) {
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
if (/Android/i.test(userAgent)) {
|
||||
return 'android';
|
||||
}
|
||||
|
||||
if (/iPhone|iPad|iPod/i.test(userAgent)) {
|
||||
return 'ios';
|
||||
}
|
||||
|
||||
return 'other';
|
||||
};
|
||||
|
||||
interface ParsedElement {
|
||||
content: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export const processMarkdownString = (
|
||||
markdown: string,
|
||||
keyword?: string,
|
||||
): ParsedElement[] => {
|
||||
const lines = markdown.trim().split('\n');
|
||||
const keywordLower = keyword?.toLowerCase();
|
||||
|
||||
const escapeRegExp = (str: string) =>
|
||||
str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
const escapedKeyword = keyword ? escapeRegExp(keyword) : '';
|
||||
const keywordRegex = keyword ? new RegExp(`(${escapedKeyword})`, 'gi') : null;
|
||||
|
||||
let isInCodeBlock = false;
|
||||
let codeBlockContent: string[] = [];
|
||||
let matchingLines: ParsedElement[] = [];
|
||||
let firstLine: ParsedElement | null = null;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const trimmedLine = lines[i].trim();
|
||||
if (!trimmedLine) continue;
|
||||
|
||||
if (trimmedLine.startsWith('```')) {
|
||||
if (!isInCodeBlock) {
|
||||
isInCodeBlock = true;
|
||||
codeBlockContent = [];
|
||||
} else {
|
||||
isInCodeBlock = false;
|
||||
const codeContent = codeBlockContent.join('\n');
|
||||
const parsedElement: ParsedElement = {
|
||||
content: codeContent,
|
||||
tag: 'code',
|
||||
};
|
||||
|
||||
if (!firstLine) {
|
||||
firstLine = parsedElement;
|
||||
}
|
||||
|
||||
if (keywordLower && codeContent.toLowerCase().includes(keywordLower)) {
|
||||
parsedElement.content = parsedElement.content.replace(
|
||||
keywordRegex!,
|
||||
'<span class="highlight">$1</span>',
|
||||
);
|
||||
matchingLines.push(parsedElement);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isInCodeBlock) {
|
||||
codeBlockContent.push(trimmedLine);
|
||||
continue;
|
||||
}
|
||||
|
||||
let parsedElement: ParsedElement | null = null;
|
||||
|
||||
const headingMatch = trimmedLine.match(/^(#{1,6})\s+(.+)$/);
|
||||
const bulletMatch = trimmedLine.match(/^[-*]\s+(.+)$/);
|
||||
const numberedMatch = trimmedLine.match(/^\d+\.\s+(.+)$/);
|
||||
const blockquoteMatch = trimmedLine.match(/^>+\s*(.+)$/);
|
||||
|
||||
let content = trimmedLine;
|
||||
|
||||
if (headingMatch) {
|
||||
content = headingMatch[2];
|
||||
parsedElement = {
|
||||
content: content,
|
||||
tag: 'heading',
|
||||
};
|
||||
} else if (bulletMatch) {
|
||||
content = bulletMatch[1];
|
||||
parsedElement = {
|
||||
content: content,
|
||||
tag: 'bulletList',
|
||||
};
|
||||
} else if (numberedMatch) {
|
||||
content = numberedMatch[1];
|
||||
parsedElement = {
|
||||
content: content,
|
||||
tag: 'numberedList',
|
||||
};
|
||||
} else if (blockquoteMatch) {
|
||||
content = blockquoteMatch[1];
|
||||
parsedElement = {
|
||||
content: content,
|
||||
tag: 'blockquote',
|
||||
};
|
||||
} else {
|
||||
parsedElement = {
|
||||
content: content,
|
||||
tag: 'text',
|
||||
};
|
||||
}
|
||||
|
||||
if (!firstLine) {
|
||||
firstLine = parsedElement;
|
||||
}
|
||||
|
||||
if (
|
||||
keywordLower &&
|
||||
parsedElement.content.toLowerCase().includes(keywordLower)
|
||||
) {
|
||||
parsedElement.content = parsedElement.content.replace(
|
||||
keywordRegex!,
|
||||
'<span class="highlight">$1</span>',
|
||||
);
|
||||
matchingLines.push(parsedElement);
|
||||
}
|
||||
}
|
||||
|
||||
if (isInCodeBlock && codeBlockContent.length > 0) {
|
||||
const codeContent = codeBlockContent.join('\n');
|
||||
const parsedElement: ParsedElement = {
|
||||
content: codeContent,
|
||||
tag: 'code',
|
||||
};
|
||||
|
||||
if (!firstLine) {
|
||||
firstLine = parsedElement;
|
||||
}
|
||||
|
||||
if (keywordLower && codeContent.toLowerCase().includes(keywordLower)) {
|
||||
parsedElement.content = parsedElement.content.replace(
|
||||
keywordRegex!,
|
||||
'<span class="highlight">$1</span>',
|
||||
);
|
||||
matchingLines.push(parsedElement);
|
||||
}
|
||||
}
|
||||
|
||||
if (keywordLower && matchingLines.length > 0) {
|
||||
return matchingLines;
|
||||
}
|
||||
|
||||
return firstLine ? [firstLine] : [];
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
/* The "typeRoots" configuration specifies the locations where
|
||||
TypeScript looks for type definitions (.d.ts files) to
|
||||
include in the compilation process.*/
|
||||
"typeRoots": ["./dist/index.d.ts", "node_modules/@types"]
|
||||
},
|
||||
/* include /index.ts*/
|
||||
"include": ["src/index.ts","custom.d.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
}
|
||||
Reference in New Issue
Block a user