no message

This commit is contained in:
小陌 2023-07-19 23:39:43 +08:00
parent ff13a53022
commit cd29687ce0
16 changed files with 432 additions and 17 deletions

View File

@ -27,15 +27,20 @@
"@solid-primitives/scheduled": "^1.3.2", "@solid-primitives/scheduled": "^1.3.2",
"@solid-primitives/scroll": "^2.0.14", "@solid-primitives/scroll": "^2.0.14",
"@unocss/reset": "^0.50.6", "@unocss/reset": "^0.50.6",
"@zag-js/dialog": "^0.9.2",
"@zag-js/menu": "^0.9.2", "@zag-js/checkbox": "^0.10.3",
"@zag-js/select": "^0.9.2", "@zag-js/dialog": "^0.10.3",
"@zag-js/slider": "^0.9.2", "@zag-js/menu": "^0.10.3",
"@zag-js/solid": "^0.9.2", "@zag-js/select": "^0.10.3",
"@zag-js/switch": "^0.9.2", "@zag-js/slider": "^0.10.3",
"@zag-js/toast": "^0.9.2", "@zag-js/solid": "^0.10.3",
"@zag-js/toggle": "^0.9.2", "@zag-js/switch": "^0.10.3",
"@zag-js/tooltip": "^0.9.2", "@zag-js/tabs": "^0.10.3",
"@zag-js/toast": "^0.10.3",
"@zag-js/toggle": "^0.10.3",
"@zag-js/tooltip": "^0.10.3",
"astro": "^2.2.0", "astro": "^2.2.0",
"bumpp": "^9.1.0", "bumpp": "^9.1.0",
"destr": "^1.2.2", "destr": "^1.2.2",
@ -69,6 +74,7 @@
"eslint-plugin-astro": "^0.24.0", "eslint-plugin-astro": "^0.24.0",
"lint-staged": "^13.2.2", "lint-staged": "^13.2.2",
"punycode": "^2.3.0", "punycode": "^2.3.0",
"html2canvas": "^1.4.1",
"simple-git-hooks": "^2.8.1", "simple-git-hooks": "^2.8.1",
"unocss": "^0.50.6", "unocss": "^0.50.6",
"vite-plugin-pwa": "^0.14.7" "vite-plugin-pwa": "^0.14.7"

View File

@ -2,12 +2,16 @@ import {
showConversationEditModal, showConversationEditModal,
showConversationSidebar, showConversationSidebar,
showEmojiPickerModal, showEmojiPickerModal,
showSelectMessageModal,
showSettingsSidebar, showSettingsSidebar,
showShareModal,
} from '@/stores/ui' } from '@/stores/ui'
import ConversationSidebar from './conversations/ConversationSidebar' import ConversationSidebar from './conversations/ConversationSidebar'
import SettingsSidebar from './settings/SettingsSidebar' import SettingsSidebar from './settings/SettingsSidebar'
import ConversationEditModal from './conversations/ConversationEditModal' import ConversationEditModal from './conversations/ConversationEditModal'
import EmojiPickerModal from './ui/EmojiPickerModal' import EmojiPickerModal from './ui/EmojiPickerModal'
import ShareModal from './ui/ShareModal'
import SelectMessageModal from './ui/SelectMessageModal'
import Modal from './ui/Modal' import Modal from './ui/Modal'
export default () => { export default () => {
@ -33,6 +37,16 @@ export default () => {
<EmojiPickerModal /> <EmojiPickerModal />
</div> </div>
</Modal> </Modal>
<Modal bindValue={showShareModal} direction="bottom" closeBtnClass="hidden">
<div class="max-h-[70vh] w-full">
<ShareModal />
</div>
</Modal>
<Modal bindValue={showSelectMessageModal} direction="bottom" closeBtnClass="top-4 right-4" closeCallback={() => { showShareModal.set(true) }}>
<div class="max-h-[70vh] w-full">
<SelectMessageModal />
</div>
</Modal>
</> </>
) )
} }

View File

@ -38,7 +38,7 @@ export default () => {
<> <>
{ $currentConversationId() && ( { $currentConversationId() && (
<div class="fcc p-2 rounded-md text-xl hv-foreground" onClick={() => { handleShareMessage(true) }} > <div class="fcc p-2 rounded-md text-xl hv-foreground" onClick={() => { handleShareMessage(true) }} >
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:1rem;height:1rem" class="w-3 h-3">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z" />
</svg> </svg>
</div> </div>

View File

@ -0,0 +1,23 @@
import { useStore } from '@nanostores/solid'
import { showConversationEditModal } from '@/stores/ui'
import { currentConversationId } from '@/stores/conversation'
export default () => {
// Retrieve the current conversation ID from the store
const $currentConversationId = useStore(currentConversationId)
return (
<>
{/* Render the following code if the current conversation ID exists */}
{$currentConversationId() && (
<div
class="fcc p-2 rounded-md text-xl hv-foreground"
onClick={() => { showConversationEditModal.set(true) }}
>
{/* Render the carbon settings adjust icon */}
<div i-carbon-settings-adjust />
</div>
)}
</>
)
}

View File

@ -0,0 +1,29 @@
import { useStore } from '@nanostores/solid'
import { currentConversationId } from '@/stores/conversation'
import { showShareModal } from '@/stores/ui'
import { getMessagesByConversationId, updateMessage } from '@/stores/messages'
export default () => {
const $currentConversationId = useStore(currentConversationId)
const handleShareContext = () => {
const messages = getMessagesByConversationId($currentConversationId())
messages.forEach((message) => {
updateMessage($currentConversationId(), message.id, { isSelected: true },
)
})
showShareModal.set(true)
}
return (
<>
{$currentConversationId() && (
<div
class="fcc p-2 rounded-md text-xl hv-foreground"
onClick={() => { handleShareContext() }}
>
<div i-carbon-export />
</div>
)}
</>
)
}

View File

@ -4,6 +4,8 @@ import { useLargeScreen } from '@/hooks'
import ConversationHeaderInfo from './ConversationHeaderInfo' import ConversationHeaderInfo from './ConversationHeaderInfo'
import ConversationMessageClearButton from './ConversationMessageClearButton' import ConversationMessageClearButton from './ConversationMessageClearButton'
import ConversationHeaderShare from './ConversationHeaderShare' import ConversationHeaderShare from './ConversationHeaderShare'
import ConversationMessageShareButton from './ConversationMessageShareButton'
import ConversationMessageSettingButton from './ConversationMessageSettingButton'
export default () => { export default () => {
onMount(() => { onMount(() => {
@ -26,7 +28,9 @@ export default () => {
</div> </div>
<div class="fi gap-1 overflow-hidden"> <div class="fi gap-1 overflow-hidden">
<ConversationHeaderShare /> <ConversationHeaderShare />
<ConversationMessageSettingButton />
<ConversationMessageClearButton /> <ConversationMessageClearButton />
{/*<ConversationMessageShareButton />*/}
<div <div
class="fcc p-2 rounded-md text-xl hv-foreground lg:hidden" class="fcc p-2 rounded-md text-xl hv-foreground lg:hidden"
onClick={() => showSettingsSidebar.set(true)} onClick={() => showSettingsSidebar.set(true)}

View File

@ -0,0 +1,63 @@
import { useStore } from '@nanostores/solid'
import { For, createSignal } from 'solid-js'
import { useI18n } from '@/hooks'
import { currentConversationId } from '@/stores/conversation'
import { getMessagesByConversationId, updateMessage } from '@/stores/messages'
import { showSelectMessageModal, showShareModal } from '@/stores/ui'
import { Checkbox } from '../ui/base'
export default () => {
const { t } = useI18n()
const $currentConversationId = useStore(currentConversationId)
const messages = getMessagesByConversationId($currentConversationId())
const [checkAll, setCheckAll] = createSignal(messages.every(item => item.isSelected))
const [selectedMessages, setSelectedMessages] = createSignal(messages)
const handleToggleMessages = (id: string) => {
messages.forEach((item) => {
if (item.id === id)
item.isSelected = !item.isSelected
})
setSelectedMessages(messages)
}
const handleSelectAll = () => {
messages.forEach((item) => {
item.isSelected = !checkAll()
})
setSelectedMessages(messages)
setCheckAll(!checkAll())
console.log(selectedMessages(), checkAll())
}
const handleSaveContext = () => {
messages.forEach((item) => {
updateMessage($currentConversationId(), item.id, { isSelected: item.isSelected })
})
showSelectMessageModal.set(false)
showShareModal.set(true)
}
return (
<div class="w-full">
<div class="fi px-6 py-4 border-base b-b-1">
<div class="text-base">{t('conversations.share.messages.title')}</div>
</div>
<div class="flex flex-col p-6 h-100 overflow-auto relative">
{/* <div class="border border-base b-b-0 last:b-b-1 p-4 hv-base">
<Checkbox setValue={() => handleSelectAll()} initValue={checkAll()} label={`${t('conversations.share.messages.selectAll')}`} />
</div> */}
<For each={selectedMessages()}>
{(item) => {
return (
<div class="border border-base b-b-0 last:b-b-1 p-4 hv-base">
<Checkbox setValue={() => handleToggleMessages(item.id)} initValue={item.isSelected} label={`${item.role}: ${item.content}`} />
</div>
)
}}
</For>
</div>
<div class="fcc px-2 py-2 bg-darker border border-base hv-base hover:border-base-100" onClick={() => handleSaveContext()}>{t('settings.save')}</div>
</div>
)
}

View File

@ -0,0 +1,120 @@
import { useStore } from '@nanostores/solid'
import { For, Show, createSignal } from 'solid-js'
import html2canvas from 'html2canvas'
import { useClipboardCopy, useI18n } from '@/hooks'
import { currentConversationId } from '@/stores/conversation'
import { getMessagesByConversationId } from '@/stores/messages'
import { showSelectMessageModal, showShareModal } from '@/stores/ui'
import { Tabs } from '../ui/base'
import type { TabItem } from './base/Tabs'
export default () => {
const { t } = useI18n()
const $currentConversationId = useStore(currentConversationId)
const messages = getMessagesByConversationId($currentConversationId()).filter(item => item.isSelected)
const [imageUrl, setImageUrl] = createSignal('')
const [imageBuffer, setImageBuffer] = createSignal<Blob>()
const [loading, setLoading] = createSignal(false)
const [copied, copy] = useClipboardCopy(messages.map(item => `${item.role}: ${item.content}`).join('\n'))
const copyImage = () => {
const [,copy] = useClipboardCopy(imageBuffer()!)
copy()
}
const handleLoadImage = async() => {
setLoading(true)
try {
const messageWrapper = document.getElementById('message_list_wrapper') as HTMLDivElement
messageWrapper.style.display = 'block'
const canvas = await html2canvas(messageWrapper, {
useCORS: true,
})
messageWrapper.style.display = 'none'
canvas.toBlob((res) => {
if (res) {
setImageBuffer(res)
const url = URL.createObjectURL(res)
setLoading(false)
setImageUrl(url)
}
})
} catch (error) {
console.log(error)
} finally {
setLoading(false)
}
}
const tabs: TabItem[] = [
{
value: 'context',
label: t('conversations.share.tabs.context'),
content: <div class="flex flex-col gap-2">
{messages.length
? (
<div class="flex flex-col gap-2">
<div class="emerald-light-button mt-0 cursor-pointer mb-2" onClick={() => copy()}>{copied() ? t('copyed') : t('conversations.share.copy')}</div>
<For each={messages}>
{item => (
<div class="flex space-x-2">
<div class="font-bold w-20 text-left">{item.role}:</div>
<div class="text-left flex-1 whitespace-normal overflow-auto">{item.content}</div>
</div>
)}
</For>
</div>
)
: <div class="text-center text-sm">{t('empty')}</div>}
</div>,
},
{
value: 'image',
label: t('conversations.share.tabs.image'),
content: <div class="flex flex-col gap-2">
{messages.length
? (
<div class="flex flex-col gap-2">
<div class="inline-block text-left">
<Show when={imageUrl().length}>
<div class="button inline-block mt-0 cursor-pointer mb-2" onClick={() => { copyImage() }}>{t('conversations.share.image.copy')}</div>
<div class="button inline-block mt-0 cursor-pointer mb-2 ml-2" onClick={() => { window.open(imageUrl()) }}>{t('conversations.share.image.open')}</div>
</Show>
<Show when={!imageUrl().length}>
<div class="emerald-light-button inline-block mt-0 cursor-pointer mb-2" onClick={() => handleLoadImage()}>{loading() ? t('conversations.share.image.loading') : t('conversations.share.image.btn')}</div>
</Show>
</div>
<Show when={imageUrl().length}>
<img src={imageUrl()} alt="" />
</Show>
</div>
)
: <div class="text-center text-sm">{t('empty')}</div>}
</div>,
},
]
return (
<div class="w-full">
<div class="fi justify-between border-base b-b-1 px-6 py-4">
<div class="text-base">{t('conversations.share.title')}</div>
{/* TODO */}
{/* <button class="button mt-0">{t('conversations.share.link.create')}</button> */}
</div>
<div class="fcc flex-col space-y-2 p-6">
<div
class="border w-full border-base fi justify-between box-border p-4 rounded-md hv-base"
onclick={() => {
showSelectMessageModal.set(true)
showShareModal.set(false)
}}
>
<span class="text-xs">{t('conversations.share.messages.selected')}</span>
<span class="text-xs op-60">{messages.length ? `${messages.length} Messages` : t('conversations.share.messages.title')}</span>
</div>
<Tabs tabs={tabs} sticky tabClass="bg-base-100" />
</div>
</div>
)
}

View File

@ -0,0 +1,34 @@
import * as checkbox from '@zag-js/checkbox'
import { normalizeProps, useMachine } from '@zag-js/solid'
import { createMemo, createUniqueId } from 'solid-js'
interface Props {
initValue?: boolean
label: string
setValue: (v?: boolean) => void
}
export const Checkbox = (props: Props) => {
const [state, send] = useMachine(checkbox.machine({
id: createUniqueId(),
checked: props.initValue ?? false,
onChange(detail) {
props.setValue(detail.checked as boolean)
},
}))
const api = createMemo(() => checkbox.connect(state, send, normalizeProps))
return (
<label {...api().rootProps}>
<div class="fi justify-revert cursor-pointer text-sm">
<input {...api().inputProps} />
{api().isChecked ? <div class="i-carbon:checkbox-checked text-xl" /> : <div class="i-carbon:checkbox text-xl" />}
<div {...api().labelProps} class="ml-2 truncate flex-1">
{props.label}
</div>
<div {...api().controlProps} />
</div>
</label>
)
}

View File

@ -0,0 +1,44 @@
import * as tabs from '@zag-js/tabs'
import { normalizeProps, useMachine } from '@zag-js/solid'
import { For, createMemo, createUniqueId } from 'solid-js'
import type { JSX } from 'solid-js'
export interface TabItem {
value: string
label: string
content: JSX.Element
}
interface Props {
tabs: TabItem[]
initValue?: string
sticky?: boolean
tabClass?: string
}
export const Tabs = (props: Props) => {
const [state, send] = useMachine(tabs.machine({ id: createUniqueId(), value: props.initValue ?? props.tabs[0].value }))
const api = createMemo(() => tabs.connect(state, send, normalizeProps))
return (
<div {...api().rootProps} class="w-full text-sm font-medium text-center">
<div {...api().tablistProps} class={`flex flex-wrap -mb-px border-b border-base ${props.sticky && 'sticky top-0 bottom-0 bg-white'} ${props.tabClass}`}>
<For each={props.tabs}>
{item => (
<button class={`inline-block p-4 border-b-2 border-transparent hover:text-gray-600 dark:hover:text-gray-300 transition-colors duration-300 cursor-pointer ${api().value === item.value && '!border-emerald-600 !text-emerald-600'}`} {...api().getTriggerProps({ value: item.value })}>
{item.label}
</button>
)}
</For>
</div>
<For each={props.tabs}>
{item => (
<div class="w-full text-sm mt-4 border border-base p-4" {...api().getContentProps({ value: item.value })}>
{item.content}
</div>
)}
</For>
</div>
)
}

View File

@ -5,3 +5,5 @@ export * from './Select'
export * from './Slider' export * from './Slider'
export * from './Tooltip' export * from './Tooltip'
export * from './Toggle' export * from './Toggle'
export * from './Checkbox'
export * from './Tabs'

View File

@ -24,17 +24,53 @@ export const en = {
recent: 'Recents', recent: 'Recents',
noRecent: 'No recents', noRecent: 'No recents',
untitled: 'Untitled', untitled: 'Untitled',
promopt: {
system: 'System Info',
desc: 'You are a helpful assistant, answer as concisely as possible...',
},
emoji: 'Search an emoji ~',
confirm: { confirm: {
title: 'Delete all messages in this chat', title: 'Delete all messages in this chat',
desc: 'This action cannot be undone.', desc: 'This action cannot be undone.',
message: 'Delete this record', message: 'Delete this record',
btn: 'confirm', btn: 'confirm',
cancel: 'cancel', cancel: 'cancel',
submit: 'submit',
},
share: {
title: 'Share Conversation',
link: {
title: 'Share with link',
copy: 'Copy Link',
create: 'Create Link',
},
save: 'Save',
copy: 'Copy Context',
messages: {
title: 'Select Message',
selected: 'Selected Messages',
selectAll: 'Select All',
},
tabs: {
context: 'Share Context',
image: 'Share Image',
},
image: {
btn: 'Generate Image',
open: 'Open in Tab',
loading: 'Generating...',
copy: 'Copy Image',
},
}, },
}, },
docs: 'Docs',
github: 'Github',
scroll: 'Scroll to bottom',
empty: 'No data',
send: { send: {
placeholder: 'Enter Something...', placeholder: 'Enter Something...',
button: 'Send', button: 'Send',
}, },
copyed: 'Copyed!',
}, },
} as language } as language

View File

@ -24,17 +24,53 @@ export const zhCN = {
recent: '最近对话', recent: '最近对话',
noRecent: '暂无最近对话', noRecent: '暂无最近对话',
untitled: '未命名对话', untitled: '未命名对话',
promopt: {
system: '系统信息',
desc: '你是个乐于助人的助手,回答尽量简洁...',
},
emoji: '搜索一个表情 ~',
confirm: { confirm: {
title: '删除本会话的所有消息', title: '删除本会话的所有消息',
desc: '这将删除本会话的所有消息,且不可恢复', desc: '这将删除本会话的所有消息,且不可恢复',
message: '删除这条记录', message: '删除这条记录',
btn: '确认', btn: '确认',
cancel: '取消', cancel: '取消',
submit: '提交',
},
share: {
title: '分享对话',
link: {
title: '分享链接',
copy: '复制链接',
create: '创建链接',
},
save: '保存',
copy: '复制上下文',
messages: {
title: '选择消息',
selected: '已选择的消息',
selectAll: '全选',
},
tabs: {
context: '分享上下文',
image: '分享图片',
},
image: {
btn: '生成图片',
open: '新窗口打开',
loading: '生成中...',
copy: '复制图片',
},
}, },
}, },
docs: '文档',
github: '源码',
scroll: '滚动到底部',
empty: '暂无数据',
send: { send: {
placeholder: '输入内容...', placeholder: '输入内容...',
button: '发送', button: '发送',
}, },
copyed: '已拷贝!',
}, },
} as language } as language

View File

@ -29,11 +29,11 @@ const providerOpenAI = () => {
description: '你可以随时切换支持服务商.', description: '你可以随时切换支持服务商.',
type: 'select', type: 'select',
options: [ options: [
{ value: 'openai', label: 'openai' }, { value: 'OpenAi', label: 'OpenAi' },
{ value: 'mbm-gpt', label: 'mbm-gpt' }, { value: 'Mbmzone', label: 'Mbmzone' },
{ value: 'microsoft', label: 'microsoft' }, { value: 'Azure', label: 'Azure' },
], ],
default: 'openai', default: 'OpenAi',
}, },
{ {
key: 'model', key: 'model',
@ -42,6 +42,7 @@ const providerOpenAI = () => {
type: 'select', type: 'select',
options: [ options: [
{ value: 'gpt-3.5-turbo', label: 'gpt-3.5-turbo' }, { value: 'gpt-3.5-turbo', label: 'gpt-3.5-turbo' },
{ value: 'gpt-3.5-turbo-16k', label: 'gpt-3.5-turbo-16k' },
{ value: 'gpt-4', label: 'gpt-4' }, { value: 'gpt-4', label: 'gpt-4' },
{ value: 'gpt-4-0314', label: 'gpt-4-0314' }, { value: 'gpt-4-0314', label: 'gpt-4-0314' },
{ value: 'gpt-4-32k', label: 'gpt-4-32k' }, { value: 'gpt-4-32k', label: 'gpt-4-32k' },

View File

@ -1,10 +1,11 @@
import { action, map } from 'nanostores' import { action, atom, map } from 'nanostores'
import { conversationMessagesMapData } from './tests/message.mock' import { conversationMessagesMapData } from './tests/message.mock'
import { db } from './storage/message' import { db } from './storage/message'
import { updateConversationById } from './conversation' import { updateConversationById } from './conversation'
import type { MessageInstance } from '@/types/message' import type { MessageInstance } from '@/types/message'
export const conversationMessagesMap = map<Record<string, MessageInstance[]>>({}) export const conversationMessagesMap = map<Record<string, MessageInstance[]>>({})
export const shareMessageIds = atom<string[]>([])
export const rebuildMessagesStore = async() => { export const rebuildMessagesStore = async() => {
const data = await db.exportData() || {} const data = await db.exportData() || {}
@ -100,4 +101,4 @@ export const spliceUpdateMessageByConversationId = action(
lastUseTime: Date.now(), lastUseTime: Date.now(),
}) })
}, },
) )

View File

@ -6,6 +6,8 @@ export const showSettingsSidebar = atom(false)
export const showConversationEditModal = atom(false) export const showConversationEditModal = atom(false)
export const showEmojiPickerModal = atom(false) export const showEmojiPickerModal = atom(false)
export const showConfirmModal = atom(false) export const showConfirmModal = atom(false)
export const showShareModal = atom(false)
export const showSelectMessageModal = atom(false)
export const isSendBoxFocus = atom(false) export const isSendBoxFocus = atom(false)
export const currentErrorMessage = atom<ErrorMessage | null>(null) export const currentErrorMessage = atom<ErrorMessage | null>(null)
@ -18,4 +20,4 @@ export const scrollController = () => {
scrollToBottom: () => elementList().forEach(element => element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' })), scrollToBottom: () => elementList().forEach(element => element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' })),
instantToBottom: () => elementList().forEach(element => element.scrollTo({ top: element.scrollHeight })), instantToBottom: () => elementList().forEach(element => element.scrollTo({ top: element.scrollHeight })),
} }
} }