220 lines
7.0 KiB
TypeScript
220 lines
7.0 KiB
TypeScript
import { Match, Switch, createSignal, onMount } from 'solid-js'
|
|
import { useStore } from '@nanostores/solid'
|
|
import { createShortcut } from '@solid-primitives/keyboard'
|
|
import { currentErrorMessage, isSendBoxFocus, scrollController } from '@/stores/ui'
|
|
import { addConversation, conversationMap, currentConversationId } from '@/stores/conversation'
|
|
import { loadingStateMap, streamsMap } from '@/stores/streams'
|
|
import { handlePrompt } from '@/logics/conversation'
|
|
import { globalAbortController } from '@/stores/settings'
|
|
import { useI18n, useMobileScreen } from '@/hooks'
|
|
import Button from './ui/Button'
|
|
import { fetchData } from '../http/api'
|
|
|
|
export default () => {
|
|
const { t } = useI18n()
|
|
let inputRef: HTMLTextAreaElement
|
|
const $conversationMap = useStore(conversationMap)
|
|
const $currentConversationId = useStore(currentConversationId)
|
|
const $isSendBoxFocus = useStore(isSendBoxFocus)
|
|
const $currentErrorMessage = useStore(currentErrorMessage)
|
|
const $streamsMap = useStore(streamsMap)
|
|
const $loadingStateMap = useStore(loadingStateMap)
|
|
const $globalAbortController = useStore(globalAbortController)
|
|
|
|
const [inputPrompt, setInputPrompt] = createSignal('')
|
|
const [footerClass, setFooterClass] = createSignal('')
|
|
const isEditing = () => inputPrompt() || $isSendBoxFocus()
|
|
const currentConversation = () => {
|
|
return $conversationMap()[$currentConversationId()]
|
|
}
|
|
const isStreaming = () => !!$streamsMap()[$currentConversationId()]
|
|
const isLoading = () => !!$loadingStateMap()[$currentConversationId()]
|
|
|
|
onMount(() => {
|
|
|
|
fetchData({}, function(data) {
|
|
if(data.code==201 || data.code==401){
|
|
currentErrorMessage.set(data)
|
|
return ;
|
|
}
|
|
|
|
// 登录后的一些操作
|
|
|
|
}, '/chatgptApi', 'POST');
|
|
|
|
|
|
createShortcut(['Control', 'Enter'], () => {
|
|
$isSendBoxFocus() && handleSend()
|
|
})
|
|
|
|
useMobileScreen(() => {
|
|
setFooterClass('sticky bottom-0 left-0 right-0 overflow-hidden')
|
|
})
|
|
})
|
|
|
|
const stateType = () => {
|
|
if ($currentErrorMessage())
|
|
return $currentErrorMessage().code==401 ? 'login' : 'error'
|
|
else if (isLoading() || isStreaming())
|
|
return 'loading'
|
|
else if (isEditing())
|
|
return 'editing'
|
|
else
|
|
return 'normal'
|
|
}
|
|
|
|
const EmptyState = () => (
|
|
<div
|
|
class="max-w-base h-full fi flex-row gap-2"
|
|
onClick={() => {
|
|
isSendBoxFocus.set(true)
|
|
inputRef.focus()
|
|
}}
|
|
>
|
|
<div class="flex-1 op-30 text-sm">{t('send.placeholder')}</div>
|
|
</div>
|
|
)
|
|
|
|
const EditState = () => (
|
|
<div class="h-full flex flex-col">
|
|
<div class="flex-1 relative">
|
|
<textarea
|
|
ref={inputRef!}
|
|
placeholder={t('send.placeholder')}
|
|
autocomplete="off"
|
|
onBlur={() => { isSendBoxFocus.set(false) }}
|
|
onInput={() => { setInputPrompt(inputRef.value) }}
|
|
onKeyDown={(e) => {
|
|
e.key === 'Enter' && !e.isComposing && !e.shiftKey && handleSend()
|
|
}}
|
|
class="h-full w-full absolute inset-0 py-4 px-[calc(max(1.5rem,(100%-48rem)/2))] scroll-pa-4 input-base text-sm"
|
|
/>
|
|
</div>
|
|
<div class="fi justify-between gap-2 h-14 px-[calc(max(1.5rem,(100%-48rem)/2)-0.5rem)] border-t border-base">
|
|
<div>
|
|
{/*<Button
|
|
icon="i-carbon-plug"
|
|
onClick={() => {}}
|
|
/>*/}
|
|
</div>
|
|
<Button
|
|
icon="i-carbon-send"
|
|
onClick={handleSend}
|
|
variant={inputPrompt() ? 'primary' : 'normal'}
|
|
prefix={t('send.button')}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
|
|
const LoginState = () => (
|
|
<div class="max-w-base h-full flex items-end flex-col justify-between gap-8 sm:(flex-row items-center) py-4 text-error text-sm" style="padding: 1rem;">
|
|
<div class="flex-1 w-full">
|
|
<div class="fi gap-0.5 mb-1">
|
|
<span i-carbon-warning />
|
|
<span class="font-semibold">{$currentErrorMessage()?.code}</span>
|
|
</div>
|
|
<div>{$currentErrorMessage()?.message}</div>
|
|
</div>
|
|
<div class="border border-error px-2 py-1 rounded-md hv-base hover:bg-white" onClick={() => { window.location.href = $currentErrorMessage()?.url }} >
|
|
登录
|
|
</div>
|
|
</div>
|
|
)
|
|
|
|
const ErrorState = () => (
|
|
<div class="max-w-base h-full flex items-end flex-col justify-between gap-8 sm:(flex-row items-center) py-4 text-error text-sm" style="padding: 1rem;">
|
|
<div class="flex-1 w-full">
|
|
<div class="fi gap-0.5 mb-1">
|
|
<span i-carbon-warning />
|
|
<span class="font-semibold">{$currentErrorMessage()?.code}</span>
|
|
</div>
|
|
<div>{$currentErrorMessage()?.message}</div>
|
|
</div>
|
|
<div class="border border-error px-2 py-1 rounded-md hv-base hover:bg-white" onClick={() => { currentErrorMessage.set(null) }} >
|
|
Dismiss
|
|
</div>
|
|
</div>
|
|
)
|
|
|
|
const clearPrompt = () => {
|
|
setInputPrompt('')
|
|
isSendBoxFocus.set(false)
|
|
}
|
|
|
|
const handleAbortFetch = () => {
|
|
$globalAbortController()?.abort()
|
|
clearPrompt()
|
|
}
|
|
|
|
const LoadingState = () => (
|
|
<div class="max-w-base h-full fi flex-row gap-2">
|
|
<div class="flex-1 op-50">Thinking...</div>
|
|
<div
|
|
class="border border-base-100 px-2 py-1 rounded-md text-sm op-40 hv-base hover:bg-white"
|
|
onClick={() => { handleAbortFetch() }}
|
|
>
|
|
Abort
|
|
</div>
|
|
</div>
|
|
)
|
|
|
|
const handleSend = () => {
|
|
if (!inputRef.value)
|
|
return
|
|
if (!currentConversation())
|
|
addConversation()
|
|
|
|
const controller = new AbortController()
|
|
globalAbortController.set(controller)
|
|
handlePrompt(currentConversation(), inputRef.value, controller.signal)
|
|
clearPrompt()
|
|
scrollController().scrollToBottom()
|
|
}
|
|
|
|
const stateRootClass = () => {
|
|
if (stateType() === 'normal')
|
|
return 'hv-base'
|
|
else if (stateType() === 'error')
|
|
return 'bg-red/8'
|
|
else if (stateType() === 'loading')
|
|
return 'loading-anim bg-base-100'
|
|
else if (stateType() === 'editing')
|
|
return 'bg-base-100'
|
|
return ''
|
|
}
|
|
|
|
const stateHeightClass = () => {
|
|
if (stateType() === 'normal')
|
|
return 'px-6 h-14'
|
|
else if (stateType() === 'error')
|
|
return 'px-6'
|
|
else if (stateType() === 'loading')
|
|
return 'px-6 h-14'
|
|
else if (stateType() === 'editing')
|
|
return 'h-54'
|
|
return ''
|
|
}
|
|
|
|
return (
|
|
<div id='infoSend' class={`relative shrink-0 border-t border-base pb-[env(safe-area-inset-bottom)] transition transition-colors duration-300 ${stateRootClass()} ${footerClass()}`}>
|
|
<div class={`relative transition transition-height duration-240 ${stateHeightClass()}`}>
|
|
<Switch fallback={<EmptyState />}>
|
|
<Match when={stateType() === 'login'}>
|
|
<LoginState />
|
|
</Match>
|
|
<Match when={stateType() === 'error'}>
|
|
<ErrorState />
|
|
</Match>
|
|
<Match when={stateType() === 'loading'}>
|
|
<LoadingState />
|
|
</Match>
|
|
<Match when={stateType() === 'editing'}>
|
|
<EditState />
|
|
</Match>
|
|
</Switch>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|