mirror of
https://github.com/chathub-dev/chathub.git
synced 2025-09-26 20:31:18 +08:00
init
This commit is contained in:
23
.eslintrc.json
Normal file
23
.eslintrc.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"overrides": [],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"react/react-in-jsx-scope": "off"
|
||||
},
|
||||
"ignorePatterns": ["build/**"]
|
||||
}
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
16
.prettierrc
Normal file
16
.prettierrc
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ".prettierrc",
|
||||
"options": {
|
||||
"parser": "json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
12
manifest.json
Normal file
12
manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "ChatHub",
|
||||
"version": "0.0.1",
|
||||
"background": {
|
||||
"service_worker": "src/background/index.ts",
|
||||
"type": "module"
|
||||
},
|
||||
"action": {},
|
||||
"host_permissions": ["https://*.bing.com/", "https://*.openai.com/"],
|
||||
"permissions": ["storage"]
|
||||
}
|
68
package.json
Normal file
68
package.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "chatbox-extension",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@crxjs/vite-plugin": "^2.0.0-beta.12",
|
||||
"@parcel/config-webextension": "^2.8.3",
|
||||
"@parcel/optimizer-data-url": "2.8.3",
|
||||
"@parcel/transformer-inline-string": "2.8.3",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-scroll-to-bottom": "^4.2.0",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/webextension-polyfill": "^0.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||
"@typescript-eslint/parser": "^5.52.0",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"parcel": "^2.8.3",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-nesting": "^11.2.1",
|
||||
"prettier": "^2.8.4",
|
||||
"process": "^0.11.10",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.1.0",
|
||||
"vite-tsconfig-paths": "^4.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.17",
|
||||
"@chakra-ui/react": "^2.5.1",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@tanstack/react-router": "^0.0.1-beta.83",
|
||||
"eventsource-parser": "^0.1.0",
|
||||
"expiry-map": "^2.0.0",
|
||||
"framer-motion": "^9.0.4",
|
||||
"github-markdown-css": "^5.2.0",
|
||||
"immer": "^9.0.19",
|
||||
"jotai": "^2.0.2",
|
||||
"jotai-immer": "^0.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-ga4": "^2.0.0",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-scroll-to-bottom": "^4.2.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-supersub": "^1.0.0",
|
||||
"use-immer": "^0.8.1",
|
||||
"uuid": "^9.0.0",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"websocket-as-promised": "^2.0.1",
|
||||
"wretch": "^2.4.1",
|
||||
"zustand": "^4.3.3"
|
||||
}
|
||||
}
|
8
postcss.config.cjs
Normal file
8
postcss.config.cjs
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
'tailwindcss/nesting': 'postcss-nesting',
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
32
src/app/bots/abstract-bot.ts
Normal file
32
src/app/bots/abstract-bot.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ErrorCode } from '~utils/errors'
|
||||
|
||||
export type Event =
|
||||
| {
|
||||
type: 'UPDATE_ANSWER'
|
||||
data: {
|
||||
text: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'DONE'
|
||||
}
|
||||
| {
|
||||
type: 'ERROR'
|
||||
data: {
|
||||
code: ErrorCode
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SendMessageParams {
|
||||
prompt: string
|
||||
onEvent: (event: Event) => void
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
export abstract class AbstractBot {
|
||||
abstract name: string
|
||||
abstract logo: string
|
||||
abstract sendMessage(params: SendMessageParams): Promise<void>
|
||||
abstract resetConversation(): Promise<void>
|
||||
}
|
17
src/app/bots/bing/api.ts
Normal file
17
src/app/bots/bing/api.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import wretch from 'wretch'
|
||||
import { uuid } from '~utils'
|
||||
import { ConversationResponse } from './types'
|
||||
|
||||
export async function createConversation(): Promise<ConversationResponse> {
|
||||
const resp: ConversationResponse = await wretch('https://www.bing.com/turing/conversation/create')
|
||||
.headers({
|
||||
'x-ms-client-request-id': uuid(),
|
||||
'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.0 OS/Win32',
|
||||
})
|
||||
.get()
|
||||
.json()
|
||||
if (resp.result.value !== 'Success') {
|
||||
throw new Error(`Failed to create conversation: ${resp.result.value} ${resp.result.message}`)
|
||||
}
|
||||
return resp
|
||||
}
|
117557
src/app/bots/bing/code.js
Normal file
117557
src/app/bots/bing/code.js
Normal file
File diff suppressed because one or more lines are too long
83611
src/app/bots/bing/code2.js
Normal file
83611
src/app/bots/bing/code2.js
Normal file
File diff suppressed because one or more lines are too long
100
src/app/bots/bing/index.ts
Normal file
100
src/app/bots/bing/index.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import WebSocketAsPromised from 'websocket-as-promised'
|
||||
import logo from '~/assets/bing-logo.png'
|
||||
import { ErrorCode } from '~utils/errors'
|
||||
import { AbstractBot, SendMessageParams } from '../abstract-bot'
|
||||
import { createConversation } from './api'
|
||||
import { ChatResponseMessage, ConversationInfo, InvocationEventType } from './types'
|
||||
import { convertMessageToMarkdown, websocketUtils } from './utils'
|
||||
|
||||
export class BingWebBot extends AbstractBot {
|
||||
name = 'Bing'
|
||||
logo = logo
|
||||
|
||||
private conversationContext?: ConversationInfo
|
||||
|
||||
private buildChatRequest(conversation: ConversationInfo, message: string) {
|
||||
return {
|
||||
arguments: [
|
||||
{
|
||||
source: 'cib',
|
||||
optionsSets: [
|
||||
'nlu_direct_response_filter',
|
||||
'deepleo',
|
||||
'disable_emoji_spoken_text',
|
||||
'responsible_ai_policy_235',
|
||||
'enablemm',
|
||||
],
|
||||
allowedMessageTypes: ['Chat', 'InternalSearchQuery'],
|
||||
isStartOfSession: conversation.invocationId === 0,
|
||||
message: {
|
||||
author: 'user',
|
||||
inputMethod: 'Keyboard',
|
||||
text: message,
|
||||
messageType: 'Chat',
|
||||
},
|
||||
conversationId: conversation.conversationId,
|
||||
conversationSignature: conversation.conversationSignature,
|
||||
participant: { id: conversation.clientId },
|
||||
},
|
||||
],
|
||||
invocationId: conversation.invocationId.toString(),
|
||||
target: 'chat',
|
||||
type: InvocationEventType.StreamInvocation,
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(params: SendMessageParams) {
|
||||
if (!this.conversationContext) {
|
||||
const conversation = await createConversation()
|
||||
this.conversationContext = {
|
||||
conversationId: conversation.conversationId,
|
||||
conversationSignature: conversation.conversationSignature,
|
||||
clientId: conversation.clientId,
|
||||
invocationId: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const conversation = this.conversationContext!
|
||||
|
||||
const wsp = new WebSocketAsPromised('wss://sydney.bing.com/sydney/ChatHub', {
|
||||
packMessage: websocketUtils.packMessage,
|
||||
unpackMessage: websocketUtils.unpackMessage,
|
||||
})
|
||||
|
||||
wsp.onUnpackedMessage.addListener((events) => {
|
||||
for (const event of events) {
|
||||
if (JSON.stringify(event) === '{}') {
|
||||
wsp.sendPacked({ type: InvocationEventType.Ping })
|
||||
wsp.sendPacked(this.buildChatRequest(conversation, params.prompt))
|
||||
conversation.invocationId += 1
|
||||
} else if (event.type === 3) {
|
||||
params.onEvent({ type: 'DONE' })
|
||||
wsp.removeAllListeners()
|
||||
wsp.close()
|
||||
} else if (event.type === 1) {
|
||||
const text = convertMessageToMarkdown(event.arguments[0].messages[0])
|
||||
params.onEvent({ type: 'UPDATE_ANSWER', data: { text } })
|
||||
} else if (event.type === 2) {
|
||||
const messages = event.item.messages as ChatResponseMessage[]
|
||||
const limited = messages.some((message) => message.contentOrigin === 'TurnLimiter')
|
||||
if (limited) {
|
||||
params.onEvent({
|
||||
type: 'ERROR',
|
||||
data: {
|
||||
code: ErrorCode.CONVERSATION_LIMIT,
|
||||
message: 'Sorry, you have reached chat turns limit in this conversation.',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await wsp.open()
|
||||
wsp.sendPacked({ protocol: 'json', version: 1 })
|
||||
}
|
||||
|
||||
async resetConversation(): Promise<void> {
|
||||
this.conversationContext = undefined
|
||||
}
|
||||
}
|
103
src/app/bots/bing/types.ts
Normal file
103
src/app/bots/bing/types.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
export interface ConversationResponse {
|
||||
conversationId: string
|
||||
clientId: string
|
||||
conversationSignature: string
|
||||
result: {
|
||||
value: string
|
||||
message: null
|
||||
}
|
||||
}
|
||||
|
||||
export enum InvocationEventType {
|
||||
Invocation = 1,
|
||||
StreamItem = 2,
|
||||
Completion = 3,
|
||||
StreamInvocation = 4,
|
||||
CancelInvocation = 5,
|
||||
Ping = 6,
|
||||
Close = 7,
|
||||
}
|
||||
|
||||
// https://github.com/bytemate/bingchat-api/blob/main/src/lib.ts
|
||||
|
||||
export interface ConversationInfo {
|
||||
conversationId: string
|
||||
clientId: string
|
||||
conversationSignature: string
|
||||
invocationId: number
|
||||
}
|
||||
|
||||
export interface BingChatResponse {
|
||||
conversationSignature: string
|
||||
conversationId: string
|
||||
clientId: string
|
||||
invocationId: number
|
||||
conversationExpiryTime: Date
|
||||
response: string
|
||||
details: ChatResponseMessage
|
||||
}
|
||||
|
||||
export interface ChatResponseMessage {
|
||||
text: string
|
||||
author: string
|
||||
createdAt: Date
|
||||
timestamp: Date
|
||||
messageId: string
|
||||
messageType?: string
|
||||
requestId: string
|
||||
offense: string
|
||||
adaptiveCards: AdaptiveCard[]
|
||||
sourceAttributions: SourceAttribution[]
|
||||
feedback: Feedback
|
||||
contentOrigin: string
|
||||
privacy: null
|
||||
suggestedResponses: SuggestedResponse[]
|
||||
}
|
||||
|
||||
export interface AdaptiveCard {
|
||||
type: string
|
||||
version: string
|
||||
body: Body[]
|
||||
}
|
||||
|
||||
export interface Body {
|
||||
type: string
|
||||
text: string
|
||||
wrap: boolean
|
||||
size?: string
|
||||
}
|
||||
|
||||
export interface Feedback {
|
||||
tag: null
|
||||
updatedOn: null
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface SourceAttribution {
|
||||
providerDisplayName: string
|
||||
seeMoreUrl: string
|
||||
searchQuery: string
|
||||
}
|
||||
|
||||
export interface SuggestedResponse {
|
||||
text: string
|
||||
author: string
|
||||
createdAt: Date
|
||||
timestamp: Date
|
||||
messageId: string
|
||||
messageType: string
|
||||
offense: string
|
||||
feedback: Feedback
|
||||
contentOrigin: string
|
||||
privacy: null
|
||||
}
|
||||
|
||||
export async function generateMarkdown(response: BingChatResponse) {
|
||||
// change `[^Number^]` to markdown link
|
||||
const regex = /\[\^(\d+)\^\]/g
|
||||
const markdown = response.details.text.replace(regex, (match, p1) => {
|
||||
const sourceAttribution = response.details.sourceAttributions[Number(p1) - 1]
|
||||
return `[${sourceAttribution.providerDisplayName}](${sourceAttribution.seeMoreUrl})`
|
||||
})
|
||||
return markdown
|
||||
}
|
31
src/app/bots/bing/utils.ts
Normal file
31
src/app/bots/bing/utils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ChatResponseMessage } from './types'
|
||||
|
||||
export function convertMessageToMarkdown(message: ChatResponseMessage): string {
|
||||
if (message.messageType === 'InternalSearchQuery') {
|
||||
return message.text
|
||||
}
|
||||
let adaptiveCardText = ''
|
||||
for (const card of message.adaptiveCards) {
|
||||
for (const block of card.body) {
|
||||
if (block.type === 'TextBlock') {
|
||||
adaptiveCardText += '\n' + block.text
|
||||
}
|
||||
}
|
||||
}
|
||||
return adaptiveCardText
|
||||
}
|
||||
|
||||
const RecordSeparator = String.fromCharCode(30)
|
||||
|
||||
export const websocketUtils = {
|
||||
packMessage(data: any) {
|
||||
return `${JSON.stringify(data)}${RecordSeparator}`
|
||||
},
|
||||
unpackMessage(data: string | ArrayBuffer | Blob) {
|
||||
return data
|
||||
.toString()
|
||||
.split(RecordSeparator)
|
||||
.filter(Boolean)
|
||||
.map((s) => JSON.parse(s))
|
||||
},
|
||||
}
|
22
src/app/bots/chatgpt-webapp/api.ts
Normal file
22
src/app/bots/chatgpt-webapp/api.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export async function getChatGPTAccessToken(): Promise<string> {
|
||||
const resp = await fetch('https://chat.openai.com/api/auth/session')
|
||||
if (resp.status === 403) {
|
||||
throw new Error('CLOUDFLARE')
|
||||
}
|
||||
const data = await resp.json().catch(() => ({}))
|
||||
if (!data.accessToken) {
|
||||
throw new Error('UNAUTHORIZED')
|
||||
}
|
||||
return data.accessToken
|
||||
}
|
||||
|
||||
export async function requestBackendAPIWithToken(token: string, method: string, path: string, data?: unknown) {
|
||||
return fetch(`https://chat.openai.com/backend-api${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: data === undefined ? undefined : JSON.stringify(data),
|
||||
})
|
||||
}
|
100
src/app/bots/chatgpt-webapp/index.ts
Normal file
100
src/app/bots/chatgpt-webapp/index.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import logo from '~/assets/chatgpt-logo.svg'
|
||||
import { fetchSSE } from '~/utils/fetch-sse'
|
||||
import { AbstractBot, SendMessageParams } from '../abstract-bot'
|
||||
import { getChatGPTAccessToken, requestBackendAPIWithToken } from './api'
|
||||
|
||||
interface ConversationContext {
|
||||
conversationId: string
|
||||
lastMessageId: string
|
||||
}
|
||||
|
||||
export class ChatGPTWebBot implements AbstractBot {
|
||||
name = 'ChatGPT'
|
||||
logo = logo
|
||||
|
||||
private accessToken?: string
|
||||
private conversationContext?: ConversationContext
|
||||
private modelName?: string
|
||||
|
||||
private async fetchModels(): Promise<{ slug: string; title: string; description: string; max_tokens: number }[]> {
|
||||
const resp = await requestBackendAPIWithToken(this.accessToken!, 'GET', '/models').then((r) => r.json())
|
||||
return resp.models
|
||||
}
|
||||
|
||||
private async getModelName(): Promise<string> {
|
||||
if (this.modelName) {
|
||||
return this.modelName
|
||||
}
|
||||
try {
|
||||
const models = await this.fetchModels()
|
||||
this.modelName = models[0].slug
|
||||
return this.modelName
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return 'text-davinci-002-render'
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(params: SendMessageParams) {
|
||||
if (!this.accessToken) {
|
||||
this.accessToken = await getChatGPTAccessToken()
|
||||
}
|
||||
const modelName = await this.getModelName()
|
||||
console.debug('Using model:', modelName)
|
||||
|
||||
await fetchSSE('https://chat.openai.com/backend-api/conversation', {
|
||||
method: 'POST',
|
||||
signal: params.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${this.accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'next',
|
||||
messages: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
role: 'user',
|
||||
content: {
|
||||
content_type: 'text',
|
||||
parts: [params.prompt],
|
||||
},
|
||||
},
|
||||
],
|
||||
model: modelName,
|
||||
conversation_id: this.conversationContext?.conversationId || undefined,
|
||||
parent_message_id: this.conversationContext?.lastMessageId || uuidv4(),
|
||||
}),
|
||||
onMessage: (message: string) => {
|
||||
console.debug('sse message', message)
|
||||
if (message === '[DONE]') {
|
||||
params.onEvent({ type: 'DONE' })
|
||||
return
|
||||
}
|
||||
let data
|
||||
try {
|
||||
data = JSON.parse(message)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return
|
||||
}
|
||||
const text = data.message?.content?.parts?.[0]
|
||||
if (text) {
|
||||
this.conversationContext = {
|
||||
conversationId: data.conversation_id,
|
||||
lastMessageId: data.message.id,
|
||||
}
|
||||
params.onEvent({
|
||||
type: 'UPDATE_ANSWER',
|
||||
data: { text },
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async resetConversation(): Promise<void> {
|
||||
this.conversationContext = undefined
|
||||
}
|
||||
}
|
10
src/app/bots/index.ts
Normal file
10
src/app/bots/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { AbstractBot } from './abstract-bot'
|
||||
import { BingWebBot } from './bing'
|
||||
import { ChatGPTWebBot } from './chatgpt-webapp'
|
||||
|
||||
export type BotId = 'chatgpt' | 'bing'
|
||||
|
||||
export const botClasses: Record<BotId, typeof ChatGPTWebBot | typeof BingWebBot> = {
|
||||
chatgpt: ChatGPTWebBot,
|
||||
bing: BingWebBot,
|
||||
}
|
43
src/app/components/Chat/ChatMessageCard.tsx
Normal file
43
src/app/components/Chat/ChatMessageCard.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Avatar } from '@chakra-ui/react'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import supersub from 'remark-supersub'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { FC, useMemo } from 'react'
|
||||
import 'github-markdown-css'
|
||||
import { ChatMessageModel } from '~/types'
|
||||
import { CHATBOTS } from '../../consts'
|
||||
import classes from './card.module.css'
|
||||
|
||||
interface Props {
|
||||
message: ChatMessageModel
|
||||
}
|
||||
|
||||
const ChatMessageCard: FC<Props> = ({ message }) => {
|
||||
const user = useMemo(() => {
|
||||
if (message.author === 'chatgpt') {
|
||||
return CHATBOTS.chatgpt
|
||||
}
|
||||
if (message.author === 'bing') {
|
||||
return CHATBOTS.bing
|
||||
}
|
||||
}, [message])
|
||||
return (
|
||||
<div className="flex flex-row gap-3">
|
||||
<div>
|
||||
<Avatar src={user?.avatar} size="sm" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm opacity-50">{user?.name || 'You'}</span>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[supersub, remarkGfm]}
|
||||
className={`markdown-body ${classes.markdown}`}
|
||||
linkTarget="_blank"
|
||||
>
|
||||
{message.text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatMessageCard
|
24
src/app/components/Chat/ChatMessageList.tsx
Normal file
24
src/app/components/Chat/ChatMessageList.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { FC } from 'react'
|
||||
import ScrollToBottom from 'react-scroll-to-bottom'
|
||||
import { BotId } from '~app/bots'
|
||||
import { ChatMessageModel } from '~types'
|
||||
import ChatMessageCard from './ChatMessageCard'
|
||||
|
||||
interface Props {
|
||||
botId: BotId
|
||||
messages: ChatMessageModel[]
|
||||
}
|
||||
|
||||
const MessageList: FC<Props> = (props) => {
|
||||
return (
|
||||
<ScrollToBottom className="overflow-scroll h-full">
|
||||
<div className="mx-auto flex flex-col gap-3 px-10 h-full">
|
||||
{props.messages.map((message) => {
|
||||
return <ChatMessageCard key={message.id} message={message} />
|
||||
})}
|
||||
</div>
|
||||
</ScrollToBottom>
|
||||
)
|
||||
}
|
||||
|
||||
export default MessageList
|
40
src/app/components/Chat/ConversationPanel.tsx
Normal file
40
src/app/components/Chat/ConversationPanel.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Container, Input } from '@chakra-ui/react'
|
||||
import { FC, useCallback } from 'react'
|
||||
import { ChatMessageModel } from '~types'
|
||||
import { BotId } from '../../bots'
|
||||
import MessageList from './ChatMessageList'
|
||||
|
||||
interface Props {
|
||||
botId: BotId
|
||||
messages: ChatMessageModel[]
|
||||
onUserSendMessage: (input: string, botId: BotId) => void
|
||||
}
|
||||
|
||||
const ConversationPanel: FC<Props> = (props) => {
|
||||
const onSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const form = e.target as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
const { input } = Object.fromEntries(formData.entries())
|
||||
form.reset()
|
||||
if (input) {
|
||||
props.onUserSendMessage(input as string, props.botId)
|
||||
}
|
||||
},
|
||||
[props],
|
||||
)
|
||||
return (
|
||||
<div className="py-5 flex flex-col overflow-hidden">
|
||||
<div className="text-center font-bold">{props.botId}</div>
|
||||
<MessageList botId={props.botId} messages={props.messages} />
|
||||
<Container maxW="md" className="my-0">
|
||||
<form onSubmit={onSubmit}>
|
||||
<Input name="input" autoComplete="off" />
|
||||
</form>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConversationPanel
|
10
src/app/components/Chat/card.module.css
Normal file
10
src/app/components/Chat/card.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.markdown {
|
||||
> p {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
> ul,
|
||||
ol {
|
||||
list-style: disc;
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
29
src/app/components/Sidebar/index.tsx
Normal file
29
src/app/components/Sidebar/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
|
||||
function Sidebar() {
|
||||
return (
|
||||
<aside className="bg-gray-900 p-2 flex flex-col text-white">
|
||||
<span>ChatHub</span>
|
||||
<div className="mt-10 flex flex-col gap-3">
|
||||
<span>
|
||||
<Link to="/">All-in-One</Link>
|
||||
</span>
|
||||
<span>
|
||||
<Link to="/chat/$botId" params={{ botId: 'chatgpt' }}>
|
||||
ChatGPT
|
||||
</Link>
|
||||
</span>
|
||||
<span>
|
||||
<Link to="/chat/$botId" params={{ botId: 'bing' }}>
|
||||
Bing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<span className="mt-auto"></span>
|
||||
<span>Feedback</span>
|
||||
<span>About</span>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sidebar
|
13
src/app/consts.ts
Normal file
13
src/app/consts.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import chatgptLogo from '~/assets/chatgpt-logo.svg'
|
||||
import bingLogo from '~/assets/bing-logo.png'
|
||||
|
||||
export const CHATBOTS = {
|
||||
chatgpt: {
|
||||
name: 'ChatGPT',
|
||||
avatar: chatgptLogo,
|
||||
},
|
||||
bing: {
|
||||
name: 'Bing',
|
||||
avatar: bingLogo,
|
||||
},
|
||||
}
|
35
src/app/hooks/use-chat.ts
Normal file
35
src/app/hooks/use-chat.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useImmer } from 'use-immer'
|
||||
import { ChatMessageModel } from '~types'
|
||||
import { uuid } from '~utils'
|
||||
import { botClasses, BotId } from '../bots'
|
||||
|
||||
export function useChat(botId: BotId) {
|
||||
const bot = useMemo(() => new botClasses[botId](), [botId])
|
||||
const [messages, setMessages] = useImmer<ChatMessageModel[]>([])
|
||||
|
||||
const sendMessage = useCallback(
|
||||
(input: string) => {
|
||||
const botMessageId = uuid()
|
||||
setMessages((draft) => {
|
||||
draft.push({ id: uuid(), text: input, author: 'user' }, { id: botMessageId, text: '...', author: botId })
|
||||
})
|
||||
bot.sendMessage({
|
||||
prompt: input,
|
||||
onEvent(event) {
|
||||
if (event.type === 'UPDATE_ANSWER') {
|
||||
setMessages((draft) => {
|
||||
const message = draft.find((m) => m.id === botMessageId)
|
||||
if (message) {
|
||||
message.text = event.data.text
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
[bot, botId, setMessages],
|
||||
)
|
||||
|
||||
return { messages, sendMessage }
|
||||
}
|
8
src/app/main.tsx
Normal file
8
src/app/main.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { RouterProvider } from '@tanstack/react-router'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import '../base.css'
|
||||
import { router } from './router'
|
||||
|
||||
const container = document.getElementById('app')!
|
||||
const root = createRoot(container)
|
||||
root.render(<RouterProvider router={router} />)
|
53
src/app/pages/MultiBotChatPanel.tsx
Normal file
53
src/app/pages/MultiBotChatPanel.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Container, Input } from '@chakra-ui/react'
|
||||
import { FC, useCallback } from 'react'
|
||||
import { useChat } from '~app/hooks/use-chat'
|
||||
import { BotId } from '../bots'
|
||||
import ConversationPanel from '../components/Chat/ConversationPanel'
|
||||
|
||||
const MultiBotChatPanel: FC = () => {
|
||||
const chatgptChat = useChat('chatgpt')
|
||||
const bingChat = useChat('bing')
|
||||
|
||||
const onUserSendMessage = useCallback(
|
||||
(input: string, botId?: BotId) => {
|
||||
if (botId === 'chatgpt') {
|
||||
chatgptChat.sendMessage(input)
|
||||
} else if (botId === 'bing') {
|
||||
bingChat.sendMessage(input)
|
||||
} else {
|
||||
chatgptChat.sendMessage(input)
|
||||
bingChat.sendMessage(input)
|
||||
}
|
||||
},
|
||||
[bingChat, chatgptChat],
|
||||
)
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const form = e.target as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
const { input } = Object.fromEntries(formData.entries())
|
||||
form.reset()
|
||||
onUserSendMessage(input as string)
|
||||
},
|
||||
[onUserSendMessage],
|
||||
)
|
||||
|
||||
return (
|
||||
<main className="grid grid-cols-[1fr_2px_1fr] grid-rows-[1fr_80px] overflow-hidden">
|
||||
<ConversationPanel botId="chatgpt" messages={chatgptChat.messages} onUserSendMessage={onUserSendMessage} />
|
||||
<div className="bg-gray-300"></div>
|
||||
<ConversationPanel botId="bing" messages={bingChat.messages} onUserSendMessage={onUserSendMessage} />
|
||||
<div className="col-span-3">
|
||||
<Container className="h-full">
|
||||
<form onSubmit={onSubmit}>
|
||||
<Input size="lg" name="input" autoComplete="off" className="shadow-[0_0_10px_rgba(0,0,0,0.10)]" />
|
||||
</form>
|
||||
</Container>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default MultiBotChatPanel
|
15
src/app/pages/SingleBotChatPanel.tsx
Normal file
15
src/app/pages/SingleBotChatPanel.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { FC } from 'react'
|
||||
import { useChat } from '~app/hooks/use-chat'
|
||||
import { BotId } from '../bots'
|
||||
import ConversationPanel from '../components/Chat/ConversationPanel'
|
||||
|
||||
interface Props {
|
||||
botId: BotId
|
||||
}
|
||||
|
||||
const SingleBotChatPanel: FC<Props> = ({ botId }) => {
|
||||
const { messages, sendMessage } = useChat(botId)
|
||||
return <ConversationPanel botId={botId} messages={messages} onUserSendMessage={sendMessage} />
|
||||
}
|
||||
|
||||
export default SingleBotChatPanel
|
54
src/app/router.tsx
Normal file
54
src/app/router.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ChakraProvider } from '@chakra-ui/react'
|
||||
import { Outlet, ReactRouter, RootRoute, Route, useParams } from '@tanstack/react-router'
|
||||
import { BotId } from './bots'
|
||||
import Sidebar from './components/Sidebar'
|
||||
import MultiBotChatPanel from './pages/MultiBotChatPanel'
|
||||
import SingleBotChatPanel from './pages/SingleBotChatPanel'
|
||||
|
||||
function Layout() {
|
||||
return (
|
||||
<ChakraProvider>
|
||||
<div className="grid grid-cols-[200px_1fr] h-screen">
|
||||
<Sidebar />
|
||||
<Outlet />
|
||||
</div>
|
||||
</ChakraProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const rootRoute = new RootRoute()
|
||||
|
||||
const layoutRoute = new Route({
|
||||
getParentRoute: () => rootRoute,
|
||||
component: Layout,
|
||||
id: 'layout',
|
||||
})
|
||||
|
||||
const indexRoute = new Route({
|
||||
getParentRoute: () => layoutRoute,
|
||||
path: 'src/index.html',
|
||||
component: MultiBotChatPanel,
|
||||
})
|
||||
|
||||
function ChatRoute() {
|
||||
const { botId } = useParams({ from: chatRoute.id })
|
||||
return <SingleBotChatPanel botId={botId as BotId} />
|
||||
}
|
||||
|
||||
const chatRoute = new Route({
|
||||
getParentRoute: () => layoutRoute,
|
||||
path: 'chat/$botId',
|
||||
component: ChatRoute,
|
||||
})
|
||||
|
||||
const routeTree = rootRoute.addChildren([layoutRoute.addChildren([indexRoute, chatRoute])])
|
||||
|
||||
const router = new ReactRouter({ routeTree })
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router
|
||||
}
|
||||
}
|
||||
|
||||
export { router }
|
BIN
src/assets/bing-logo.png
Normal file
BIN
src/assets/bing-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
19
src/assets/bing-logo.svg
Normal file
19
src/assets/bing-logo.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="678" height="1024" viewBox="0 0 678 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M342.444 322.924C323.57 325.117 309.176 340.445 307.838 359.775C307.261 368.104 307.442 368.669 326.321 417.25C369.273 527.78 379.679 554.382 381.429 558.126C385.669 567.193 391.631 575.723 399.08 583.379C404.796 589.254 408.566 592.413 414.942 596.672C426.148 604.156 431.709 606.225 475.314 619.136C517.79 631.713 540.996 640.072 560.993 649.997C586.899 662.855 604.974 677.481 616.407 694.835C624.61 707.287 631.875 729.616 635.036 752.093C636.272 760.879 636.28 780.301 635.051 788.244C632.384 805.483 627.057 819.929 618.908 832.018C614.574 838.447 616.082 837.371 622.383 829.536C640.215 807.365 658.38 769.472 667.649 735.11C678.866 693.523 680.392 648.866 672.04 606.599C655.775 524.29 603.814 453.257 530.632 413.289C526.034 410.777 508.52 401.597 484.776 389.252C481.173 387.378 476.26 384.813 473.858 383.552C471.456 382.29 466.543 379.725 462.94 377.852C459.337 375.979 448.965 370.575 439.891 365.844C430.817 361.112 420.664 355.818 417.328 354.079C407.159 348.777 400.336 345.215 395.249 342.552C371.721 330.235 361.762 325.256 358.923 324.392C355.945 323.486 348.38 322.323 346.482 322.479C346.082 322.512 344.265 322.712 342.444 322.924Z" fill="url(#paint0_radial_2_17)"/>
|
||||
<path d="M393.737 735.544C392.433 736.316 390.603 737.434 389.669 738.027C388.734 738.621 386.66 739.91 385.059 740.893C379.182 744.5 363.552 754.131 350.121 762.422C341.294 767.871 339.984 768.683 328.771 775.642C324.767 778.126 320.509 780.744 319.308 781.46C318.107 782.176 312.976 785.336 307.905 788.482C302.834 791.627 293.991 797.087 288.253 800.614C282.515 804.14 272.252 810.471 265.447 814.682C258.641 818.892 249.688 824.413 245.552 826.95C241.415 829.486 237.594 831.936 237.06 832.394C236.267 833.074 199.475 855.865 181.014 867.112C166.993 875.653 150.773 881.366 134.169 883.61C126.439 884.654 111.811 884.658 104.103 883.616C83.2021 880.794 63.9476 872.999 47.4576 860.687C40.9893 855.857 28.8117 843.689 24.1563 837.403C13.1855 822.592 6.08829 806.705 2.41258 788.729C1.56681 784.592 0.766658 781.099 0.635158 780.965C0.291606 780.618 0.912197 786.867 2.03165 795.037C3.19575 803.534 5.67635 815.824 8.3481 826.335C29.0233 907.68 87.8556 973.842 167.5 1005.32C190.434 1014.38 213.577 1020.09 238.758 1022.89C248.22 1023.95 275.003 1024.37 284.878 1023.62C330.165 1020.19 369.597 1006.86 410.049 981.295C413.652 979.018 420.421 974.75 425.091 971.809C429.762 968.869 435.657 965.131 438.193 963.504C440.728 961.876 443.785 959.953 444.986 959.231C446.187 958.508 448.589 956.999 450.324 955.876C452.059 954.754 459.483 950.058 466.822 945.441L496.17 926.904L506.247 920.539L506.61 920.31L507.72 919.609L508.248 919.275L515.667 914.589L541.307 898.394C573.977 877.865 583.719 870.658 598.897 855.79C605.225 849.593 614.765 839.013 615.239 837.67C615.335 837.397 617.031 834.781 619.007 831.857C627.039 819.972 632.395 805.413 635.051 788.244C636.28 780.301 636.272 760.879 635.036 752.093C632.647 735.106 627.219 715.838 621.367 703.569C611.77 683.451 591.326 665.171 561.957 650.449C553.848 646.384 545.474 642.664 544.539 642.713C544.096 642.736 516.766 659.441 483.806 679.837C450.846 700.233 422.24 717.936 420.239 719.178C418.237 720.421 414.798 722.522 412.596 723.846L393.737 735.544Z" fill="url(#paint1_radial_2_17)"/>
|
||||
<path d="M0.141154 637.697L0.282367 779.752L2.12098 788.001C7.87013 813.792 17.8312 832.387 35.148 849.658C43.2933 857.782 49.5219 862.68 58.3485 867.903C77.0259 878.956 97.1276 884.409 119.146 884.399C142.207 884.387 162.156 878.635 182.713 866.07C186.182 863.95 199.775 855.581 212.919 847.472L236.817 832.729V664.186V495.643L236.81 341.457C236.805 243.089 236.625 184.67 236.314 180.087C234.354 151.286 222.309 124.809 202.055 104.782C195.839 98.6357 190.528 94.5305 174.706 83.6427C166.833 78.2244 152.421 68.2988 142.68 61.586C132.939 54.8727 116.89 43.8135 107.015 37.0094C97.1402 30.2058 83.056 20.4986 75.7167 15.4385C60.4272 4.89657 59.2306 4.16335 54.6087 2.50964C48.597 0.359048 42.2263 -0.430914 36.1695 0.223193C18.5163 2.12971 4.38462 14.8756 0.711338 32.2041C0.139722 34.9001 0.0344077 70.7794 0.027129 265.516L0.0188956 495.643H0L0.141154 637.697Z" fill="url(#paint2_linear_2_17)"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_2_17" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(654.126 722.251) rotate(-130.909) scale(529.064 380.685)">
|
||||
<stop stop-color="#00CACC"/>
|
||||
<stop offset="1" stop-color="#048FCE"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_2_17" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(88.8183 915.135) rotate(-23.1954) scale(572.26 953.69)">
|
||||
<stop stop-color="#00BBEC"/>
|
||||
<stop offset="1" stop-color="#2756A9"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint2_linear_2_17" x1="118.409" y1="0" x2="118.409" y2="884.399" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00BBEC"/>
|
||||
<stop offset="1" stop-color="#2756A9"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 4.9 KiB |
1
src/assets/chatgpt-logo.svg
Normal file
1
src/assets/chatgpt-logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.9 KiB |
7
src/background/index.ts
Normal file
7
src/background/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Browser from 'webextension-polyfill'
|
||||
|
||||
Browser.action.onClicked.addListener(() => {
|
||||
Browser.tabs.create({
|
||||
url: 'src/index.html',
|
||||
})
|
||||
})
|
7
src/base.css
Normal file
7
src/base.css
Normal file
@@ -0,0 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
font-size: 100%;
|
||||
}
|
11
src/index.html
Normal file
11
src/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>ChatHub</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./app/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
10
src/types/chat.ts
Normal file
10
src/types/chat.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface ChatMessageModel {
|
||||
id: string
|
||||
author: string
|
||||
text: string
|
||||
metadata?: unknown
|
||||
}
|
||||
|
||||
export interface ConversationModel {
|
||||
messages: ChatMessageModel[]
|
||||
}
|
1
src/types/index.ts
Normal file
1
src/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './chat'
|
3
src/utils/errors.ts
Normal file
3
src/utils/errors.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export enum ErrorCode {
|
||||
CONVERSATION_LIMIT = 'CONVERSATION_LIMIT',
|
||||
}
|
21
src/utils/fetch-sse.ts
Normal file
21
src/utils/fetch-sse.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createParser } from 'eventsource-parser'
|
||||
import { isEmpty } from 'lodash-es'
|
||||
import { streamAsyncIterable } from './stream-async-iterable'
|
||||
|
||||
export async function fetchSSE(resource: string, options: RequestInit & { onMessage: (message: string) => void }) {
|
||||
const { onMessage, ...fetchOptions } = options
|
||||
const resp = await fetch(resource, fetchOptions)
|
||||
if (!resp.ok) {
|
||||
const error = await resp.json().catch(() => ({}))
|
||||
throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)
|
||||
}
|
||||
const parser = createParser((event) => {
|
||||
if (event.type === 'event') {
|
||||
onMessage(event.data)
|
||||
}
|
||||
})
|
||||
for await (const chunk of streamAsyncIterable(resp.body!)) {
|
||||
const str = new TextDecoder().decode(chunk)
|
||||
parser.feed(str)
|
||||
}
|
||||
}
|
5
src/utils/index.ts
Normal file
5
src/utils/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
export function uuid() {
|
||||
return v4()
|
||||
}
|
14
src/utils/stream-async-iterable.ts
Normal file
14
src/utils/stream-async-iterable.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export async function* streamAsyncIterable(stream: ReadableStream) {
|
||||
const reader = stream.getReader()
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
return
|
||||
}
|
||||
yield value
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
}
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
8
tailwind.config.cjs
Normal file
8
tailwind.config.cjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{html,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
9
tsconfig.node.json
Normal file
9
tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
15
vite.config.ts
Normal file
15
vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { crx } from '@crxjs/vite-plugin'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
import manifest from './manifest.json'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths(), react(), crx({ manifest })],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: ['src/index.html'],
|
||||
},
|
||||
},
|
||||
})
|
Reference in New Issue
Block a user