diff --git a/src/app/bots/abstract-bot.ts b/src/app/bots/abstract-bot.ts index d9e51d41..2c5a404b 100644 --- a/src/app/bots/abstract-bot.ts +++ b/src/app/bots/abstract-bot.ts @@ -1,3 +1,4 @@ +import { Prompt } from '~services/prompts' import { ChatError, ErrorCode } from '~utils/errors' export type Event = @@ -17,6 +18,7 @@ export type Event = export interface SendMessageParams { prompt: string + role: Prompt['role'] onEvent: (event: Event) => void signal?: AbortSignal } diff --git a/src/app/bots/chatgpt-api/index.ts b/src/app/bots/chatgpt-api/index.ts index 7604ab3d..85999073 100644 --- a/src/app/bots/chatgpt-api/index.ts +++ b/src/app/bots/chatgpt-api/index.ts @@ -16,7 +16,13 @@ export class ChatGPTApiBot extends AbstractBot { private conversationContext?: ConversationContext buildMessages(): ChatMessage[] { - return [SYSTEM_MESSAGE, ...this.conversationContext!.messages.slice(-(CONTEXT_SIZE + 1))] + let systemMessage = SYSTEM_MESSAGE + let otherMessages = this.conversationContext!.messages + if (this.conversationContext!.messages[0].role === 'system') { + systemMessage = this.conversationContext!.messages[0] + otherMessages = this.conversationContext!.messages.slice(1) + } + return [systemMessage, ...otherMessages.slice(-(CONTEXT_SIZE + 1))] } async doSendMessage(params: SendMessageParams) { @@ -27,7 +33,7 @@ export class ChatGPTApiBot extends AbstractBot { if (!this.conversationContext) { this.conversationContext = { messages: [] } } - this.conversationContext.messages.push({ role: 'user', content: params.prompt }) + this.conversationContext.messages.push({ role: params.role, content: params.prompt }) const resp = await fetch(`${openaiApiHost}/v1/chat/completions`, { method: 'POST', diff --git a/src/app/components/Chat/ChatMessageInput.tsx b/src/app/components/Chat/ChatMessageInput.tsx index 825dc73e..bcd58ede 100644 --- a/src/app/components/Chat/ChatMessageInput.tsx +++ b/src/app/components/Chat/ChatMessageInput.tsx @@ -2,13 +2,19 @@ import cx from 'classnames' import { FC, memo, ReactNode, useCallback, useEffect, useRef, useState } from 'react' import { GoBook } from 'react-icons/go' import { trackEvent } from '~app/plausible' +import { Prompt } from '~services/prompts' import Button from '../Button' import PromptLibraryDialog from '../PromptLibrary/Dialog' import TextInput from './TextInput' +export interface Message { + text: string + role: Prompt['role'] +} + interface Props { mode: 'full' | 'compact' - onSubmit: (value: string) => void + onSubmit: (message: Message) => void className?: string disabled?: boolean placeholder?: string @@ -21,6 +27,7 @@ const ChatMessageInput: FC = (props) => { const formRef = useRef(null) const inputRef = useRef(null) const [isPromptLibraryDialogOpen, setIsPromptLibraryDialogOpen] = useState(false) + const role = useRef('user') useEffect(() => { if (!props.disabled && props.autoFocus) { @@ -32,19 +39,20 @@ const ChatMessageInput: FC = (props) => { (e: React.FormEvent) => { e.preventDefault() if (value.trim()) { - props.onSubmit(value) + props.onSubmit({ text: value, role: role.current }) } setValue('') }, [props, value], ) - const insertTextAtCursor = useCallback( - (text: string) => { + const insertPromptAtCursor = useCallback( + (prompt: Prompt) => { const cursorPosition = inputRef.current?.selectionStart || 0 const textBeforeCursor = value.slice(0, cursorPosition) const textAfterCursor = value.slice(cursorPosition) - setValue(`${textBeforeCursor}${text}${textAfterCursor}`) + setValue(`${textBeforeCursor}${prompt.prompt}${textAfterCursor}`) + role.current = prompt.role setIsPromptLibraryDialogOpen(false) inputRef.current?.focus() }, @@ -65,7 +73,7 @@ const ChatMessageInput: FC = (props) => { setIsPromptLibraryDialogOpen(false)} - insertPrompt={insertTextAtCursor} + insertPrompt={insertPromptAtCursor} /> )} diff --git a/src/app/components/Chat/ConversationPanel.tsx b/src/app/components/Chat/ConversationPanel.tsx index 4b6eb3f8..f5787772 100644 --- a/src/app/components/Chat/ConversationPanel.tsx +++ b/src/app/components/Chat/ConversationPanel.tsx @@ -1,25 +1,25 @@ import cx from 'classnames' import { FC, useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' import clearIcon from '~/assets/icons/clear.svg' import historyIcon from '~/assets/icons/history.svg' import shareIcon from '~/assets/icons/share.svg' import { CHATBOTS } from '~app/consts' import { ConversationContext, ConversationContextValue } from '~app/context' import { trackEvent } from '~app/plausible' -import ShareDialog from '../Share/Dialog' import { ChatMessageModel } from '~types' import { BotId } from '../../bots' import Button from '../Button' import HistoryDialog from '../History/Dialog' +import ShareDialog from '../Share/Dialog' import SwitchBotDropdown from '../SwitchBotDropdown' -import ChatMessageInput from './ChatMessageInput' +import ChatMessageInput, { Message } from './ChatMessageInput' import ChatMessageList from './ChatMessageList' -import { useTranslation } from 'react-i18next' interface Props { botId: BotId messages: ChatMessageModel[] - onUserSendMessage: (input: string, botId: BotId) => void + onUserSendMessage: (message: Message, botId: BotId) => void resetConversation: () => void generating: boolean stopGenerating: () => void @@ -42,8 +42,8 @@ const ConversationPanel: FC = (props) => { }, [props.resetConversation]) const onSubmit = useCallback( - async (input: string) => { - props.onUserSendMessage(input as string, props.botId) + async (message: Message) => { + props.onUserSendMessage(message, props.botId) }, [props], ) diff --git a/src/app/components/PromptLibrary/Dialog.tsx b/src/app/components/PromptLibrary/Dialog.tsx index 7b488423..56492500 100644 --- a/src/app/components/PromptLibrary/Dialog.tsx +++ b/src/app/components/PromptLibrary/Dialog.tsx @@ -1,10 +1,11 @@ import PromptLibrary from './Library' import Dialog from '../Dialog' +import { Prompt } from '~services/prompts' interface Props { isOpen: boolean onClose: () => void - insertPrompt: (text: string) => void + insertPrompt: (prompt: Prompt) => void } const PromptLibraryDialog = (props: Props) => { diff --git a/src/app/components/PromptLibrary/Library.tsx b/src/app/components/PromptLibrary/Library.tsx index 241ed666..4e5eb522 100644 --- a/src/app/components/PromptLibrary/Library.tsx +++ b/src/app/components/PromptLibrary/Library.tsx @@ -1,13 +1,15 @@ -import { Suspense, useCallback, useMemo, useState } from 'react' +import { Suspense, useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { BeatLoader } from 'react-spinners' import useSWR from 'swr' import closeIcon from '~/assets/icons/close.svg' +import { ChatMessage } from '~app/bots/chatgpt-api/consts' import { trackEvent } from '~app/plausible' import { Prompt, loadLocalPrompts, loadRemotePrompts, removeLocalPrompt, saveLocalPrompt } from '~services/prompts' import { uuid } from '~utils' import Button from '../Button' import { Input, Textarea } from '../Input' +import Select from '../Select' import Tabs, { Tab } from '../Tabs' const ActionButton = (props: { text: string; onClick: () => void }) => { @@ -23,11 +25,11 @@ const ActionButton = (props: { text: string; onClick: () => void }) => { const PromptItem = (props: { title: string - prompt: string + prompt: Prompt edit?: () => void remove?: () => void copyToLocal?: () => void - insertPrompt: (text: string) => void + insertPrompt: (prompt: Prompt) => void }) => { const { t } = useTranslation() const [saved, setSaved] = useState(false) @@ -42,7 +44,12 @@ const PromptItem = (props: {

{props.title}

-
+
+ {props.prompt.role === 'system' && ( +
+ $ +
+ )} {props.edit && } {props.copyToLocal && } props.insertPrompt(props.prompt)} /> @@ -58,8 +65,14 @@ const PromptItem = (props: { ) } +const PROMPT_ROLE_OPTIONS = [ + { value: 'user', name: 'User' }, + { value: 'system', name: 'System' }, +] + function PromptForm(props: { initialData: Prompt; onSubmit: (data: Prompt) => void }) { const { t } = useTranslation() + const roleRef = useRef(null) const onSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault() @@ -71,6 +84,7 @@ function PromptForm(props: { initialData: Prompt; onSubmit: (data: Prompt) => vo id: props.initialData.id, title: json.title as string, prompt: json.prompt as string, + role: json.role as ChatMessage['role'], }) } }, @@ -82,6 +96,17 @@ function PromptForm(props: { initialData: Prompt; onSubmit: (data: Prompt) => vo Prompt {t('Title')}
+
+ + Prompt {t('Role')} ({t('PromptRoleWarning')}) + + + +
Prompt {t('Content')}