Skip to content

Commit

Permalink
feat: stop & regenerate latest answers
Browse files Browse the repository at this point in the history
  • Loading branch information
ddiu8081 committed Mar 4, 2023
1 parent e40b1d0 commit 4f9ee75
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 74 deletions.
164 changes: 97 additions & 67 deletions src/components/Generator.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createSignal, For, Show } from 'solid-js'
import { createSignal, For, Show, Index } from 'solid-js'
import MessageItem from './MessageItem'
import IconClear from './icons/Clear'
import type { ChatMessage } from '@/types'

export default () => {
let inputRef: HTMLTextAreaElement
const [messageList, setMessageList] = createSignal<ChatMessage[]>([])
const [messageList, setMessageList] = createSignal<ChatMessage[]>([{"role":"user","content":"你好!"},{"role":"assistant","content":"\n\n你好!我是AI助手,有什么可以帮助你的吗?"},{"role":"user","content":"给我讲一个故事"},{"role":"assistant","content":"好的,这是一个叫做《勇士和怪兽》的故事:\n\n在一个遥远的国度里,有一个勇士,他的名字叫做杰克。他是这个国度最勇敢的战士,因为他曾经打败了一只凶猛的巨龙。杰克有一个朋友叫做汤姆,他是一个年轻的牧羊人。\n\n有一天,这个国度遭到了一只可怕的怪兽的攻击,这只怪兽是一个巨大的狮子。它每天晚上都会来到村庄,抓走一些羊和小孩。人们都非常害怕,因为他们不知道该怎么办。\n\n杰克听到了这个消息,他知道他必须要做些什么来帮助这个国度。他决定去打败这只怪兽,但他也知道这不是一件容易的事情。\n\n汤姆知道杰克的计划,他决定和杰克一起去。他知道他不能让杰克一个人去面对这个怪兽。于是,杰克和汤姆一起出发了。\n\n他们走了很长时间,终于来到了怪兽的巢穴。他们看到了那只巨大的狮子,它看起来非常凶猛。\n\n杰克和汤姆决定一起攻击怪兽。他们使用了各种武器,但是怪兽非常强大,他们一直无法打败它。他们都感到非常沮丧,但是他们不想放弃。\n\n最终,他们想到了一个计划。他们决定将怪兽引诱到一个陷阱里,然后用巨石砸死它。这个计划非常危险,但他们决定还是要试一试。\n\n他们开始引诱怪兽,怪兽跟着他们走,最终掉进了陷阱里。杰克和汤姆立刻用巨石砸向怪兽,最终成功地打败了它。\n\n人们听到了这个消息,他们非常高兴。他们感谢杰克和汤姆,因为他们的勇气和智慧,拯救了整个国度。\n\n故事告诉我们勇气和智慧的重要性,只有勇敢地面对困难,才能获得胜利。"}])
const [currentAssistantMessage, setCurrentAssistantMessage] = createSignal('')
const [loading, setLoading] = createSignal(false)
const [controller, setController] = createSignal<AbortController>(null)
Expand All @@ -15,7 +15,6 @@ export default () => {
if (!inputValue) {
return
}
setLoading(true)
// @ts-ignore
if (window?.umami) umami.trackEvent('chat_generate')
inputRef.value = ''
Expand All @@ -26,73 +25,70 @@ export default () => {
content: inputValue,
},
])
requestWithLatestMessage()
}

