From 441023aa4ba55a5c82accf38d5fcaa2b750478c8 Mon Sep 17 00:00:00 2001 From: yzh990918 <251205668@qq.com> Date: Sat, 6 May 2023 17:40:18 +0800 Subject: [PATCH 01/16] feat(message-settings): add copy, show raw code, delete message btns --- src/components/Main.astro | 2 +- src/components/Markdown.tsx | 5 +-- src/components/StreamableText.tsx | 2 ++ src/components/main/MessageItem.tsx | 46 ++++++++++++++++++------- src/components/ui/base/DropdownMenu.tsx | 2 ++ src/components/ui/base/Tooltip.tsx | 5 +-- src/stores/messages.ts | 13 +++++++ 7 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/components/Main.astro b/src/components/Main.astro index d027d0eb..339d62f5 100644 --- a/src/components/Main.astro +++ b/src/components/Main.astro @@ -12,7 +12,7 @@ import Conversation from './main/Conversation'
- +
diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index e8ae73be..d85abc24 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -11,6 +11,7 @@ import 'katex/dist/katex.min.css' interface Props { class?: string text: string + showRawCode?: boolean } const parseMarkdown = (raw: string) => { @@ -27,11 +28,11 @@ const parseMarkdown = (raw: string) => { } export default (props: Props) => { - const htmlString = () => parseMarkdown(props.text) + const htmlString = () => props.showRawCode ? props.text : parseMarkdown(props.text) return (
) diff --git a/src/components/StreamableText.tsx b/src/components/StreamableText.tsx index 7abd1f1d..02a4bec6 100644 --- a/src/components/StreamableText.tsx +++ b/src/components/StreamableText.tsx @@ -7,6 +7,7 @@ import Markdown from './Markdown' interface Props { class?: string text: string + showRawCode?: boolean streamInfo?: () => { conversationId: string messageId: string @@ -47,6 +48,7 @@ export default (props: Props) => { ) } diff --git a/src/components/main/MessageItem.tsx b/src/components/main/MessageItem.tsx index 7f53cab7..f3e58c16 100644 --- a/src/components/main/MessageItem.tsx +++ b/src/components/main/MessageItem.tsx @@ -1,6 +1,9 @@ import { For } from 'solid-js/web' +import { createSignal } from 'solid-js' +import { useClipboardCopy } from '@/hooks' +import { deleteMessageByConversationId } from '@/stores/messages' import StreamableText from '../StreamableText' -import { DropDownMenu, Tooltip } from '../ui/base' +import { Tooltip } from '../ui/base' import type { MenuItem } from '../ui/base' import type { MessageInstance } from '@/types/message' @@ -18,18 +21,29 @@ export default (props: Props) => { assistant: 'bg-gradient-to-b from-[#fccb90] to-[#d57eeb]', } - const handleCopyMessage = () => { + const [copied, copy] = useClipboardCopy(props.message.content) + const [showRawCode, setShowRawCode] = createSignal(false) + const handleDeleteMessageItem = () => { + deleteMessageByConversationId(props.conversationId, props.message) } - const menuList: MenuItem[] = [ - { id: 'retry', label: 'Retry message', icon: 'i-ion:refresh-outline' }, - { id: 'show', label: 'Show raw code', icon: 'i-carbon-code' }, - { id: 'share', label: 'Share message', icon: 'i-ion:ios-share-alt' }, - { id: 'edit', label: 'Edit message', icon: 'i-ion:md-create' }, - { id: 'copy', label: 'Copy message', icon: 'i-carbon-copy' }, - { id: 'delete', label: 'Delete message', icon: 'i-carbon-trash-can' }, - ] + const [menuList, setMenuList] = createSignal([ + // TODO: Retry send message + // { id: 'retry', label: 'Retry send', icon: 'i-ion:refresh-outline', role: 'all' }, + { id: 'raw', label: 'Show raw code', icon: 'i-carbon-code', role: 'system', action: () => setShowRawCode(!showRawCode()) }, + // TODO: Share message + // { id: 'share', label: 'Share message', icon: 'i-ion:ios-share-alt' }, + // TODO: Edit message + // { id: 'edit', label: 'Edit message', icon: 'i-ion:md-create', role: 'user' }, + { id: 'copy', label: 'Copy message', icon: 'i-carbon-copy', role: 'all', action: copy }, + { id: 'delete', label: 'Delete message', icon: 'i-carbon-trash-can', role: 'all', action: handleDeleteMessageItem }, + ]) + + if (props.message.role === 'user') + setMenuList(menuList().filter(item => ['all', 'user'].includes(item.role!))) + else + setMenuList(menuList().filter(item => ['all', 'system'].includes(item.role!))) return (
{
- - {item => (
)} + + {item => ( + + { + item.id === 'copy' + ?
+ :
+ } + )}
@@ -63,6 +84,7 @@ export default (props: Props) => { handleStreaming: props.handleStreaming, }) : undefined} + showRawCode={showRawCode()} />
diff --git a/src/components/ui/base/DropdownMenu.tsx b/src/components/ui/base/DropdownMenu.tsx index d99d74f5..de4ce9a1 100644 --- a/src/components/ui/base/DropdownMenu.tsx +++ b/src/components/ui/base/DropdownMenu.tsx @@ -10,6 +10,8 @@ export interface MenuItem { icon?: string // TODO: nested menu children?: MenuItem[] + role?: string + action?: (params?: any) => void } interface Props { diff --git a/src/components/ui/base/Tooltip.tsx b/src/components/ui/base/Tooltip.tsx index 20af22d0..54cff90a 100644 --- a/src/components/ui/base/Tooltip.tsx +++ b/src/components/ui/base/Tooltip.tsx @@ -9,6 +9,7 @@ interface Props { children: JSX.Element openDelay?: number closeDelay?: number + handleChildClick?: () => void placement?: 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end' | 'right-start' | 'right-end' } @@ -30,9 +31,9 @@ export const Tooltip = (props: Props) => { const resolvedChild = () => { const child = children(() => props.children) createEffect(() => { - spread(child() as Element, { ...api().triggerProps }) + spread(child() as Element, { ...api().triggerProps, onClick: props.handleChildClick }) }) - return child + return child() } return ( diff --git a/src/stores/messages.ts b/src/stores/messages.ts index b287280a..b7a22703 100644 --- a/src/stores/messages.ts +++ b/src/stores/messages.ts @@ -60,3 +60,16 @@ export const clearMessagesByConversationId = action( }) }, ) + +export const deleteMessageByConversationId = action( + conversationMessagesMap, + 'deleteMessageByConversationId', + (map, id: string, payload: MessageInstance) => { + const oldMessages = map.get()[id] || [] + map.setKey(id, [...oldMessages.filter(message => message.id !== payload.id)]) + db.setItem(id, [...oldMessages.filter(message => message.id !== payload.id)]) + updateConversationById(id, { + lastUseTime: Date.now(), + }) + }, +) From 61b6907f9e2b1f23e9d9c72c295a43c40d500895 Mon Sep 17 00:00:00 2001 From: yzh990918 <251205668@qq.com> Date: Sat, 6 May 2023 17:58:16 +0800 Subject: [PATCH 02/16] chore: update --- src/components/main/MessageItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/main/MessageItem.tsx b/src/components/main/MessageItem.tsx index f3e58c16..f4825bcf 100644 --- a/src/components/main/MessageItem.tsx +++ b/src/components/main/MessageItem.tsx @@ -30,12 +30,12 @@ export default (props: Props) => { const [menuList, setMenuList] = createSignal([ // TODO: Retry send message - // { id: 'retry', label: 'Retry send', icon: 'i-ion:refresh-outline', role: 'all' }, + { id: 'retry', label: 'Retry send', icon: 'i-ion:refresh-outline', role: 'all' }, { id: 'raw', label: 'Show raw code', icon: 'i-carbon-code', role: 'system', action: () => setShowRawCode(!showRawCode()) }, // TODO: Share message // { id: 'share', label: 'Share message', icon: 'i-ion:ios-share-alt' }, // TODO: Edit message - // { id: 'edit', label: 'Edit message', icon: 'i-ion:md-create', role: 'user' }, + { id: 'edit', label: 'Edit message', icon: 'i-ion:md-create', role: 'user' }, { id: 'copy', label: 'Copy message', icon: 'i-carbon-copy', role: 'all', action: copy }, { id: 'delete', label: 'Delete message', icon: 'i-carbon-trash-can', role: 'all', action: handleDeleteMessageItem }, ]) From 897b159035479bd38849394c760607f086a8bd80 Mon Sep 17 00:00:00 2001 From: Evan Yang <251205668@qq.com> Date: Sat, 6 May 2023 23:23:36 +0800 Subject: [PATCH 03/16] chore: update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30c09064..2fa52974 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ Anse is a fully optimized UI for AI Chats. -- [Visit anse.app](https://anse.app) -- [Documentation](https://docs.anse.app) +- 🍿 **Live preview**: https://anse.app +- 📖 **Documentation**: https://docs.anse.app +- ✨ **Release Notes**: https://github.com/anse-app/anse/releases ## Features From b9a071bc3927eed9f95be980920014094af9fad6 Mon Sep 17 00:00:00 2001 From: yzh990918 <251205668@qq.com> Date: Mon, 8 May 2023 17:05:10 +0800 Subject: [PATCH 04/16] feat: Improve the logical part of message-settings --- src/components/Send.tsx | 8 +- src/components/main/MessageItem.tsx | 127 ++++++++++++++++++------ src/components/ui/base/DropdownMenu.tsx | 9 +- src/logics/conversation.ts | 21 ++-- src/stores/messages.ts | 28 ++++++ src/stores/settings.ts | 3 +- src/types/provider.ts | 2 +- 7 files changed, 149 insertions(+), 49 deletions(-) diff --git a/src/components/Send.tsx b/src/components/Send.tsx index b2fd9c67..b84bc7b8 100644 --- a/src/components/Send.tsx +++ b/src/components/Send.tsx @@ -5,6 +5,7 @@ import { currentErrorMessage, isSendBoxFocus, scrollController } from '@/stores/ import { addConversation, conversationMap, currentConversationId } from '@/stores/conversation' import { loadingStateMap, streamsMap } from '@/stores/streams' import { handlePrompt } from '@/logics/conversation' +import { globalAbortController } from '@/stores/settings' export default () => { let inputRef: HTMLTextAreaElement @@ -14,7 +15,7 @@ export default () => { const $currentErrorMessage = useStore(currentErrorMessage) const $streamsMap = useStore(streamsMap) const $loadingStateMap = useStore(loadingStateMap) - const [controller, setController] = createSignal() + const $globalAbortController = useStore(globalAbortController) const [inputPrompt, setInputPrompt] = createSignal('') const isEditing = () => inputPrompt() || $isSendBoxFocus() @@ -96,12 +97,11 @@ export default () => { const clearPrompt = () => { setInputPrompt('') - inputRef.value = '' isSendBoxFocus.set(false) } const handleAbortFetch = () => { - controller()!.abort() + $globalAbortController()?.abort() clearPrompt() } @@ -124,7 +124,7 @@ export default () => { addConversation() const controller = new AbortController() - setController(controller) + globalAbortController.set(controller) handlePrompt(currentConversation(), inputRef.value, controller.signal) clearPrompt() scrollController().scrollToBottom() diff --git a/src/components/main/MessageItem.tsx b/src/components/main/MessageItem.tsx index f4825bcf..4e4bbe6c 100644 --- a/src/components/main/MessageItem.tsx +++ b/src/components/main/MessageItem.tsx @@ -1,9 +1,14 @@ -import { For } from 'solid-js/web' +import { For, Show } from 'solid-js/web' import { createSignal } from 'solid-js' +import { useStore } from '@nanostores/solid' import { useClipboardCopy } from '@/hooks' -import { deleteMessageByConversationId } from '@/stores/messages' +import { deleteMessageByConversationId, spliceMessageByConversationId, spliceUpdateMessageByConversationId } from '@/stores/messages' +import { conversationMap } from '@/stores/conversation' +import { handlePrompt } from '@/logics/conversation' +import { scrollController } from '@/stores/ui' +import { globalAbortController } from '@/stores/settings' import StreamableText from '../StreamableText' -import { Tooltip } from '../ui/base' +import { DropDownMenu, Tooltip } from '../ui/base' import type { MenuItem } from '../ui/base' import type { MessageInstance } from '@/types/message' @@ -15,28 +20,65 @@ interface Props { } export default (props: Props) => { - const roleClass = { - system: 'bg-gradient-to-b from-gray-300 via-gray-200 to-gray-300', - user: 'bg-gradient-to-b from-gray-300 via-gray-200 to-gray-300', - assistant: 'bg-gradient-to-b from-[#fccb90] to-[#d57eeb]', - } + const $conversationMap = useStore(conversationMap) - const [copied, copy] = useClipboardCopy(props.message.content) const [showRawCode, setShowRawCode] = createSignal(false) + const [copied, setCopied] = createSignal(false) + const [isEditing, setIsEditing] = createSignal(false) + let inputRef: HTMLTextAreaElement + const [inputPrompt, setInputPrompt] = createSignal(props.message.content) + + const currentConversation = () => { + return $conversationMap()[props.conversationId] + } + const handleCopyMessageItem = () => { + const [Iscopied, copy] = useClipboardCopy(props.message.content) + copy() + setCopied(Iscopied()) + setTimeout(() => setCopied(false), 1000) + } const handleDeleteMessageItem = () => { deleteMessageByConversationId(props.conversationId, props.message) } + const handleRetryMessageItem = () => { + const controller = new AbortController() + globalAbortController.set(controller) + spliceMessageByConversationId(props.conversationId, props.message) + handlePrompt(currentConversation(), '', controller.signal) + // TODO: scrollController seems not working + scrollController().scrollToBottom() + } + + const handleEditMessageItem = () => { + setIsEditing(true) + inputRef.focus() + } + + const handleSend = () => { + if (!inputRef.value) + return + const controller = new AbortController() + const currentMessage: MessageInstance = { + ...props.message, + content: inputPrompt(), + } + + globalAbortController.set(controller) + spliceUpdateMessageByConversationId(props.conversationId, currentMessage) + setIsEditing(false) + handlePrompt(currentConversation(), '', controller.signal) + scrollController().scrollToBottom() + } + const [menuList, setMenuList] = createSignal([ - // TODO: Retry send message - { id: 'retry', label: 'Retry send', icon: 'i-ion:refresh-outline', role: 'all' }, + { id: 'retry', label: 'Retry send', icon: 'i-ion:refresh-outline', role: 'all', action: handleRetryMessageItem }, { id: 'raw', label: 'Show raw code', icon: 'i-carbon-code', role: 'system', action: () => setShowRawCode(!showRawCode()) }, // TODO: Share message // { id: 'share', label: 'Share message', icon: 'i-ion:ios-share-alt' }, - // TODO: Edit message - { id: 'edit', label: 'Edit message', icon: 'i-ion:md-create', role: 'user' }, - { id: 'copy', label: 'Copy message', icon: 'i-carbon-copy', role: 'all', action: copy }, + { id: 'edit', label: 'Edit message', icon: 'i-ion:md-create', role: 'user', action: handleEditMessageItem }, + { id: 'copy', label: 'Copy message', icon: 'i-carbon-copy', role: 'all', action: handleCopyMessageItem }, { id: 'delete', label: 'Delete message', icon: 'i-carbon-trash-can', role: 'all', action: handleDeleteMessageItem }, ]) @@ -45,6 +87,12 @@ export default (props: Props) => { else setMenuList(menuList().filter(item => ['all', 'system'].includes(item.role!))) + const roleClass = { + system: 'bg-gradient-to-b from-gray-300 via-gray-200 to-gray-300', + user: 'bg-gradient-to-b from-gray-300 via-gray-200 to-gray-300', + assistant: 'bg-gradient-to-b from-[#fccb90] to-[#d57eeb]', + } + return (
{ >
- {/* TODO: MessageItem options menu */} -
- {/* +
+
- */} +
-
+
{item => ( @@ -75,17 +122,37 @@ export default (props: Props) => {
- ({ - conversationId: props.conversationId, - messageId: props.message.id || '', - handleStreaming: props.handleStreaming, - }) - : undefined} - showRawCode={showRawCode()} - /> + +