Skip to content

Commit

Permalink
Merge pull request #200 from xesrc/add_import_export
Browse files Browse the repository at this point in the history
support for conversation export and import
  • Loading branch information
WongSaang authored May 10, 2023
2 parents da03f36 + cd9b6ae commit b626bde
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 94 deletions.
312 changes: 218 additions & 94 deletions components/NavigationDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ const setLang = (lang) => {
const conversations = useConversations()
const editingConversation = ref(null)
const deletingConversationIndex = ref(null)
const editingConversation = ref(false)
const deletingConversationIndex = ref(false)
const editConversation = (index) => {
editingConversation.value = conversations.value[index]
Expand All @@ -46,9 +46,11 @@ const updateConversation = async (index) => {
})
})
if (!error.value) {
editingConversation.value.updating = false
conversations.value[index] = editingConversation.value
}
editingConversation.value = null
conversations.value[index].updating = false
editingConversation.value = false
}
const deleteConversation = async (index) => {
Expand All @@ -66,6 +68,91 @@ const deleteConversation = async (index) => {
}
}
const snackbar = ref(false)
const snackbarText = ref('')
const showSnackbar = (text) => {
snackbarText.value = text
snackbar.value = true
}
const loadMessage = async (conversation_id) => {
const { data, error } = await useAuthFetch(`/api/chat/messages/?conversationId=${conversation_id}`)
if (!error.value) {
return data.value
}
return error.value
}
const exportConversation = async (index) => {
let conversation = conversations.value[index]
let data = {}
data.conversation_topic = conversation.topic
data.messages = []
let messages = await loadMessage(conversation.id)
for (let message of messages) {
let msg = {}
msg.role = message.is_bot ? "assistant" : "user"
msg.content = message.message
data.messages.push(msg)
}
let file_content = JSON.stringify(data)
let file_name = `${conversation.topic}_${new Date()}`.replace(/[\/\\:*?"<>]/g, "_")
const element = document.createElement('a');
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(file_content),
);
element.setAttribute("download", file_name);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
const openImportFileChooser = async () => {
let input_element = document.getElementById("import_conversation_input")
input_element.click()
}
const importConversation = async () => {
let input_element = document.getElementById("import_conversation_input")
let fileHandles = input_element.files
let imports = []
const reader = new FileReader()
for (let handle of fileHandles) {
let content = await new Promise((resolve, reject) => {
reader.readAsText(handle)
reader.onload = () => resolve(reader.result);
reader.onerror = eror => reject(error);
})
let json = JSON.parse(content)
imports.push(json)
}
let new_conversation_ids = []
try {
const { data, error } = await useAuthFetch('/api/upload_conversations/', {
method: 'POST',
headers: {
'accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
imports: imports,
}),
})
if (!error.value) {
new_conversation_ids = data.value
loadConversations()
} else {
console.log(err)
showSnackbar(err.message)
}
} catch (err) {
console.log(err.message)
showSnackbar(err.message)
}
}
const clearConversations = async () => {
deletingConversations.value = true
const { data, error } = await useAuthFetch(`/api/chat/conversations/delete_all`, {
Expand Down Expand Up @@ -218,6 +305,13 @@ const drawer = useDrawer()
@click.prevent="deleteConversation(cIdx)"
>
</v-btn>
<v-btn
icon="download"
size="small"
variant="text"
@click.prevent="exportConversation(cIdx)"
>
</v-btn>
</div>
</template>
</v-list-item>
Expand All @@ -227,104 +321,134 @@ const drawer = useDrawer()
</div>

<template v-slot:append>
<div class="px-1">
<v-divider></v-divider>
<v-list>

<v-dialog
v-model="clearConfirmDialog"
persistent
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
rounded="xl"
prepend-icon="delete_forever"
:title="$t('clearConversations')"
></v-list-item>
</template>
<v-card>
<v-card-title class="text-h5">
Are you sure you want to delete all conversations?
</v-card-title>
<v-card-text>This will be a permanent deletion and cannot be retrieved once deleted. Please proceed with caution.</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="green-darken-1"
variant="text"
@click="clearConfirmDialog = false"
class="text-none"
>
Cancel deletion
</v-btn>
<v-btn
color="green-darken-1"
variant="text"
@click="clearConversations"
class="text-none"
:loading="deletingConversations"
<v-divider></v-divider>
<v-expansion-panels style="flex-direction: column;">
<v-expansion-panel rounded="rounded-pill">
<v-expansion-panel-title expand-icon="add" collapse-icon="close">
<v-icon icon="settings" class="mr-4"></v-icon> {{ $t("settingDraw") }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<div class="px-1">
<v-list density="compact">

<v-dialog
v-model="clearConfirmDialog"
persistent
>
Confirm deletion
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
rounded="xl"
prepend-icon="delete_forever"
:title="$t('clearConversations')"
></v-list-item>
</template>
<v-card>
<v-card-title class="text-h5">
Are you sure you want to delete all conversations?
</v-card-title>
<v-card-text>This will be a permanent deletion and cannot be retrieved once deleted. Please proceed with caution.</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="green-darken-1"
variant="text"
@click="clearConfirmDialog = false"
class="text-none"
>
Cancel deletion
</v-btn>
<v-btn
color="green-darken-1"
variant="text"
@click="clearConversations"
class="text-none"
:loading="deletingConversations"
>
Confirm deletion
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>

<ApiKeyDialog
v-if="$settings.open_api_key_setting === 'True'"
/>

<ModelParameters/>

<v-menu
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
<v-list-item
rounded="xl"
:title="$t('themeMode')"
>
<template
v-slot:prepend
prepend-icon="input"
:title="$t('importConversation')"
@click="openImportFileChooser()"
></v-list-item>

<ApiKeyDialog
v-if="$settings.open_api_key_setting === 'True'"
/>

<ModelParameters/>

<v-menu
>
<v-icon
v-show="$colorMode.value === 'light'"
icon="light_mode"
></v-icon>
<v-icon
v-show="$colorMode.value !== 'light'"
icon="dark_mode"
></v-icon>
</template>
</v-list-item>
</template>
<v-list
bg-color="white"
>
<v-list-item
v-for="(theme, idx) in themes"
:key="idx"
@click="setTheme(theme.value)"
>
<v-list-item-title>{{ theme.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>

<SettingsLanguages/>

<v-list-item
rounded="xl"
prepend-icon="help_outline"
:title="$t('feedback')"
@click="feedback"
></v-list-item>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
rounded="xl"
:title="$t('themeMode')"
>
<template
v-slot:prepend
>
<v-icon
v-show="$colorMode.value === 'light'"
icon="light_mode"
></v-icon>
<v-icon
v-show="$colorMode.value !== 'light'"
icon="dark_mode"
></v-icon>
</template>
</v-list-item>
</template>
<v-list
bg-color="white"
>
<v-list-item
v-for="(theme, idx) in themes"
:key="idx"
@click="setTheme(theme.value)"
>
<v-list-item-title>{{ theme.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>

<SettingsLanguages/>

<v-list-item
rounded="xl"
prepend-icon="help_outline"
:title="$t('feedback')"
@click="feedback"
></v-list-item>

</v-list>
</div>
</v-list>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</template>
</v-navigation-drawer>
<v-snackbar v-model="snackbar" multi-line location="top">
{{ snackbarText }}
<template v-slot:actions>
<v-btn color="red" variant="text" @click="snackbar = false" density="compact" size="default">
Close
</v-btn>
</template>
</v-snackbar>
<input
type="file" id="import_conversation_input" style="display:none"
accept="text/plain, text/json"
multiple
@change="importConversation"
>
</template>

<style>
Expand Down
2 changes: 2 additions & 0 deletions lang/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
"maxTokenTips2": "token, which includes the length of the prompt and the length of the generated text. The `Max Tokens` here refers to the length of the generated text. Therefore, you should leave some space for your prompt and not set it too large or to the maximum.",
"frugalMode": "Frugal mode",
"frugalModeTip": "Activate frugal mode, the client will not send historical messages to ChatGPT, which can save token consumption. If you want ChatGPT to understand the context of the conversation, please turn off frugal mode.",
"settingDraw": "Settings",
"importConversation": "Import Conversation",
"messageLabel": {
"chat": "Write a message",
"web_search": "Search keywords",
Expand Down
2 changes: 2 additions & 0 deletions lang/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
"maxTokenTips2": "个 token,它包括了指令的长度和生成的文本长度。此处的最大 token 数量是指生成的文本长度。所以您应该为您的指令预留一些空间,不宜设置过大或拉满。",
"frugalMode": "节俭模式",
"frugalModeTip": "开启节俭模式,客户端不会把历史消息发送给ChatGPT,可以节省 token 的消耗。如果你想让 ChatGPT 了解对话的上下文,请关闭节俭模式。",
"settingDraw": "配置",
"importConversation": "导入对话",
"messageLabel": {
"chat": "输入信息",
"web_search": "输入关键词",
Expand Down

0 comments on commit b626bde

Please sign in to comment.