for (;;) {
try {
const controller = new AbortController()
setController(controller)
const response = await fetch('/api/generate', {
method: 'POST',
body: JSON.stringify({
messages: messageList(),
}),
signal: controller.signal,
})
if (!response.ok) {
throw new Error(response.statusText)
}
const data = response.body
if (!data) {
throw new Error('No data')
}
const reader = data.getReader()
const decoder = new TextDecoder('utf-8')
let done = false
const requestWithLatestMessage = async () => {
setLoading(true)
setCurrentAssistantMessage('')
try {
const controller = new AbortController()
setController(controller)
const response = await fetch('/api/generate', {
method: 'POST',
body: JSON.stringify({
messages: messageList(),
}),
signal: controller.signal,
})
if (!response.ok) {
throw new Error(response.statusText)
}
const data = response.body
if (!data) {
throw new Error('No data')
}
const reader = data.getReader()
const decoder = new TextDecoder('utf-8')
let done = false

while (!done) {
const { value, done: readerDone } = await reader.read()
if (value) {
let char = decoder.decode(value)
if (char === '\n' && currentAssistantMessage().endsWith('\n')) {
continue
}
if (char) {
setCurrentAssistantMessage(currentAssistantMessage() + char)
}
window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'})
while (!done) {
const { value, done: readerDone } = await reader.read()
if (value) {
let char = decoder.decode(value)
if (char === '\n' && currentAssistantMessage().endsWith('\n')) {
continue
}
done = readerDone
}
} catch (e) {
console.error(e)

// Retry when the request is aborted by user.
if (e.name === 'AbortError') {
setCurrentAssistantMessage('')
continue
if (char) {
setCurrentAssistantMessage(currentAssistantMessage() + char)
}
window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'})
}

// Keep the assistant message when there is an error.
// This enables the user to retry.
setLoading(false)
setController(null)
return
done = readerDone
}

break
} catch (e) {
console.error(e)
setLoading(false)
setController(null)
return
}
archiveCurrentMessage()
}

setMessageList([
...messageList(),
{
role: 'assistant',
content: currentAssistantMessage(),
},
])
setCurrentAssistantMessage('')
setLoading(false)
setController(null)
inputRef.focus()
const archiveCurrentMessage = () => {
if (currentAssistantMessage()) {
setMessageList([
...messageList(),
{
role: 'assistant',
content: currentAssistantMessage(),
},
])
setCurrentAssistantMessage('')
setLoading(false)
setController(null)
inputRef.focus()
}
}

const clear = () => {
Expand All @@ -101,9 +97,21 @@ export default () => {
setCurrentAssistantMessage('')
}

const retry = () => {
const stopStreamFetch = () => {
if (controller()) {
controller().abort()
archiveCurrentMessage()
}
}

const retryLastFetch = () => {
if (messageList().length > 0) {
const lastMessage = messageList()[messageList().length - 1]
console.log(lastMessage)
if (lastMessage.role === 'assistant') {
setMessageList(messageList().slice(0, -1))
requestWithLatestMessage()
}
}
}

Expand All @@ -118,9 +126,31 @@ export default () => {

return (
<div my-6>
<For each={messageList()}>{(message) => <MessageItem role={message.role} message={message.content} />}</For>
{currentAssistantMessage() && <MessageItem role="assistant" message={currentAssistantMessage} onRetry={retry} />}
<Show when={!loading()} fallback={() => <div class="h-12 my-4 flex items-center justify-center bg-slate bg-op-15 text-slate rounded-sm">AI is thinking...</div>}>
<Index each={messageList()}>
{(message, index) => (
<MessageItem
role={message().role}
message={message().content}
showRetry={() => (message().role === 'assistant' && index === messageList().length - 1)}
onRetry={retryLastFetch}
/>
)}
</Index>
{currentAssistantMessage() && (
<MessageItem
role="assistant"
message={currentAssistantMessage}
/>
)}
<Show
when={!loading()}
fallback={() => (
<div class="h-12 my-4 flex gap-4 items-center justify-center bg-slate bg-op-15 text-slate rounded-sm">
<span>AI is thinking...</span>
<div class="px-2 py-0.5 border border-slate text-slate rounded-md text-sm op-70 cursor-pointer hover:bg-slate/10" onClick={stopStreamFetch}>Stop</div>
</div>
)}
>
<div class="my-4 flex items-center gap-2">
<textarea
ref={inputRef!}
Expand Down
21 changes: 14 additions & 7 deletions src/components/MessageItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import MarkdownIt from 'markdown-it'
// @ts-ignore
import mdKatex from 'markdown-it-katex'
import mdHighlight from 'markdown-it-highlightjs'
import IconRefresh from './icons/Refresh'

interface Props {
role: ChatMessage['role']
message: Accessor<string> | string
showRetry?: Accessor<boolean>
onRetry?: () => void
}

export default ({ role, message, onRetry }: Props) => {
export default ({ role, message, showRetry, onRetry }: Props) => {
const roleClass = {
system: 'bg-gradient-to-r from-gray-300 via-gray-200 to-gray-300',
user: 'bg-gradient-to-r from-purple-400 to-yellow-400',
Expand All @@ -28,12 +30,17 @@ export default ({ role, message, onRetry }: Props) => {
return ''
}
return (
<div class="flex py-2 gap-3 -mx-4 px-4 rounded-lg transition-colors md:hover:bg-slate/3 position-relative" class:op-75={role === 'user'}>
<div class={`shrink-0 w-7 h-7 mt-4 rounded-full op-80 ${roleClass[role]}`}></div>
<div class="message prose text-slate break-words overflow-hidden" innerHTML={htmlString()} />
{onRetry && (
<div class="position-absolute text-black px-6 right-0 top-0 bg-slate cursor-pointer" onClick={onRetry}>
Retry
<div class="py-2 -mx-4 px-4 transition-colors md:hover:bg-slate/3">
<div class="flex gap-3 rounded-lg" class:op-75={role === 'user'}>
<div class={`shrink-0 w-7 h-7 mt-4 rounded-full op-80 ${roleClass[role]}`}></div>
<div class="message prose text-slate break-words overflow-hidden" innerHTML={htmlString()} />
</div>
{ showRetry?.() && onRetry && (
<div class="flex items-center justify-end px-3 mb-2">
<div onClick={onRetry} class="flex items-center gap-1 px-2 py-0.5 op-70 border border-slate text-slate rounded-md text-sm cursor-pointer hover:bg-slate/10">
<IconRefresh />
<span>Regenerate</span>
</div>
</div>
)}
</div>
Expand Down
5 changes: 5 additions & 0 deletions src/components/icons/Refresh.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default () => {
return (
<svg xmlns="http:https://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><path d="M25.95 7.65l.005-.004c-.092-.11-.197-.206-.293-.312c-.184-.205-.367-.41-.563-.603c-.139-.136-.286-.262-.43-.391c-.183-.165-.366-.329-.558-.482c-.16-.128-.325-.247-.49-.367c-.192-.14-.385-.277-.585-.406a13.513 13.513 0 0 0-.533-.324q-.308-.179-.625-.341c-.184-.094-.37-.185-.56-.27c-.222-.1-.449-.191-.678-.28c-.19-.072-.378-.145-.571-.208c-.246-.082-.498-.15-.75-.217c-.186-.049-.368-.102-.556-.143c-.29-.063-.587-.107-.883-.15c-.16-.023-.315-.056-.476-.073A12.933 12.933 0 0 0 6 7.703V4H4v8h8v-2H6.811A10.961 10.961 0 0 1 16 5a11.111 11.111 0 0 1 1.189.067c.136.015.268.042.403.061c.25.037.501.075.746.128c.16.035.315.08.472.121c.213.057.425.114.633.183c.164.054.325.116.486.178c.193.074.384.15.57.235c.162.072.32.15.477.23q.268.136.526.286c.153.09.305.18.453.276c.168.11.33.224.492.342c.14.102.282.203.417.312c.162.13.316.268.47.406c.123.11.248.217.365.332c.167.164.323.338.479.512A10.993 10.993 0 1 1 5 16H3a13 13 0 1 0 22.95-8.35z" fill="currentColor"/></svg>
)
}

0 comments on commit 4f9ee75

Please sign in to comment.