diff --git a/package.json b/package.json
index ba42900..0b0fe35 100644
--- a/package.json
+++ b/package.json
@@ -27,15 +27,20 @@
"@solid-primitives/scheduled": "^1.3.2",
"@solid-primitives/scroll": "^2.0.14",
"@unocss/reset": "^0.50.6",
- "@zag-js/dialog": "^0.9.2",
- "@zag-js/menu": "^0.9.2",
- "@zag-js/select": "^0.9.2",
- "@zag-js/slider": "^0.9.2",
- "@zag-js/solid": "^0.9.2",
- "@zag-js/switch": "^0.9.2",
- "@zag-js/toast": "^0.9.2",
- "@zag-js/toggle": "^0.9.2",
- "@zag-js/tooltip": "^0.9.2",
+
+ "@zag-js/checkbox": "^0.10.3",
+ "@zag-js/dialog": "^0.10.3",
+ "@zag-js/menu": "^0.10.3",
+ "@zag-js/select": "^0.10.3",
+ "@zag-js/slider": "^0.10.3",
+ "@zag-js/solid": "^0.10.3",
+ "@zag-js/switch": "^0.10.3",
+ "@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",
"bumpp": "^9.1.0",
"destr": "^1.2.2",
@@ -69,6 +74,7 @@
"eslint-plugin-astro": "^0.24.0",
"lint-staged": "^13.2.2",
"punycode": "^2.3.0",
+ "html2canvas": "^1.4.1",
"simple-git-hooks": "^2.8.1",
"unocss": "^0.50.6",
"vite-plugin-pwa": "^0.14.7"
diff --git a/src/components/ModalsLayer.tsx b/src/components/ModalsLayer.tsx
index 885096d..44cbe93 100644
--- a/src/components/ModalsLayer.tsx
+++ b/src/components/ModalsLayer.tsx
@@ -2,12 +2,16 @@ import {
showConversationEditModal,
showConversationSidebar,
showEmojiPickerModal,
+ showSelectMessageModal,
showSettingsSidebar,
+ showShareModal,
} from '@/stores/ui'
import ConversationSidebar from './conversations/ConversationSidebar'
import SettingsSidebar from './settings/SettingsSidebar'
import ConversationEditModal from './conversations/ConversationEditModal'
import EmojiPickerModal from './ui/EmojiPickerModal'
+import ShareModal from './ui/ShareModal'
+import SelectMessageModal from './ui/SelectMessageModal'
import Modal from './ui/Modal'
export default () => {
@@ -33,6 +37,16 @@ export default () => {
+
+
+
+
+
+ { showShareModal.set(true) }}>
+
+
+
+
>
)
}
diff --git a/src/components/header/ConversationHeaderShare.tsx b/src/components/header/ConversationHeaderShare.tsx
index 94dbbad..0136c83 100644
--- a/src/components/header/ConversationHeaderShare.tsx
+++ b/src/components/header/ConversationHeaderShare.tsx
@@ -38,7 +38,7 @@ export default () => {
<>
{ $currentConversationId() && (
{ handleShareMessage(true) }} >
-
+
diff --git a/src/components/header/ConversationMessageSettingButton.tsx b/src/components/header/ConversationMessageSettingButton.tsx
new file mode 100644
index 0000000..6ebccef
--- /dev/null
+++ b/src/components/header/ConversationMessageSettingButton.tsx
@@ -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() && (
+ { showConversationEditModal.set(true) }}
+ >
+ {/* Render the carbon settings adjust icon */}
+
+
+ )}
+ >
+ )
+}
\ No newline at end of file
diff --git a/src/components/header/ConversationMessageShareButton.tsx b/src/components/header/ConversationMessageShareButton.tsx
new file mode 100644
index 0000000..07899e9
--- /dev/null
+++ b/src/components/header/ConversationMessageShareButton.tsx
@@ -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() && (
+ { handleShareContext() }}
+ >
+
+
+ )}
+ >
+ )
+}
\ No newline at end of file
diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx
index 5d77a71..8ef24f0 100644
--- a/src/components/header/Header.tsx
+++ b/src/components/header/Header.tsx
@@ -4,6 +4,8 @@ import { useLargeScreen } from '@/hooks'
import ConversationHeaderInfo from './ConversationHeaderInfo'
import ConversationMessageClearButton from './ConversationMessageClearButton'
import ConversationHeaderShare from './ConversationHeaderShare'
+import ConversationMessageShareButton from './ConversationMessageShareButton'
+import ConversationMessageSettingButton from './ConversationMessageSettingButton'
export default () => {
onMount(() => {
@@ -26,7 +28,9 @@ export default () => {
+
+ {/*
*/}
showSettingsSidebar.set(true)}
diff --git a/src/components/ui/SelectMessageModal.tsx b/src/components/ui/SelectMessageModal.tsx
new file mode 100644
index 0000000..1be753c
--- /dev/null
+++ b/src/components/ui/SelectMessageModal.tsx
@@ -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 (
+
+
+
{t('conversations.share.messages.title')}
+
+
+ {/*
+ handleSelectAll()} initValue={checkAll()} label={`${t('conversations.share.messages.selectAll')}`} />
+
*/}
+
+ {(item) => {
+ return (
+
+ handleToggleMessages(item.id)} initValue={item.isSelected} label={`${item.role}: ${item.content}`} />
+
+ )
+ }}
+
+
+
handleSaveContext()}>{t('settings.save')}
+
+ )
+}
diff --git a/src/components/ui/ShareModal.tsx b/src/components/ui/ShareModal.tsx
new file mode 100644
index 0000000..78261fa
--- /dev/null
+++ b/src/components/ui/ShareModal.tsx
@@ -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
()
+ 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:
+ {messages.length
+ ? (
+
+
copy()}>{copied() ? t('copyed') : t('conversations.share.copy')}
+
+ {item => (
+
+
{item.role}:
+
{item.content}
+
+ )}
+
+
+ )
+ :
{t('empty')}
}
+
,
+ },
+ {
+ value: 'image',
+ label: t('conversations.share.tabs.image'),
+ content:
+ {messages.length
+ ? (
+
+
+
+ { copyImage() }}>{t('conversations.share.image.copy')}
+ { window.open(imageUrl()) }}>{t('conversations.share.image.open')}
+
+
+ handleLoadImage()}>{loading() ? t('conversations.share.image.loading') : t('conversations.share.image.btn')}
+
+
+
+
+
+
+ )
+ :
{t('empty')}
}
+
,
+ },
+ ]
+
+ return (
+
+
+
{t('conversations.share.title')}
+ {/* TODO */}
+ {/*
{t('conversations.share.link.create')} */}
+
+
+
{
+ showSelectMessageModal.set(true)
+ showShareModal.set(false)
+ }}
+ >
+ {t('conversations.share.messages.selected')}
+ {messages.length ? `${messages.length} Messages` : t('conversations.share.messages.title')}
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/ui/base/Checkbox.tsx b/src/components/ui/base/Checkbox.tsx
new file mode 100644
index 0000000..7a904b9
--- /dev/null
+++ b/src/components/ui/base/Checkbox.tsx
@@ -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 (
+
+
+
+ {api().isChecked ?
:
}
+
+ {props.label}
+
+
+
+
+ )
+}
diff --git a/src/components/ui/base/Tabs.tsx b/src/components/ui/base/Tabs.tsx
new file mode 100644
index 0000000..b8c3149
--- /dev/null
+++ b/src/components/ui/base/Tabs.tsx
@@ -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 (
+
+
+
+ {item => (
+
+ {item.label}
+
+ )}
+
+
+
+ {item => (
+
+ {item.content}
+
+ )}
+
+
+ )
+}
diff --git a/src/components/ui/base/index.ts b/src/components/ui/base/index.ts
index bbec035..6256a64 100644
--- a/src/components/ui/base/index.ts
+++ b/src/components/ui/base/index.ts
@@ -5,3 +5,5 @@ export * from './Select'
export * from './Slider'
export * from './Tooltip'
export * from './Toggle'
+export * from './Checkbox'
+export * from './Tabs'
diff --git a/src/locale/lang/en.ts b/src/locale/lang/en.ts
index eacf4e8..4fc607d 100644
--- a/src/locale/lang/en.ts
+++ b/src/locale/lang/en.ts
@@ -24,17 +24,53 @@ export const en = {
recent: 'Recents',
noRecent: 'No recents',
untitled: 'Untitled',
+ promopt: {
+ system: 'System Info',
+ desc: 'You are a helpful assistant, answer as concisely as possible...',
+ },
+ emoji: 'Search an emoji ~',
confirm: {
title: 'Delete all messages in this chat',
desc: 'This action cannot be undone.',
message: 'Delete this record',
btn: 'confirm',
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: {
placeholder: 'Enter Something...',
button: 'Send',
},
+ copyed: 'Copyed!',
},
} as language
diff --git a/src/locale/lang/zh-cn.ts b/src/locale/lang/zh-cn.ts
index 3842a30..a974fbe 100644
--- a/src/locale/lang/zh-cn.ts
+++ b/src/locale/lang/zh-cn.ts
@@ -24,17 +24,53 @@ export const zhCN = {
recent: '最近对话',
noRecent: '暂无最近对话',
untitled: '未命名对话',
+ promopt: {
+ system: '系统信息',
+ desc: '你是个乐于助人的助手,回答尽量简洁...',
+ },
+ emoji: '搜索一个表情 ~',
confirm: {
title: '删除本会话的所有消息',
desc: '这将删除本会话的所有消息,且不可恢复',
message: '删除这条记录',
btn: '确认',
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: {
placeholder: '输入内容...',
button: '发送',
},
+ copyed: '已拷贝!',
},
} as language
diff --git a/src/providers/openai/index.ts b/src/providers/openai/index.ts
index bf47a59..81aaa36 100644
--- a/src/providers/openai/index.ts
+++ b/src/providers/openai/index.ts
@@ -29,11 +29,11 @@ const providerOpenAI = () => {
description: '你可以随时切换支持服务商.',
type: 'select',
options: [
- { value: 'openai', label: 'openai' },
- { value: 'mbm-gpt', label: 'mbm-gpt' },
- { value: 'microsoft', label: 'microsoft' },
+ { value: 'OpenAi', label: 'OpenAi' },
+ { value: 'Mbmzone', label: 'Mbmzone' },
+ { value: 'Azure', label: 'Azure' },
],
- default: 'openai',
+ default: 'OpenAi',
},
{
key: 'model',
@@ -42,6 +42,7 @@ const providerOpenAI = () => {
type: 'select',
options: [
{ 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-0314', label: 'gpt-4-0314' },
{ value: 'gpt-4-32k', label: 'gpt-4-32k' },
diff --git a/src/stores/messages.ts b/src/stores/messages.ts
index 418b5cf..9a28915 100644
--- a/src/stores/messages.ts
+++ b/src/stores/messages.ts
@@ -1,10 +1,11 @@
-import { action, map } from 'nanostores'
+import { action, atom, map } from 'nanostores'
import { conversationMessagesMapData } from './tests/message.mock'
import { db } from './storage/message'
import { updateConversationById } from './conversation'
import type { MessageInstance } from '@/types/message'
export const conversationMessagesMap = map>({})
+export const shareMessageIds = atom([])
export const rebuildMessagesStore = async() => {
const data = await db.exportData() || {}
@@ -100,4 +101,4 @@ export const spliceUpdateMessageByConversationId = action(
lastUseTime: Date.now(),
})
},
-)
+)
\ No newline at end of file
diff --git a/src/stores/ui.ts b/src/stores/ui.ts
index 7990c17..8e103b9 100644
--- a/src/stores/ui.ts
+++ b/src/stores/ui.ts
@@ -6,6 +6,8 @@ export const showSettingsSidebar = atom(false)
export const showConversationEditModal = atom(false)
export const showEmojiPickerModal = atom(false)
export const showConfirmModal = atom(false)
+export const showShareModal = atom(false)
+export const showSelectMessageModal = atom(false)
export const isSendBoxFocus = atom(false)
export const currentErrorMessage = atom(null)
@@ -18,4 +20,4 @@ export const scrollController = () => {
scrollToBottom: () => elementList().forEach(element => element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' })),
instantToBottom: () => elementList().forEach(element => element.scrollTo({ top: element.scrollHeight })),
}
-}
+}
\ No newline at end of file