feat(gpt-runner-core): add client

This commit is contained in:
JinmingYang
2023-05-15 02:06:31 +08:00
parent acb65b5677
commit 0006062984
26 changed files with 1774 additions and 191 deletions

15
.editorconfig Normal file
View File

@@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
max_line_length = 80
insert_final_newline = true
trim_trailing_whitespace = true
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@@ -3,7 +3,8 @@
"rules": {
"yml/no-empty-document": "off",
"react/no-unknown-property": "off",
"no-console": "off"
"no-console": "off",
"no-mixed-operators": "off"
},
"overrides": [
{

View File

@@ -4,7 +4,12 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": false,
"editor.formatOnSave": true,
"typescript.inlayHints.parameterNames.enabled": "all",
// "typescript.inlayHints.variableTypes.enabled": true,
// "typescript.inlayHints.propertyDeclarationTypes.enabled": true,
"typescript.inlayHints.parameterTypes.enabled": true,
// "typescript.inlayHints.functionLikeReturnTypes.enabled": true,
"cSpell.words": [
"Chatgpt",
"langchain",

View File

@@ -2,7 +2,7 @@ import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'client/src/index',
'index',
'server/src/index',
],
clean: true,

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GPT Runner</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,49 @@
import type { EventSourceMessage } from '@microsoft/fetch-event-source'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import type { ChatStreamReqParams } from '../../../server/src/controllers/chatgpt.controller'
import { getConfig } from '../../src/utils/config'
export interface fetchChatStreamReqParams extends ChatStreamReqParams {
namespace?: string
onMessage?: (ev: EventSourceMessage) => void
onError?: (error: any) => void
}
export async function fetchChatgptStream(
params: fetchChatStreamReqParams,
) {
const {
messages,
prompt,
systemPrompt,
temperature,
namespace,
onMessage = () => {},
onError = () => {},
} = params
const ctrl = new AbortController()
try {
fetchEventSource(`${getConfig()}/api/chatgpt/chat-stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'namespace': namespace || 'default-namespace',
},
body: JSON.stringify({
prompt,
messages,
systemPrompt,
temperature,
}),
signal: ctrl.signal,
onmessage: onMessage,
})
}
catch (error) {
onError(error)
}
return ctrl
}

View File

@@ -0,0 +1,20 @@
import { useEffect, useRef } from 'react'
export function useAnimationFrame(callback: (time: number) => void, deps?: any[]) {
const requestRef = useRef<number>()
const previousTimeRef = useRef<number>()
const animate = (time: number) => {
if (previousTimeRef.current !== undefined) {
const deltaTime = time - previousTimeRef.current
callback(deltaTime)
}
previousTimeRef.current = time
requestRef.current = requestAnimationFrame(animate)
}
useEffect(() => {
requestRef.current = requestAnimationFrame(animate)
return () => cancelAnimationFrame(requestRef.current!)
}, deps) // Make sure the effect runs only once
}

View File

@@ -0,0 +1,65 @@
import { EventEmitter } from 'eventemitter3'
import { useEffect, useRef } from 'react'
import type { ClientEventName } from '../../../index'
import type { MessageItemModel } from '../types/chat'
export interface ClientEventData {
[ClientEventName.SyncState]: void
[ClientEventName.SetIsReady]: boolean
[ClientEventName.SetHasSelection]: boolean
[ClientEventName.AddMessageAction]: MessageItemModel
[ClientEventName.UpdateMessageAction]: MessageItemModel
[ClientEventName.ClearMessageAction]: void
[ClientEventName.ConfirmPrompt]: string
[ClientEventName.InsertCodeSnippet]: string
}
if (!window.__emitter__)
window.__emitter__ = new EventEmitter()
export const emitter = window.__emitter__
export function useEventEmitter() {
const listenersRef = useRef<{
[event in ClientEventName]?: (...args: any[]) => void;
}>({})
const on = <T extends ClientEventName>(
event: T,
listener: (arg: ClientEventData[T]) => void,
) => {
listenersRef.current[event] = listener
emitter.on(event, listener)
}
const once = <T extends ClientEventName>(
event: T,
listener: (arg: ClientEventData[T]) => void,
) => {
listenersRef.current[event] = listener
emitter.once(event, listener)
}
const off = <T extends ClientEventName>(
event: T,
listener: (arg: ClientEventData[T]) => void,
) => {
delete listenersRef.current[event]
emitter.off(event, listener)
}
const emit = <T extends ClientEventName>(event: T, arg: ClientEventData[T] extends void ? null : ClientEventData[T]) => {
emitter.emit(event, arg)
}
useEffect(() => {
return () => {
Object.entries(listenersRef.current).forEach(([event, listener]) => {
if (listener)
emitter.off(event as ClientEventName, listener)
})
}
}, [])
return { on, once, off, emit }
}

View File

@@ -0,0 +1,17 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import { getConfig } from './utils/config'
import { GlobalStyle } from './styles/global.styles'
import { ChatPage } from './views/chat'
function App() {
const { pageName } = getConfig()
return <>
<GlobalStyle />
{pageName ? <ChatPage /> : null}
</>
}
const root = createRoot(document.getElementById('root')!)
root.render(<App />)

View File

@@ -0,0 +1,19 @@
import { createGlobalStyle } from 'styled-components'
export const GlobalStyle = createGlobalStyle`
#root {
width: 100%;
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
background: var(--background);
}
`

View File

@@ -0,0 +1,161 @@
// looks we can get value like var(--background)
// so this file is unused
function create<T>(defaultVariableName: string, backupVariableName?: string) {
return {
withDefault: (defaultValue: T) => {
const backupVariable = backupVariableName
? `var(${backupVariableName}, ${defaultValue})`
: defaultValue
return `var(--${defaultVariableName}, ${backupVariable});`
},
}
}
// see: https://github.com/microsoft/vscode-webview-ui-toolkit/blob/main/src/design-tokens.ts
/**
* Developer note:
*
* There are some tokens defined in this file that make use of `--fake-vscode-token`. This is
* done when a toolkit token should be added to the tokenMappings map (and subsequently altered
* in the applyTheme function) but does not have a corresponding VS Code token that can be used.
*
* An example is buttonIconHoverBackground token which does not have a corresponding VS Code token
* at this time (it's a hardcoded value in VS Code), but needs to be adjusted to be transparent when a
* high contrast theme is applied.
*
* As a rule of thumb, if there are special cases where a token needs to be adjusted based on the
* VS Code theme and does not have a corresponding VS Code token, `--fake-vscode-token` can be used
* to indicate that it should be added to the tokenMappings map and thus make it accessible to the
* applyTheme function where it can be dynamically adjusted.
*/
/**
* Global design tokens.
*/
export const background = create<string>('background', '--vscode-editor-background').withDefault('#1e1e1e')
export const borderWidth = create<number>('border-width').withDefault(1)
export const contrastActiveBorder = create<string>('contrast-active-border', '--vscode-contrastActiveBorder').withDefault('#f38518')
export const contrastBorder = create<string>('contrast-border', '--vscode-contrastBorder').withDefault('#6fc3df')
export const cornerRadius = create<number>('corner-radius').withDefault(0)
export const designUnit = create<number>('design-unit').withDefault(4)
export const disabledOpacity = create<number>('disabled-opacity').withDefault(0.4)
export const focusBorder = create<string>('focus-border', '--vscode-focusBorder').withDefault('#007fd4')
export const fontFamily = create<string>('font-family', '--vscode-font-family').withDefault(
'-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol',
)
export const fontWeight = create<string>('font-weight', '--vscode-font-weight').withDefault('400')
export const foreground = create<string>('foreground', '--vscode-foreground').withDefault('#cccccc')
export const inputHeight = create<string>('input-height').withDefault('26')
export const inputMinWidth = create<string>('input-min-width').withDefault('100px')
export const typeRampBaseFontSize = create<string>('type-ramp-base-font-size', '--vscode-font-size').withDefault('13px')
export const typeRampBaseLineHeight = create<string>('type-ramp-base-line-height').withDefault('normal')
export const typeRampMinus1FontSize = create<string>('type-ramp-minus1-font-size').withDefault('11px')
export const typeRampMinus1LineHeight = create<string>('type-ramp-minus1-line-height').withDefault('16px')
export const typeRampMinus2FontSize = create<string>('type-ramp-minus2-font-size').withDefault('9px')
export const typeRampMinus2LineHeight = create<string>('type-ramp-minus2-line-height').withDefault('16px')
export const typeRampPlus1FontSize = create<string>('type-ramp-plus1-font-size').withDefault('16px')
export const typeRampPlus1LineHeight = create<string>('type-ramp-plus1-line-height').withDefault('24px')
export const scrollbarWidth = create<string>('scrollbarWidth').withDefault('10px')
export const scrollbarHeight = create<string>('scrollbarHeight').withDefault('10px')
export const scrollbarSliderBackground = create<string>('scrollbar-slider-background', '--vscode-scrollbarSlider-background').withDefault('#79797966')
export const scrollbarSliderHoverBackground = create<string>('scrollbar-slider-hover-background', '--vscode-scrollbarSlider-hoverBackground').withDefault('#646464b3')
export const scrollbarSliderActiveBackground = create<string>('scrollbar-slider-active-background', '--vscode-scrollbarSlider-activeBackground').withDefault('#bfbfbf66')
/**
* Badge design tokens.
*/
export const badgeBackground = create<string>('badge-background', '--vscode-badge-background').withDefault('#4d4d4d')
export const badgeForeground = create<string>('badge-foreground', '--vscode-badge-foreground').withDefault('#ffffff')
/**
* Button design tokens.
*/
// Note: Button border is used only for high contrast themes and should be left as transparent otherwise.
export const buttonBorder = create<string>('button-border', '--vscode-button-border').withDefault('transparent')
export const buttonIconBackground = create<string>('button-icon-background').withDefault('transparent')
export const buttonIconCornerRadius = create<string>('button-icon-corner-radius').withDefault('5px')
export const buttonIconFocusBorderOffset = create<number>('button-icon-outline-offset').withDefault(0)
// Note usage of `--fake-vscode-token` (refer to doc comment at top of file for explanation).
export const buttonIconHoverBackground = create<string>('button-icon-hover-background', '--fake-vscode-token').withDefault('rgba(90, 93, 94, 0.31)')
export const buttonIconPadding = create<string>('button-icon-padding').withDefault('3px')
export const buttonPrimaryBackground = create<string>('button-primary-background', '--vscode-button-background').withDefault('#0e639c')
export const buttonPrimaryForeground = create<string>('button-primary-foreground', '--vscode-button-foreground').withDefault('#ffffff')
export const buttonPrimaryHoverBackground = create<string>('button-primary-hover-background', '--vscode-button-hoverBackground').withDefault('#1177bb')
export const buttonSecondaryBackground = create<string>('button-secondary-background', '--vscode-button-secondaryBackground').withDefault('#3a3d41')
export const buttonSecondaryForeground = create<string>('button-secondary-foreground', '--vscode-button-secondaryForeground').withDefault('#ffffff')
export const buttonSecondaryHoverBackground = create<string>('button-secondary-hover-background', '--vscode-button-secondaryHoverBackground').withDefault('#45494e')
export const buttonPaddingHorizontal = create<string>('button-padding-horizontal').withDefault('11px')
export const buttonPaddingVertical = create<string>('button-padding-vertical').withDefault('4px')
/**
* Checkbox design tokens.
*/
export const checkboxBackground = create<string>('checkbox-background', '--vscode-checkbox-background').withDefault('#3c3c3c')
export const checkboxBorder = create<string>('checkbox-border', '--vscode-checkbox-border').withDefault('#3c3c3c')
export const checkboxCornerRadius = create<number>('checkbox-corner-radius').withDefault(3)
export const checkboxForeground = create<string>('checkbox-foreground', '--vscode-checkbox-foreground').withDefault('#f0f0f0')
/**
* Data Grid design tokens
*/
export const listActiveSelectionBackground = create<string>('list-active-selection-background', '--vscode-list-activeSelectionBackground').withDefault('#094771')
export const listActiveSelectionForeground = create<string>('list-active-selection-foreground', '--vscode-list-activeSelectionForeground').withDefault('#ffffff')
export const listHoverBackground = create<string>('list-hover-background', '--vscode-list-hoverBackground').withDefault('#2a2d2e')
/**
* Divider design tokens.
*/
export const dividerBackground = create<string>('divider-background', '--vscode-settings-dropdownListBorder').withDefault('#454545')
/**
* Dropdown design tokens.
*/
export const dropdownBackground = create<string>('dropdown-background', '--vscode-dropdown-background').withDefault('#3c3c3c')
export const dropdownBorder = create<string>('dropdown-border', '--vscode-dropdown-border').withDefault('#3c3c3c')
export const dropdownForeground = create<string>('dropdown-foreground', '--vscode-dropdown-foreground').withDefault('#f0f0f0')
export const dropdownListMaxHeight = create<string>('dropdown-list-max-height').withDefault('200px')
/**
* Text Field & Area design tokens.
*/
export const inputBackground = create<string>('input-background', '--vscode-input-background').withDefault('#3c3c3c')
export const inputForeground = create<string>('input-foreground', '--vscode-input-foreground').withDefault('#cccccc')
export const inputPlaceholderForeground = create<string>('input-placeholder-foreground', '--vscode-input-placeholderForeground').withDefault('#cccccc')
/**
* Link design tokens.
*/
export const linkActiveForeground = create<string>('link-active-foreground', '--vscode-textLink-activeForeground').withDefault('#3794ff')
export const linkForeground = create<string>('link-foreground', '--vscode-textLink-foreground').withDefault('#3794ff')
/**
* Progress ring design tokens.
*/
export const progressBackground = create<string>('progress-background', '--vscode-progressBar-background').withDefault('#0e70c0')
/**
* Panels design tokens.
*/
export const panelTabActiveBorder = create<string>('panel-tab-active-border', '--vscode-panelTitle-activeBorder').withDefault('#e7e7e7')
export const panelTabActiveForeground = create<string>('panel-tab-active-foreground', '--vscode-panelTitle-activeForeground').withDefault('#e7e7e7')
export const panelTabForeground = create<string>('panel-tab-foreground', '--vscode-panelTitle-inactiveForeground').withDefault('#e7e7e799')
export const panelViewBackground = create<string>('panel-view-background', '--vscode-panel-background').withDefault('#1e1e1e')
export const panelViewBorder = create<string>('panel-view-border', '--vscode-panel-border').withDefault('#80808059')
/**
* Tag design tokens.
*/
export const tagCornerRadius = create<string>('tag-corner-radius').withDefault('2px')

View File

@@ -0,0 +1,6 @@
export interface MessageItemModel {
id: string
contents: string
isReply?: boolean
isFinished?: boolean
}

View File

@@ -0,0 +1,9 @@
import { ClientConfig } from "../../../types";
import { EventEmitter } from 'eventemitter3'
declare global {
interface Window {
__config__?: ClientConfig
__emitter__?: InstanceType<typeof EventEmitter>
}
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,12 @@
import type { ClientConfig } from '../../../types'
export function getConfig() {
const defaultConfig: ClientConfig = {
pageName: 'GPT Runner',
baseServerUrl: 'http://localhost:3003',
}
return {
...defaultConfig,
...window.__config__,
}
}

View File

@@ -0,0 +1,75 @@
import * as React from 'react'
import { useRef } from 'react'
import { useAnimationFrame } from '../../hooks/use-animation-frame.hook'
function cubicBezier(t: number) {
return 3 * (1 - t) * t ** 2 * 0.7 + t ** 3
}
export function IndeterminateProgressBar() {
const indicatorRef = useRef<HTMLDivElement>(null)
const shortLength = 20
const longLength = 50
const lengthRatio = 100 + shortLength + longLength
const duration = 2.1 * 1e3
const elapsedRef = useRef(0)
useAnimationFrame((deltaTime) => {
if (!indicatorRef.current)
return
const progress = cubicBezier(elapsedRef.current / duration)
const newLeft = (100 + lengthRatio) * progress - lengthRatio
if (progress > 1) {
indicatorRef.current.style.left = `-${lengthRatio}%`
elapsedRef.current = 0
}
else {
indicatorRef.current.style.left = `${newLeft}%`
elapsedRef.current += deltaTime
}
})
return (
<div
style={{
flex: 1,
width: '100%',
height: '2px',
position: 'relative',
}}
>
<div
ref={indicatorRef}
style={{
position: 'absolute',
width: `${lengthRatio}%`,
height: '100%',
top: 0,
left: `-${lengthRatio}%`,
}}
>
<div
style={{
position: 'absolute',
background: 'var(--vscode-progressBar-background)',
width: `${(longLength / lengthRatio) * 100}%`,
height: '100%',
}}
/>
<div
style={{
position: 'absolute',
background: 'var(--vscode-progressBar-background)',
width: `${(shortLength / lengthRatio) * 100}%`,
height: '100%',
right: 0,
}}
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,157 @@
import * as React from 'react'
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import { VSCodeButton, VSCodeTextArea } from '@vscode/webview-ui-toolkit/react'
import type { MessageItemModel } from '../../types/chat'
import { useEventEmitter } from '../../hooks/use-emitter.hook'
import { ClientEventName } from '../../../../index'
import { MessageItem } from './message-item'
function messagesWithUpdatedBotMessage(
msgs: MessageItemModel[],
updatedMsg: MessageItemModel,
): MessageItemModel[] {
return msgs.map((msg) => {
if (updatedMsg.id === msg.id)
return updatedMsg
return msg
})
}
interface UseConfirmShortcut {
label: string
keyDownHandler: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
}
function useConfirmShortcut(handler: () => void): UseConfirmShortcut {
const isMac = useMemo(() => {
const userAgentData = (window.navigator as any).userAgentData
if (userAgentData)
return userAgentData.platform === 'macOS'
return window.navigator.platform === 'MacIntel'
}, [])
return {
label: isMac ? '⌘⏎' : 'Ctrl+Enter',
keyDownHandler: useCallback(
(e) => {
if (e.key !== 'Enter')
return
const expected = isMac ? e.metaKey : e.ctrlKey
const unexpected = isMac ? e.ctrlKey : e.metaKey
if (!expected || e.altKey || e.shiftKey || unexpected)
return
handler()
},
[isMac, handler],
),
}
}
const AUTO_SCROLL_FLAG_NONE = 0
const AUTO_SCROLL_FLAG_FORCED = 1
const AUTO_SCROLL_FLAG_AUTOMATIC = 2
export function ChatPage() {
const [messages, setMessages] = useState([] as MessageItemModel[])
const [hasSelection, setHasSelection] = useState(false)
const [isReady, setIsReady] = useState(false)
const [prompt, setPrompt] = useState('')
const [autoScrollFlag, setAutoScrollFlag] = useState(AUTO_SCROLL_FLAG_NONE)
const chatListRef = useRef<HTMLDivElement>(null)
const { emit, on } = useEventEmitter()
// Dependent on `setMessages`, which will never change.
const addMessageAction = useCallback((msg: MessageItemModel) => {
setMessages((prev) => {
return [...prev, msg]
})
setAutoScrollFlag(AUTO_SCROLL_FLAG_FORCED)
}, [])
const updateMessageAction = useCallback((msg: MessageItemModel) => {
setMessages((prev) => {
return messagesWithUpdatedBotMessage(prev, msg)
})
setAutoScrollFlag(AUTO_SCROLL_FLAG_AUTOMATIC)
}, [])
const clearMessageAction = useCallback(() => {
setMessages([])
}, [])
const handleAskAction = useCallback(async () => {
emit(ClientEventName.ConfirmPrompt, prompt)
setPrompt('')
}, [prompt, setPrompt, setMessages])
const confirmShortcut = useConfirmShortcut(handleAskAction)
useLayoutEffect(() => {
if (!autoScrollFlag)
return
const chatListEl = chatListRef.current
if (!chatListEl)
return
setAutoScrollFlag(AUTO_SCROLL_FLAG_NONE)
const targetScrollTop
= chatListEl.scrollHeight - chatListEl.clientHeight
// TODO: implement `AUTO_SCROLL_FLAG_AUTOMATIC` flag.
chatListEl.scrollTop = targetScrollTop
}, [messages, autoScrollFlag, setAutoScrollFlag, chatListRef])
useEffect(() => {
on(ClientEventName.SetIsReady, setIsReady)
on(ClientEventName.SetHasSelection, setHasSelection)
on(ClientEventName.AddMessageAction, addMessageAction)
on(ClientEventName.UpdateMessageAction, updateMessageAction)
on(ClientEventName.ClearMessageAction, clearMessageAction)
emit(ClientEventName.SyncState, null)
}, [])
return (
<div className="chat-root">
<div ref={chatListRef} className="chat-list">
{messages.map((m) => {
return <MessageItem key={m.id} model={m} />
})}
</div>
<div className="chat-input-area">
<VSCodeTextArea
style={{ width: '100%' }}
rows={3}
placeholder={`Talk about the ${hasSelection ? 'selected contents' : 'whole document'
}...`}
disabled={!isReady}
value={prompt}
onInput={(_e) => {
const e = _e as React.ChangeEvent<HTMLTextAreaElement>
setPrompt(e.target.value)
}}
onKeyDown={confirmShortcut.keyDownHandler}
/>
<VSCodeButton
disabled={!isReady || prompt.length === 0}
onClick={handleAskAction}
>
{`Ask (${confirmShortcut.label})`}
</VSCodeButton>
</div>
</div>
)
}

View File

@@ -0,0 +1,54 @@
import * as React from 'react'
import { useCallback } from 'react'
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { useEventEmitter } from '../../hooks/use-emitter.hook'
import { ClientEventName } from '../../../../index'
export interface MessageCodeBlockProps {
contents: string
language: string
}
export function MessageCodeBlock(props: MessageCodeBlockProps) {
const { contents, language } = props
const { emit } = useEventEmitter()
const handleCopyAction = useCallback(() => {
navigator.clipboard.writeText(contents)
}, [contents])
const handleInsertCodeSnippetAction = useCallback(async () => {
emit(ClientEventName.InsertCodeSnippet, contents)
}, [contents])
return (
<>
<div className="chat-msg-block-toolbar">
<VSCodeButton
appearance="icon"
ariaLabel="Copy"
title="Copy"
onClick={handleCopyAction}
>
<span className="codicon codicon-copy"></span>
</VSCodeButton>
<VSCodeButton
appearance="icon"
ariaLabel="Insert or Replace"
title="Insert or Replace"
onClick={handleInsertCodeSnippetAction}
>
<span className="codicon codicon-insert"></span>
</VSCodeButton>
</div>
<SyntaxHighlighter
useInlineStyles={false}
codeTagProps={{ style: {} }}
language={language}
>
{contents}
</SyntaxHighlighter>
</>
)
}

View File

@@ -0,0 +1,59 @@
import * as React from 'react'
import ReactMarkdown from 'react-markdown'
import type { MessageItemModel } from '../../types/chat'
import { IndeterminateProgressBar } from './indeterminate-progress-bar'
import { MessageCodeBlock } from './message-code-block'
export interface MessageItemProps {
model: MessageItemModel
}
export function MessageItem(props: MessageItemProps) {
const { model } = props
const { contents, isReply, isFinished } = model
return (
<div className={`chat-msg ${isReply ? 'reply' : ''}`}>
<div className="chat-msg-contents">
<MessageTextView
contents={
contents + (isReply && !isFinished ? '\u{258A}' : '')
}
/>
</div>
{isReply && !isFinished ? <IndeterminateProgressBar /> : null}
</div>
)
}
interface MessageTextViewProps {
contents: string
}
function MessageTextView(props: MessageTextViewProps) {
const { contents } = props
return (
<ReactMarkdown
components={{
pre({ children, ...props }) {
if (children.length !== 1) {
// Not code block.
return <pre {...props}>{children}</pre>
}
const child = children[0] as React.ReactElement
const codeContents = child.props.children[0]
const codeClassName = child.props.className
const languageMatch
= /language-(\w+)/.exec(codeClassName || '') || []
return (
<MessageCodeBlock
contents={codeContents}
language={languageMatch[1] || ''}
/>
)
},
}}
>
{contents}
</ReactMarkdown>
)
}

View File

@@ -0,0 +1,12 @@
export const enum ClientEventName {
SyncState = 'sync-state',
SetIsReady = 'set-is-ready',
SetHasSelection = 'set-has-selection',
AddMessageAction = 'add-message-action',
UpdateMessageAction = 'update-message-action',
ClearMessageAction = 'clear-message-action',
ConfirmPrompt = 'confirm-prompt',
InsertCodeSnippet = 'insert-code-snippet',
}
export * from './types'

View File

@@ -35,17 +35,32 @@
"dist"
],
"scripts": {
"dev": "pnpm dev:server",
"dev": "pnpm dev:server & pnpm dev:client",
"dev:client": "vite",
"dev:server": "esno src/server/src/index.ts",
"build": "unbuild",
"stub": "unbuild --stub"
},
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
"@vscode/webview-ui-toolkit": "^1.2.2",
"clsx": "^1.2.1",
"eventemitter": "^0.3.3",
"express": "^4.18.2",
"langchain": "^0.0.75"
"langchain": "^0.0.75",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0",
"styled-components": "^6.0.0-rc.1"
},
"devDependencies": {
"@types/express": "^4.17.17",
"unconfig": "^0.3.7"
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
"@types/react-syntax-highlighter": "^15.5.6",
"@vitejs/plugin-react": "^4.0.0",
"unconfig": "^0.3.7",
"vite": "^4.3.5"
}
}

View File

@@ -8,8 +8,8 @@ import type { ControllerConfig } from './../types'
export interface ChatStreamReqParams {
messages: BaseChatMessage[]
prompt: string
systemPrompt: string
temperature: number
systemPrompt?: string
temperature?: number
}
export const chatgptControllers: ControllerConfig = {
namespacePath: '/chatgpt',
@@ -24,7 +24,7 @@ export const chatgptControllers: ControllerConfig = {
'Connection': 'keep-alive',
})
const { messages, prompt, systemPrompt, temperature } = req.body as ChatStreamReqParams
const { messages, prompt, systemPrompt, temperature = 0.7 } = req.body as ChatStreamReqParams
const sendSuccessData = (options: Omit<SuccessResponse, 'type'>) => {
return res.write(`data: ${JSON.stringify(buildSuccessResponse(options))}\n\n`)
@@ -55,6 +55,9 @@ export const chatgptControllers: ControllerConfig = {
console.log('error', error)
}
finally {
sendSuccessData({
data: '[DONE]',
})
res.end()
}
},

View File

@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
}
}

View File

@@ -0,0 +1,4 @@
export interface ClientConfig {
pageName?: string
baseServerUrl?: string
}

View File

@@ -0,0 +1,13 @@
import path from 'node:path'
import { defineConfig } from 'vite'
import React from '@vitejs/plugin-react'
const resolvePath = (...paths: string[]) => path.resolve(__dirname, ...paths)
// https://vitejs.dev/config/
export default defineConfig({
root: resolvePath('./client'),
plugins: [
React(),
],
})

1140
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff