Skip to content

Commit b9a071b

Browse files
committed
feat: Improve the logical part of message-settings
1 parent 61b6907 commit b9a071b

File tree

7 files changed

+149
-49
lines changed

7 files changed

+149
-49
lines changed

src/components/Send.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { currentErrorMessage, isSendBoxFocus, scrollController } from '@/stores/
55
import { addConversation, conversationMap, currentConversationId } from '@/stores/conversation'
66
import { loadingStateMap, streamsMap } from '@/stores/streams'
77
import { handlePrompt } from '@/logics/conversation'
8+
import { globalAbortController } from '@/stores/settings'
89

910
export default () => {
1011
let inputRef: HTMLTextAreaElement
@@ -14,7 +15,7 @@ export default () => {
1415
const $currentErrorMessage = useStore(currentErrorMessage)
1516
const $streamsMap = useStore(streamsMap)
1617
const $loadingStateMap = useStore(loadingStateMap)
17-
const [controller, setController] = createSignal<AbortController>()
18+
const $globalAbortController = useStore(globalAbortController)
1819

1920
const [inputPrompt, setInputPrompt] = createSignal('')
2021
const isEditing = () => inputPrompt() || $isSendBoxFocus()
@@ -96,12 +97,11 @@ export default () => {
9697

9798
const clearPrompt = () => {
9899
setInputPrompt('')
99-
inputRef.value = ''
100100
isSendBoxFocus.set(false)
101101
}
102102

103103
const handleAbortFetch = () => {
104-
controller()!.abort()
104+
$globalAbortController()?.abort()
105105
clearPrompt()
106106
}
107107

@@ -124,7 +124,7 @@ export default () => {
124124
addConversation()
125125

126126
const controller = new AbortController()
127-
setController(controller)
127+
globalAbortController.set(controller)
128128
handlePrompt(currentConversation(), inputRef.value, controller.signal)
129129
clearPrompt()
130130
scrollController().scrollToBottom()

src/components/main/MessageItem.tsx

Lines changed: 97 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import { For } from 'solid-js/web'
1+
import { For, Show } from 'solid-js/web'
22
import { createSignal } from 'solid-js'
3+
import { useStore } from '@nanostores/solid'
34
import { useClipboardCopy } from '@/hooks'
4-
import { deleteMessageByConversationId } from '@/stores/messages'
5+
import { deleteMessageByConversationId, spliceMessageByConversationId, spliceUpdateMessageByConversationId } from '@/stores/messages'
6+
import { conversationMap } from '@/stores/conversation'
7+
import { handlePrompt } from '@/logics/conversation'
8+
import { scrollController } from '@/stores/ui'
9+
import { globalAbortController } from '@/stores/settings'
510
import StreamableText from '../StreamableText'
6-
import { Tooltip } from '../ui/base'
11+
import { DropDownMenu, Tooltip } from '../ui/base'
712
import type { MenuItem } from '../ui/base'
813
import type { MessageInstance } from '@/types/message'
914

@@ -15,28 +20,65 @@ interface Props {
1520
}
1621

1722
export default (props: Props) => {
18-
const roleClass = {
19-
system: 'bg-gradient-to-b from-gray-300 via-gray-200 to-gray-300',
20-
user: 'bg-gradient-to-b from-gray-300 via-gray-200 to-gray-300',
21-
assistant: 'bg-gradient-to-b from-[#fccb90] to-[#d57eeb]',
22-
}
23+
const $conversationMap = useStore(conversationMap)
2324

24-
const [copied, copy] = useClipboardCopy(props.message.content)
2525
const [showRawCode, setShowRawCode] = createSignal(false)
26+
const [copied, setCopied] = createSignal(false)
27+
const [isEditing, setIsEditing] = createSignal(false)
28+
let inputRef: HTMLTextAreaElement
29+
const [inputPrompt, setInputPrompt] = createSignal(props.message.content)
30+
31+
const currentConversation = () => {
32+
return $conversationMap()[props.conversationId]
33+
}
2634

35+
const handleCopyMessageItem = () => {
36+
const [Iscopied, copy] = useClipboardCopy(props.message.content)
37+
copy()
38+
setCopied(Iscopied())
39+
setTimeout(() => setCopied(false), 1000)
40+
}
2741
const handleDeleteMessageItem = () => {
2842
deleteMessageByConversationId(props.conversationId, props.message)
2943
}
3044

45+
const handleRetryMessageItem = () => {
46+
const controller = new AbortController()
47+
globalAbortController.set(controller)
48+
spliceMessageByConversationId(props.conversationId, props.message)
49+
handlePrompt(currentConversation(), '', controller.signal)
50+
// TODO: scrollController seems not working
51+
scrollController().scrollToBottom()
52+
}
53+
54+
const handleEditMessageItem = () => {
55+
setIsEditing(true)
56+
inputRef.focus()
57+
}
58+
59+
const handleSend = () => {
60+
if (!inputRef.value)
61+
return
62+
const controller = new AbortController()
63+
const currentMessage: MessageInstance = {
64+
...props.message,
65+
content: inputPrompt(),
66+
}
67+
68+
globalAbortController.set(controller)
69+
spliceUpdateMessageByConversationId(props.conversationId, currentMessage)
70+
setIsEditing(false)
71+
handlePrompt(currentConversation(), '', controller.signal)
72+
scrollController().scrollToBottom()
73+
}
74+
3175
const [menuList, setMenuList] = createSignal<MenuItem[]>([
32-
// TODO: Retry send message
33-
{ id: 'retry', label: 'Retry send', icon: 'i-ion:refresh-outline', role: 'all' },
76+
{ id: 'retry', label: 'Retry send', icon: 'i-ion:refresh-outline', role: 'all', action: handleRetryMessageItem },
3477
{ id: 'raw', label: 'Show raw code', icon: 'i-carbon-code', role: 'system', action: () => setShowRawCode(!showRawCode()) },
3578
// TODO: Share message
3679
// { id: 'share', label: 'Share message', icon: 'i-ion:ios-share-alt' },
37-
// TODO: Edit message
38-
{ id: 'edit', label: 'Edit message', icon: 'i-ion:md-create', role: 'user' },
39-
{ id: 'copy', label: 'Copy message', icon: 'i-carbon-copy', role: 'all', action: copy },
80+
{ id: 'edit', label: 'Edit message', icon: 'i-ion:md-create', role: 'user', action: handleEditMessageItem },
81+
{ id: 'copy', label: 'Copy message', icon: 'i-carbon-copy', role: 'all', action: handleCopyMessageItem },
4082
{ id: 'delete', label: 'Delete message', icon: 'i-carbon-trash-can', role: 'all', action: handleDeleteMessageItem },
4183
])
4284

@@ -45,6 +87,12 @@ export default (props: Props) => {
4587
else
4688
setMenuList(menuList().filter(item => ['all', 'system'].includes(item.role!)))
4789

90+
const roleClass = {
91+
system: 'bg-gradient-to-b from-gray-300 via-gray-200 to-gray-300',
92+
user: 'bg-gradient-to-b from-gray-300 via-gray-200 to-gray-300',
93+
assistant: 'bg-gradient-to-b from-[#fccb90] to-[#d57eeb]',
94+
}
95+
4896
return (
4997
<div
5098
class="p-6 break-words group relative"
@@ -54,13 +102,12 @@ export default (props: Props) => {
54102
>
55103
<div class="max-w-base flex gap-4 overflow-hidden">
56104
<div class={`shrink-0 w-7 h-7 rounded-md op-80 ${roleClass[props.message.role]}`} />
57-
{/* TODO: MessageItem options menu */}
58-
<div class="sm:hidden block absolute bottom-2 right-2 z-10 op-70 cursor-pointer">
59-
{/* <DropDownMenu menuList={menuList}>
105+
<div class={`sm:hidden block absolute bottom-2 right-4 z-10 op-70 cursor-pointer ${isEditing() && '!hidden'}`}>
106+
<DropDownMenu menuList={menuList()}>
60107
<div class="text-xl i-carbon:overflow-menu-horizontal" />
61-
</DropDownMenu> */}
108+
</DropDownMenu>
62109
</div>
63-
<div class={`hidden sm:block absolute right-6 -top-4 ${!props.index && 'top-0'}`}>
110+
<div class={`hidden sm:block absolute right-6 -top-4 ${!props.index && 'top-0'} ${isEditing() && '!hidden'}`}>
64111
<div class="op-0 group-hover:op-80 fcc space-x-2 !bg-base px-4 py-1 rounded-xl b border-base transition-opacity duration-400">
65112
<For each={menuList()}>
66113
{item => (
@@ -75,17 +122,37 @@ export default (props: Props) => {
75122
</div>
76123
</div>
77124
<div class="flex-1 min-w-0">
78-
<StreamableText
79-
text={props.message.content}
80-
streamInfo={props.message.stream
81-
? () => ({
82-
conversationId: props.conversationId,
83-
messageId: props.message.id || '',
84-
handleStreaming: props.handleStreaming,
85-
})
86-
: undefined}
87-
showRawCode={showRawCode()}
88-
/>
125+
<Show when={isEditing()} >
126+
<textarea
127+
ref={inputRef!}
128+
value={inputPrompt()}
129+
autocomplete="off"
130+
onInput={() => { setInputPrompt(inputRef.value) }}
131+
onKeyDown={(e) => {
132+
e.key === 'Enter' && !e.isComposing && !e.shiftKey && handleSend()
133+
}}
134+
class="op-70 bg-darker py-4 px-[calc(max(1.5rem,(100%-48rem)/2))] w-full inset-0 scroll-pa-4 input-base rounded-md"
135+
/>
136+
137+
<div class="flex justify-end space-x-2 -mt-1">
138+
<div class="inline-flex items-center button" onClick={() => setIsEditing(false)}>Cancel</div>
139+
<div class="inline-flex items-center button" onClick={() => handleSend()}>Submit</div>
140+
</div>
141+
</Show>
142+
<Show when={!isEditing()}>
143+
<StreamableText
144+
text={props.message.content}
145+
streamInfo={props.message.stream
146+
? () => ({
147+
conversationId: props.conversationId,
148+
messageId: props.message.id || '',
149+
handleStreaming: props.handleStreaming,
150+
})
151+
: undefined}
152+
showRawCode={showRawCode()}
153+
/>
154+
</Show>
155+
89156
</div>
90157

91158
</div>

src/components/ui/base/DropdownMenu.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export interface MenuItem {
88
id: string
99
label: string | JSXElement
1010
icon?: string
11-
// TODO: nested menu
1211
children?: MenuItem[]
1312
role?: string
1413
action?: (params?: any) => void
@@ -24,7 +23,11 @@ export const DropDownMenu = (props: Props) => {
2423
menu.machine({
2524
id: createUniqueId(),
2625
onSelect(details) {
27-
console.log(details)
26+
if (details.value) {
27+
const currentAction = props.menuList.find(item => item.id === details.value)?.action
28+
if (typeof currentAction === 'function')
29+
currentAction()
30+
}
2831
},
2932
}),
3033
)
@@ -45,7 +48,7 @@ export const DropDownMenu = (props: Props) => {
4548
})
4649

4750
return (
48-
<div>
51+
<div class="!outline-none">
4952
<Dynamic component={resolvedChild} />
5053
<Show when={api().isOpen}>
5154
<Portal>

src/logics/conversation.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { HandlerPayload, PromptResponse } from '@/types/provider'
1010
import type { Conversation } from '@/types/conversation'
1111
import type { ErrorMessage, Message } from '@/types/message'
1212

13-
export const handlePrompt = async(conversation: Conversation, prompt: string, signal?: AbortSignal) => {
13+
export const handlePrompt = async(conversation: Conversation, prompt?: string, signal?: AbortSignal) => {
1414
const generalSettings = getGeneralSettings()
1515
const bot = getBotMetaById(conversation.bot)
1616
const [providerId, botId] = conversation.bot.split(':')
@@ -22,13 +22,14 @@ export const handlePrompt = async(conversation: Conversation, prompt: string, si
2222

2323
if (bot.type !== 'chat_continuous')
2424
clearMessagesByConversationId(conversation.id)
25-
26-
pushMessageByConversationId(conversation.id, {
27-
id: `${conversation.id}:user:${Date.now()}`,
28-
role: 'user',
29-
content: prompt,
30-
dateTime: new Date().getTime(),
31-
})
25+
if (prompt) {
26+
pushMessageByConversationId(conversation.id, {
27+
id: `${conversation.id}:user:${Date.now()}`,
28+
role: 'user',
29+
content: prompt,
30+
dateTime: new Date().getTime(),
31+
})
32+
}
3233

3334
setLoadingStateByConversationId(conversation.id, true)
3435
let providerResponse: PromptResponse
@@ -85,7 +86,7 @@ export const handlePrompt = async(conversation: Conversation, prompt: string, si
8586

8687
// Update conversation title
8788
if (providerResponse && bot.type === 'chat_continuous' && !conversation.name) {
88-
const inputText = conversation.systemInfo || prompt
89+
const inputText = conversation.systemInfo || prompt!
8990
const rapidPayload = generateRapidProviderPayload(promptHelper.summarizeText(inputText), provider.id)
9091
const generatedTitle = await getProviderResponse(provider.id, rapidPayload).catch(() => {}) as string || inputText
9192
updateConversationById(conversation.id, {
@@ -128,7 +129,7 @@ export const callProviderHandler = async(providerId: string, payload: HandlerPay
128129

129130
let response: PromptResponse
130131
if (payload.botId === 'temp')
131-
response = await provider.handleRapidPrompt?.(payload.prompt, payload.globalSettings)
132+
response = await provider.handleRapidPrompt?.(payload.prompt!, payload.globalSettings)
132133
else
133134
response = await provider.handlePrompt?.(payload, signal)
134135

src/stores/messages.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,31 @@ export const deleteMessageByConversationId = action(
7373
})
7474
},
7575
)
76+
77+
export const spliceMessageByConversationId = action(
78+
conversationMessagesMap,
79+
'spliceMessagesByConversationId',
80+
(map, id: string, payload: MessageInstance) => {
81+
const oldMessages = map.get()[id] || []
82+
const currentIndex = oldMessages.findIndex(message => message.id === payload.id)
83+
map.setKey(id, [...oldMessages.slice(0, currentIndex + 1)])
84+
db.setItem(id, [...oldMessages.slice(0, currentIndex + 1)])
85+
updateConversationById(id, {
86+
lastUseTime: Date.now(),
87+
})
88+
},
89+
)
90+
91+
export const spliceUpdateMessageByConversationId = action(
92+
conversationMessagesMap,
93+
'spliceMessagesByConversationId',
94+
(map, id: string, payload: MessageInstance) => {
95+
const oldMessages = map.get()[id] || []
96+
const currentIndex = oldMessages.findIndex(message => message.id === payload.id)
97+
map.setKey(id, [...oldMessages.slice(0, currentIndex), payload])
98+
db.setItem(id, [...oldMessages.slice(0, currentIndex), payload])
99+
updateConversationById(id, {
100+
lastUseTime: Date.now(),
101+
})
102+
},
103+
)

src/stores/settings.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { action, map } from 'nanostores'
1+
import { action, atom, map } from 'nanostores'
22
import { db } from './storage/settings'
33
import { getProviderById, providerMetaList } from './provider'
44
import type { SettingsPayload } from '@/types/provider'
55
import type { GeneralSettings } from '@/types/app'
66

77
export const providerSettingsMap = map<Record<string, SettingsPayload>>({})
8+
export const globalAbortController = atom<AbortController | null>(null)
89

910
export const rebuildSettingsStore = async() => {
1011
const exportData = await db.exportData()

src/types/provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export interface HandlerPayload {
3434
botId: string
3535
globalSettings: SettingsPayload
3636
botSettings: SettingsPayload
37-
prompt: string
37+
prompt?: string
3838
messages: Message[]
3939
}
4040

0 commit comments

Comments
 (0)