2023-05-29 17:57:14 -05:00
< script lang = "ts" >
import Fa from 'svelte-fa/src/fa.svelte'
import {
faGear,
faTrash,
faClone,
2023-05-30 17:35:36 -05:00
// faEllipsisVertical,
faEllipsis,
2023-05-29 17:57:14 -05:00
faDownload,
faUpload,
faEraser,
faRotateRight,
faSquarePlus,
faKey,
faFileExport,
2023-05-29 20:59:24 -05:00
faTrashCan,
faEye,
faEyeSlash
2023-05-29 17:57:14 -05:00
} from '@fortawesome/free-solid-svg-icons/index'
2023-06-24 19:13:14 -05:00
import { faSquareMinus , faSquarePlus as faSquarePlusOutline } from '@fortawesome/free-regular-svg-icons/index'
2023-08-15 20:32:30 -05:00
import { addChatFromJSON , chatsStorage , checkStateChange , clearChats , clearMessages , copyChat , globalStorage , setGlobalSettingValueByKey , showSetChatSettings , pinMainMenu , getChat , deleteChat , saveChatStore , saveCustomProfile } from './Storage.svelte'
2023-05-29 17:57:14 -05:00
import { exportAsMarkdown , exportChatAsJSON } from './Export.svelte'
2023-07-17 14:01:16 -05:00
import { newNameForProfile , restartProfile } from './Profiles.svelte'
2023-05-29 17:57:14 -05:00
import { replace } from 'svelte-spa-router'
import { clickOutside } from 'svelte-use-click-outside'
2023-06-05 21:29:20 -05:00
import { openModal } from 'svelte-modals'
import PromptConfirm from './PromptConfirm.svelte'
2023-07-17 14:01:16 -05:00
import { startNewChatWithWarning , startNewChatFromChatId , errorNotice , encodeHTMLEntities } from './Util.svelte'
import type { ChatSettings } from './Types.svelte'
2023-08-15 20:32:30 -05:00
import { hasActiveModels } from './Models.svelte'
2023-05-29 17:57:14 -05:00
export let chatId
2023-05-30 17:35:36 -05:00
export const show = (showHide:boolean = true) => {
showChatMenu = showHide
2023-05-29 17:57:14 -05:00
}
2023-05-30 17:35:36 -05:00
export let style: string = 'is-right'
2023-05-29 17:57:14 -05:00
2023-06-05 21:29:20 -05:00
$: sortedChats = $chatsStorage.sort((a, b) => b.id - a.id)
2023-05-29 17:57:14 -05:00
let showChatMenu = false
let chatFileInput
2023-07-17 14:01:16 -05:00
let profileFileInput
2023-05-29 17:57:14 -05:00
const importChatFromFile = (e) => {
2023-05-29 18:24:29 -05:00
close()
2023-05-29 17:57:14 -05:00
const image = e.target.files[0]
2023-07-17 14:01:16 -05:00
e.target.value = null
2023-05-29 17:57:14 -05:00
const reader = new FileReader()
reader.readAsText(image)
reader.onload = e => {
const json = (e.target || {} ).result as string
addChatFromJSON(json)
}
}
2023-06-05 21:29:20 -05:00
const delChat = () => {
2023-05-29 18:24:29 -05:00
close()
2023-06-05 21:29:20 -05:00
openModal(PromptConfirm, {
title: 'Delete Chat',
message: 'Are you sure you want to delete this chat?',
class: 'is-warning',
confirmButtonClass: 'is-warning',
confirmButton: 'Delete Chat',
onConfirm: () => {
const thisChat = getChat(chatId)
const thisIndex = sortedChats.indexOf(thisChat)
const prevChat = sortedChats[thisIndex - 1]
const nextChat = sortedChats[thisIndex + 1]
const newChat = nextChat || prevChat
if (!newChat) {
// No other chats, clear all and go to home
replace('/').then(() => { deleteChat ( chatId ) } )
} else {
// Delete the current chat and go to the max chatId
replace(`/chat/${ newChat . id } `).then(() => { deleteChat ( chatId ) } )
}
2023-06-05 22:47:55 -05:00
}
2023-06-05 21:29:20 -05:00
})
2023-05-29 17:57:14 -05:00
}
const confirmClearChats = () => {
2023-06-14 12:52:07 -05:00
if (!sortedChats.length) return
2023-05-29 18:24:29 -05:00
close()
2023-06-05 21:29:20 -05:00
openModal(PromptConfirm, {
title: 'Delete ALL Chat',
message: 'Are you sure you want to delete ALL of your chats?',
class: 'is-danger',
confirmButtonClass: 'is-danger',
confirmButton: 'Delete ALL',
onConfirm: () => {
2023-06-08 12:22:19 -05:00
replace('/').then(() => { deleteChat ( chatId ) } )
2023-06-05 21:29:20 -05:00
clearChats()
2023-06-05 22:47:55 -05:00
}
2023-06-05 21:29:20 -05:00
})
2023-05-29 17:57:14 -05:00
}
2023-05-29 18:24:29 -05:00
const close = () => {
2023-05-31 08:09:54 -05:00
$pinMainMenu = false
2023-05-29 18:24:29 -05:00
showChatMenu = false
}
const restartChatSession = () => {
close()
2023-06-01 22:53:48 -05:00
restartProfile(chatId)
2023-05-29 18:24:29 -05:00
$checkStateChange++ // signal chat page to start profile
}
2023-05-29 20:59:24 -05:00
const toggleHideSummarized = () => {
close()
setGlobalSettingValueByKey('hideSummarized', !$globalStorage.hideSummarized)
}
2023-06-24 19:13:14 -05:00
const clearUsage = () => {
openModal(PromptConfirm, {
title: 'Clear Chat Usage',
message: 'Are you sure you want to clear your token usage stats for the current chat?',
class: 'is-warning',
confirmButtonClass: 'is-warning',
confirmButton: 'Clear Usage',
onConfirm: () => {
const chat = getChat(chatId)
chat.usage = {}
saveChatStore()
}
})
}
2023-07-17 14:01:16 -05:00
const importProfileFromFile = (e) => {
const image = e.target.files[0]
e.target.value = null
const reader = new FileReader()
reader.onload = e => {
const json = (e.target || {} ).result as string
try {
const profile = JSON.parse(json) as ChatSettings
profile.profileName = newNameForProfile(profile.profileName || '')
profile.profile = null as any
saveCustomProfile(profile)
openModal(PromptConfirm, {
title: 'Profile Restored',
class: 'is-info',
message: 'Profile restored as:< br > < strong > ' + encodeHTMLEntities(profile.profileName) +
'< / strong > < br > < br > Start new chat with this profile?',
asHtml: true,
onConfirm: () => {
startNewChatWithWarning(chatId, profile)
},
onCancel: () => {}
})
} catch (e) {
errorNotice('Unable to import profile:', e)
}
}
reader.onerror = e => {
errorNotice('Unable to import profile:', new Error('Unknown error'))
}
reader.readAsText(image)
}
2025-07-05 22:32:51 +09:00
function dumpLocalStorage () {
try {
const storageObject = {}
2024-09-22 06:49:24 +09:00
for (let i = 0; i < localStorage.length ; i ++) {
2025-07-05 22:32:51 +09:00
const key = localStorage.key(i)
2024-09-22 06:49:24 +09:00
if (key) {
2025-07-05 22:32:51 +09:00
storageObject[key] = localStorage.getItem(key)
2024-09-22 06:49:24 +09:00
}
}
2025-07-05 22:32:51 +09:00
const dataStr = JSON.stringify(storageObject, null, 2)
const blob = new Blob([dataStr], { type : 'application/json' } )
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
const now = new Date()
const dateTimeStr = now.toISOString().replace(/:\d+\.\d+Z$/, '').replace(/-|:/g, '_')
link.download = `ChatGPT-web-${ dateTimeStr } .json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
2024-09-22 06:49:24 +09:00
} catch (error) {
2025-07-05 22:32:51 +09:00
console.error('Error dumping localStorage:', error)
2024-09-22 06:49:24 +09:00
}
}
2025-07-05 22:32:51 +09:00
function loadLocalStorage () {
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.addEventListener('change', function (e) {
const file = e.target.files[0]
2024-09-22 06:49:24 +09:00
if (file) {
2025-07-05 22:32:51 +09:00
const reader = new FileReader()
reader.onload = function (e) {
const data = JSON.parse(e.target.result)
Object.keys(data).forEach(function (key) {
localStorage.setItem(key, data[key])
})
window.location.reload()
}
reader.readAsText(file)
2024-09-22 06:49:24 +09:00
}
2025-07-05 22:32:51 +09:00
})
document.body.appendChild(fileInput)
fileInput.click()
fileInput.remove()
2024-09-22 06:49:24 +09:00
}
2025-07-05 22:32:51 +09:00
function backupLocalStorage () {
try {
const storageObject = {}
2024-09-22 06:49:24 +09:00
for (let i = 0; i < localStorage.length ; i ++) {
2025-07-05 22:32:51 +09:00
const key = localStorage.key(i)
2024-09-22 06:49:24 +09:00
if (key) {
2025-07-05 22:32:51 +09:00
storageObject[key] = localStorage.getItem(key)
2024-09-22 06:49:24 +09:00
}
}
2025-07-05 22:32:51 +09:00
const dataStr = JSON.stringify(storageObject, null, 2)
const now = new Date()
const dateTimeStr = now.toISOString().replace(/:\d+\.\d+Z$/, '').replace(/-|:/g, '_')
localStorage.setItem(`prev-${ dateTimeStr } `, dataStr)
2024-09-22 06:49:24 +09:00
} catch (error) {
2025-07-05 22:32:51 +09:00
console.error('Error backing up localStorage:', error)
2024-09-22 06:49:24 +09:00
}
2025-07-05 22:32:51 +09:00
}
2024-09-22 06:49:24 +09:00
2023-05-29 17:57:14 -05:00
< / script >
2023-05-30 17:35:36 -05:00
< div class = "dropdown { style } " class:is-active = { showChatMenu } use:clickOutside= {() => { showChatMenu = false }} >
2023-05-29 17:57:14 -05:00
< div class = "dropdown-trigger" >
2023-05-30 17:35:36 -05:00
< button class = "button is-ghost default-text" aria-haspopup = "true"
2023-05-29 17:57:14 -05:00
aria-controls="dropdown-menu3"
on:click| preventDefault| stopPropagation={() => { showChatMenu = ! showChatMenu }}
>
2023-05-30 17:35:36 -05:00
< span class = "icon " >< Fa icon = { faEllipsis } / ></ span >
2023-05-29 17:57:14 -05:00
< / button >
< / div >
< div class = "dropdown-menu" id = "dropdown-menu3" role = "menu" >
< div class = "dropdown-content" >
2023-05-29 18:24:29 -05:00
< a href = { '#' } class="dropdown-item" class:is-disabled = { ! chatId } on:click | preventDefault= {() => { if ( chatId ) close (); $showSetChatSettings = true }} >
2023-05-29 17:57:14 -05:00
< span class = "menu-icon" >< Fa icon = { faGear } / ></ span > Chat Profile Settings
< / a >
< hr class = "dropdown-divider" >
2023-07-24 15:26:17 -05:00
< a href = { '#' } class:is-disabled= { ! hasActiveModels ()} on:click | preventDefault = {() => { hasActiveModels () && close (); hasActiveModels () && startNewChatWithWarning ( chatId ) }} class="dropdown-item" >
2023-06-22 21:08:50 -05:00
< span class = "menu-icon" >< Fa icon = { faSquarePlus } / ></ span > New Chat from Default
< / a >
< a href = { '#' } class:is-disabled= { ! chatId } on:click | preventDefault = {() => { chatId && close (); chatId && startNewChatFromChatId ( chatId ) }} class="dropdown-item" >
2023-06-22 21:15:25 -05:00
< span class = "menu-icon" >< Fa icon = { faSquarePlusOutline } / ></ span > New Chat from Current
2023-05-29 17:57:14 -05:00
< / a >
2023-05-29 18:24:29 -05:00
< a href = { '#' } class="dropdown-item" class:is-disabled = { ! chatId } on:click | preventDefault= {() => { if ( chatId ) close (); copyChat ( chatId ) }} >
2023-05-29 17:57:14 -05:00
< span class = "menu-icon" >< Fa icon = { faClone } / ></ span > Clone Chat
< / a >
< hr class = "dropdown-divider" >
2023-05-29 21:25:40 -05:00
< a href = { '#' } class="dropdown-item" class:is-disabled = { ! chatId } on:click | preventDefault= {() => { if ( chatId ) restartChatSession () }} >
< span class = "menu-icon" >< Fa icon = { faRotateRight } / ></ span > Restart Chat Session
< / a >
< a href = { '#' } class="dropdown-item" class:is-disabled = { ! chatId } on:click | preventDefault= {() => { if ( chatId ) close (); clearMessages ( chatId ) }} >
< span class = "menu-icon" >< Fa icon = { faEraser } / ></ span > Clear Chat Messages
< / a >
2023-06-24 19:13:14 -05:00
< a href = { '#' } class="dropdown-item" class:is-disabled = { ! chatId } on:click | preventDefault= {() => { if ( chatId ) close (); clearUsage () }} >
< span class = "menu-icon" >< Fa icon = { faSquareMinus } / ></ span > Clear Chat Usage
< / a >
2023-05-29 21:25:40 -05:00
< hr class = "dropdown-divider" >
2023-05-29 18:24:29 -05:00
< a href = { '#' } class="dropdown-item" class:is-disabled = { ! chatId } on:click | preventDefault= {() => { close (); exportChatAsJSON ( chatId ) }} >
2023-05-29 21:10:39 -05:00
< span class = "menu-icon" >< Fa icon = { faDownload } / ></ span > Backup Chat JSON
2023-05-29 17:57:14 -05:00
< / a >
2023-07-24 15:26:17 -05:00
< a href = { '#' } class="dropdown-item" class:is-disabled = { ! hasActiveModels ()} on:click | preventDefault= {() => { if ( chatId ) close (); chatFileInput . click () }} >
2023-05-29 17:57:14 -05:00
< span class = "menu-icon" >< Fa icon = { faUpload } / ></ span > Restore Chat JSON
< / a >
2023-06-01 22:11:38 -05:00
< a href = { '#' } class="dropdown-item" class:is-disabled = { ! chatId } on:click | preventDefault= {() => { if ( chatId ) close (); exportAsMarkdown ( chatId ) }} >
2023-05-29 17:57:14 -05:00
< span class = "menu-icon" >< Fa icon = { faFileExport } / ></ span > Export Chat Markdown
< / a >
< hr class = "dropdown-divider" >
2023-07-24 15:26:17 -05:00
< a href = { '#' } class="dropdown-item" class:is-disabled = { ! hasActiveModels ()} on:click | preventDefault= {() => { if ( chatId ) close (); profileFileInput . click () }} >
2023-07-17 14:01:16 -05:00
< span class = "menu-icon" >< Fa icon = { faUpload } / ></ span > Restore Profile JSON
< / a >
< hr class = "dropdown-divider" >
2024-09-22 06:49:24 +09:00
< a href = { '#' } class="dropdown-item" on:click | preventDefault = {() => { close (); dumpLocalStorage () }} >
< span class = "menu-icon" >< Fa icon = { faUpload } / ></ span > Dump All Data
< / a >
< a href = { '#' } class="dropdown-item" on:click | preventDefault = {() => { if ( chatId ) close (); backupLocalStorage (); loadLocalStorage () }} >
< span class = "menu-icon" >< Fa icon = { faDownload } / ></ span > Load All Data
< / a >
< hr class = "dropdown-divider" >
2023-06-05 21:29:20 -05:00
< a href = { '#' } class="dropdown-item" class:is-disabled = { ! chatId } on:click | preventDefault= {() => { if ( chatId ) close (); delChat () }} >
2023-05-29 17:57:14 -05:00
< span class = "menu-icon" >< Fa icon = { faTrash } / ></ span > Delete Chat
< / a >
2023-06-14 12:52:07 -05:00
< a href = { '#' } class="dropdown-item" class:is-disabled = { $chatsStorage && ! $chatsStorage [ 0 ]} on:click | preventDefault= {() => { confirmClearChats () }} >
2023-05-29 17:57:14 -05:00
< span class = "menu-icon" >< Fa icon = { faTrashCan } / ></ span > Delete ALL Chats
< / a >
< hr class = "dropdown-divider" >
2023-05-29 20:59:24 -05:00
< a href = { '#' } class="dropdown-item" on:click | preventDefault = {() => { if ( chatId ) toggleHideSummarized () }} >
{ #if $globalStorage . hideSummarized }
< span class = "menu-icon" >< Fa icon = { faEye } / ></ span > Show Summarized Messages
{ : else }
< span class = "menu-icon" >< Fa icon = { faEyeSlash } / ></ span > Hide Summarized Messages
{ /if }
< / a >
< hr class = "dropdown-divider" >
2023-05-29 21:34:53 -05:00
< a href = { '#/' } class="dropdown-item" on:click = { close } >
2023-08-15 20:32:30 -05:00
< span class = "menu-icon" >< Fa icon = { faKey } / ></ span > API Setting
2023-05-29 17:57:14 -05:00
< / a >
< / div >
< / div >
< / div >
< input style = "display:none" type = "file" accept = ".json" on:change = {( e ) => importChatFromFile ( e )} bind:this= { chatFileInput } >
2023-07-17 14:01:16 -05:00
< input style = "display:none" type = "file" accept = ".json" on:change = {( e ) => importProfileFromFile ( e )} bind:this= { profileFileInput } >