commit
79dd6c5e18
@ -1,17 +1,16 @@
|
||||
import type { GenericAbortSignal } from 'axios'
|
||||
import { post } from '@/utils/request'
|
||||
|
||||
export const controller = new AbortController()
|
||||
|
||||
export function fetchChatAPI<T = any>(
|
||||
prompt: string,
|
||||
options?: { conversationId?: string; parentMessageId?: string },
|
||||
signal?: GenericAbortSignal,
|
||||
) {
|
||||
return post<T>({
|
||||
url: '/chat',
|
||||
data: { prompt, options },
|
||||
})
|
||||
}
|
||||
|
||||
export function clearConversations<T = any>() {
|
||||
return post<T>({
|
||||
url: '/clear',
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { useHistoryStore } from '@/store'
|
||||
|
||||
export function useChat() {
|
||||
const historyStore = useHistoryStore()
|
||||
|
||||
function addChat(
|
||||
message: string,
|
||||
args?: { reversal?: boolean; error?: boolean; options?: Chat.ChatOptions },
|
||||
uuid?: number | null,
|
||||
) {
|
||||
historyStore.addChat(
|
||||
{
|
||||
dateTime: new Date().toLocaleString(),
|
||||
message,
|
||||
reversal: args?.reversal ?? false,
|
||||
error: args?.error ?? false,
|
||||
options: args?.options ?? undefined,
|
||||
},
|
||||
uuid,
|
||||
)
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
historyStore.clearChat()
|
||||
}
|
||||
|
||||
return { addChat, clearChat }
|
||||
}
|
@ -1,14 +1,67 @@
|
||||
<script setup lang='ts'>
|
||||
import { NScrollbar } from 'naive-ui'
|
||||
import ListItem from './ListItem.vue'
|
||||
import { ref } from 'vue'
|
||||
import { NInput, NScrollbar } from 'naive-ui'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { useHistoryStore } from '@/store'
|
||||
|
||||
const historyStore = useHistoryStore()
|
||||
|
||||
const dataSources = ref(historyStore.historyChat)
|
||||
|
||||
function handleSelect(index: number) {
|
||||
historyStore.chooseHistory(index)
|
||||
}
|
||||
|
||||
function handleEdit(index: number, isEdit: boolean) {
|
||||
historyStore.editHistory(index, isEdit)
|
||||
}
|
||||
|
||||
function handleRemove(index: number) {
|
||||
historyStore.removeHistory(index)
|
||||
}
|
||||
|
||||
function handleEnter(index: number, isEdit: boolean, event: KeyboardEvent) {
|
||||
if (event.key === 'Enter')
|
||||
handleEdit(index, isEdit)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NScrollbar class="px-4">
|
||||
<div class="flex flex-col gap-2 text-sm">
|
||||
<ListItem text="Learning correlation" />
|
||||
<ListItem text="Write Code" />
|
||||
<ListItem text="docs..." />
|
||||
<div v-for="(item, index) of dataSources" :key="index">
|
||||
<a
|
||||
class="relative flex items-center gap-3 px-3 py-3 break-all border rounded-md cursor-pointer pr-14 hover:bg-neutral-100 group"
|
||||
:class="historyStore.active === index && ['border-[#4b9e5f]', 'bg-neutral-100', 'text-[#4b9e5f]']"
|
||||
@click="handleSelect(index)"
|
||||
>
|
||||
<span>
|
||||
<SvgIcon icon="ri:message-3-line" />
|
||||
</span>
|
||||
<div class="relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap">
|
||||
<NInput
|
||||
v-if="item.isEdit" v-model:value="item.title" size="tiny"
|
||||
@keypress="handleEnter(index, false, $event)"
|
||||
/>
|
||||
<span v-else>{{ item.title }}</span>
|
||||
</div>
|
||||
<div class="absolute z-10 flex visible right-1">
|
||||
<template v-if="item.isEdit">
|
||||
<button class="p-1" @click="handleEdit(index, false)">
|
||||
<SvgIcon icon="ri:save-line" />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="p-1">
|
||||
<SvgIcon icon="ri:edit-line" @click="handleEdit(index, true)" />
|
||||
</button>
|
||||
<button class="p-1" @click="handleRemove(index)">
|
||||
<SvgIcon icon="ri:delete-bin-line" />
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</NScrollbar>
|
||||
</template>
|
||||
|
@ -1,52 +0,0 @@
|
||||
<script setup lang='ts'>
|
||||
import { SvgIcon } from '@/components/common'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(e: 'click',): void
|
||||
(e: 'edit',): void
|
||||
(e: 'delete',): void
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
function handleClick(event: Event) {
|
||||
emit('click')
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
emit('edit')
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
emit('delete')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
class="relative flex items-center gap-3 px-3 py-3 break-all rounded-md cursor-pointer bg-neutral-50 pr-14 hover:bg-neutral-100 group"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span>
|
||||
<SvgIcon icon="ri:message-3-line" />
|
||||
</span>
|
||||
<div class="relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap max-h-5">
|
||||
<span>{{ text }}</span>
|
||||
</div>
|
||||
<div class="absolute z-10 flex visible right-1">
|
||||
<button class="p-1" @click="handleEdit">
|
||||
<SvgIcon icon="ri:edit-line" />
|
||||
</button>
|
||||
<button class="p-1" @click="handleDelete">
|
||||
<SvgIcon icon="ri:delete-bin-line" />
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
@ -1,12 +0,0 @@
|
||||
export interface ChatOptions {
|
||||
conversationId?: string
|
||||
parentMessageId?: string
|
||||
}
|
||||
|
||||
export interface ChatProps {
|
||||
dateTime: string
|
||||
message: string
|
||||
reversal?: boolean
|
||||
error?: boolean
|
||||
options?: ChatOptions
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import '@/styles/global.css'
|
||||
|
||||
/** Tailwind's Preflight Style Override */
|
||||
function naiveStyleOverride() {
|
||||
const meta = document.createElement('meta')
|
||||
meta.name = 'naive-ui-style'
|
||||
document.head.appendChild(meta)
|
||||
}
|
||||
|
||||
function setupAssets() {
|
||||
naiveStyleOverride()
|
||||
}
|
||||
|
||||
export default setupAssets
|
@ -0,0 +1,3 @@
|
||||
import setupAssets from './assets'
|
||||
|
||||
export { setupAssets }
|
@ -0,0 +1,9 @@
|
||||
import type { App } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
export function setupStore(app: App) {
|
||||
const store = createPinia()
|
||||
app.use(store)
|
||||
}
|
||||
|
||||
export * from './modules'
|
@ -0,0 +1,20 @@
|
||||
import { ss } from '@/utils/storage'
|
||||
|
||||
const LOCAL_NAME = 'appSetting'
|
||||
|
||||
export interface AppState {
|
||||
siderCollapsed: boolean
|
||||
}
|
||||
|
||||
export function defaultSetting() {
|
||||
return { siderCollapsed: false }
|
||||
}
|
||||
|
||||
export function getLocalSetting() {
|
||||
const localSetting: AppState | undefined = ss.get(LOCAL_NAME)
|
||||
return localSetting ?? defaultSetting()
|
||||
}
|
||||
|
||||
export function setLocalSetting(setting: AppState) {
|
||||
ss.set(LOCAL_NAME, setting)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { AppState } from './helper'
|
||||
import { getLocalSetting, setLocalSetting } from './helper'
|
||||
|
||||
export const useAppStore = defineStore('app-store', {
|
||||
state: (): AppState => getLocalSetting(),
|
||||
actions: {
|
||||
setSiderCollapsed(collapsed: boolean) {
|
||||
this.siderCollapsed = collapsed
|
||||
setLocalSetting(this.$state)
|
||||
},
|
||||
toggleSiderCollapse() {
|
||||
this.setSiderCollapsed(!this.siderCollapsed)
|
||||
},
|
||||
},
|
||||
})
|
@ -0,0 +1,21 @@
|
||||
import { ss } from '@/utils/storage'
|
||||
|
||||
const LOCAL_NAME = 'historyChat'
|
||||
|
||||
export interface HistoryState {
|
||||
historyChat: Chat.HistoryChat[]
|
||||
active: number | null
|
||||
}
|
||||
|
||||
export function defaultSetting() {
|
||||
return { historyChat: [], active: null }
|
||||
}
|
||||
|
||||
export function getLocalHistory() {
|
||||
const localSetting: HistoryState | undefined = ss.get(LOCAL_NAME)
|
||||
return localSetting ?? defaultSetting()
|
||||
}
|
||||
|
||||
export function setLocalHistory(data: HistoryState) {
|
||||
ss.set(LOCAL_NAME, data)
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { HistoryState } from './helper'
|
||||
import { getLocalHistory, setLocalHistory } from './helper'
|
||||
export const useHistoryStore = defineStore('history-store', {
|
||||
state: (): HistoryState => getLocalHistory(),
|
||||
getters: {
|
||||
getCurrentHistory(state): Chat.HistoryChat {
|
||||
if (state.historyChat.length) {
|
||||
if (state.active === null || state.active >= state.historyChat.length || state.active < 0)
|
||||
state.active = 0
|
||||
return state.historyChat[state.active] ?? { title: '', isEdit: false, data: [] }
|
||||
}
|
||||
state.active = null
|
||||
return { title: '', isEdit: false, data: [] }
|
||||
},
|
||||
getCurrentChat(): Chat.Chat[] {
|
||||
return this.getCurrentHistory.data ?? []
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
addChat(data: Chat.Chat, uuid: number | null = null) {
|
||||
if (this.active === null) {
|
||||
this.historyChat.push({ title: data.message, isEdit: false, data: [data] })
|
||||
this.active = this.historyChat.length - 1
|
||||
}
|
||||
else {
|
||||
const active = uuid !== null ? uuid : this.active
|
||||
if (this.historyChat[active].title === 'New Chat')
|
||||
this.historyChat[active].title = data.message
|
||||
this.historyChat[active].data.push(data)
|
||||
}
|
||||
setLocalHistory(this.$state)
|
||||
},
|
||||
|
||||
clearChat() {
|
||||
if (this.active !== null) {
|
||||
this.historyChat[this.active].data = []
|
||||
setLocalHistory(this.$state)
|
||||
}
|
||||
},
|
||||
|
||||
addHistory(data: Chat.HistoryChat) {
|
||||
this.historyChat.push(data)
|
||||
this.active = this.historyChat.length - 1
|
||||
setLocalHistory(this.$state)
|
||||
},
|
||||
|
||||
editHistory(index: number, isEdit: boolean) {
|
||||
this.historyChat[index].isEdit = isEdit
|
||||
setLocalHistory(this.$state)
|
||||
},
|
||||
|
||||
removeHistory(index: number) {
|
||||
this.historyChat.splice(index, 1)
|
||||
if (this.active === index) {
|
||||
if (this.historyChat.length === 0)
|
||||
this.active = null
|
||||
else if (this.active === this.historyChat.length)
|
||||
this.active--
|
||||
else
|
||||
this.active = 0
|
||||
}
|
||||
setLocalHistory(this.$state)
|
||||
},
|
||||
|
||||
chooseHistory(index: number) {
|
||||
if (this.active === index)
|
||||
return
|
||||
this.active = index
|
||||
setLocalHistory(this.$state)
|
||||
},
|
||||
},
|
||||
})
|
@ -0,0 +1,2 @@
|
||||
export * from './app'
|
||||
export * from './history'
|
@ -0,0 +1,20 @@
|
||||
declare namespace Chat{
|
||||
interface ChatOptions {
|
||||
conversationId?: string
|
||||
parentMessageId?: string
|
||||
}
|
||||
|
||||
interface Chat {
|
||||
dateTime: string
|
||||
message: string
|
||||
reversal?: boolean
|
||||
error?: boolean
|
||||
options?: ChatOptions
|
||||
}
|
||||
|
||||
interface HistoryChat {
|
||||
title: string
|
||||
isEdit: boolean
|
||||
data: Chat[]
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
const CryptoSecret = '__CRYPTO_SECRET__'
|
||||
|
||||
export function enCrypto(data: any) {
|
||||
const str = JSON.stringify(data)
|
||||
return CryptoJS.AES.encrypt(str, CryptoSecret).toString()
|
||||
}
|
||||
|
||||
export function deCrypto(data: string) {
|
||||
const bytes = CryptoJS.AES.decrypt(data, CryptoSecret)
|
||||
const str = bytes.toString(CryptoJS.enc.Utf8)
|
||||
|
||||
if (str)
|
||||
return JSON.parse(str)
|
||||
|
||||
return null
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
export function isNumber<T extends number>(value: T | unknown): value is number {
|
||||
return Object.prototype.toString.call(value) === '[object Number]'
|
||||
}
|
||||
|
||||
export function isString<T extends string>(value: T | unknown): value is string {
|
||||
return Object.prototype.toString.call(value) === '[object String]'
|
||||
}
|
||||
|
||||
export function isBoolean<T extends boolean>(value: T | unknown): value is boolean {
|
||||
return Object.prototype.toString.call(value) === '[object Boolean]'
|
||||
}
|
||||
|
||||
export function isNull<T extends null>(value: T | unknown): value is null {
|
||||
return Object.prototype.toString.call(value) === '[object Null]'
|
||||
}
|
||||
|
||||
export function isUndefine<T extends undefined>(value: T | unknown): value is undefined {
|
||||
return Object.prototype.toString.call(value) === '[object Undefined]'
|
||||
}
|
||||
|
||||
export function isObject<T extends object>(value: T | unknown): value is object {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
|
||||
export function isArray<T extends any[]>(value: T | unknown): value is T {
|
||||
return Object.prototype.toString.call(value) === '[object Array]'
|
||||
}
|
||||
|
||||
export function isFunction<T extends (...args: any[]) => any | void | never>(value: T | unknown): value is T {
|
||||
return Object.prototype.toString.call(value) === '[object Function]'
|
||||
}
|
||||
|
||||
export function isDate<T extends Date>(value: T | unknown): value is T {
|
||||
return Object.prototype.toString.call(value) === '[object Date]'
|
||||
}
|
||||
|
||||
export function isRegExp<T extends RegExp>(value: T | unknown): value is T {
|
||||
return Object.prototype.toString.call(value) === '[object RegExp]'
|
||||
}
|
||||
|
||||
export function isPromise<T extends Promise<any>>(value: T | unknown): value is T {
|
||||
return Object.prototype.toString.call(value) === '[object Promise]'
|
||||
}
|
||||
|
||||
export function isSet<T extends Set<any>>(value: T | unknown): value is T {
|
||||
return Object.prototype.toString.call(value) === '[object Set]'
|
||||
}
|
||||
|
||||
export function isMap<T extends Map<any, any>>(value: T | unknown): value is T {
|
||||
return Object.prototype.toString.call(value) === '[object Map]'
|
||||
}
|
||||
|
||||
export function isFile<T extends File>(value: T | unknown): value is T {
|
||||
return Object.prototype.toString.call(value) === '[object File]'
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './local'
|
@ -0,0 +1,70 @@
|
||||
import { deCrypto, enCrypto } from '../crypto'
|
||||
|
||||
interface StorageData<T = any> {
|
||||
data: T
|
||||
expire: number | null
|
||||
}
|
||||
|
||||
export function createLocalStorage(options?: { expire?: number | null; crypto?: boolean }) {
|
||||
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7
|
||||
|
||||
const { expire, crypto } = Object.assign(
|
||||
{
|
||||
expire: DEFAULT_CACHE_TIME,
|
||||
crypto: true,
|
||||
},
|
||||
options,
|
||||
)
|
||||
|
||||
function set<T = any>(key: string, data: T) {
|
||||
const storageData: StorageData<T> = {
|
||||
data,
|
||||
expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
|
||||
}
|
||||
|
||||
const json = crypto ? enCrypto(storageData) : JSON.stringify(storageData)
|
||||
window.localStorage.setItem(key, json)
|
||||
}
|
||||
|
||||
function get(key: string) {
|
||||
const json = window.localStorage.getItem(key)
|
||||
if (json) {
|
||||
let storageData: StorageData | null = null
|
||||
|
||||
try {
|
||||
storageData = crypto ? deCrypto(json) : JSON.parse(json)
|
||||
}
|
||||
catch {
|
||||
// Prevent failure
|
||||
}
|
||||
|
||||
if (storageData) {
|
||||
const { data, expire } = storageData
|
||||
if (expire === null || expire >= Date.now())
|
||||
return data
|
||||
}
|
||||
|
||||
remove(key)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function remove(key: string) {
|
||||
window.localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
window.localStorage.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
set,
|
||||
get,
|
||||
remove,
|
||||
clear,
|
||||
}
|
||||
}
|
||||
|
||||
export const ls = createLocalStorage()
|
||||
|
||||
export const ss = createLocalStorage({ expire: null, crypto: false })
|
Loading…
Reference in New Issue