import { Index, Show, createEffect, createSignal, onCleanup, onMount } from 'solid-js' import { useThrottleFn } from 'solidjs-use' import { generateSignature } from '@/utils/auth' import IconClear from './icons/Clear' import MessageItem from './MessageItem' import SystemRoleSettings from './SystemRoleSettings' import ErrorMessageItem from './ErrorMessageItem' import type { ChatMessage, ErrorMessage } from '@/types' export default () => { let inputRef: HTMLTextAreaElement const [currentSystemRoleSettings, setCurrentSystemRoleSettings] = createSignal('') const [systemRoleEditing, setSystemRoleEditing] = createSignal(false) const [messageList, setMessageList] = createSignal([]) const [currentError, setCurrentError] = createSignal() const [currentAssistantMessage, setCurrentAssistantMessage] = createSignal('') const [loading, setLoading] = createSignal(false) const [controller, setController] = createSignal(null) const [isStick, setStick] = createSignal(false) createEffect(() => (isStick() && smoothToBottom())) onMount(() => { let lastPostion = window.scrollY window.addEventListener('scroll', () => { const nowPostion = window.scrollY nowPostion < lastPostion && setStick(false) lastPostion = nowPostion }) try { if (sessionStorage.getItem('messageList')) setMessageList(JSON.parse(sessionStorage.getItem('messageList'))) if (sessionStorage.getItem('systemRoleSettings')) setCurrentSystemRoleSettings(sessionStorage.getItem('systemRoleSettings')) if (localStorage.getItem('stickToBottom') === 'stick') setStick(true) } catch (err) { console.error(err) } window.addEventListener('beforeunload', handleBeforeUnload) onCleanup(() => { window.removeEventListener('beforeunload', handleBeforeUnload) }) }) const handleBeforeUnload = () => { sessionStorage.setItem('messageList', JSON.stringify(messageList())) sessionStorage.setItem('systemRoleSettings', currentSystemRoleSettings()) isStick() ? localStorage.setItem('stickToBottom', 'stick') : localStorage.removeItem('stickToBottom') } const handleButtonClick = async() => { const inputValue = inputRef.value if (!inputValue) return inputRef.value = '' setMessageList([ ...messageList(), { role: 'user', content: inputValue, }, ]) requestWithLatestMessage() instantToBottom() } const smoothToBottom = useThrottleFn(() => { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) }, 300, false, true) const instantToBottom = () => { window.scrollTo({ top: document.body.scrollHeight, behavior: 'instant' }) } const requestWithLatestMessage = async() => { setLoading(true) setCurrentAssistantMessage('') setCurrentError(null) const storagePassword = localStorage.getItem('pass') try { const controller = new AbortController() setController(controller) const requestMessageList = [...messageList()] if (currentSystemRoleSettings()) { requestMessageList.unshift({ role: 'system', content: currentSystemRoleSettings(), }) } const timestamp = Date.now() const response = await fetch('/api/generate', { method: 'POST', body: JSON.stringify({ messages: requestMessageList, time: timestamp, pass: storagePassword, sign: await generateSignature({ t: timestamp, m: requestMessageList?.[requestMessageList.length - 1]?.content || '', }), }), signal: controller.signal, }) if (!response.ok) { const error = await response.json() console.error(error.error) setCurrentError(error.error) throw new Error('Request failed') } 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) { const char = decoder.decode(value) if (char === '\n' && currentAssistantMessage().endsWith('\n')) continue if (char) setCurrentAssistantMessage(currentAssistantMessage() + char) isStick() && instantToBottom() } done = readerDone } } catch (e) { console.error(e) setLoading(false) setController(null) return } archiveCurrentMessage() isStick() && instantToBottom() } const archiveCurrentMessage = () => { if (currentAssistantMessage()) { setMessageList([ ...messageList(), { role: 'assistant', content: currentAssistantMessage(), }, ]) setCurrentAssistantMessage('') setLoading(false) setController(null) // Disable auto-focus on touch devices if (!('ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0)) inputRef.focus() } } const clear = () => { inputRef.value = '' inputRef.style.height = 'auto' setMessageList([]) setCurrentAssistantMessage('') setCurrentError(null) } const stopStreamFetch = () => { if (controller()) { controller().abort() archiveCurrentMessage() } } const retryLastFetch = () => { if (messageList().length > 0) { const lastMessage = messageList()[messageList().length - 1] if (lastMessage.role === 'assistant') setMessageList(messageList().slice(0, -1)) requestWithLatestMessage() } } const handleKeydown = (e: KeyboardEvent) => { if (e.isComposing || e.shiftKey) return if (e.keyCode === 13) { e.preventDefault() handleButtonClick() } } return (
messageList().length === 0} systemRoleEditing={systemRoleEditing} setSystemRoleEditing={setSystemRoleEditing} currentSystemRoleSettings={currentSystemRoleSettings} setCurrentSystemRoleSettings={setCurrentSystemRoleSettings} /> {(message, index) => ( (message().role === 'assistant' && index === messageList().length - 1)} onRetry={retryLastFetch} /> )} {currentAssistantMessage() && ( )} { currentError() && } (
AI is thinking...
Stop
)} >