Browse Source

feat: conversation app support pin and delete conversation (#467)

Joel 1 year ago
parent
commit
ec261aea54

File diff suppressed because it is too large
+ 1 - 1
web/app/components/base/confirm-ui/index.tsx


File diff suppressed because it is too large
+ 2 - 2
web/app/components/explore/item-operation/index.tsx


+ 10 - 6
web/app/components/share/chat/hooks/use-conversation.ts

@@ -1,12 +1,13 @@
 import { useState } from 'react'
-import type { ConversationItem } from '@/models/share'
 import produce from 'immer'
+import type { ConversationItem } from '@/models/share'
 
 const storageConversationIdKey = 'conversationIdInfo'
 
 type ConversationInfoType = Omit<ConversationItem, 'inputs' | 'id'>
 function useConversation() {
   const [conversationList, setConversationList] = useState<ConversationItem[]>([])
+  const [pinnedConversationList, setPinnedConversationList] = useState<ConversationItem[]>([])
   const [currConversationId, doSetCurrConversationId] = useState<string>('-1')
   // when set conversation id, we do not have set appId
   const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => {
@@ -29,9 +30,10 @@ function useConversation() {
   // input can be updated by user
   const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any> | null>(null)
   const resetNewConversationInputs = () => {
-    if (!newConversationInputs) return
-    setNewConversationInputs(produce(newConversationInputs, draft => {
-      Object.keys(draft).forEach(key => {
+    if (!newConversationInputs)
+      return
+    setNewConversationInputs(produce(newConversationInputs, (draft) => {
+      Object.keys(draft).forEach((key) => {
         draft[key] = ''
       })
     }))
@@ -48,6 +50,8 @@ function useConversation() {
   return {
     conversationList,
     setConversationList,
+    pinnedConversationList,
+    setPinnedConversationList,
     currConversationId,
     setCurrConversationId,
     getConversationIdFromStorage,
@@ -59,8 +63,8 @@ function useConversation() {
     setCurrInputs,
     currConversationInfo,
     setNewConversationInfo,
-    setExistConversationInfo
+    setExistConversationInfo,
   }
 }
 
-export default useConversation;
+export default useConversation

+ 76 - 16
web/app/components/share/chat/index.tsx

@@ -14,7 +14,7 @@ import { ToastContext } from '@/app/components/base/toast'
 import Sidebar from '@/app/components/share/chat/sidebar'
 import ConfigSence from '@/app/components/share/chat/config-scence'
 import Header from '@/app/components/share/header'
-import { fetchAppInfo, fetchAppParams, fetchChatList, fetchConversations, fetchSuggestedQuestions, sendChatMessage, stopChatMessageResponding, updateFeedback } from '@/service/share'
+import { delConversation, fetchAppInfo, fetchAppParams, fetchChatList, fetchConversations, fetchSuggestedQuestions, pinConversation, sendChatMessage, stopChatMessageResponding, unpinConversation, updateFeedback } from '@/service/share'
 import type { ConversationItem, SiteInfo } from '@/models/share'
 import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
 import type { Feedbacktype, IChatItem } from '@/app/components/app/chat'
@@ -25,6 +25,7 @@ import Loading from '@/app/components/base/loading'
 import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
 import { userInputsFormToPromptVariables } from '@/utils/model-config'
 import type { InstalledApp } from '@/models/explore'
+import Confirm from '@/app/components/base/confirm'
 
 export type IMainProps = {
   isInstalledApp?: boolean
@@ -65,9 +66,12 @@ const Main: FC<IMainProps> = ({
   /*
   * conversation info
   */
+  const [allConversationList, setAllConversationList] = useState<ConversationItem[]>([])
   const {
     conversationList,
     setConversationList,
+    pinnedConversationList,
+    setPinnedConversationList,
     currConversationId,
     setCurrConversationId,
     getConversationIdFromStorage,
@@ -81,11 +85,50 @@ const Main: FC<IMainProps> = ({
     setNewConversationInfo,
     setExistConversationInfo,
   } = useConversation()
-  const [hasMore, setHasMore] = useState<boolean>(false)
+  const [hasMore, setHasMore] = useState<boolean>(true)
+  const [hasPinnedMore, setHasPinnedMore] = useState<boolean>(true)
   const onMoreLoaded = ({ data: conversations, has_more }: any) => {
     setHasMore(has_more)
     setConversationList([...conversationList, ...conversations])
   }
+  const onPinnedMoreLoaded = ({ data: conversations, has_more }: any) => {
+    setHasPinnedMore(has_more)
+    setPinnedConversationList([...pinnedConversationList, ...conversations])
+  }
+  const [controlUpdateConversationList, setControlUpdateConversationList] = useState(0)
+  const noticeUpdateList = () => {
+    setConversationList([])
+    setHasMore(true)
+    setPinnedConversationList([])
+    setHasPinnedMore(true)
+    setControlUpdateConversationList(Date.now())
+  }
+  const handlePin = async (id: string) => {
+    await pinConversation(isInstalledApp, installedAppInfo?.id, id)
+    notify({ type: 'success', message: t('common.api.success') })
+    noticeUpdateList()
+  }
+
+  const handleUnpin = async (id: string) => {
+    await unpinConversation(isInstalledApp, installedAppInfo?.id, id)
+    notify({ type: 'success', message: t('common.api.success') })
+    noticeUpdateList()
+  }
+  const [isShowConfirm, { setTrue: showConfirm, setFalse: hideConfirm }] = useBoolean(false)
+  const [toDeleteConversationId, setToDeleteConversationId] = useState('')
+  const handleDelete = (id: string) => {
+    setToDeleteConversationId(id)
+    hideSidebar() // mobile
+    showConfirm()
+  }
+
+  const didDelete = async () => {
+    await delConversation(isInstalledApp, installedAppInfo?.id, toDeleteConversationId)
+    notify({ type: 'success', message: t('common.api.success') })
+    hideConfirm()
+    noticeUpdateList()
+  }
+
   const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
 
   const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false)
@@ -121,7 +164,7 @@ const Main: FC<IMainProps> = ({
     let notSyncToStateIntroduction = ''
     let notSyncToStateInputs: Record<string, any> | undefined | null = {}
     if (!isNewConversation) {
-      const item = conversationList.find(item => item.id === currConversationId)
+      const item = allConversationList.find(item => item.id === currConversationId)
       notSyncToStateInputs = item?.inputs || {}
       setCurrInputs(notSyncToStateInputs)
       notSyncToStateIntroduction = item?.introduction || ''
@@ -229,6 +272,10 @@ const Main: FC<IMainProps> = ({
     return []
   }
 
+  const fetchAllConversations = () => {
+    return fetchConversations(isInstalledApp, installedAppInfo?.id, undefined, undefined, 100)
+  }
+
   const fetchInitData = () => {
     return Promise.all([isInstalledApp
       ? {
@@ -240,7 +287,7 @@ const Main: FC<IMainProps> = ({
         },
         plan: 'basic',
       }
-      : fetchAppInfo(), fetchConversations(isInstalledApp, installedAppInfo?.id), fetchAppParams(isInstalledApp, installedAppInfo?.id)])
+      : fetchAppInfo(), fetchAllConversations(), fetchAppParams(isInstalledApp, installedAppInfo?.id)])
   }
 
   // init
@@ -255,10 +302,10 @@ const Main: FC<IMainProps> = ({
         setIsPublicVersion(tempIsPublicVersion)
         const prompt_template = ''
         // handle current conversation id
-        const { data: conversations, has_more } = conversationData as { data: ConversationItem[]; has_more: boolean }
+        const { data: allConversations } = conversationData as { data: ConversationItem[]; has_more: boolean }
         const _conversationId = getConversationIdFromStorage(appId)
-        const isNotNewConversation = conversations.some(item => item.id === _conversationId)
-        setHasMore(has_more)
+        const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
+        setAllConversationList(allConversations)
         // fetch new conversation info
         const { user_input_form, opening_statement: introduction, suggested_questions_after_answer }: any = appParams
         const prompt_variables = userInputsFormToPromptVariables(user_input_form)
@@ -276,7 +323,7 @@ const Main: FC<IMainProps> = ({
         } as PromptConfig)
         setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer)
 
-        setConversationList(conversations as ConversationItem[])
+        // setConversationList(conversations as ConversationItem[])
 
         if (isNotNewConversation)
           setCurrConversationId(_conversationId, appId, false)
@@ -403,12 +450,10 @@ const Main: FC<IMainProps> = ({
         if (hasError)
           return
 
-        let currChatList = conversationList
         if (getConversationIdChangeBecauseOfNew()) {
-          const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppInfo?.id)
-          setHasMore(has_more)
-          setConversationList(conversations as ConversationItem[])
-          currChatList = conversations
+          const { data: allConversations }: any = await fetchAllConversations()
+          setAllConversationList(allConversations)
+          noticeUpdateList()
         }
         setConversationIdChangeBecauseOfNew(false)
         resetNewConversationInputs()
@@ -451,14 +496,21 @@ const Main: FC<IMainProps> = ({
     return (
       <Sidebar
         list={conversationList}
+        pinnedList={pinnedConversationList}
         onMoreLoaded={onMoreLoaded}
+        onPinnedMoreLoaded={onPinnedMoreLoaded}
         isNoMore={!hasMore}
+        isPinnedNoMore={!hasPinnedMore}
         onCurrentIdChange={handleConversationIdChange}
         currentId={currConversationId}
         copyRight={siteInfo.copyright || siteInfo.title}
         isInstalledApp={isInstalledApp}
         installedAppId={installedAppInfo?.id}
         siteInfo={siteInfo}
+        onPin={handlePin}
+        onUnpin={handleUnpin}
+        controlUpdateList={controlUpdateConversationList}
+        onDelete={handleDelete}
       />
     )
   }
@@ -482,9 +534,6 @@ const Main: FC<IMainProps> = ({
         />
       )}
 
-      {/* {isNewConversation ? 'new' : 'exist'}
-        {JSON.stringify(newConversationInputs ? newConversationInputs : {})}
-        {JSON.stringify(existConversationInputs ? existConversationInputs : {})} */}
       <div
         className={cn(
           'flex rounded-t-2xl bg-white overflow-hidden',
@@ -551,6 +600,17 @@ const Main: FC<IMainProps> = ({
                 </div>
               </div>)
           }
+
+          {isShowConfirm && (
+            <Confirm
+              title={t('share.chat.deleteConversation.title')}
+              content={t('share.chat.deleteConversation.content')}
+              isShow={isShowConfirm}
+              onClose={hideConfirm}
+              onConfirm={didDelete}
+              onCancel={hideConfirm}
+            />
+          )}
         </div>
       </div>
     </div>

+ 76 - 61
web/app/components/share/chat/sidebar/index.tsx

@@ -1,32 +1,34 @@
-import React, { useRef } from 'react'
+import React, { useEffect, useState } from 'react'
 import type { FC } from 'react'
 import { useTranslation } from 'react-i18next'
 import {
-  ChatBubbleOvalLeftEllipsisIcon,
   PencilSquareIcon,
 } from '@heroicons/react/24/outline'
-import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon } from '@heroicons/react/24/solid'
-import { useInfiniteScroll } from 'ahooks'
+import cn from 'classnames'
 import Button from '../../../base/button'
+import List from './list'
 import AppInfo from '@/app/components/share/chat/sidebar/app-info'
 // import Card from './card'
 import type { ConversationItem, SiteInfo } from '@/models/share'
 import { fetchConversations } from '@/service/share'
 
-function classNames(...classes: any[]) {
-  return classes.filter(Boolean).join(' ')
-}
-
 export type ISidebarProps = {
   copyRight: string
   currentId: string
   onCurrentIdChange: (id: string) => void
   list: ConversationItem[]
+  pinnedList: ConversationItem[]
   isInstalledApp: boolean
   installedAppId?: string
   siteInfo: SiteInfo
   onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void
+  onPinnedMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void
   isNoMore: boolean
+  isPinnedNoMore: boolean
+  onPin: (id: string) => void
+  onUnpin: (id: string) => void
+  controlUpdateList: number
+  onDelete: (id: string) => void
 }
 
 const Sidebar: FC<ISidebarProps> = ({
@@ -34,37 +36,42 @@ const Sidebar: FC<ISidebarProps> = ({
   currentId,
   onCurrentIdChange,
   list,
+  pinnedList,
   isInstalledApp,
   installedAppId,
   siteInfo,
   onMoreLoaded,
+  onPinnedMoreLoaded,
   isNoMore,
+  isPinnedNoMore,
+  onPin,
+  onUnpin,
+  controlUpdateList,
+  onDelete,
 }) => {
   const { t } = useTranslation()
-  const listRef = useRef<HTMLDivElement>(null)
+  const [hasPinned, setHasPinned] = useState(false)
 
-  useInfiniteScroll(
-    async () => {
-      if (!isNoMore) {
-        const lastId = list[list.length - 1].id
-        const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId)
-        onMoreLoaded({ data: conversations, has_more })
-      }
-      return { list: [] }
-    },
-    {
-      target: listRef,
-      isNoMore: () => {
-        return isNoMore
-      },
-      reloadDeps: [isNoMore],
-    },
-  )
+  const checkHasPinned = async () => {
+    const { data }: any = await fetchConversations(isInstalledApp, installedAppId, undefined, true)
+    setHasPinned(data.length > 0)
+  }
+
+  useEffect(() => {
+    checkHasPinned()
+  }, [])
+
+  useEffect(() => {
+    if (controlUpdateList !== 0)
+      checkHasPinned()
+  }, [controlUpdateList])
+
+  const maxListHeight = isInstalledApp ? 'max-h-[30vh]' : 'max-h-[40vh]'
 
   return (
     <div
       className={
-        classNames(
+        cn(
           isInstalledApp ? 'tablet:h-[calc(100vh_-_74px)]' : 'tablet:h-[calc(100vh_-_3rem)]',
           'shrink-0 flex flex-col bg-white pc:w-[244px] tablet:w-[192px] mobile:w-[240px]  border-r border-gray-200 mobile:h-screen',
         )
@@ -85,40 +92,48 @@ const Sidebar: FC<ISidebarProps> = ({
           <PencilSquareIcon className="mr-2 h-4 w-4" /> {t('share.chat.newChat')}
         </Button>
       </div>
-
-      <nav
-        ref={listRef}
-        className="mt-4 flex-1 space-y-1 bg-white p-4 !pt-0 overflow-y-auto"
-      >
-        {list.map((item) => {
-          const isCurrent = item.id === currentId
-          const ItemIcon
-            = isCurrent ? ChatBubbleOvalLeftEllipsisSolidIcon : ChatBubbleOvalLeftEllipsisIcon
-          return (
-            <div
-              onClick={() => onCurrentIdChange(item.id)}
-              key={item.id}
-              className={classNames(
-                isCurrent
-                  ? 'bg-primary-50 text-primary-600'
-                  : 'text-gray-700 hover:bg-gray-100 hover:text-gray-700',
-                'group flex items-center rounded-md px-2 py-2 text-sm font-medium cursor-pointer',
-              )}
-            >
-              <ItemIcon
-                className={classNames(
-                  isCurrent
-                    ? 'text-primary-600'
-                    : 'text-gray-400 group-hover:text-gray-500',
-                  'mr-3 h-5 w-5 flex-shrink-0',
-                )}
-                aria-hidden="true"
-              />
-              {item.name}
-            </div>
-          )
-        })}
-      </nav>
+      <div className='flex-grow h-0 overflow-y-auto overflow-x-hidden'>
+        {/* pinned list */}
+        {hasPinned && (
+          <div className='mt-4 px-4'>
+            <div className='mb-1.5 leading-[18px] text-xs text-gray-500 font-medium uppercase'>{t('share.chat.pinnedTitle')}</div>
+            <List
+              className={maxListHeight}
+              currentId={currentId}
+              onCurrentIdChange={onCurrentIdChange}
+              list={pinnedList}
+              isInstalledApp={isInstalledApp}
+              installedAppId={installedAppId}
+              onMoreLoaded={onPinnedMoreLoaded}
+              isNoMore={isPinnedNoMore}
+              isPinned={true}
+              onPinChanged={id => onUnpin(id)}
+              controlUpdate={controlUpdateList + 1}
+              onDelete={onDelete}
+            />
+          </div>
+        )}
+        {/* unpinned list */}
+        <div className='mt-4 px-4'>
+          {hasPinned && (
+            <div className='mb-1.5 leading-[18px] text-xs text-gray-500 font-medium uppercase'>{t('share.chat.unpinnedTitle')}</div>
+          )}
+          <List
+            className={cn(hasPinned ? maxListHeight : 'flex-grow')}
+            currentId={currentId}
+            onCurrentIdChange={onCurrentIdChange}
+            list={list}
+            isInstalledApp={isInstalledApp}
+            installedAppId={installedAppId}
+            onMoreLoaded={onMoreLoaded}
+            isNoMore={isNoMore}
+            isPinned={false}
+            onPinChanged={id => onPin(id)}
+            controlUpdate={controlUpdateList + 1}
+            onDelete={onDelete}
+          />
+        </div>
+      </div>
       <div className="flex flex-shrink-0 pr-4 pb-4 pl-4">
         <div className="text-gray-400 font-normal text-xs">© {copyRight} {(new Date()).getFullYear()}</div>
       </div>

+ 115 - 0
web/app/components/share/chat/sidebar/list/index.tsx

@@ -0,0 +1,115 @@
+'use client'
+import type { FC } from 'react'
+import React, { useRef } from 'react'
+import {
+  ChatBubbleOvalLeftEllipsisIcon,
+} from '@heroicons/react/24/outline'
+import { useInfiniteScroll } from 'ahooks'
+import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon } from '@heroicons/react/24/solid'
+import cn from 'classnames'
+import s from './style.module.css'
+import type { ConversationItem } from '@/models/share'
+import { fetchConversations } from '@/service/share'
+import ItemOperation from '@/app/components/explore/item-operation'
+
+export type IListProps = {
+  className: string
+  currentId: string
+  onCurrentIdChange: (id: string) => void
+  list: ConversationItem[]
+  isInstalledApp: boolean
+  installedAppId?: string
+  onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void
+  isNoMore: boolean
+  isPinned: boolean
+  onPinChanged: (id: string) => void
+  controlUpdate: number
+  onDelete: (id: string) => void
+}
+
+const List: FC<IListProps> = ({
+  className,
+  currentId,
+  onCurrentIdChange,
+  list,
+  isInstalledApp,
+  installedAppId,
+  onMoreLoaded,
+  isNoMore,
+  isPinned,
+  onPinChanged,
+  controlUpdate,
+  onDelete,
+}) => {
+  const listRef = useRef<HTMLDivElement>(null)
+
+  useInfiniteScroll(
+    async () => {
+      if (!isNoMore) {
+        const lastId = list[list.length - 1]?.id
+        const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppId, lastId, isPinned)
+        onMoreLoaded({ data: conversations, has_more })
+      }
+      return { list: [] }
+    },
+    {
+      target: listRef,
+      isNoMore: () => {
+        return isNoMore
+      },
+      reloadDeps: [isNoMore, controlUpdate],
+    },
+  )
+  return (
+    <nav
+      ref={listRef}
+      className={cn(className, 'shrink-0 space-y-1 bg-white pb-[60px] overflow-y-auto')}
+    >
+      {list.map((item) => {
+        const isCurrent = item.id === currentId
+        const ItemIcon
+            = isCurrent ? ChatBubbleOvalLeftEllipsisSolidIcon : ChatBubbleOvalLeftEllipsisIcon
+        return (
+          <div
+            onClick={() => onCurrentIdChange(item.id)}
+            key={item.id}
+            className={cn(s.item,
+              isCurrent
+                ? 'bg-primary-50 text-primary-600'
+                : 'text-gray-700 hover:bg-gray-200 hover:text-gray-700',
+              'group flex justify-between items-center rounded-md px-2 py-2 text-sm font-medium cursor-pointer',
+            )}
+          >
+            <div className='flex items-center w-0 grow'>
+              <ItemIcon
+                className={cn(
+                  isCurrent
+                    ? 'text-primary-600'
+                    : 'text-gray-400 group-hover:text-gray-500',
+                  'mr-3 h-5 w-5 flex-shrink-0',
+                )}
+                aria-hidden="true"
+              />
+              <span>{item.name}</span>
+            </div>
+
+            {
+              !isCurrent && (
+                <div className={cn(s.opBtn, 'shrink-0')} onClick={e => e.stopPropagation()}>
+                  <ItemOperation
+                    isPinned={isPinned}
+                    togglePin={() => onPinChanged(item.id)}
+                    isShowDelete
+                    onDelete={() => onDelete(item.id)}
+                  />
+                </div>
+              )
+            }
+          </div>
+        )
+      })}
+    </nav>
+  )
+}
+
+export default React.memo(List)

+ 7 - 0
web/app/components/share/chat/sidebar/list/style.module.css

@@ -0,0 +1,7 @@
+.opBtn {
+  visibility: hidden;
+}
+
+.item:hover .opBtn {
+  visibility: visible;
+}

+ 34 - 28
web/i18n/lang/share-app.en.ts

@@ -1,45 +1,51 @@
 const translation = {
   common: {
-    welcome: "Welcome to use",
-    appUnavailable: "App is unavailable",
-    appUnkonwError: "App is unavailable"
+    welcome: 'Welcome to use',
+    appUnavailable: 'App is unavailable',
+    appUnkonwError: 'App is unavailable',
   },
   chat: {
-    newChat: "New chat",
-    newChatDefaultName: "New conversation",
-    powerBy: "Powered by",
-    prompt: "Prompt",
-    privatePromptConfigTitle: "Conversation settings",
-    publicPromptConfigTitle: "Initial Prompt",
-    configStatusDes: "Before start, you can modify conversation settings",
+    newChat: 'New chat',
+    pinnedTitle: 'Pinned',
+    unpinnedTitle: 'Chats',
+    newChatDefaultName: 'New conversation',
+    powerBy: 'Powered by',
+    prompt: 'Prompt',
+    privatePromptConfigTitle: 'Conversation settings',
+    publicPromptConfigTitle: 'Initial Prompt',
+    configStatusDes: 'Before start, you can modify conversation settings',
     configDisabled:
-      "Previous session settings have been used for this session.",
-    startChat: "Start Chat",
+      'Previous session settings have been used for this session.',
+    startChat: 'Start Chat',
     privacyPolicyLeft:
-      "Please read the ",
+      'Please read the ',
     privacyPolicyMiddle:
-      "privacy policy",
+      'privacy policy',
     privacyPolicyRight:
-      " provided by the app developer.",
+      ' provided by the app developer.',
+    deleteConversation: {
+      title: 'Delete conversation',
+      content: 'Are you sure you want to delete this conversation?',
+    },
   },
   generation: {
     tabs: {
-      create: "Create",
-      saved: "Saved",
+      create: 'Create',
+      saved: 'Saved',
     },
     savedNoData: {
-      title: "You haven't saved a result yet!",
+      title: 'You haven\'t saved a result yet!',
       description: 'Start generating content, and find your saved results here.',
-      startCreateContent: 'Start create content'
+      startCreateContent: 'Start create content',
     },
-    title: "AI Completion",
-    queryTitle: "Query content",
-    queryPlaceholder: "Write your query content...",
-    run: "RUN",
-    copy: "Copy",
-    resultTitle: "AI Completion",
-    noData: "AI will give you what you want here.",
+    title: 'AI Completion',
+    queryTitle: 'Query content',
+    queryPlaceholder: 'Write your query content...',
+    run: 'RUN',
+    copy: 'Copy',
+    resultTitle: 'AI Completion',
+    noData: 'AI will give you what you want here.',
   },
-};
+}
 
-export default translation;
+export default translation

+ 34 - 28
web/i18n/lang/share-app.zh.ts

@@ -1,41 +1,47 @@
 const translation = {
   common: {
-    welcome: "欢迎使用",
-    appUnavailable: "应用不可用",
-    appUnkonwError: "应用不可用",
+    welcome: '欢迎使用',
+    appUnavailable: '应用不可用',
+    appUnkonwError: '应用不可用',
   },
   chat: {
-    newChat: "新对话",
-    newChatDefaultName: "新的对话",
-    powerBy: "Powered by",
-    prompt: "提示词",
-    privatePromptConfigTitle: "对话设置",
-    publicPromptConfigTitle: "对话前提示词",
-    configStatusDes: "开始前,您可以修改对话设置",
-    configDisabled: "此次会话已使用上次会话表单",
-    startChat: "开始对话",
-    privacyPolicyLeft: "请阅读由该应用开发者提供的",
-    privacyPolicyMiddle: "隐私政策",
-    privacyPolicyRight: "。",
+    newChat: '新对话',
+    pinnedTitle: '已置顶',
+    unpinnedTitle: '对话列表',
+    newChatDefaultName: '新的对话',
+    powerBy: 'Powered by',
+    prompt: '提示词',
+    privatePromptConfigTitle: '对话设置',
+    publicPromptConfigTitle: '对话前提示词',
+    configStatusDes: '开始前,您可以修改对话设置',
+    configDisabled: '此次会话已使用上次会话表单',
+    startChat: '开始对话',
+    privacyPolicyLeft: '请阅读由该应用开发者提供的',
+    privacyPolicyMiddle: '隐私政策',
+    privacyPolicyRight: '。',
+    deleteConversation: {
+      title: '删除对话',
+      content: '您确定要删除此对话吗?',
+    },
   },
   generation: {
     tabs: {
-      create: "创建",
-      saved: "已保存",
+      create: '创建',
+      saved: '已保存',
     },
     savedNoData: {
-      title: "您还没有保存结果!",
+      title: '您还没有保存结果!',
       description: '开始生成内容,您可以在这里找到保存的结果。',
-      startCreateContent: '开始生成内容'
+      startCreateContent: '开始生成内容',
     },
-    title: "AI 智能书写",
-    queryTitle: "查询内容",
-    queryPlaceholder: "请输入文本内容",
-    run: "运行",
-    copy: "拷贝",
-    resultTitle: "AI 书写",
-    noData: "AI 会在这里给你惊喜。",
+    title: 'AI 智能书写',
+    queryTitle: '查询内容',
+    queryPlaceholder: '请输入文本内容',
+    run: '运行',
+    copy: '拷贝',
+    resultTitle: 'AI 书写',
+    noData: 'AI 会在这里给你惊喜。',
   },
-};
+}
 
-export default translation;
+export default translation

+ 19 - 5
web/service/share.ts

@@ -1,16 +1,18 @@
 import type { IOnCompleted, IOnData, IOnError } from './base'
 import {
-  del as consoleDel, get as consoleGet, post as consolePost,
-  delPublic as del, getPublic as get, postPublic as post, ssePost,
+  del as consoleDel, get as consoleGet, patch as consolePatch, post as consolePost,
+  delPublic as del, getPublic as get, patchPublic as patch, postPublic as post, ssePost,
 } from './base'
 import type { Feedbacktype } from '@/app/components/app/chat'
 
-function getAction(action: 'get' | 'post' | 'del', isInstalledApp: boolean) {
+function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boolean) {
   switch (action) {
     case 'get':
       return isInstalledApp ? consoleGet : get
     case 'post':
       return isInstalledApp ? consolePost : post
+    case 'patch':
+      return isInstalledApp ? consolePatch : patch
     case 'del':
       return isInstalledApp ? consoleDel : del
   }
@@ -55,8 +57,20 @@ export const fetchAppInfo = async () => {
   return get('/site')
 }
 
-export const fetchConversations = async (isInstalledApp: boolean, installedAppId = '', last_id?: string) => {
-  return getAction('get', isInstalledApp)(getUrl('conversations', isInstalledApp, installedAppId), { params: { ...{ limit: 20 }, ...(last_id ? { last_id } : {}) } })
+export const fetchConversations = async (isInstalledApp: boolean, installedAppId = '', last_id?: string, pinned?: boolean, limit?: number) => {
+  return getAction('get', isInstalledApp)(getUrl('conversations', isInstalledApp, installedAppId), { params: { ...{ limit: limit || 20 }, ...(last_id ? { last_id } : {}), ...(pinned !== undefined ? { pinned } : {}) } })
+}
+
+export const pinConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => {
+  return getAction('patch', isInstalledApp)(getUrl(`conversations/${id}/pin`, isInstalledApp, installedAppId))
+}
+
+export const unpinConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => {
+  return getAction('patch', isInstalledApp)(getUrl(`conversations/${id}/unpin`, isInstalledApp, installedAppId))
+}
+
+export const delConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => {
+  return getAction('del', isInstalledApp)(getUrl(`conversations/${id}`, isInstalledApp, installedAppId))
 }
 
 export const fetchChatList = async (conversationId: string, isInstalledApp: boolean, installedAppId = '') => {