Merge pull request #2328 from ManishMadan2882/main

Chore(React widget): automate publish via actions
This commit is contained in:
Alex authored and GitHub committed 2026-03-26 11:15:52 +00:00
commit 187e5da61e
16 files changed
+4822 -1067

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
name: Publish npm libraries
on:
workflow_dispatch:
inputs:
version:
description: >
Version bump type (patch | minor | major) or explicit semver (e.g. 1.2.3).
Applies to both docsgpt and docsgpt-react.
required: true
default: patch
jobs:
publish:
runs-on: ubuntu-latest
environment: npm-release
defaults:
run:
working-directory: extensions/react-widget
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- name: Install dependencies
run: npm ci
# ── docsgpt (HTML embedding bundle) ──────────────────────────────────
# Uses the `build` script (parcel build src/browser.tsx) and keeps
# the `targets` field so Parcel produces browser-optimised bundles.
- name: Set package name → docsgpt
run: jq --arg n "docsgpt" '.name=$n' package.json > _tmp.json && mv _tmp.json package.json
- name: Bump version (docsgpt)
id: version_docsgpt
run: |
VERSION="${{ github.event.inputs.version }}"
NEW_VER=$(npm version "${VERSION:-patch}" --no-git-tag-version)
echo "version=${NEW_VER#v}" >> "$GITHUB_OUTPUT"
- name: Build docsgpt
run: npm run build
- name: Publish docsgpt
run: npm publish --verbose
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# ── docsgpt-react (React library bundle) ─────────────────────────────
# Uses `build:react` script (parcel build src/index.ts) and strips
# the `targets` field so Parcel treats the output as a plain library
# without browser-specific target resolution, producing a smaller bundle.
- name: Reset package.json from source control
run: git checkout -- package.json
- name: Set package name → docsgpt-react
run: jq --arg n "docsgpt-react" '.name=$n' package.json > _tmp.json && mv _tmp.json package.json
- name: Remove targets field (react library build)
run: jq 'del(.targets)' package.json > _tmp.json && mv _tmp.json package.json
- name: Bump version (docsgpt-react) to match docsgpt
run: npm version "${{ steps.version_docsgpt.outputs.version }}" --no-git-tag-version
- name: Clean dist before react build
run: rm -rf dist
- name: Build docsgpt-react
run: npm run build:react
- name: Publish docsgpt-react
run: npm publish --verbose
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# ── Commit the bumped version back to the repository ─────────────────
- name: Reset package.json and write final version
run: |
git checkout -- package.json
jq --arg v "${{ steps.version_docsgpt.outputs.version }}" '.version=$v' \
package.json > _tmp.json && mv _tmp.json package.json
npm install --package-lock-only
- name: Commit version bump
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json package-lock.json
git commit -m "chore: bump npm libraries to v${{ steps.version_docsgpt.outputs.version }}"
git push
+34
View File
@@ -0,0 +1,34 @@
name: React Widget Build
on:
push:
paths:
- 'extensions/react-widget/**'
pull_request:
paths:
- 'extensions/react-widget/**'
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: extensions/react-widget
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: extensions/react-widget/package-lock.json
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
+72
View File
@@ -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',
},
},
},
]
+3317 -22
View File
File diff suppressed because it is too large. Load diff
+13 -1
View File
@@ -35,7 +35,9 @@
"serve": "parcel serve -p 3000",
"dev": "parcel -p 3000",
"test": "jest",
"lint": "eslint",
"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"
},
@@ -60,14 +62,24 @@
"@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": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.57.2",
"@typescript-eslint/parser": "^8.57.2",
"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": "^15.15.0",
"parcel": "^2.16.4",
"prettier": "^3.8.1",
"process": "^0.11.10",
"svgo": "^3.3.3",
"typescript": "^5.3.3"
@@ -0,0 +1,8 @@
module.exports = {
trailingComma: 'all',
tabWidth: 2,
semi: true,
singleQuote: true,
printWidth: 80,
};
+7 -7
View File
@@ -1,11 +1,11 @@
import React from "react"
import {DocsGPTWidget} from "./components/DocsGPTWidget"
import {SearchBar} from "./components/SearchBar"
import React from 'react';
import { DocsGPTWidget } from './components/DocsGPTWidget';
import { SearchBar } from './components/SearchBar';
export const App = () => {
return (
<div>
<SearchBar/>
<DocsGPTWidget/>
<SearchBar />
<DocsGPTWidget />
</div>
)
}
);
};
+6 -4
View File
@@ -1,10 +1,10 @@
//exports browser ready methods
import { createRoot } from "react-dom/client";
import { createRoot } from 'react-dom/client';
import { DocsGPTWidget } from './components/DocsGPTWidget';
import { SearchBar } from './components/SearchBar';
import React from "react";
import React from 'react';
if (typeof window !== 'undefined') {
const renderWidget = (elementId: string, props = {}) => {
const root = createRoot(document.getElementById(elementId) as HTMLElement);
@@ -14,9 +14,11 @@ if (typeof window !== 'undefined') {
const root = createRoot(document.getElementById(elementId) as HTMLElement);
root.render(<SearchBar {...props} />);
};
(window as any).renderDocsGPTWidget = renderWidget;
(window as unknown as Record<string, unknown>).renderDocsGPTWidget =
renderWidget;
(window as any).renderSearchBar = renderSearchBar;
(window as unknown as Record<string, unknown>).renderSearchBar =
renderSearchBar;
}
export { DocsGPTWidget, SearchBar };
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,3 +1,3 @@
//exports methods for React
export {SearchBar} from "./components/SearchBar"
export { DocsGPTWidget } from "./components/DocsGPTWidget";
export { SearchBar } from './components/SearchBar';
export { DocsGPTWidget } from './components/DocsGPTWidget';
+5 -6
View File
@@ -1,8 +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)
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 />);
@@ -1,20 +1,24 @@
import { Result } from "@/types";
async function getSearchResults(question: string, apiKey: string, apiHost: string, signal: AbortSignal): Promise<Result[]> {
import { Result } from '@/types';
async function getSearchResults(
question: string,
apiKey: string,
apiHost: string,
signal: AbortSignal,
): Promise<Result[]> {
const payload = {
question,
api_key: apiKey
api_key: apiKey,
};
try {
const response = await fetch(`${apiHost}/api/search`, {
method: "POST",
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
signal: signal
signal: signal,
});
if (!response.ok) {
@@ -23,15 +27,12 @@ async function getSearchResults(question: string, apiKey: string, apiHost: strin
const data: Result[] = await response.json();
return data;
} catch (error) {
if (!(error instanceof DOMException && error.name == "AbortError")) {
console.error("Failed to fetch documents:", error);
if (!(error instanceof DOMException && error.name == 'AbortError')) {
console.error('Failed to fetch documents:', error);
}
throw error;
}
}
export {
getSearchResults
}
export { getSearchResults };
@@ -1,5 +1,3 @@
import { FEEDBACK } from "@/types";
interface HistoryItem {
prompt: string;
response?: string;
@@ -30,7 +28,9 @@ export function fetchAnswerStreaming({
history = [],
conversationId = null,
apiHost = '',
onEvent = () => { console.log("Event triggered, but no handler provided."); }
onEvent = () => {
console.log('Event triggered, but no handler provided.');
},
}: FetchAnswerStreamingProps): Promise<void> {
return new Promise<void>((resolve, reject) => {
const body = {
@@ -38,7 +38,7 @@ export function fetchAnswerStreaming({
history: JSON.stringify(history),
conversation_id: conversationId,
model: 'default',
api_key: apiKey
api_key: apiKey,
};
fetch(apiHost + '/stream', {
method: 'POST',
@@ -52,7 +52,7 @@ export function fetchAnswerStreaming({
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let counter = 0;
let counter = 0; // eslint-disable-line @typescript-eslint/no-unused-vars
const processStream = ({
done,
value,
@@ -95,12 +95,14 @@ export function fetchAnswerStreaming({
});
}
export const sendFeedback = (payload: FeedbackPayload, apiHost: string): Promise<Response> => {
export const sendFeedback = (
payload: FeedbackPayload,
apiHost: string,
): Promise<Response> => {
return fetch(`${apiHost}/api/feedback`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
},
body: JSON.stringify({
question: payload.question,
@@ -108,7 +110,7 @@ export const sendFeedback = (payload: FeedbackPayload, apiHost: string): Promis
feedback: payload.feedback,
api_key: payload.apikey,
conversation_id: payload.conversation_id,
question_index: payload.question_index
question_index: payload.question_index,
}),
});
};
+26 -22
View File
@@ -38,7 +38,7 @@ export interface Query {
response?: string;
feedback?: FEEDBACK;
error?: string;
sources?: { title: string; text: string, source:string }[];
sources?: { title: string; text: string; source: string }[];
conversationId?: string | null;
title?: string | null;
}
@@ -51,30 +51,34 @@ export interface WidgetProps {
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;
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;
export interface WidgetCoreProps extends WidgetProps {
widgetRef?: React.RefObject<HTMLDivElement> | null;
handleClose?: React.MouseEventHandler | undefined;
isOpen: boolean;
prefilledQuery?: string;
}
export interface SearchBarProps {
export interface SearchBarProps {
apiHost?: string;
apiKey?: string;
theme?: THEME;
@@ -84,7 +88,7 @@ export interface SearchBarProps {
}
export interface Result {
text:string;
title:string;
source:string;
text: string;
title: string;
source: string;
}
+29 -13
View File
@@ -30,11 +30,15 @@ interface ParsedElement {
tag: string;
}
export const processMarkdownString = (markdown: string, keyword?: string): ParsedElement[] => {
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 escapeRegExp = (str: string) =>
str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
const escapedKeyword = keyword ? escapeRegExp(keyword) : '';
const keywordRegex = keyword ? new RegExp(`(${escapedKeyword})`, 'gi') : null;
@@ -56,7 +60,7 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse
const codeContent = codeBlockContent.join('\n');
const parsedElement: ParsedElement = {
content: codeContent,
tag: 'code'
tag: 'code',
};
if (!firstLine) {
@@ -64,7 +68,10 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse
}
if (keywordLower && codeContent.toLowerCase().includes(keywordLower)) {
parsedElement.content = parsedElement.content.replace(keywordRegex!, '<span class="highlight">$1</span>');
parsedElement.content = parsedElement.content.replace(
keywordRegex!,
'<span class="highlight">$1</span>',
);
matchingLines.push(parsedElement);
}
}
@@ -89,30 +96,30 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse
content = headingMatch[2];
parsedElement = {
content: content,
tag: 'heading'
tag: 'heading',
};
} else if (bulletMatch) {
content = bulletMatch[1];
parsedElement = {
content: content,
tag: 'bulletList'
tag: 'bulletList',
};
} else if (numberedMatch) {
content = numberedMatch[1];
parsedElement = {
content: content,
tag: 'numberedList'
tag: 'numberedList',
};
} else if (blockquoteMatch) {
content = blockquoteMatch[1];
parsedElement = {
content: content,
tag: 'blockquote'
tag: 'blockquote',
};
} else {
parsedElement = {
content: content,
tag: 'text'
tag: 'text',
};
}
@@ -120,8 +127,14 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse
firstLine = parsedElement;
}
if (keywordLower && parsedElement.content.toLowerCase().includes(keywordLower)) {
parsedElement.content = parsedElement.content.replace(keywordRegex!, '<span class="highlight">$1</span>');
if (
keywordLower &&
parsedElement.content.toLowerCase().includes(keywordLower)
) {
parsedElement.content = parsedElement.content.replace(
keywordRegex!,
'<span class="highlight">$1</span>',
);
matchingLines.push(parsedElement);
}
}
@@ -130,7 +143,7 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse
const codeContent = codeBlockContent.join('\n');
const parsedElement: ParsedElement = {
content: codeContent,
tag: 'code'
tag: 'code',
};
if (!firstLine) {
@@ -138,7 +151,10 @@ export const processMarkdownString = (markdown: string, keyword?: string): Parse
}
if (keywordLower && codeContent.toLowerCase().includes(keywordLower)) {
parsedElement.content = parsedElement.content.replace(keywordRegex!, '<span class="highlight">$1</span>');
parsedElement.content = parsedElement.content.replace(
keywordRegex!,
'<span class="highlight">$1</span>',
);
matchingLines.push(parsedElement);
}
}