提交 d5f82065 编写于 作者: D DebugIsFalse

fix: eslint

上级 ddccb0a3
...@@ -22,4 +22,6 @@ logs ...@@ -22,4 +22,6 @@ logs
.env .env
.env.* .env.*
!.env.example !.env.example
yarn.lock yarn.lock
\ No newline at end of file .eslintcache
.vscode/
\ No newline at end of file
<template> <template>
<NuxtPwaManifest /> <NuxtPwaManifest />
<NuxtLoadingIndicator /> <NuxtLoadingIndicator />
<NuxtLayout> <NuxtLayout>
<NuxtPage /> <NuxtPage />
</NuxtLayout> </NuxtLayout>
<UNotifications /> <UNotifications />
<UModals /> <UModals />
</template> </template>
<script setup> <script setup>
const { $updateUserInfo } = useUserStore() const { $updateUserInfo } = useUserStore()
useHead({ useHead({
title: 'GitBot AI' title: 'GitBot AI'
}) })
nextTick(() => { nextTick(() => {
$updateUserInfo() $updateUserInfo()
}) })
</script> </script>
<template> <template>
<UCard :ui="{ body: { padding: 'p-4 sm:p-4' } }"> <UCard :ui="{ body: { padding: 'p-4 sm:p-4' } }">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex items-center gap-2 text-lg"> <div class="flex items-center gap-2 text-lg">
<UIcon name="i-heroicons-sparkles-20-solid" /> <UIcon name="i-heroicons-sparkles-20-solid" />
搜索过程 搜索过程
</div> </div>
<UButton <UButton
size="md" size="md"
color="gray" color="gray"
variant="ghost" variant="ghost"
:icon="openCollapse ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'" :icon="openCollapse ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'"
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
@click="handleToggleCollapse" @click="handleToggleCollapse"
/> />
</div> </div>
<ICollapse :open="openCollapse" class="mt-2"> <ICollapse :open="openCollapse" class="mt-2">
<div class="flex flex-col gap-2 w-full text-gray-500 dark:text-gray-400"> <div class="flex flex-col gap-2 w-full text-gray-500 dark:text-gray-400">
<div class="text-base flex items-center gap-1"> <div class="text-base flex items-center gap-1">
<UIcon name="i-heroicons-inbox-arrow-down" /> <UIcon name="i-heroicons-inbox-arrow-down" />
理解问题 理解问题
</div> </div>
<template v-if="item.desLoading"> <template v-if="item.desLoading">
<USkeleton class="h-4" /> <USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" /> <USkeleton class="h-4 w-2/3" />
</template> </template>
<IMdMdc v-else :content="item.description" /> <IMdMdc v-else :content="item.description" />
<template v-if="item.searchLoading !== undefined"> <template v-if="item.searchLoading !== undefined">
<div class="text-base flex items-center gap-1"> <div class="text-base flex items-center gap-1">
<UIcon name="i-heroicons-magnifying-glass" /> <UIcon name="i-heroicons-magnifying-glass" />
搜索项目 搜索项目
</div> </div>
<template v-if="item.searchLoading"> <template v-if="item.searchLoading">
<USkeleton class="h-4" /> <USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" /> <USkeleton class="h-4 w-2/3" />
</template> </template>
<div class="text-xs pl-5" v-else>找到 {{ item.source && item.source.length || 0 }} 条来源</div> <div v-else class="text-xs pl-5">找到 {{ item.source && item.source.length || 0 }} 条来源</div>
</template> </template>
<div class="text-base flex items-center gap-1" v-if="item.ansLoading !== undefined"> <div v-if="item.ansLoading !== undefined" class="text-base flex items-center gap-1">
<UIcon name="i-heroicons-pencil-square" /> <UIcon name="i-heroicons-pencil-square" />
整理答案 整理答案
</div> </div>
</div> </div>
</ICollapse> </ICollapse>
</UCard> </UCard>
</template> </template>
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
type: { type: {
type: String, type: String,
default: 'search' default: 'search'
}, },
item: { item: {
type: Object, type: Object,
default: (() => {}) default: (() => {})
}, },
collapse: { collapse: {
type: Boolean, type: Boolean,
default: true default: true
} }
}) })
const openCollapse = ref(props.collapse) const openCollapse = ref(props.collapse)
function handleToggleCollapse () { function handleToggleCollapse () {
openCollapse.value = !openCollapse.value openCollapse.value = !openCollapse.value
} }
function handleCollapse (state) { function handleCollapse (state) {
openCollapse.value = state openCollapse.value = state
} }
watch(() => props.collapse, () => { watch(() => props.collapse, () => {
if (props.collapse) { if (props.collapse) {
handleCollapse(true) handleCollapse(true)
} else { } else {
setTimeout(() => { setTimeout(() => {
handleCollapse(false) handleCollapse(false)
}, 500) }, 500)
} }
}, { immediate: true}) }, { immediate: true})
defineExpose({ handleCollapse }) defineExpose({ handleCollapse })
</script> </script>
<template> <template>
<div class="flex flex-col overflow-hidden"> <div class="flex flex-col overflow-hidden">
<div class="flex justify-between"> <div class="flex justify-between">
<UButton <UButton
class="flex-grow" class="flex-grow"
leading-icon="i-heroicons-magnifying-glass" leading-icon="i-heroicons-magnifying-glass"
color="gray" color="gray"
variant="ghost" variant="ghost"
size="md" size="md"
label="搜索记录" label="搜索记录"
@click="handleOpen" @click="handleOpen"
/> />
<UButton <UButton
v-if="$isSignIn" v-if="$isSignIn"
label="清空" label="清空"
size="md" size="md"
variant="link" variant="link"
@click="handleClear" @click="handleClear"
/> />
</div> </div>
<div v-if="$isSignIn" class="flex overflow-y-auto flex-col gap-1 border-l border-gray-200 dark:border-gray-800 pl-2 ml-5"> <div v-if="$isSignIn" class="flex overflow-y-auto flex-col gap-1 border-l border-gray-200 dark:border-gray-800 pl-2 ml-5">
<template v-for="(item, index) in $searchHistory" :key="index"> <template v-for="(item, index) in $searchHistory" :key="index">
<UButton <UButton
class="flex group text-gray-400" class="flex group text-gray-400"
color="gray" color="gray"
variant="ghost" variant="ghost"
size="xs" size="xs"
long long
:to="`/search/${item.c_id}`" :to="`/search/${item.c_id}`"
> >
<div class="flex-grow truncate">{{ item.title }}</div> <div class="flex-grow truncate">{{ item.title }}</div>
<UButton <UButton
class="hidden group-hover:flex" class="hidden group-hover:flex"
color="red" color="red"
variant="ghost" variant="ghost"
size="xs" size="xs"
:padded="false" :padded="false"
leading-icon="i-heroicons-x-mark-20-solid" leading-icon="i-heroicons-x-mark-20-solid"
@click.stop.prevent="handleRemoveRecordItem(item.c_id)" @click.stop.prevent="handleRemoveRecordItem(item.c_id)"
/> />
</UButton> </UButton>
</template> </template>
</div> </div>
</div> </div>
<UModal v-model="isOpenHistory" :ui="{ width: 'w-full sm:max-w-screen-md' }"> <UModal v-model="isOpenHistory" :ui="{ width: 'w-full sm:max-w-screen-md' }">
<div class="flex items-center p-2"> <div class="flex items-center p-2">
<UInput <UInput
class="w-full" v-model="query"
v-model="query" class="w-full"
:padded="false" :padded="false"
variant="none" variant="none"
leading-icon="i-heroicons-magnifying-glass-20-solid" leading-icon="i-heroicons-magnifying-glass-20-solid"
placeholder="输入关键字搜索..." placeholder="输入关键字搜索..."
/> />
<UButton <UButton
leading-icon="i-heroicons-x-mark-20-solid" leading-icon="i-heroicons-x-mark-20-solid"
color="gray" color="gray"
variant="ghost" variant="ghost"
@click="handleClose" @click="handleClose"
/> />
</div> </div>
<UDivider /> <UDivider />
<div class="flex flex-col p-2"> <div class="flex flex-col p-2">
<template v-for="(item, index) in $searchHistory" :key="index"> <template v-for="(item, index) in $searchHistory" :key="index">
<UButton <UButton
class="flex group" class="flex group"
color="gray" color="gray"
variant="ghost" variant="ghost"
long long
leading-icon="i-heroicons-document-text" leading-icon="i-heroicons-document-text"
:to="`/search/${item.c_id}`" :to="`/search/${item.c_id}`"
@click="handleClose" @click="handleClose"
> >
<div class="flex-grow truncate font-light">{{ item.title }}</div> <div class="flex-grow truncate font-light">{{ item.title }}</div>
<UButton <UButton
class="hidden group-hover:flex" class="hidden group-hover:flex"
color="red" color="red"
variant="ghost" variant="ghost"
:padded="false" :padded="false"
leading-icon="i-heroicons-x-mark-20-solid" leading-icon="i-heroicons-x-mark-20-solid"
@click.stop.prevent="handleRemoveRecordItem(item.c_id)" @click.stop.prevent="handleRemoveRecordItem(item.c_id)"
/> />
</UButton> </UButton>
</template> </template>
</div> </div>
</UModal> </UModal>
</template> </template>
<script setup> <script setup>
import { IConfirm } from '#components' import { IConfirm } from '#components'
...@@ -95,45 +95,45 @@ const { $getSearchHistory } = useSearchStore() ...@@ -95,45 +95,45 @@ const { $getSearchHistory } = useSearchStore()
const isOpenHistory = ref(false) const isOpenHistory = ref(false)
const query = ref('') const query = ref('')
function handleClear () { function handleClear () {
modal.open(IConfirm, { modal.open(IConfirm, {
title: '清空确认', title: '清空确认',
description: '确定要清空全部搜索记录吗?', description: '确定要清空全部搜索记录吗?',
onSuccess () { onSuccess () {
modal.close() modal.close()
emits('clear') emits('clear')
handleRemoveRecords() handleRemoveRecords()
}, },
onCancel () { onCancel () {
modal.close() modal.close()
} }
}) })
} }
function handleRemoveRecordItem (id) { function handleRemoveRecordItem (id) {
handleRemoveRecords([id]) handleRemoveRecords([id])
} }
async function handleRemoveRecords (ids) { async function handleRemoveRecords (ids) {
if (!ids) { if (!ids) {
ids = $searchHistory.value.map(item => item.c_id) ids = $searchHistory.value.map(item => item.c_id)
} }
const { data} = await useRequest('/v1/chat/completion/remove', { const { data} = await useRequest('/v1/chat/completion/remove', {
method: 'post', method: 'post',
body: ids body: ids
}) })
if (data.value) { if (data.value) {
$getSearchHistory() $getSearchHistory()
navigateTo('/') navigateTo('/')
} }
} }
function handleOpen () { function handleOpen () {
if (!$isSignIn) emits('sign') if (!$isSignIn) emits('sign')
else { else {
isOpenHistory.value = true isOpenHistory.value = true
} }
} }
function handleClose () { function handleClose () {
isOpenHistory.value = false isOpenHistory.value = false
} }
nextTick(() => { nextTick(() => {
$getSearchHistory() $getSearchHistory()
}) })
</script> </script>
<template> <template>
<UPopover :popper="{ strategy: 'absolute' }" :ui="{ width: 'w-[156px]' }"> <UPopover :popper="{ strategy: 'absolute' }" :ui="{ width: 'w-[156px]' }">
<template #default="{ open }"> <template #default="{ open }">
<UButton <UButton
color="gray" color="gray"
variant="ghost" variant="ghost"
square square
:class="[open && 'bg-gray-50 dark:bg-gray-800']" :class="[open && 'bg-gray-50 dark:bg-gray-800']"
icon="i-heroicons-swatch-16-solid" icon="i-heroicons-swatch-16-solid"
:ui="{ icon: { base: 'text-primary-500 dark:text-primary-400' } }" :ui="{ icon: { base: 'text-primary-500 dark:text-primary-400' } }"
aria-label="Color picker" aria-label="Color picker"
/> />
</template> </template>
<template #panel> <template #panel>
<div class="flex flex-col p-2 gap-2"> <div class="flex flex-col p-2 gap-2">
<div class="grid grid-cols-5 gap-px"> <div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in primaryColors" :key="color.value" :color="color" :selected="primary" @select="primary = color" /> <ColorPickerPill v-for="color in primaryColors" :key="color.value" :color="color" :selected="primary" @select="primary = color" />
</div> </div>
<UDivider /> <UDivider />
<div class="grid grid-cols-5 gap-px"> <div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in grayColors" :key="color.value" :color="color" :selected="gray" @select="gray = color" /> <ColorPickerPill v-for="color in grayColors" :key="color.value" :color="color" :selected="gray" @select="gray = color" />
</div> </div>
</div> </div>
</template> </template>
</UPopover> </UPopover>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
...@@ -38,25 +38,25 @@ const colorMode = useColorMode() ...@@ -38,25 +38,25 @@ const colorMode = useColorMode()
const primaryColors = computed(() => appConfig.ui.colors.filter(color => color !== 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] }))) const primaryColors = computed(() => appConfig.ui.colors.filter(color => color !== 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const primary = computed({ const primary = computed({
get () { get () {
return primaryColors.value.find(option => option.value === appConfig.ui.primary) return primaryColors.value.find(option => option.value === appConfig.ui.primary)
}, },
set (option) { set (option) {
appConfig.ui.primary = option.value appConfig.ui.primary = option.value
window.localStorage.setItem('nuxt-ui-primary', appConfig.ui.primary) window.localStorage.setItem('nuxt-ui-primary', appConfig.ui.primary)
} }
}) })
const grayColors = computed(() => ['slate', 'cool', 'zinc', 'neutral', 'stone'].map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] }))) const grayColors = computed(() => ['slate', 'cool', 'zinc', 'neutral', 'stone'].map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const gray = computed({ const gray = computed({
get () { get () {
return grayColors.value.find(option => option.value === appConfig.ui.gray) return grayColors.value.find(option => option.value === appConfig.ui.gray)
}, },
set (option) { set (option) {
appConfig.ui.gray = option.value appConfig.ui.gray = option.value
window.localStorage.setItem('nuxt-ui-gray', appConfig.ui.gray) window.localStorage.setItem('nuxt-ui-gray', appConfig.ui.gray)
} }
}) })
</script> </script>
\ No newline at end of file
<template> <template>
<UTooltip :text="color.value" class="capitalize" :open-delay="500"> <UTooltip :text="color.value" class="capitalize" :open-delay="500">
<UButton <UButton
color="white" color="white"
square square
:ui="{ :ui="{
color: { color: {
white: { white: {
solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800', solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800',
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50' ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
} }
} }
}" }"
:variant="color.value === selected.value ? 'solid' : 'ghost'" :variant="color.value === selected.value ? 'solid' : 'ghost'"
@click.stop.prevent="$emit('select')" @click.stop.prevent="$emit('select')"
> >
<span class="inline-block w-3 h-3 rounded-full" :style="{ backgroundColor: color.hex }" /> <span class="inline-block w-3 h-3 rounded-full" :style="{ backgroundColor: color.hex }" />
</UButton> </UButton>
</UTooltip> </UTooltip>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
......
<template> <template>
<div class="flex-grow overflow-hidden bg-gray-100 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800"> <div class="flex-grow overflow-hidden bg-gray-100 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800">
<div class="p-4 h-full flex flex-col"> <div class="p-4 h-full flex flex-col">
<ILogo class="mt-2" /> <ILogo class="mt-2" />
<UButton <UButton
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
class="flex w-full mt-6" class="flex w-full mt-6"
color="gray" color="gray"
leading-icon="i-heroicons-plus-20-solid" leading-icon="i-heroicons-plus-20-solid"
size="md" size="md"
@click="handleShowCreate" @click="handleShowCreate"
> >
<div class="flex flex-grow justify-between items-center"> <div class="flex flex-grow justify-between items-center">
<span>新主题</span> <span>新主题</span>
<div class="flex items-center gap-0.5" v-if="$device.isDesktop"> <div v-if="$device.isDesktop" class="flex items-center gap-0.5">
<UKbd>{{ metaSymbol }}</UKbd> <UKbd>{{ metaSymbol }}</UKbd>
<UKbd>K</UKbd> <UKbd>K</UKbd>
</div> </div>
</div> </div>
</UButton> </UButton>
<div class="flex flex-grow overflow-hidden mt-4"> <div class="flex flex-grow overflow-hidden mt-4">
<INav /> <INav />
</div> </div>
</div> </div>
</div> </div>
<UDivider /> <UDivider />
<div class="bg-gray-100 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col items-center justify-center"> <div class="bg-gray-100 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col items-center justify-center">
<IUserInfo /> <IUserInfo />
</div> </div>
<UModal v-model="isOpenCreate" :ui="{ width: 'w-full sm:max-w-screen-md' }"> <UModal v-model="isOpenCreate" :ui="{ width: 'w-full sm:max-w-screen-md' }">
<ICreate @search="handleCloseCreate" /> <ICreate @search="handleCloseCreate" />
</UModal> </UModal>
</template> </template>
<script setup> <script setup>
const { metaSymbol } = useShortcuts() const { metaSymbol } = useShortcuts()
const isOpenCreate = ref(false) const isOpenCreate = ref(false)
const handleShowCreate = () => { const handleShowCreate = () => {
isOpenCreate.value = true isOpenCreate.value = true
} }
const handleCloseCreate = () => { const handleCloseCreate = () => {
isOpenCreate.value = false isOpenCreate.value = false
} }
defineShortcuts({ defineShortcuts({
meta_k: { meta_k: {
handler: () => { handler: () => {
handleShowCreate() handleShowCreate()
}
} }
}
}) })
</script> </script>
<template> <template>
<Transition <Transition
@enter="onEnter" @enter="onEnter"
@after-enter="onAfterEnter" @after-enter="onAfterEnter"
@before-leave="onBeforeLeave" @before-leave="onBeforeLeave"
@leave="onLeave" @leave="onLeave"
> >
<div v-show="open" class="flex transition-[height] overflow-hidden"><slot /></div> <div v-show="open" class="flex transition-[height] overflow-hidden"><slot /></div>
</Transition> </Transition>
</template> </template>
<script setup> <script setup>
const props = defineProps({ defineProps({
open: { open: {
type: Boolean, type: Boolean,
default: true default: true
} }
}) })
function onEnter(_el, done) { function onEnter(_el, done) {
const el = _el const el = _el
el.style.height = '0' el.style.height = '0'
el.offsetHeight el.offsetHeight
el.style.height = el.scrollHeight + 'px' el.style.height = el.scrollHeight + 'px'
el.addEventListener('transitionend', done, { once: true }) el.addEventListener('transitionend', done, { once: true })
} }
function onBeforeLeave(_el) { function onBeforeLeave(_el) {
const el = _el const el = _el
el.style.height = el.scrollHeight + 'px' el.style.height = el.scrollHeight + 'px'
el.offsetHeight el.offsetHeight
} }
function onAfterEnter(_el) { function onAfterEnter(_el) {
const el = _el const el = _el
el.style.height = 'auto' el.style.height = 'auto'
} }
function onLeave(_el, done) { function onLeave(_el, done) {
const el = _el const el = _el
el.style.height = '0' el.style.height = '0'
el.addEventListener('transitionend', done, { once: true }) el.addEventListener('transitionend', done, { once: true })
} }
</script> </script>
<template> <template>
<UModal :ui="{ width: 'w-96 sm:max-w-screen-md' }"> <UModal :ui="{ width: 'w-96 sm:max-w-screen-md' }">
<div class="flex p-6 gap-4"> <div class="flex p-6 gap-4">
<div class="flex"> <div class="flex">
<div class="mx-auto flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-red-100"> <div class="mx-auto flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-red-100">
<UIcon name="i-heroicons-exclamation-triangle" class="w-6 h-6 text-red-600" /> <UIcon name="i-heroicons-exclamation-triangle" class="w-6 h-6 text-red-600" />
</div> </div>
</div> </div>
<div class="flex-grow flex flex-col"> <div class="flex-grow flex flex-col">
<div class="text-base leading-6 text-gray-900 dark:text-gray-300 truncate">{{ title }}</div> <div class="text-base leading-6 text-gray-900 dark:text-gray-300 truncate">{{ title }}</div>
<div class="mt-2 text-sm text-gray-500">{{ description }}</div> <div class="mt-2 text-sm text-gray-500">{{ description }}</div>
</div> </div>
</div> </div>
<div class="p-4 flex justify-end gap-2"> <div class="p-4 flex justify-end gap-2">
<UButton color="white" @click="handleCancel">取消</UButton> <UButton color="white" @click="handleCancel">取消</UButton>
<UButton color="red" @click="handleSuccess">确定</UButton> <UButton color="red" @click="handleSuccess">确定</UButton>
</div> </div>
</UModal> </UModal>
</template> </template>
<script setup> <script setup>
defineProps({ defineProps({
title: { title: {
type: String, type: String,
default: '' default: ''
}, },
description: { description: {
type: String, type: String,
default: '确认要全部清空吗?' default: '确认要全部清空吗?'
}, },
async: { async: {
type: Boolean, type: Boolean,
default: false default: false
} }
}) })
const emits = defineEmits(['success', 'cancel']) const emits = defineEmits(['success', 'cancel'])
function handleCancel () { function handleCancel () {
emits('cancel') emits('cancel')
} }
function handleSuccess () { function handleSuccess () {
emits('success') emits('success')
} }
</script> </script>
<template> <template>
<div class="max-w-screen-md w-full flex flex-col space-y-4 p-6"> <div class="max-w-screen-md w-full flex flex-col space-y-4 p-6">
<UCard <UCard
class="transition-[box-shadow] hover:ring-2 has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-500 dark:has-[textarea:focus]:ring-primary-400" class="transition-[box-shadow] hover:ring-2 has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-500 dark:has-[textarea:focus]:ring-primary-400"
:ui="cardUI" :ui="cardUI"
> >
<UTextarea <UTextarea
name="createInput" v-model="query"
v-model="query" name="createInput"
autoresize autoresize
placeholder="输入搜索内容..." placeholder="输入搜索内容..."
:rows="5" :rows="5"
variant="none" variant="none"
:padded="false" :padded="false"
maxlength="2000" maxlength="2000"
/> />
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<UTooltip class="relative" :text="isPro ? '已开启专家搜索' : '已关闭专家搜索'" :shortcuts="[metaSymbol, 'O']"> <UTooltip class="relative" :text="isPro ? '已开启专家搜索' : '已关闭专家搜索'" :shortcuts="[metaSymbol, 'O']">
<div class="absolute w-7 h-0.5 rotate-45 top-3.5 left-1 bg-gray-700 dark:bg-gray-200 hover:bg-gray-900 dark:hover:bg-white rounded" v-show="!isPro"></div> <div v-show="!isPro" class="absolute w-7 h-0.5 rotate-45 top-3.5 left-1 bg-gray-700 dark:bg-gray-200 hover:bg-gray-900 dark:hover:bg-white rounded"/>
<UButton <UButton
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
:icon="isPro ? 'i-heroicons-sparkles-20-solid' : 'i-heroicons-sparkles-20-solid'" :icon="isPro ? 'i-heroicons-sparkles-20-solid' : 'i-heroicons-sparkles-20-solid'"
color="gray" color="gray"
variant="ghost" variant="ghost"
@click="handleTogglePro" @click="handleTogglePro"
/> />
</UTooltip> </UTooltip>
<USelectMenu <USelectMenu
:ui-menu="menuUI" v-model="selectedRepo"
v-model="selectedRepo" :ui-menu="menuUI"
:options="$repos" :options="$repos"
placeholder="选择 GitHub 项目" placeholder="选择 GitHub 项目"
value-attribute="label" value-attribute="label"
option-attribute="label" option-attribute="label"
> >
<UTooltip :text="isPro ? '选择 GitHub 项目' : '选择 GitHub 项目(需开启专家搜索)'" v-if="!selectedRepo"> <UTooltip v-if="!selectedRepo" :text="isPro ? '选择 GitHub 项目' : '选择 GitHub 项目(需开启专家搜索)'">
<UButton <UButton
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
icon="i-simple-icons-github" icon="i-simple-icons-github"
color="gray" color="gray"
variant="ghost" variant="ghost"
:disabled="!isPro" :disabled="!isPro"
/> />
</UTooltip> </UTooltip>
<UButton v-else color="gray" variant="ghost" :class="{ 'group': selectedRepo }"> <UButton v-else color="gray" variant="ghost" :class="{ 'group': selectedRepo }">
<UIcon name="i-simple-icons-github" /> <UIcon name="i-simple-icons-github" />
<span>{{ selectedRepo }}</span> <span>{{ selectedRepo }}</span>
<UIcon name="i-heroicons-chevron-down-20-solid" class="text-xl flex group-hover:hidden" /> <UIcon name="i-heroicons-chevron-down-20-solid" class="text-xl flex group-hover:hidden" />
<UButton <UButton
v-if="selectedRepo" v-if="selectedRepo"
class="hidden group-hover:flex" class="hidden group-hover:flex"
@click.stop.prevent="handleClearRepo" icon="i-heroicons-x-mark-20-solid"
icon="i-heroicons-x-mark-20-solid" :padded="false"
:padded="false" color="gray"
color="gray" variant="link"
variant="link" @click.stop.prevent="handleClearRepo"
/> />
</UButton> </UButton>
</USelectMenu> </USelectMenu>
</div> </div>
<UTooltip text="搜索" :shortcuts="[metaSymbol, '↵']"> <UTooltip text="搜索" :shortcuts="[metaSymbol, '↵']">
<UButton <UButton
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
icon="i-heroicons-chevron-right-20-solid" icon="i-heroicons-chevron-right-20-solid"
@click="handleSearch" :loading="loading"
:loading="loading" :disabled="query === ''"
:disabled="query === ''" size="md"
size="md" @click="handleSearch"
/> />
</UTooltip> </UTooltip>
</div> </div>
</UCard> </UCard>
</div> </div>
</template> </template>
<script setup> <script setup>
const { $repos } = storeToRefs(useReposStore()) const { $repos } = storeToRefs(useReposStore())
...@@ -84,74 +84,74 @@ const query = ref('') ...@@ -84,74 +84,74 @@ const query = ref('')
const selectedRepo = ref('') const selectedRepo = ref('')
const loading = ref(false) const loading = ref(false)
const cardUI = { const cardUI = {
body: { body: {
padding: 'p-3 sm:p-3' padding: 'p-3 sm:p-3'
}, },
rounded: 'rounded-xl' rounded: 'rounded-xl'
} }
const menuUI = { const menuUI = {
width: 'w-auto' width: 'w-auto'
} }
const isPro = ref(true) const isPro = ref(true)
const handleSearch = async () => { const handleSearch = async () => {
if (loading.value || query.value === '') return if (loading.value || query.value === '') return
loading.value = true loading.value = true
const currentRepo = $repos.value.find(item => item.label === selectedRepo.value) const currentRepo = $repos.value.find(item => item.label === selectedRepo.value)
const repo_path = currentRepo ? currentRepo.url : '' const repo_path = currentRepo ? currentRepo.url : ''
const body = { const body = {
title: query.value, title: query.value,
repo_path: repo_path || '' repo_path: repo_path || ''
} }
const { data, error } = await useRequest('/v1/chat/completion/create', { method: 'post', body }) const { data, error } = await useRequest('/v1/chat/completion/create', { method: 'post', body })
loading.value = false loading.value = false
if (error.value) return if (error.value) return
// todo 临时存到pina给搜索使用 // todo 临时存到pina给搜索使用
$setFirstRecordTitle(query.value) $setFirstRecordTitle(query.value)
navigateTo(`/search/${data.value.data.c_id}`) navigateTo(`/search/${data.value.data.c_id}`)
emits('search') emits('search')
} }
function handleQuickSearch (title) { function handleQuickSearch (title) {
query.value = title query.value = title
handleSearch() handleSearch()
} }
defineExpose({ defineExpose({
handleQuickSearch handleQuickSearch
}) })
function handleClearRepo () { function handleClearRepo () {
selectedRepo.value = '' selectedRepo.value = ''
} }
nextTick(async () => { nextTick(async () => {
if (!$repos.value.length) { if (!$repos.value.length) {
const { data } = await useRequest('/v1/chat/repository') const { data } = await useRequest('/v1/chat/repository')
const repoData = data.value.data.map(item => { const repoData = data.value.data.map(item => {
return { return {
label: item.name, label: item.name,
url: item.path, url: item.path,
branch: item.branch branch: item.branch
} }
}) })
$setRepo(repoData) $setRepo(repoData)
} }
}) })
defineShortcuts({ defineShortcuts({
meta_enter: { meta_enter: {
usingInput: 'createInput', usingInput: 'createInput',
handler: () => { handler: () => {
handleSearch() handleSearch()
} }
}, },
meta_o: { meta_o: {
usingInput: 'createInput', usingInput: 'createInput',
handler: () => { handler: () => {
handleTogglePro() handleTogglePro()
}
} }
}
}) })
function handleTogglePro () { function handleTogglePro () {
isPro.value = !isPro.value isPro.value = !isPro.value
if (!isPro.value) handleClearRepo() if (!isPro.value) handleClearRepo()
} }
</script> </script>
<template> <template>
<div class="flex flex-col justify-center h-72 items-center text-gray-300" :class="{ 'h-36': size === 'xs' }"> <div class="flex flex-col justify-center h-72 items-center text-gray-300" :class="{ 'h-36': size === 'xs' }">
<UIcon name="i-heroicons-inbox" class="text-6xl" /> <UIcon name="i-heroicons-inbox" class="text-6xl" />
<div>暂无数据</div> <div>暂无数据</div>
</div> </div>
</template> </template>
<script setup> <script setup>
defineProps({ defineProps({
size: { size: {
type: String, type: String,
default: 'sm' default: 'sm'
} }
}) })
</script> </script>
\ No newline at end of file
<template> <template>
<div class="grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8"> <div class="grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8">
<div class="text-center"> <div class="text-center">
<p class="text-base font-semibold text-red-600">{{ code }}</p> <p class="text-base font-semibold text-red-600">{{ code }}</p>
<h1 class="mt-4 text-3xl font-bold tracking-tight sm:text-5xl">{{ errorTitle }}</h1> <h1 class="mt-4 text-3xl font-bold tracking-tight sm:text-5xl">{{ errorTitle }}</h1>
<p class="mt-6 text-base leading-7">{{ errorDescription }}</p> <p class="mt-6 text-base leading-7">{{ errorDescription }}</p>
<div class="mt-10 flex items-center justify-center gap-x-6"> <div class="mt-10 flex items-center justify-center gap-x-6">
<UButton to="/" size="lg">返回首页</UButton> <UButton to="/" size="lg">返回首页</UButton>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
code: { code: {
type: Number, type: Number,
default: 404 default: 404
}, },
title: { title: {
type: String, type: String,
default: '' default: ''
}, },
description: { description: {
type: String, type: String,
default: '' default: ''
} }
}) })
const errorTitle = computed(() => { const errorTitle = computed(() => {
let title = props.title let title = props.title
if (!title) { if (!title) {
const code = props.code const code = props.code
if (code === 404) title = 'Not found' if (code === 404) title = 'Not found'
else if (code === 500 || code === 502) title = 'Error' else if (code === 500 || code === 502) title = 'Error'
else if (code === 403) title= 'Forbidden' else if (code === 403) title= 'Forbidden'
} }
return title return title
}) })
const errorDescription = computed(() => { const errorDescription = computed(() => {
let description = props.description let description = props.description
if (!description) { if (!description) {
const code = props.code const code = props.code
if (code === 404) description = '抱歉,当前访问的内容不存在或被删除' if (code === 404) description = '抱歉,当前访问的内容不存在或被删除'
else if (code === 500 || code === 502) description = '抱歉,当前内容出现错误' else if (code === 500 || code === 502) description = '抱歉,当前内容出现错误'
else if (code === 403) description= '抱歉,您没有权限访问当前内容' else if (code === 403) description= '抱歉,您没有权限访问当前内容'
} }
return description return description
}) })
</script> </script>
\ No newline at end of file
<template> <template>
<div class="flex justify-center"> <div class="flex justify-center">
<NuxtLink to="/" class="font-mono text-2xl font-medium">GitBot.<span class="text-primary">AI</span></NuxtLink> <NuxtLink to="/" class="font-mono text-2xl font-medium">GitBot.<span class="text-primary">AI</span></NuxtLink>
</div> </div>
</template> </template>
\ No newline at end of file
<template> <template>
<div class="flex md:hidden flex-shrink-0"> <div class="flex md:hidden flex-shrink-0">
<UButton <UButton
class="z-20" class="z-20"
:class="{ 'fixed top-2 left-2' : fixed }" :class="{ 'fixed top-2 left-2' : fixed }"
color="white" color="white"
trailing-icon="i-heroicons-bars-3-20-solid" trailing-icon="i-heroicons-bars-3-20-solid"
@click="handleToggleAside" @click="handleToggleAside"
/> />
<USlideover <USlideover
class="w-64" v-model="isOpenAside"
v-model="isOpenAside" class="w-64"
side="left" side="left"
:overlay="false" :overlay="false"
> >
<UButton <UButton
class="absolute top-2 right-2 z-20" class="absolute top-2 right-2 z-20"
color="white" color="white"
trailing-icon="i-heroicons-x-mark-20-solid" trailing-icon="i-heroicons-x-mark-20-solid"
@click="handleToggleAside" @click="handleToggleAside"
/> />
<IAside /> <IAside />
</USlideover> </USlideover>
</div> </div>
</template> </template>
<script setup> <script setup>
defineProps({ defineProps({
fixed: { fixed: {
type: Boolean, type: Boolean,
default: false default: false
} }
}) })
const isOpenAside = ref(false) const isOpenAside = ref(false)
function handleToggleAside () { function handleToggleAside () {
isOpenAside.value = !isOpenAside.value isOpenAside.value = !isOpenAside.value
} }
</script> </script>
\ No newline at end of file
<template> <template>
<div class="flex flex-col w-full gap-2"> <div class="flex flex-col w-full gap-2">
<UButton <UButton
leading-icon="i-heroicons-home" leading-icon="i-heroicons-home"
color="gray" color="gray"
variant="ghost" variant="ghost"
size="md" size="md"
label="首页" label="首页"
to="/" to="/"
/> />
<UButton <UButton
leading-icon="i-heroicons-rectangle-stack" leading-icon="i-heroicons-rectangle-stack"
color="gray" color="gray"
variant="ghost" variant="ghost"
size="md" size="md"
label="主题" label="主题"
to="/library" to="/library"
/> />
<ISearchHistory @sign="$openSign" /> <ISearchHistory @sign="$openSign" />
<ClientOnly> <ClientOnly>
<UButton <UButton
v-if="!$isSignIn" v-if="!$isSignIn"
class="flex gap-2 justify-center" class="flex gap-2 justify-center"
size="lg" size="lg"
label="登录" label="登录"
@click="$openSign" @click="$openSign"
/> />
</ClientOnly> </ClientOnly>
</div> </div>
<UModal v-model="$isOpenSign"> <UModal v-model="$isOpenSign">
<ISign @close="$closeSign" /> <ISign @close="$closeSign" />
</UModal> </UModal>
</template> </template>
<script setup> <script setup>
const { $isSignIn, $isOpenSign } = storeToRefs(useUserStore()) const { $isSignIn, $isOpenSign } = storeToRefs(useUserStore())
......
<template> <template>
<div class="flex flex-col overflow-hidden group"> <div class="flex flex-col overflow-hidden group">
<div class="flex justify-between"> <div class="flex justify-between">
<UButton <UButton
class="flex-grow cursor-default hover:bg-transparent dark:hover:bg-transparent" class="flex-grow cursor-default hover:bg-transparent dark:hover:bg-transparent"
leading-icon="i-heroicons-queue-list" leading-icon="i-heroicons-queue-list"
color="gray" color="gray"
variant="ghost" variant="ghost"
size="md" size="md"
label="浏览记录" label="浏览记录"
/> />
<UButton <UButton
class="hidden group-hover:flex" class="hidden group-hover:flex"
label="清空" label="清空"
size="md" size="md"
variant="link" variant="link"
@click="handleClear" @click="handleClear"
/> />
</div> </div>
<ClientOnly> <ClientOnly>
<div v-auto-animate class="flex overflow-y-auto flex-col gap-1 border-l border-gray-200 dark:border-gray-800 pl-2 ml-5"> <div v-auto-animate class="flex overflow-y-auto flex-col gap-1 border-l border-gray-200 dark:border-gray-800 pl-2 ml-5">
<template v-for="item in $searchHistory.reverse()" :key="item.c_id"> <template v-for="item in $searchHistory.reverse()" :key="item.c_id">
<UButton <UButton
class="flex text-gray-400" class="flex text-gray-400"
color="gray" color="gray"
variant="ghost" variant="ghost"
size="xs" size="xs"
long long
:to="`/search/${item.c_id}`" :to="`/search/${item.c_id}`"
> >
<div class="flex-grow truncate" :title="item.title">{{ item.title }}</div> <div class="flex-grow truncate" :title="item.title">{{ item.title }}</div>
</UButton> </UButton>
</template> </template>
</div> </div>
</ClientOnly> </ClientOnly>
</div> </div>
</template> </template>
<script setup> <script setup>
const emits = defineEmits(['sign', 'clear'])
const modal = useModal()
const { $searchHistory } = storeToRefs(useSearchStore()) const { $searchHistory } = storeToRefs(useSearchStore())
const { $clearSearchHistory } = useSearchStore() const { $clearSearchHistory } = useSearchStore()
const query = ref('')
function handleClear () { function handleClear () {
$clearSearchHistory() $clearSearchHistory()
} }
</script> </script>
<template> <template>
<div class="flex flex-col items-start gap-4 p-4"> <div class="flex flex-col items-start gap-4 p-4">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<ILogo /> <ILogo />
<UButton <UButton
color="gray" color="gray"
variant="ghost" variant="ghost"
leading-icon="i-heroicons-x-mark-20-solid" leading-icon="i-heroicons-x-mark-20-solid"
@click="handleClose" @click="handleClose"
/> />
</div> </div>
<div>登录以继续使用</div> <div>登录以继续使用</div>
<UButton <UButton
block block
color="gray" color="gray"
size="md" size="md"
@click="handleGetSignUrl('gitcode')" @click="handleGetSignUrl('gitcode')"
> >
<img src="~/assets/svg/logo-gitcode.svg" /> <img src="~/assets/svg/logo-gitcode.svg" >
使用 GitCode 登录 使用 GitCode 登录
</UButton> </UButton>
<UButton <UButton
block block
leading-icon="i-simple-icons-github" leading-icon="i-simple-icons-github"
label="使用 GitHub 登录" label="使用 GitHub 登录"
color="gray" color="gray"
size="md" size="md"
@click="handleGetSignUrl('github')" @click="handleGetSignUrl('github')"
/> />
<UButton <UButton
block block
leading-icon="i-simple-icons-google" leading-icon="i-simple-icons-google"
label="使用 Google 登录" label="使用 Google 登录"
color="gray" color="gray"
size="md" size="md"
disabled disabled
/> />
<!-- <UDivider label="或" />--> <!-- <UDivider label="或" />-->
<!-- <UInput--> <!-- <UInput-->
<!-- class="w-full"--> <!-- class="w-full"-->
<!-- v-model="email"--> <!-- v-model="email"-->
<!-- placeholder="输入邮箱地址..."--> <!-- placeholder="输入邮箱地址..."-->
<!-- size="md"--> <!-- size="md"-->
<!-- />--> <!-- />-->
<!-- <UButton--> <!-- <UButton-->
<!-- block--> <!-- block-->
<!-- leading-icon="i-heroicons-envelope-20-solid"--> <!-- leading-icon="i-heroicons-envelope-20-solid"-->
<!-- label="邮箱登录"--> <!-- label="邮箱登录"-->
<!-- size="md"--> <!-- size="md"-->
<!-- @click="handleSign"--> <!-- @click="handleSign"-->
<!-- />--> <!-- />-->
</div> </div>
</template> </template>
<script setup> <script setup>
const emits = defineEmits(['close', 'signIn']) const emits = defineEmits(['close', 'signIn'])
const email = ref('') // const email = ref('')
function handleClose () { function handleClose () {
emits('close') emits('close')
}
function handleSign () {
emits('signIn')
} }
// function handleSign () {
// emits('signIn')
// }
async function handleGetSignUrl (source) { async function handleGetSignUrl (source) {
let url let url
if (source === 'github') { if (source === 'github') {
const { data } = await useRequest('/v1/user/github/authorize_url') const { data } = await useRequest('/v1/user/github/authorize_url')
url = data.value.data.url url = data.value.data.url
} else if (source === 'gitcode') { } else if (source === 'gitcode') {
const { data } = await useRequest('/v1/user/gitcode/authorize_url') const { data } = await useRequest('/v1/user/gitcode/authorize_url')
url = data.value.data.url url = data.value.data.url
} }
window.location.href = url window.location.href = url
} }
</script> </script>
<template> <template>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<ClientOnly> <ClientOnly>
<template v-if="$isSignIn"> <template v-if="$isSignIn">
<div class="flex flex-grow justify-between items-center p-4"> <div class="flex flex-grow justify-between items-center p-4">
<UDropdown class="flex flex-grow" :items="items"> <UDropdown class="flex flex-grow" :items="items">
<UButton class="flex flex-grow" color="gray" variant="ghost"> <UButton class="flex flex-grow" color="gray" variant="ghost">
<div class="flex flex-grow items-center gap-2"> <div class="flex flex-grow items-center gap-2">
<UAvatar :src="$info.avatar_url" /> <UAvatar :src="$info.avatar_url" />
<div>{{ $info.nickname }}</div> <div>{{ $info.nickname }}</div>
<UBadge v-if="$info.pro" size="xs" color="yellow" variant="soft" label="PRO" /> <UBadge v-if="$info.pro" size="xs" color="yellow" variant="soft" label="PRO" />
<UBadge v-else size="xs" color="gray" variant="soft" label="FREE" /> <UBadge v-else size="xs" color="gray" variant="soft" label="FREE" />
</div> </div>
<UIcon name="i-heroicons-chevron-down-20-solid" class="text-lg" /> <UIcon name="i-heroicons-chevron-down-20-solid" class="text-lg" />
</UButton> </UButton>
</UDropdown> </UDropdown>
</div> </div>
<UDivider /> <UDivider />
</template> </template>
</ClientOnly> </ClientOnly>
<div class="flex justify-between gap-2 p-4"> <div class="flex justify-between gap-2 p-4">
<UButton <UButton
color="gray" color="gray"
variant="ghost" variant="ghost"
label="问题反馈" label="问题反馈"
/> />
<ClientOnly> <ClientOnly>
<div class="flex"> <div class="flex">
<ColorPicker /> <ColorPicker />
<USelectMenu <USelectMenu
v-model="colorMode.preference" v-model="colorMode.preference"
:ui-menu="{ width: 'w-32' }" :ui-menu="{ width: 'w-32' }"
:options="themeItems" :options="themeItems"
value-attribute="value" value-attribute="value"
> >
<UButton <UButton
color="gray" color="gray"
variant="ghost" variant="ghost"
square square
:icon="colorMode.value === 'dark' ? 'i-heroicons-moon-16-solid' : 'i-heroicons-sun-16-solid'" :icon="colorMode.value === 'dark' ? 'i-heroicons-moon-16-solid' : 'i-heroicons-sun-16-solid'"
aria-label="Theme" aria-label="Theme"
/> />
</USelectMenu> </USelectMenu>
<USelectMenu <USelectMenu
v-if="false" v-if="false"
:ui-menu="{ width: 'w-32' }" :ui-menu="{ width: 'w-32' }"
:model-value="$lang" :model-value="$lang"
:options="$langOptions" :options="$langOptions"
value-attribute="value" value-attribute="value"
> >
<UButton <UButton
icon="i-heroicons-language-16-solid" icon="i-heroicons-language-16-solid"
color="gray" color="gray"
variant="ghost" variant="ghost"
/> />
</USelectMenu> </USelectMenu>
</div> </div>
</ClientOnly> </ClientOnly>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
const { $signOut } = useUserStore() const { $signOut } = useUserStore()
...@@ -65,41 +65,41 @@ const { $isSignIn, $info } = storeToRefs(useUserStore()) ...@@ -65,41 +65,41 @@ const { $isSignIn, $info } = storeToRefs(useUserStore())
const { $lang, $langOptions } = useI18nStore() const { $lang, $langOptions } = useI18nStore()
const colorMode = useColorMode() const colorMode = useColorMode()
const items = [ const items = [
[ [
{
label: '账号信息',
icon: 'i-heroicons-user'
},
{
label: '用户反馈',
icon: 'i-heroicons-chat-bubble-oval-left-ellipsis'
}
],
[
{
label: '退出登录',
icon: 'i-heroicons-power',
click: () => {
$signOut()
}
}
]
]
const themeItems = [
{ {
label: '亮色模式', label: '账号信息',
value: 'light', icon: 'i-heroicons-user'
icon: 'i-heroicons-sun'
}, },
{ {
label: '深色模式', label: '用户反馈',
value: 'dark', icon: 'i-heroicons-chat-bubble-oval-left-ellipsis'
icon: 'i-heroicons-moon' }
}, ],
[
{ {
label: '跟随系统', label: '退出登录',
value: 'system', icon: 'i-heroicons-power',
icon: 'i-heroicons-computer-desktop' click: () => {
$signOut()
}
} }
]
]
const themeItems = [
{
label: '亮色模式',
value: 'light',
icon: 'i-heroicons-sun'
},
{
label: '深色模式',
value: 'dark',
icon: 'i-heroicons-moon'
},
{
label: '跟随系统',
value: 'system',
icon: 'i-heroicons-computer-desktop'
}
] ]
</script> </script>
<template> <template>
<UDropdown class="flex flex-grow" :items="actionItems"> <UDropdown class="flex flex-grow" :items="actionItems">
<UButton <UButton
color="gray" color="gray"
variant="ghost" variant="ghost"
square square
icon="i-heroicons-ellipsis-horizontal" icon="i-heroicons-ellipsis-horizontal"
/> />
</UDropdown> </UDropdown>
<ILibraryEdit :id="id" ref="refEdit" /> <ILibraryEdit :id="id" ref="refEdit" />
</template> </template>
<script setup> <script setup>
import { IConfirm } from '#components' import { IConfirm } from '#components'
const { deleteCollection } = useCollectionRequest() const { deleteCollection } = useCollectionRequest()
const modal = useModal() const modal = useModal()
const props = defineProps({ const props = defineProps({
id: { id: {
type: [String, Number], type: [String, Number],
default: '' default: ''
} }
}) })
const actionItems = [ const actionItems = [
[ [
{ {
label: '编辑合集', label: '编辑合集',
icon: 'i-heroicons-pencil-square', icon: 'i-heroicons-pencil-square',
click: () => { click: () => {
handleOpen() handleOpen()
} }
}, },
{ {
label: '删除合集', label: '删除合集',
icon: 'i-heroicons-trash', icon: 'i-heroicons-trash',
click: () => { click: () => {
handleOpenDelete() handleOpenDelete()
} }
} }
] ]
] ]
const refEdit = ref(null) const refEdit = ref(null)
function handleOpen () { function handleOpen () {
refEdit.value.open() refEdit.value.open()
} }
function handleOpenDelete () { function handleOpenDelete () {
modal.open(IConfirm, { modal.open(IConfirm, {
title: '删除确认', title: '删除确认',
description: '确定要删除该合集吗?', description: '确定要删除该合集吗?',
async onSuccess() { async onSuccess() {
await deleteCollection(props.id) await deleteCollection(props.id)
modal.close() modal.close()
navigateTo('/library') navigateTo('/library')
}, },
onCancel () { onCancel () {
modal.close() modal.close()
} }
}) })
} }
</script> </script>
<template> <template>
<UDropdown class="flex flex-grow" :items="actionItems"> <UDropdown class="flex flex-grow" :items="actionItems">
<UButton <UButton
color="gray" color="gray"
variant="ghost" variant="ghost"
:size="size" :size="size"
square square
icon="i-heroicons-ellipsis-horizontal" icon="i-heroicons-ellipsis-horizontal"
/> />
</UDropdown> </UDropdown>
</template> </template>
<script setup> <script setup>
import { IConfirm } from '#components' import { IConfirm } from '#components'
...@@ -16,73 +16,73 @@ const { deleteCollectionRecord, deleteThread } = useCollectionRequest() ...@@ -16,73 +16,73 @@ const { deleteCollectionRecord, deleteThread } = useCollectionRequest()
const Layout = inject('Layout') const Layout = inject('Layout')
const modal = useModal() const modal = useModal()
const props = defineProps({ const props = defineProps({
collection_id: { collectionId: {
type: [String, Number], type: [String, Number],
default: '' default: ''
}, },
c_id: { cId: {
type: [String, Number], type: [String, Number],
default: '' default: ''
}, },
size: { size: {
type: String, type: String,
default: 'sm' default: 'sm'
} }
}) })
const emit = defineEmits(['delete']) const emit = defineEmits(['delete'])
const actionItems = computed(() => { const actionItems = computed(() => {
let items let items
if (props.collection_id && props.c_id) { if (props.collection_id && props.c_id) {
items = [ items = [
{ {
label: '更改合集', label: '更改合集',
icon: 'i-heroicons-squares-plus', icon: 'i-heroicons-squares-plus',
click: () => { click: () => {
$openLibrarySelect(props.c_id, [props.collection_id]) $openLibrarySelect(props.c_id, [props.collection_id])
} }
}, },
{ {
label: '从收藏中移除', label: '从收藏中移除',
icon: 'i-heroicons-x-mark', icon: 'i-heroicons-x-mark',
click: async () => {
await deleteCollectionRecord(props.collection_id, props.c_id)
Layout.handleRemoveCollectData({
collection_id: props.collection_id,
c_id: props.c_id
})
}
}
]
} else {
items = [
{
label: '添加到收藏',
icon: 'i-heroicons-plus',
click: () => {
$openLibrarySelect(props.c_id)
}
}
]
}
items.push({
label: '删除主题',
icon: 'i-heroicons-trash',
click: async () => { click: async () => {
modal.open(IConfirm, { await deleteCollectionRecord(props.collection_id, props.c_id)
title: '删除确认', Layout.handleRemoveCollectData({
description: '确定要删除该主题吗?', collection_id: props.collection_id,
async onSuccess() { c_id: props.c_id
modal.close() })
await deleteThread([props.c_id])
$getCollection()
emit('delete')
},
onCancel () {
modal.close()
}
})
} }
}) }
return [items] ]
} else {
items = [
{
label: '添加到收藏',
icon: 'i-heroicons-plus',
click: () => {
$openLibrarySelect(props.c_id)
}
}
]
}
items.push({
label: '删除主题',
icon: 'i-heroicons-trash',
click: async () => {
modal.open(IConfirm, {
title: '删除确认',
description: '确定要删除该主题吗?',
async onSuccess() {
modal.close()
await deleteThread([props.c_id])
$getCollection()
emit('delete')
},
onCancel () {
modal.close()
}
})
}
})
return [items]
}) })
</script> </script>
<template> <template>
<ULink :to="`/library/${item.id}`"> <ULink :to="`/library/${item.id}`">
<UCard :ui="cardUI"> <UCard :ui="cardUI">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div>{{ item.name }}</div> <div>{{ item.name }}</div>
<div class="flex"> <div class="flex">
<div class="flex items-center text-gray-500 text-sm gap-0.5"> <div class="flex items-center text-gray-500 text-sm gap-0.5">
<UIcon name="i-heroicons-square-3-stack-3d" /> <UIcon name="i-heroicons-square-3-stack-3d" />
<span>{{ item.record_count }}</span> <span>{{ item.record_count }}</span>
</div> </div>
</div> </div>
</div> </div>
</UCard> </UCard>
</ULink> </ULink>
</template> </template>
<script setup> <script setup>
defineProps({ defineProps({
item: { item: {
type: Object, type: Object,
default: (() => {})() default: (() => {})()
} }
}) })
const cardUI = { const cardUI = {
body: { body: {
padding: 'sm:p-3 p-3', padding: 'sm:p-3 p-3',
base: 'h-full' base: 'h-full'
}, },
background: 'transition bg-gray-50 hover:bg-gray-100 dark:hover:bg-gray-800' background: 'transition bg-gray-50 hover:bg-gray-100 dark:hover:bg-gray-800'
} }
</script> </script>
\ No newline at end of file
<template> <template>
<UModal <UModal
:model-value="!id ? $isLibraryCreateOpen : updateVisible" :model-value="!id ? $isLibraryCreateOpen : updateVisible"
@update:modelValue="handleCloseModal" :ui="{ width: 'w-96 sm:max-w-screen-md' }"
:ui="{ width: 'w-96 sm:max-w-screen-md' }" @update:model-value="handleCloseModal"
> >
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="text-xl" v-if="!id">创建合集</div> <div v-if="!id" class="text-xl">创建合集</div>
<div class="text-xl" v-else>更新合集</div> <div v-else class="text-xl">更新合集</div>
<UButton <UButton
leading-icon="i-heroicons-x-mark-20-solid" leading-icon="i-heroicons-x-mark-20-solid"
color="gray" color="gray"
variant="ghost" variant="ghost"
@click="handleClose" @click="handleClose"
/> />
</div> </div>
</template> </template>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex content-center items-center justify-between text-sm"> <div class="flex content-center items-center justify-between text-sm">
<label class="block font-medium text-gray-700 dark:text-gray-200">标题</label> <label class="block font-medium text-gray-700 dark:text-gray-200">标题</label>
</div> </div>
<UInput <UInput
v-model="title" v-model="title"
placeholder="合集标题" placeholder="合集标题"
autofocus autofocus
/> />
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex content-center items-center justify-between text-sm"> <div class="flex content-center items-center justify-between text-sm">
<label class="block font-medium text-gray-700 dark:text-gray-200"> <label class="block font-medium text-gray-700 dark:text-gray-200">
描述 描述
<span class="text-gray-400 dark:text-gray-100">(可选)</span> <span class="text-gray-400 dark:text-gray-100">(可选)</span>
</label> </label>
</div> </div>
<UTextarea <UTextarea
v-model="description" v-model="description"
placeholder="合集描述" placeholder="合集描述"
/> />
</div> </div>
</div> </div>
<template #footer> <template #footer>
<div class="flex justify-end"> <div class="flex justify-end">
<UButton <UButton
v-if="!id" v-if="!id"
size="md" size="md"
label="创建" label="创建"
:loading="loading" :loading="loading"
:disabled="!title" :disabled="!title"
@click="handleCreate" @click="handleCreate"
/> />
<UButton <UButton
v-else v-else
size="md" size="md"
label="更新" label="更新"
:loading="loading" :loading="loading"
:disabled="!title" :disabled="!title"
@click="handleUpdate" @click="handleUpdate"
/> />
</div> </div>
</template> </template>
</UCard> </UCard>
</UModal> </UModal>
</template> </template>
<script setup> <script setup>
const { $isLibraryCreateOpen, $collection } = storeToRefs(useLibraryStore()) const { $isLibraryCreateOpen, $collection } = storeToRefs(useLibraryStore())
const { $closeLibraryCreate, $getCollection } = useLibraryStore() const { $closeLibraryCreate, $getCollection } = useLibraryStore()
const { setOrUpdateCollection } = useCollectionRequest() const { setOrUpdateCollection } = useCollectionRequest()
const props = defineProps({ const props = defineProps({
id: { id: {
type: [String, Number], type: [String, Number],
default: '' default: ''
} }
}) })
const updateVisible = ref(false) const updateVisible = ref(false)
const title = ref('') const title = ref('')
const description = ref('') const description = ref('')
const loading = ref(false) const loading = ref(false)
function handleClose () { function handleClose () {
if (!props.id) $closeLibraryCreate() if (!props.id) $closeLibraryCreate()
else updateVisible.value = false else updateVisible.value = false
} }
async function handleCreate () { async function handleCreate () {
if (loading.value) return if (loading.value) return
loading.value = true loading.value = true
const { error } = await setOrUpdateCollection({ const { error } = await setOrUpdateCollection({
name: title.value, name: title.value,
description: description.value description: description.value
}) })
loading.value = false loading.value = false
if (error.value) return if (error.value) return
title.value = '' title.value = ''
description.value = '' description.value = ''
$closeLibraryCreate() $closeLibraryCreate()
$getCollection() $getCollection()
} }
function handleCloseModal () { function handleCloseModal () {
if (!props.id) $isLibraryCreateOpen.value = false if (!props.id) $isLibraryCreateOpen.value = false
else updateVisible.value = false else updateVisible.value = false
} }
async function handleUpdate () { async function handleUpdate () {
if (loading.value) return if (loading.value) return
loading.value = true loading.value = true
const { error } = await setOrUpdateCollection({ const { error } = await setOrUpdateCollection({
id: Number(props.id), id: Number(props.id),
name: title.value, name: title.value,
description: description.value description: description.value
}) })
loading.value = false loading.value = false
if (error.value) return if (error.value) return
handleCloseModal() handleCloseModal()
$getCollection() $getCollection()
} }
function handleFillInfo () { function handleFillInfo () {
const collection =$collection.value const collection =$collection.value
const id = Number(props.id) const id = Number(props.id)
const { name, description:currentDes } = collection.find(item => item.id === id) const { name, description:currentDes } = collection.find(item => item.id === id)
if (name) { if (name) {
title.value = name title.value = name
description.value = currentDes description.value = currentDes
} }
} }
function openUpdate () { function openUpdate () {
updateVisible.value = true updateVisible.value = true
// todo 获取合集数据 // todo 获取合集数据
if (props.id) { if (props.id) {
handleFillInfo() handleFillInfo()
} }
} }
defineExpose({ defineExpose({
openUpdate openUpdate
}) })
</script> </script>
<template> <template>
<ILibraryCreate :id="id" ref="refCreate" /> <ILibraryCreate :id="id" ref="refCreate" />
</template> </template>
<script setup> <script setup>
defineProps({ defineProps({
id: { id: {
type: [String, Number], type: [String, Number],
default: '' default: ''
} }
}) })
const refCreate = ref(null) const refCreate = ref(null)
function open () { function open () {
refCreate.value.openUpdate() refCreate.value.openUpdate()
} }
defineExpose({ defineExpose({
open open
}) })
</script> </script>
\ No newline at end of file
<template> <template>
<div class="flex flex-col w-full items-center sticky top-0 bg-white dark:bg-black z-10"> <div class="flex flex-col w-full items-center sticky top-0 bg-white dark:bg-black z-10">
<div class="container max-w-screen-lg 2xl:max-w-screen-xl flex flex-col p-6"> <div class="container max-w-screen-lg 2xl:max-w-screen-xl flex flex-col p-6">
<div class="flex justify-between items-center gap-4"> <div class="flex justify-between items-center gap-4">
<div class="flex gap-4"> <div class="flex gap-4">
<IMenuSider /> <IMenuSider />
<div class="flex flex-shrink-0 items-center text-xl gap-2" v-if="!collect"> <div v-if="!collect" class="flex flex-shrink-0 items-center text-xl gap-2">
<UIcon name="i-heroicons-rectangle-stack-20-solid" /> <UIcon name="i-heroicons-rectangle-stack-20-solid" />
<div>主题</div> <div>主题</div>
</div> </div>
<template v-else> <template v-else>
<UButton <UButton
icon="i-heroicons-chevron-left" icon="i-heroicons-chevron-left"
color="gray" color="gray"
variant="ghost" variant="ghost"
to="/library" to="/library"
/> />
<div class="flex items-center text-xl gap-2"> <div class="flex items-center text-xl gap-2">
<UIcon name="i-heroicons-squares-2x2" /> <UIcon name="i-heroicons-squares-2x2" />
<div>{{ collect }}</div> <div>{{ collect }}</div>
<UBadge color="gray" variant="soft" :label="count" /> <UBadge color="gray" variant="soft" :label="count" />
</div> </div>
</template> </template>
</div> </div>
<div class="flex flex-grow justify-end items-center gap-4"> <div class="flex flex-grow justify-end items-center gap-4">
<div class="flex"> <div class="flex">
<IActionCollect :id="collectId" v-if="collect" /> <IActionCollect v-if="collect" :id="collectId" />
</div> </div>
<div class="flex" v-if="!collect"> <div v-if="!collect" class="flex">
<UInput <UInput
name="queryInput" v-model="searchQuery"
:ui="{ icon: { trailing: { pointer: '' } } }" name="queryInput"
v-model="searchQuery" :ui="{ icon: { trailing: { pointer: '' } } }"
:loading="searchLoading" :loading="searchLoading"
icon="i-heroicons-magnifying-glass-20-solid" icon="i-heroicons-magnifying-glass-20-solid"
placeholder="搜索你的主题..." placeholder="搜索你的主题..."
size="md" size="md"
> >
<template #trailing> <template #trailing>
<UButton <UButton
v-show="searchQuery !== ''" v-show="searchQuery !== ''"
color="gray" color="gray"
variant="link" variant="link"
icon="i-heroicons-x-mark-20-solid" icon="i-heroicons-x-mark-20-solid"
:padded="false" :padded="false"
@click="handleClear" @click="handleClear"
/> />
</template> </template>
</UInput> </UInput>
</div> </div>
</div> </div>
</div> </div>
<div v-if="description" class="text-gray-500 ml-12 mt-2">{{ description }}</div> <div v-if="description" class="text-gray-500 ml-12 mt-2">{{ description }}</div>
</div> </div>
<UDivider /> <UDivider />
<div v-if="showTabs" class="w-full p-6 block lg:hidden"> <div v-if="showTabs" class="w-full p-6 block lg:hidden">
<UTabs :model-value="tab" @update:modelValue="handleChangeTab" :items="tabs" /> <UTabs :model-value="tab" :items="tabs" @update:model-value="handleChangeTab" />
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
defineProps({ defineProps({
collect: { collect: {
type: String, type: String,
default: '' default: ''
}, },
description: { description: {
type: String, type: String,
default: '' default: ''
}, },
count: { count: {
type: Number, type: Number,
default: 0 default: 0
}, },
collectId: { collectId: {
type: [String, Number], type: [String, Number],
default: '' default: ''
}, },
showTabs: { showTabs: {
type: Boolean, type: Boolean,
default: false default: false
}, },
tab: { tab: {
type: Number, type: Number,
default: 0 default: 0
} }
}) })
const emit = defineEmits(['search', 'clear', 'change-tab']) const emit = defineEmits(['search', 'clear', 'change-tab'])
const searchQuery = ref('') const searchQuery = ref('')
const searchLoading = ref(false) const searchLoading = ref(false)
defineShortcuts({ defineShortcuts({
enter: { enter: {
usingInput: 'queryInput', usingInput: 'queryInput',
handler: async () => { handler: async () => {
searchLoading.value = true searchLoading.value = true
const { data } = await useRequest(`/v1/chat/completion/list?keyword=${searchQuery.value}`) const { data } = await useRequest(`/v1/chat/completion/list?keyword=${searchQuery.value}`)
searchLoading.value = false searchLoading.value = false
emit('search', data.value.data) emit('search', data.value.data)
}
} }
}
}) })
function handleClear () { function handleClear () {
searchQuery.value = '' searchQuery.value = ''
emit('clear') emit('clear')
} }
const tabs = [ const tabs = [
{ {
label: '全部主题', label: '全部主题',
icon: 'i-heroicons-square-3-stack-3d' icon: 'i-heroicons-square-3-stack-3d'
}, },
{ {
label: '合集' label: '合集'
} }
] ]
function handleChangeTab (index) { function handleChangeTab (index) {
emit('change-tab', index) emit('change-tab', index)
} }
</script> </script>
<template> <template>
<UModal v-model="$isLibrarySelectOpen" :ui="{ width: 'w-96 sm:max-w-screen-md' }"> <UModal v-model="$isLibrarySelectOpen" :ui="{ width: 'w-96 sm:max-w-screen-md' }">
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"> <UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="text-xl">选择合集</div> <div class="text-xl">选择合集</div>
<UButton <UButton
leading-icon="i-heroicons-x-mark-20-solid" leading-icon="i-heroicons-x-mark-20-solid"
color="gray" color="gray"
variant="ghost" variant="ghost"
@click="handleClose" @click="handleClose"
/> />
</div> </div>
</template> </template>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<UButton <UButton
leading-icon="i-heroicons-plus-20-solid" leading-icon="i-heroicons-plus-20-solid"
label="创建新合集" label="创建新合集"
color="gray" color="gray"
@click="handleOpenCreate" @click="handleOpenCreate"
/> />
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<UButton <UButton
v-for="item in $collection" v-for="item in $collection"
color="white" :key="item.id"
size="md" color="white"
class="flex" size="md"
:key="item.id" class="flex"
@click="handleSelected(item.id)" @click="handleSelected(item.id)"
> >
<div class="flex flex-grow justify-between items-center"> <div class="flex flex-grow justify-between items-center">
<div>{{ item.name }}</div> <div>{{ item.name }}</div>
<UIcon v-if="selected.includes(item.id)" name="i-heroicons-check-circle-20-solid" class="text-primary text-lg" /> <UIcon v-if="selected.includes(item.id)" name="i-heroicons-check-circle-20-solid" class="text-primary text-lg" />
</div> </div>
</UButton> </UButton>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<div class="flex justify-end"> <div class="flex justify-end">
<UButton <UButton
size="md" size="md"
label="保存" label="保存"
:loading="loading" :loading="loading"
@click="handleSave" @click="handleSave"
/> />
</div> </div>
</template> </template>
</UCard> </UCard>
</UModal> </UModal>
</template> </template>
<script setup> <script setup>
const { $isLibrarySelectOpen, $selectCollectionId, $selectThreadId, $collection } = storeToRefs(useLibraryStore()) const { $isLibrarySelectOpen, $selectCollectionId, $selectThreadId, $collection } = storeToRefs(useLibraryStore())
...@@ -57,43 +57,43 @@ const emits = defineEmits(['success']) ...@@ -57,43 +57,43 @@ const emits = defineEmits(['success'])
const selected = ref([]) const selected = ref([])
const loading = ref(false) const loading = ref(false)
function handleClose () { function handleClose () {
$closeLibrarySelect() $closeLibrarySelect()
} }
function handleOpenCreate () { function handleOpenCreate () {
handleClose() handleClose()
$openLibraryCreate() $openLibraryCreate()
} }
async function handleSelected(id) { async function handleSelected(id) {
selected.value = [id] selected.value = [id]
} }
async function handleSave() { async function handleSave() {
if (loading.value) return if (loading.value) return
loading.value = true loading.value = true
// 取消合集 // 取消合集
const hasSelected = $selectCollectionId.value const hasSelected = $selectCollectionId.value
if (hasSelected.length) { if (hasSelected.length) {
const { error } = await deleteCollectionRecord(hasSelected[0], $selectThreadId.value) const { error } = await deleteCollectionRecord(hasSelected[0], $selectThreadId.value)
!error.value && $setSelectCollectionId([]) !error.value && $setSelectCollectionId([])
} }
// 添加合集 // 添加合集
if (selected.value.length) { if (selected.value.length) {
const selectedItem = selected.value[0] const selectedItem = selected.value[0]
const { error } = await saveCollection({ collection_id: selectedItem, c_id: $selectThreadId.value }) const { error } = await saveCollection({ collection_id: selectedItem, c_id: $selectThreadId.value })
!error.value && $setSelectCollectionId([selectedItem]) !error.value && $setSelectCollectionId([selectedItem])
} }
loading.value = false loading.value = false
handleClose() handleClose()
$getCollection() $getCollection()
emits('success', { emits('success', {
c_id: $selectThreadId.value, c_id: $selectThreadId.value,
collection_id: selected.value[0], collection_id: selected.value[0],
collection_name: $collection.value.find(i => i.id === selected.value[0]).name collection_name: $collection.value.find(i => i.id === selected.value[0]).name
}) })
} }
watch(() => $isLibrarySelectOpen.value, () => { watch(() => $isLibrarySelectOpen.value, () => {
selected.value = [...$selectCollectionId.value] selected.value = [...$selectCollectionId.value]
if (!$collection.value.length) { if (!$collection.value.length) {
$getCollection() $getCollection()
} }
}) })
</script> </script>
<template> <template>
<ULink :to="`/search/${thread.c_id}`" class="flex flex-col group"> <ULink :to="`/search/${thread.c_id}`" class="flex flex-col group">
<div class="flex items-center gap-2 transition group-hover:text-primary">{{ thread.title }}</div> <div class="flex items-center gap-2 transition group-hover:text-primary">{{ thread.title }}</div>
<div class="break-word text-balance line-clamp-2 font-sans text-sm" :class="textColor" v-if="false"> <div v-if="false" class="break-word text-balance line-clamp-2 font-sans text-sm" :class="textColor">
{{ thread.description }} {{ thread.description }}
</div> </div>
</ULink> </ULink>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex gap-4"> <div class="flex gap-4">
<UTooltip class="flex items-center text-sm gap-0.5" :class="textColor" :text="thread.is_public ? '公开主题,链接可被发现' : '私密主题,仅自己可见'"> <UTooltip class="flex items-center text-sm gap-0.5" :class="textColor" :text="thread.is_public ? '公开主题,链接可被发现' : '私密主题,仅自己可见'">
<UIcon :name="thread.is_public ? 'i-heroicons-lock-open' : 'i-heroicons-lock-closed'" /> <UIcon :name="thread.is_public ? 'i-heroicons-lock-open' : 'i-heroicons-lock-closed'" />
<span>{{ thread.is_public ? '公开' : '私有' }}</span> <span>{{ thread.is_public ? '公开' : '私有' }}</span>
</UTooltip> </UTooltip>
<!-- <div class="flex items-center text-sm gap-0.5" :class="textColor">--> <!-- <div class="flex items-center text-sm gap-0.5" :class="textColor">-->
<!-- <UIcon name="i-heroicons-eye" />--> <!-- <UIcon name="i-heroicons-eye" />-->
<!-- <span>1</span>--> <!-- <span>1</span>-->
<!-- </div>--> <!-- </div>-->
<div class="flex" :class="textColor"> <div class="flex" :class="textColor">
<UTooltip class="flex items-center text-sm gap-0.5"> <UTooltip class="flex items-center text-sm gap-0.5">
<UIcon name="i-heroicons-clock" /> <UIcon name="i-heroicons-clock" />
<span>{{ useTime(thread.create_time) }}</span> <span>{{ useTime(thread.create_time) }}</span>
<template #text> <template #text>
{{ toValue(useDateFormat(thread.create_time, 'YYYY年M月D日 HH:mm')) }} {{ toValue(useDateFormat(thread.create_time, 'YYYY年M月D日 HH:mm')) }}
</template> </template>
</UTooltip> </UTooltip>
</div> </div>
</div> </div>
<div class="flex gap-4"> <div class="flex gap-4">
<template v-for="collect in thread.collections" :key="collect.collection_id"> <template v-for="collect in thread.collections" :key="collect.collection_id">
<UButton <UButton
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
:to="`/library/${collect.collection_id}`" :to="`/library/${collect.collection_id}`"
color="white" color="white"
size="2xs" size="2xs"
:label="collect.collection_name" :label="collect.collection_name"
/> />
</template> </template>
<UTooltip text="添加到收藏" v-if="!thread.collections.length"> <UTooltip v-if="!thread.collections.length" text="添加到收藏">
<UButton <UButton
color="gray" color="gray"
variant="ghost" variant="ghost"
size="2xs" size="2xs"
square square
icon="i-heroicons-plus" icon="i-heroicons-plus"
@click="handleOpenSelect" @click="handleOpenSelect"
/> />
</UTooltip> </UTooltip>
<IActionThread <IActionThread
:collection_id="thread.collections.length ? thread.collections[0].collection_id : ''" :collection_id="thread.collections.length ? thread.collections[0].collection_id : ''"
:c_id="item.c_id" :c_id="item.c_id"
size="2xs" size="2xs"
@delete="handleDeletedThread" @delete="handleDeletedThread"
/> />
</div> </div>
</div> </div>
<UDivider /> <UDivider />
</template> </template>
<script setup> <script setup>
const Layout = inject('Layout') const Layout = inject('Layout')
const { $openLibrarySelect } = useLibraryStore() const { $openLibrarySelect } = useLibraryStore()
const textColor = 'text-gray-500 dark:text-gray-400' const textColor = 'text-gray-500 dark:text-gray-400'
const props = defineProps({ const props = defineProps({
item: { item: {
type: Object, type: Object,
default: (() => {})() default: (() => {})()
}, },
isItem: { isItem: {
type: Boolean, type: Boolean,
default: false default: false
} }
}) })
const emit = defineEmits(['delete']) const emit = defineEmits(['delete'])
const thread = computed(() => { const thread = computed(() => {
if (!props.isItem) return props.item if (!props.isItem) return props.item
else { else {
const item = props.item const item = props.item
item.title = props.item.c_title item.title = props.item.c_title
item.collections = [] item.collections = []
if (props.item.collection_id && props.item.collection_name) { if (props.item.collection_id && props.item.collection_name) {
item.collections = [ item.collections = [
{ {
collection_id: props.item.collection_id, collection_id: props.item.collection_id,
collection_name: props.item.collection_name collection_name: props.item.collection_name
}
]
} }
return item ]
} }
return item
}
}) })
function handleOpenSelect () { function handleOpenSelect () {
$openLibrarySelect(props.item.c_id) $openLibrarySelect(props.item.c_id)
} }
function handleUpdateCollect (data) { function handleUpdateCollect (data) {
const { c_id } = data.value const { c_id } = data.value
if (c_id === thread.value.c_id) { if (c_id === thread.value.c_id) {
thread.value.collections = [data.value] thread.value.collections = [data.value]
Layout.handleClearCollectData() Layout.handleClearCollectData()
} }
} }
function handleRemoveCollect (data) { function handleRemoveCollect (data) {
const { c_id } = data.value const { c_id } = data.value
if (c_id === thread.value.c_id) { if (c_id === thread.value.c_id) {
thread.value.collections = [] thread.value.collections = []
Layout.handleClearRemoveCollectData() Layout.handleClearRemoveCollectData()
} }
} }
watch(()=> Layout.selectCollectData, (data) => { watch(()=> Layout.selectCollectData, (data) => {
if (data.value !== null) handleUpdateCollect(data) if (data.value !== null) handleUpdateCollect(data)
}, { deep: true }) }, { deep: true })
watch(()=> Layout.removeCollectData, (data) => { watch(()=> Layout.removeCollectData, (data) => {
if (data.value !== null) handleRemoveCollect(data) if (data.value !== null) handleRemoveCollect(data)
}, { deep: true }) }, { deep: true })
function handleDeletedThread () { function handleDeletedThread () {
emit('delete', thread.value.c_id) emit('delete', thread.value.c_id)
} }
</script> </script>
\ No newline at end of file
<template> <template>
<div class="grid grid-cols-1"> <div class="grid grid-cols-1">
<MDC class="prose dark:prose-invert max-w-none" :class="'prose-' + size" v-if="content" :value="content" tag="article" /> <MDC v-if="content" class="prose dark:prose-invert max-w-none" :class="'prose-' + size" :value="content" tag="article" />
</div> </div>
</template> </template>
<script setup> <script setup>
defineProps({ defineProps({
content: { content: {
type: String, type: String,
default: '' default: ''
}, },
size: { size: {
type: String, type: String,
default: 'base' default: 'base'
} }
}) })
</script> </script>
\ No newline at end of file
<template> <template>
<div class="flex flex-col lg:flex-row gap-6 space-x-0 lg:space-x-6"> <div class="flex flex-col lg:flex-row gap-6 space-x-0 lg:space-x-6">
<div class="flex flex-grow flex-col gap-6"> <div class="flex flex-grow flex-col gap-6">
<div class="grid"> <div class="grid">
<slot name="title" /> <slot name="title" />
</div> </div>
<slot /> <slot />
</div> </div>
<div class="flex flex-col w-full lg:w-64 flex-shrink-0 gap-6" v-if="$slots.extra"> <div v-if="$slots.extra" class="flex flex-col w-full lg:w-64 flex-shrink-0 gap-6">
<slot name="extra" /> <slot name="extra" />
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
......
<template> <template>
<div class="flex w-full justify-center"> <div class="flex w-full justify-center">
<UCard <UCard
class="hover:ring-2 has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-500 dark:has-[textarea:focus]:ring-primary-400" class="hover:ring-2 has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-500 dark:has-[textarea:focus]:ring-primary-400"
:ui="cardUI" :ui="cardUI"
> >
<UTextarea <UTextarea
ref="queryInput" ref="queryInput"
class="flex-grow" v-model="continueQuestion"
name="queryInput" class="flex-grow"
:rows="1" name="queryInput"
:maxrows="10" :rows="1"
autoresize :maxrows="10"
v-model="continueQuestion" autoresize
:placeholder="placeholder" :placeholder="placeholder"
size="xl" size="xl"
:padded="false" :padded="false"
variant="none" variant="none"
maxlength="2000" maxlength="2000"
@focus="handleInputFocus" @focus="handleInputFocus"
@blur="handleInputBlur" @blur="handleInputBlur"
/> />
<div class="flex flex-shrink-0 gap-2"> <div class="flex flex-shrink-0 gap-2">
<UButton <UButton
v-if="!asking" v-if="!asking"
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
:disabled="!continueQuestion" :disabled="!continueQuestion"
trailing-icon="i-heroicons-chevron-right-20-solid" trailing-icon="i-heroicons-chevron-right-20-solid"
size="xl" size="xl"
@click.stop="handleAsk" @click.stop="handleAsk"
/> />
<UTooltip v-else text="停止生成"> <UTooltip v-else text="停止生成">
<UButton <UButton
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
color="red" color="red"
trailing-icon="i-heroicons-stop-20-solid" trailing-icon="i-heroicons-stop-20-solid"
size="xl" size="xl"
variant="ghost" variant="ghost"
@click.stop="handleStop" @click.stop="handleStop"
/> />
</UTooltip> </UTooltip>
</div> </div>
</UCard> </UCard>
</div> </div>
</template> </template>
<script setup> <script setup>
const { metaSymbol } = useShortcuts() const { metaSymbol } = useShortcuts()
const { isDesktop } = useDevice() const { isDesktop } = useDevice()
const placeholder = computed(() => `提出后续问题${isDesktop ? '' + metaSymbol.value + 'L)' : ''}`) const placeholder = computed(() => `提出后续问题${isDesktop ? '' + metaSymbol.value + 'L)' : ''}`)
const props = defineProps({ const props = defineProps({
asking: { asking: {
type: Boolean, type: Boolean,
default: false default: false
} }
}) })
const emits = defineEmits(['ask', 'stop']) const emits = defineEmits(['ask', 'stop'])
const isFocus = ref(false) const isFocus = ref(false)
const cardUI = computed(() => { const cardUI = computed(() => {
const base = { const base = {
body: { body: {
padding: '', padding: '',
base: 'flex items-end has-[textarea[rows="1"]]:items-center pl-4 pr-1 py-1 gap-2' base: 'flex items-end has-[textarea[rows="1"]]:items-center pl-4 pr-1 py-1 gap-2'
}, },
// base: 'transition-[width] w-3/5 has-[textarea:focus]:w-full has-[button:focus]:w-full', // base: 'transition-[width] w-3/5 has-[textarea:focus]:w-full has-[button:focus]:w-full',
rounded: 'rounded has-[textarea[rows="1"]]:rounded-full' rounded: 'rounded has-[textarea[rows="1"]]:rounded-full'
} }
if (isFocus.value) { if (isFocus.value) {
base.base = 'transition-[width] w-full' base.base = 'transition-[width] w-full'
} else { } else {
base.base = 'transition-[width] w-full sm:w-3/5' base.base = 'transition-[width] w-full sm:w-3/5'
} }
return base return base
}) })
defineShortcuts({ defineShortcuts({
meta_enter: { meta_enter: {
usingInput: 'queryInput', usingInput: 'queryInput',
handler: () => { handler: () => {
handleAsk() handleAsk()
} }
}, },
escape: { escape: {
usingInput: 'queryInput', usingInput: 'queryInput',
handler: () => { handler: () => {
handleBlur() handleBlur()
} }
}, },
meta_l: { meta_l: {
handler: () => { handler: () => {
handleFocus() handleFocus()
}
} }
}
}) })
function handleFocus () { function handleFocus () {
queryInput.value.textarea.focus() queryInput.value.textarea.focus()
} }
function handleBlur () { function handleBlur () {
queryInput.value.textarea.blur() queryInput.value.textarea.blur()
} }
const continueQuestion = ref('') const continueQuestion = ref('')
function handleStop () { function handleStop () {
emits('stop') emits('stop')
} }
function handleAsk () { function handleAsk () {
if (props.asking) return if (props.asking) return
emits('ask', continueQuestion.value) emits('ask', continueQuestion.value)
continueQuestion.value = '' continueQuestion.value = ''
handleBlur() handleBlur()
} }
const queryInput = ref(null) const queryInput = ref(null)
function handleInputFocus () { function handleInputFocus () {
isFocus.value = true isFocus.value = true
} }
function handleInputBlur () { function handleInputBlur () {
setTimeout(() => { setTimeout(() => {
isFocus.value = false isFocus.value = false
}, 100) }, 100)
} }
</script> </script>
<template> <template>
<ISearchProcess :collapse="collapse" :actions="actions" :status="processStatus" /> <ISearchProcess :collapse="collapse" :actions="actions" :status="processStatus" />
<template v-if="item.source && item.source.length > 0"> <template v-if="item.source && item.source.length > 0">
<div class="text-xl flex items-center space-x-1"> <div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-link-20-solid" /> <UIcon name="i-heroicons-link-20-solid" />
<span>来源</span> <span>来源</span>
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<ISearchSource :source="item.source" /> <ISearchSource :source="item.source" />
</div> </div>
</template> </template>
<div class="text-xl flex items-center space-x-1" v-if="processStatus ==='finish'"> <div v-if="processStatus ==='finish'" class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-chat-bubble-left-right-20-solid" /> <UIcon name="i-heroicons-chat-bubble-left-right-20-solid" />
<span>{{ item.ansLoading ? '回答中' : '回答' }}</span> <span>{{ item.ansLoading ? '回答中' : '回答' }}</span>
</div> </div>
<IMdMdc :content="item.article" /> <IMdMdc :content="item.article" />
<div class="space-x-2" v-if="item.showActions"> <div v-if="item.showActions" class="space-x-2">
<UButton size="xs" color="gray" @click="handleCopyMD" leading-icon="i-heroicons-document-duplicate-20-solid" label="复制" /> <UButton size="xs" color="gray" leading-icon="i-heroicons-document-duplicate-20-solid" label="复制" @click="handleCopyMD" />
<UButton size="xs" color="gray" @click="handleShare" leading-icon="i-heroicons-share-20-solid" label="分享" /> <UButton size="xs" color="gray" leading-icon="i-heroicons-share-20-solid" label="分享" @click="handleShare" />
<UButton v-if="isLastIndex" size="xs" color="gray" @click="handleReGenerate" leading-icon="i-heroicons-arrow-path-rounded-square-20-solid" label="重写" /> <UButton v-if="isLastIndex" size="xs" color="gray" leading-icon="i-heroicons-arrow-path-rounded-square-20-solid" label="重写" @click="handleReGenerate" />
</div> </div>
</template> </template>
<script setup> <script setup>
const Search = inject('Search') const Search = inject('Search')
const toast = useToast() const toast = useToast()
const props = defineProps({ const props = defineProps({
item: { item: {
type: Object, type: Object,
default: (() => {}) default: (() => {})
}, },
asking: { asking: {
type: Boolean, type: Boolean,
default: false default: false
}, },
isLastIndex: { isLastIndex: {
type: Boolean, type: Boolean,
default: false default: false
}, },
index: { index: {
type: Number, type: Number,
default: 0 default: 0
}, },
collapse: { collapse: {
type: Boolean, type: Boolean,
default: true default: true
}, },
actions: { actions: {
type: Array, type: Array,
default: () => [] default: () => []
}, },
processStatus: { processStatus: {
type: String, type: String,
default: '' // start | finish default: '' // start | finish
} }
}) })
const emits = defineEmits(['regenerate']) const emits = defineEmits(['regenerate'])
function handleReGenerate () { function handleReGenerate () {
emits('regenerate', props.index) emits('regenerate', props.index)
} }
function handleCopyMD () { function handleCopyMD () {
useCopyToClipboard().copy(props.item.article) useCopyToClipboard().copy(props.item.article)
toast.add({ toast.add({
icon: 'i-heroicons-information-circle-20-solid', icon: 'i-heroicons-information-circle-20-solid',
timeout: 1000, timeout: 1000,
title: '复制成功' title: '复制成功'
}) })
} }
function handleShare () { function handleShare() {
const hash = props.index + 1; const hash = props.index + 1;
const url = window.location.href + (hash ? '#' + hash : '') const url = window.location.href + (hash ? '#' + hash : '')
Search.handleUpdateOpenState(url) Search.handleUpdateOpenState(url)
} }
</script> </script>
\ No newline at end of file
<template> <template>
<div class="flex flex-col gap-4 sticky top-16"> <div class="flex flex-col gap-4 sticky top-16">
<div class="text-xl flex items-center space-x-1"> <div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-square-3-stack-3d-20-solid" /> <UIcon name="i-heroicons-square-3-stack-3d-20-solid" />
<span>补充信息</span> <span>补充信息</span>
</div> </div>
<template v-for="(item, index) in data" :key="index"> <template v-for="(item, index) in data" :key="index">
<template v-if="item.ready"> <template v-if="item.ready">
<template v-if="item.type === 'search_relate_repo'"> <template v-if="item.type === 'search_relate_repo'">
<template v-if="item.data.length"> <template v-if="item.data.length">
<template v-for="(chart, cIndex) in item.data" :key="cIndex"> <template v-for="(chart, cIndex) in item.data" :key="cIndex">
<UCard :ui="cardUI"> <UCard :ui="cardUI">
<template v-if="chart.info"> <template v-if="chart.info">
<div class="flex mb-2"> <div class="flex mb-2">
<div class="flex items-center gap-1 text-sm overflow-hidden"> <div class="flex items-center gap-1 text-sm overflow-hidden">
<UIcon class="flex-shrink-0" name="i-simple-icons-github" /> <UIcon class="flex-shrink-0" name="i-simple-icons-github" />
<ULink class="flex-grow truncate underline" :to="chart.info.url" target="_blank" :title="chart.info.description">{{ chart.info.name }}</ULink> <ULink class="flex-grow truncate underline" :to="chart.info.url" target="_blank" :title="chart.info.description">{{ chart.info.name }}</ULink>
<UIcon class="flex-shrink-0" name="i-heroicons-arrow-top-right-on-square" /> <UIcon class="flex-shrink-0" name="i-heroicons-arrow-top-right-on-square" />
</div> </div>
</div> </div>
</template> </template>
<ProseChart <ProseChart
type="line" type="line"
:labels="chart.labels" :labels="chart.labels"
:data="chart.data" :data="chart.data"
:info="chart.info" :info="chart.info"
simple simple
/> />
<div class="text-xs text-center text-gray-500 dark:text-gray-400 mt-2">{{ item.title }}</div> <div class="text-xs text-center text-gray-500 dark:text-gray-400 mt-2">{{ item.title }}</div>
</UCard> </UCard>
</template> </template>
</template> </template>
<template v-else> <template v-else>
<UCard :ui="cardUI"> <UCard :ui="cardUI">
<IEmpty size="xs"/> <IEmpty size="xs"/>
</UCard> </UCard>
</template> </template>
</template> </template>
</template> </template>
<template v-else> <template v-else>
<UCard :ui="cardUI"> <UCard :ui="cardUI">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<USkeleton class="h-4" /> <USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" /> <USkeleton class="h-4 w-2/3" />
<USkeleton class="h-4" /> <USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" /> <USkeleton class="h-4 w-2/3" />
</div> </div>
</UCard> </UCard>
</template> </template>
</template> </template>
</div> </div>
</template> </template>
<script setup> <script setup>
defineProps({ defineProps({
data: { data: {
type: Array, type: Array,
default: () => [] default: () => []
} }
}) })
const cardUI = { const cardUI = {
body: { body: {
padding: 'sm:p-2 p-2', padding: 'sm:p-2 p-2',
base: 'h-full' base: 'h-full'
} }
} }
</script> </script>
<template> <template>
<header class="sticky top-0 z-10 bg-white dark:bg-black w-full flex flex-col"> <header class="sticky top-0 z-10 bg-white dark:bg-black w-full flex flex-col">
<div class="w-full p-2 justify-between flex"> <div class="w-full p-2 justify-between flex">
<IMenuSider /> <IMenuSider />
<div class="hidden md:flex"> <div class="hidden md:flex">
<div class="flex items-center gap-2" v-if="repo"> <div v-if="repo" class="flex items-center gap-2">
<UIcon name="i-simple-icons-github" />{{ repo }} <UIcon name="i-simple-icons-github" />{{ repo }}
</div> </div>
</div> </div>
<div class="flex-grow justify-center items-center space-x-2 hidden sm:flex"> <div class="flex-grow justify-center items-center space-x-2 hidden sm:flex">
<UTooltip text="点击修改标题" v-if="!isEditTitle"> <UTooltip v-if="!isEditTitle" text="点击修改标题">
<div @click="handleFocusTitle">{{ editTitle }}</div> <div @click="handleFocusTitle">{{ editTitle }}</div>
</UTooltip> </UTooltip>
<UInput <UInput
v-else v-else
ref="titleRef" ref="titleRef"
autofocus autofocus
:model-value="editTitle" :model-value="editTitle"
@blur="handleBlurTitle" @blur="handleBlurTitle"
/> />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<IActionThread <IActionThread
:collection_id="$selectCollectionId.length ? $selectCollectionId[0] : ''" :collection_id="$selectCollectionId.length ? $selectCollectionId[0] : ''"
:c_id="state.id" :c_id="state.id"
@delete="handleDeletedThread" @delete="handleDeletedThread"
/> />
<UButton <UButton
color="gray" color="gray"
variant="ghost" variant="ghost"
leading-icon="i-heroicons-plus-small" leading-icon="i-heroicons-plus-small"
:label="$selectCollectionId.length ? '已收藏' : '收藏'" :label="$selectCollectionId.length ? '已收藏' : '收藏'"
@click="handleOpenSelect" @click="handleOpenSelect"
/> />
<UPopover v-model:open="isShareOpen"> <UPopover v-model:open="isShareOpen">
<UButton <UButton
:leading-icon="isOpen ? 'i-heroicons-share-16-solid' : 'i-heroicons-lock-closed-16-solid'" :leading-icon="isOpen ? 'i-heroicons-share-16-solid' : 'i-heroicons-lock-closed-16-solid'"
label="分享" label="分享"
@click="handleSetOpenState" @click="handleSetOpenState"
/> />
<template #panel> <template #panel>
<div class="flex flex-col p-3 gap-2 min-w-72"> <div class="flex flex-col p-3 gap-2 min-w-72">
<div>访问权限</div> <div>访问权限</div>
<div class="flex flex-col border dark:border-gray-800 rounded"> <div class="flex flex-col border dark:border-gray-800 rounded">
<div <div
class="flex flex-grow justify-between m-1 p-1 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-gray-800" class="flex flex-grow justify-between m-1 p-1 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-gray-800"
@click="handleUpdateOpenState(false)" @click="handleUpdateOpenState(false)"
> >
<div class="flex-grow flex flex-col gap-1 text-sm"> <div class="flex-grow flex flex-col gap-1 text-sm">
<div class="flex items-center gap-1" :class="{ 'text-primary-500': !isOpen }"> <div class="flex items-center gap-1" :class="{ 'text-primary-500': !isOpen }">
<UIcon name="i-heroicons-lock-closed-16-solid" /> <UIcon name="i-heroicons-lock-closed-16-solid" />
<span>私密</span> <span>私密</span>
</div> </div>
<div class="text-xs text-gray-500">只有作者可以查看</div> <div class="text-xs text-gray-500">只有作者可以查看</div>
</div> </div>
<div v-if="!isOpen"> <div v-if="!isOpen">
<UIcon name="i-heroicons-check-circle-20-solid" class="text-primary-500 text-xl" /> <UIcon name="i-heroicons-check-circle-20-solid" class="text-primary-500 text-xl" />
</div> </div>
</div> </div>
<UDivider /> <UDivider />
<div <div
class="flex flex-grow justify-between m-1 p-1 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-gray-800" class="flex flex-grow justify-between m-1 p-1 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-gray-800"
@click="handleUpdateOpenState(true)" @click="handleUpdateOpenState(true)"
> >
<div class="flex-grow flex flex-col gap-1 text-sm" :class="{ 'text-primary-500': isOpen }"> <div class="flex-grow flex flex-col gap-1 text-sm" :class="{ 'text-primary-500': isOpen }">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<UIcon name="i-heroicons-share-20-solid" /> <UIcon name="i-heroicons-share-20-solid" />
<span>可分享的</span> <span>可分享的</span>
</div> </div>
<div class="text-xs text-gray-500">任何持有链接的人都可以查看</div> <div class="text-xs text-gray-500">任何持有链接的人都可以查看</div>
</div> </div>
<div v-if="isOpen"> <div v-if="isOpen">
<UIcon name="i-heroicons-check-circle-20-solid" class="text-primary-500 text-xl" /> <UIcon name="i-heroicons-check-circle-20-solid" class="text-primary-500 text-xl" />
</div> </div>
</div> </div>
</div> </div>
<div class="flex gap-1 items-center text-xs text-primary-500" v-if="isOpen"> <div v-if="isOpen" class="flex gap-1 items-center text-xs text-primary-500">
<UIcon name="i-heroicons-clipboard-document-check" /> <UIcon name="i-heroicons-clipboard-document-check" />
<span>链接已复制</span> <span>链接已复制</span>
</div> </div>
</div> </div>
</template> </template>
</UPopover> </UPopover>
</div> </div>
</div> </div>
<UDivider /> <UDivider />
</header> </header>
</template> </template>
<script setup> <script setup>
const toast = useToast() const toast = useToast()
...@@ -94,18 +94,21 @@ const { $selectCollectionId } = storeToRefs(useLibraryStore()) ...@@ -94,18 +94,21 @@ const { $selectCollectionId } = storeToRefs(useLibraryStore())
const { $openLibrarySelect, $setSelectCollectionId } = useLibraryStore() const { $openLibrarySelect, $setSelectCollectionId } = useLibraryStore()
const { findRecordCollection } = useCollectionRequest() const { findRecordCollection } = useCollectionRequest()
const props = defineProps({ const props = defineProps({
query: String, query: {
isPublic: { type: String,
type: Boolean, default: ''
default: false },
}, isPublic: {
repo: { type: Boolean,
type: String, default: false
default: '' },
} repo: {
type: String,
default: ''
}
}) })
const state = reactive({ const state = reactive({
id: route.params.id id: route.params.id
}) })
const Layout = inject('Layout') const Layout = inject('Layout')
const emits = defineEmits(['update-query']) const emits = defineEmits(['update-query'])
...@@ -116,72 +119,72 @@ const isOpen = ref(false) ...@@ -116,72 +119,72 @@ const isOpen = ref(false)
const isShareOpen = ref(false) const isShareOpen = ref(false)
watch(()=> props.query, () => { watch(()=> props.query, () => {
editTitle.value = props.query editTitle.value = props.query
}, { immediate: true }) }, { immediate: true })
watch(() => props.isPublic, () => { watch(() => props.isPublic, () => {
isOpen.value = props.isPublic isOpen.value = props.isPublic
}, { immediate: true }) }, { immediate: true })
function handleFocusTitle () { function handleFocusTitle () {
isEditTitle.value = true isEditTitle.value = true
} }
function handleBlurTitle () { function handleBlurTitle () {
isEditTitle.value = false isEditTitle.value = false
emits('update-query', editTitle.value) emits('update-query', editTitle.value)
} }
async function handleUpdateOpenState (s) { async function handleUpdateOpenState (s) {
const { data } = await useRequest(`/v1/chat/${state.id}/public/${s}`, { method: 'post' }) const { data } = await useRequest(`/v1/chat/${state.id}/public/${s}`, { method: 'post' })
if (data.value) isOpen.value = s if (data.value) isOpen.value = s
} }
function handleCopyLink (url) { function handleCopyLink (url) {
useCopyToClipboard().copy(url || window.location.href) useCopyToClipboard().copy(url || window.location.href)
toast.add({ toast.add({
icon: 'i-heroicons-information-circle-20-solid', icon: 'i-heroicons-information-circle-20-solid',
timeout: 2000, timeout: 2000,
title: '链接已复制到剪贴板' title: '链接已复制到剪贴板'
}) })
} }
async function handleSetOpenState() { async function handleSetOpenState() {
if (isShareOpen.value) return if (isShareOpen.value) return
if (!isOpen.value) { if (!isOpen.value) {
setTimeout(async () => { setTimeout(async () => {
await handleUpdateOpenState(true) await handleUpdateOpenState(true)
handleCopyLink() handleCopyLink()
}, 200) }, 200)
} else { } else {
await handleUpdateOpenState(true) await handleUpdateOpenState(true)
handleCopyLink() handleCopyLink()
} }
} }
function handleOpenSelect () { function handleOpenSelect () {
$openLibrarySelect(state.id, $selectCollectionId.value) $openLibrarySelect(state.id, $selectCollectionId.value)
} }
defineExpose({ defineExpose({
isOpen, isOpen,
handleUpdateOpenState, handleUpdateOpenState,
handleCopyLink handleCopyLink
}) })
async function initData () { async function initData () {
const { data, error } = await findRecordCollection(state.id) const { data, error } = await findRecordCollection(state.id)
if (!error.value) { if (!error.value) {
const ids = data.value.data.map(item => item.collection_id) const ids = data.value.data.map(item => item.collection_id)
$setSelectCollectionId(ids) $setSelectCollectionId(ids)
} }
} }
// 初始化登录的时候判断是否已收藏 // 初始化登录的时候判断是否已收藏
if ($isSignIn.value) { if ($isSignIn.value) {
await initData() await initData()
} }
function handleRemoveCollect (data) { function handleRemoveCollect (data) {
const { c_id } = data.value const { c_id } = data.value
if (c_id === state.id) { if (c_id === state.id) {
$setSelectCollectionId([]) $setSelectCollectionId([])
Layout.handleClearRemoveCollectData() Layout.handleClearRemoveCollectData()
} }
} }
watch(()=> Layout.removeCollectData, (data) => { watch(()=> Layout.removeCollectData, (data) => {
if (data.value !== null) handleRemoveCollect(data) if (data.value !== null) handleRemoveCollect(data)
}, { deep: true }) }, { deep: true })
function handleDeletedThread () { function handleDeletedThread () {
navigateTo('/') navigateTo('/')
} }
</script> </script>
\ No newline at end of file
<template> <template>
<UCard :ui="{ body: { padding: 'p-4 sm:p-4' } }"> <UCard :ui="{ body: { padding: 'p-4 sm:p-4' } }">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex items-center gap-2 text-lg"> <div class="flex items-center gap-2 text-lg">
<UIcon name="i-heroicons-sparkles-20-solid" /> <UIcon name="i-heroicons-sparkles-20-solid" />
搜索过程 搜索过程
</div> </div>
<UButton <UButton
size="md" size="md"
color="gray" color="gray"
variant="ghost" variant="ghost"
:icon="openCollapse ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'" :icon="openCollapse ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'"
:ui="{ rounded: 'rounded-full' }" :ui="{ rounded: 'rounded-full' }"
@click="handleToggleCollapse" @click="handleToggleCollapse"
/> />
</div> </div>
<ICollapse :open="openCollapse" class="mt-2"> <ICollapse :open="openCollapse" class="mt-2">
<div v-auto-animate class="flex flex-col gap-2 w-full text-gray-500 dark:text-gray-400"> <div v-auto-animate class="flex flex-col gap-2 w-full text-gray-500 dark:text-gray-400">
<template v-if="status !== 'finish' && !actions.length"> <template v-if="status !== 'finish' && !actions.length">
<USkeleton class="h-4" /> <USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" /> <USkeleton class="h-4 w-2/3" />
</template> </template>
<template v-for="(action, index) in actions" :key="action.action"> <template v-for="(action, index) in actions" :key="action.action">
<ISearchProcessAction <ISearchProcessAction
:action="action" :action="action"
:last="actions.length === index + 1" :last="actions.length === index + 1"
:status="status" :status="status"
/> />
</template> </template>
</div> </div>
</ICollapse> </ICollapse>
</UCard> </UCard>
</template> </template>
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
type: { type: {
type: String, type: String,
default: 'search' default: 'search'
}, },
collapse: { collapse: {
type: Boolean, type: Boolean,
default: true default: true
}, },
actions: { actions: {
type: Array, type: Array,
default: () => [] default: () => []
}, },
status: { status: {
type: String, type: String,
default: '' default: ''
} }
}) })
const openCollapse = ref(props.collapse) const openCollapse = ref(props.collapse)
function handleToggleCollapse () { function handleToggleCollapse () {
openCollapse.value = !openCollapse.value openCollapse.value = !openCollapse.value
} }
function handleCollapse (state) { function handleCollapse (state) {
openCollapse.value = state openCollapse.value = state
} }
watch(() => props.collapse, () => { watch(() => props.collapse, () => {
if (props.collapse) { if (props.collapse) {
handleCollapse(true) handleCollapse(true)
} else { } else {
setTimeout(() => { setTimeout(() => {
handleCollapse(false) handleCollapse(false)
}, 1000) }, 1000)
} }
}, { immediate: true}) }, { immediate: true})
defineExpose({ handleCollapse }) defineExpose({ handleCollapse })
</script> </script>
<template> <template>
<div class="text-base flex items-center gap-1"> <div class="text-base flex items-center gap-1">
<UIcon :name="item.icon" /> <UIcon :name="item.icon" />
{{ item.name }} {{ item.name }}
</div> </div>
<template v-if="['rephrase_question', 'tool_select'].includes((action.action))"> <template v-if="['rephrase_question', 'tool_select'].includes((action.action))">
<div class="pl-5"> <div class="pl-5">
<IMdMdc :content="action.output" size="sm" /> <IMdMdc :content="action.output" size="sm" />
</div> </div>
</template> </template>
<template v-else-if="['search_file', 'search_web'].includes(action.action)"> <template v-else-if="['search_file', 'search_web'].includes(action.action)">
<div class="text-xs pl-5">找到 {{ action.output.length }} 条来源</div> <div class="text-xs pl-5">找到 {{ action.output.length }} 条来源</div>
</template> </template>
<template v-if="last && status !== 'finish'"> <template v-if="last && status !== 'finish'">
<USkeleton class="h-4" /> <USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" /> <USkeleton class="h-4 w-2/3" />
</template> </template>
</template> </template>
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
action: { action: {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
last: { last: {
type: Boolean, type: Boolean,
default: false default: false
}, },
status: { status: {
type: String, type: String,
default: '' default: ''
} }
}) })
const item = computed(() => { const item = computed(() => {
const action = props.action.action const action = props.action.action
let name, icon let name, icon
if (action === 'rephrase_question') { if (action === 'rephrase_question') {
name = '理解问题' name = '理解问题'
icon = 'i-heroicons-inbox-arrow-down' icon = 'i-heroicons-inbox-arrow-down'
} }
else if (action === 'search_file') { else if (action === 'search_file') {
name = '搜索项目' name = '搜索项目'
icon = 'i-heroicons-magnifying-glass' icon = 'i-heroicons-magnifying-glass'
} }
else if (action === 'search_web') { else if (action === 'search_web') {
name = '搜索网页' name = '搜索网页'
icon = 'i-heroicons-magnifying-glass' icon = 'i-heroicons-magnifying-glass'
} }
else if (action === 'tool_select') { else if (action === 'tool_select') {
name = '使用工具' name = '使用工具'
icon = 'i-heroicons-puzzle-piece' icon = 'i-heroicons-puzzle-piece'
} }
else if (action === 'search_relate_repo') { else if (action === 'search_relate_repo') {
name = '查找相关项目' name = '查找相关项目'
icon = 'i-heroicons-rectangle-group' icon = 'i-heroicons-rectangle-group'
} }
return { return {
name, name,
icon icon
} }
}) })
</script> </script>
\ No newline at end of file
<template> <template>
<div class="text-xl flex items-center space-x-1"> <div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-rectangle-group-20-solid" /> <UIcon name="i-heroicons-rectangle-group-20-solid" />
<span>相关问题</span> <span>相关问题</span>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<UButton <UButton
v-for="item in recommendQuestions" v-for="(item, index) in recommendQuestions"
color="gray" :key="index"
size="md" color="gray"
variant="soft" size="md"
@click="handleClick(item.title)" variant="soft"
> @click="handleClick(item.title)"
<div class="justify-between w-full flex items-center"> >
<div class="truncate">{{ item.title }}</div> <div class="justify-between w-full flex items-center">
<UIcon name="i-heroicons-plus-20-solid" class="text-base" /> <div class="truncate">{{ item.title }}</div>
</div> <UIcon name="i-heroicons-plus-20-solid" class="text-base" />
</UButton> </div>
</div> </UButton>
</div>
</template> </template>
<script setup> <script setup>
defineProps({ defineProps({
recommendQuestions: { recommendQuestions: {
type: Array, type: Array,
default: () => [] default: () => []
} }
}) })
const emits = defineEmits(['click']) const emits = defineEmits(['click'])
function handleClick(title) { function handleClick(title) {
emits('click', title) emits('click', title)
} }
</script> </script>
\ No newline at end of file
<template> <template>
<ULink v-for="(item, index) in limitSource" :to="item.url" :title="item.url" target="_blank"> <ULink v-for="(item, index) in limitSource" :key="index" :to="item.url" :title="item.url" target="_blank">
<UCard :ui="cardUI"> <UCard :ui="cardUI">
<div class="flex flex-col h-full gap-1"> <div class="flex flex-col h-full gap-1">
<template v-if="getIconPath(item.url) === 'github'"> <template v-if="getIconPath(item.url) === 'github'">
<div class="items-center flex gap-1"> <div class="items-center flex gap-1">
<UIcon name="i-simple-icons-github" class="flex-shrink-0" /> <UIcon name="i-simple-icons-github" class="flex-shrink-0" />
<div class="flex flex-grow overflow-hidden"> <div class="flex flex-grow overflow-hidden">
<div class="truncate">{{ item.title }}</div> <div class="truncate">{{ item.title }}</div>
</div> </div>
<div class="text-gray-200">{{ index + 1 }}</div> <div class="text-gray-200">{{ index + 1 }}</div>
</div> </div>
<div class="text-blue-500 line-clamp-1">{{ item.label }}</div> <div class="text-blue-500 line-clamp-1">{{ item.label }}</div>
</template> </template>
<template v-else> <template v-else>
<div class="flex gap-1 h-full"> <div class="flex gap-1 h-full">
<div class="line-clamp-2 text-sm">{{ item.title }}</div> <div class="line-clamp-2 text-sm">{{ item.title }}</div>
</div> </div>
<div class="items-center flex gap-1"> <div class="items-center flex gap-1">
<UAvatar :src="getIconPath(item.url)" size="2xs" class="flex-shrink-0" /> <UAvatar :src="getIconPath(item.url)" size="2xs" class="flex-shrink-0" />
<div class="flex flex-grow overflow-hidden"> <div class="flex flex-grow overflow-hidden">
<div class="truncate text-xs text-gray-500">{{ getDomain(item.url) }}</div> <div class="truncate text-xs text-gray-500">{{ getDomain(item.url) }}</div>
</div> </div>
<div class="text-gray-200 text-sm">{{ index + 1 }}</div> <div class="text-gray-200 text-sm">{{ index + 1 }}</div>
</div> </div>
</template> </template>
</div> </div>
</UCard> </UCard>
</ULink> </ULink>
<UCard class="cursor-pointer" :ui="cardUI" v-if="source.length > 6" @click="handleToggleShowAll"> <UCard v-if="source.length > 6" class="cursor-pointer" :ui="cardUI" @click="handleToggleShowAll">
<div class="flex items-center justify-center h-full gap-1"> <div class="flex items-center justify-center h-full gap-1">
<UIcon v-if="!showAllSource" name="i-heroicons-chevron-down-20-solid" /> <UIcon v-if="!showAllSource" name="i-heroicons-chevron-down-20-solid" />
<UIcon v-else name="i-heroicons-chevron-up-20-solid" /> <UIcon v-else name="i-heroicons-chevron-up-20-solid" />
<div v-if="!showAllSource">查看全部{{ source.length }}个来源</div> <div v-if="!showAllSource">查看全部{{ source.length }}个来源</div>
<div v-else>收起</div> <div v-else>收起</div>
</div> </div>
</UCard> </UCard>
</template> </template>
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
source: { source: {
type: Array, type: Array,
default: () => [] default: () => []
} }
}) })
const cardUI = { const cardUI = {
base: 'h-full', base: 'h-full',
body: { body: {
padding: 'sm:p-2 p-2', padding: 'sm:p-2 p-2',
base: 'h-full' base: 'h-full'
}, },
background: 'transition hover:bg-gray-100 dark:hover:bg-gray-800' background: 'transition hover:bg-gray-100 dark:hover:bg-gray-800'
} }
const showAllSource = ref(false) const showAllSource = ref(false)
const limitSource = computed(() => { const limitSource = computed(() => {
if (props.source.length <= 6) return props.source if (props.source.length <= 6) return props.source
else if (showAllSource.value) return props.source else if (showAllSource.value) return props.source
else return props.source.slice(0, 5) else return props.source.slice(0, 5)
}) })
function getIconPath (url) { function getIconPath (url) {
if (!url || !url.startsWith('http')) return '' if (!url || !url.startsWith('http')) return ''
const uri = new URL(url) const uri = new URL(url)
if (uri.origin.endsWith('github.com')) return 'github' if (uri.origin.endsWith('github.com')) return 'github'
return `https://toolb.cn/favicon/${url}` return `https://toolb.cn/favicon/${url}`
} }
function getDomain(url) { function getDomain(url) {
// 使用正则表达式匹配协议和域名部分 // 使用正则表达式匹配协议和域名部分
const regex = /^(https?:\/\/)?([^\/]+)/ const regex = /^(https?:\/\/)?([^/]+)/
const match = url.match(regex) const match = url.match(regex)
// 如果匹配不到,返回空字符串 // 如果匹配不到,返回空字符串
if (!match) { if (!match) {
return '' return ''
} }
// 获取域名部分 // 获取域名部分
const domain = match[2] const domain = match[2]
// 去除可能存在的端口号 // 去除可能存在的端口号
// 返回域名 // 返回域名
return domain.split(':')[0]; return domain.split(':')[0];
} }
const handleToggleShowAll = () => { const handleToggleShowAll = () => {
showAllSource.value = !showAllSource.value showAllSource.value = !showAllSource.value
} }
</script> </script>
<template> <template>
<component :is="titleTag" :class="{ 'text-3xl': titleTag !== 'div' }" :id="id">{{ title }}</component> <component :is="titleTag" :id="id" :class="{ 'text-3xl': titleTag !== 'div' }">{{ title }}</component>
</template> </template>
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
as: { as: {
type: String, type: String,
default: 'h2' default: 'h2'
}, },
title: { title: {
type: String, type: String,
default: '' default: ''
}, },
id: { id: {
type: [String, Number], type: [String, Number],
default: '' default: ''
} }
}) })
const titleTag = computed(() => { const titleTag = computed(() => {
let tag = props.as let tag = props.as
const title = props.title const title = props.title
if (title.indexOf('\n') > -1) tag = 'div' if (title.indexOf('\n') > -1) tag = 'div'
else if (title.length > 50) tag = 'div' else if (title.length > 50) tag = 'div'
return tag return tag
}) })
</script> </script>
<template> <template>
<div class="grid grid-cols-1"> <div class="grid grid-cols-1">
<article class="prose dark:prose-invert max-w-none" v-html="mdHtml"></article> <article class="prose dark:prose-invert max-w-none" v-html="mdHtml"/>
</div> </div>
</template> </template>
<script setup> <script setup>
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js' import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css' import 'highlight.js/styles/atom-one-dark.css'
const props = defineProps({ const props = defineProps({
content: String content: {
type: String,
default: ''
}
}) })
const mdHtml = ref('') const mdHtml = ref('')
const initMarkdownIt = new MarkdownIt({ const initMarkdownIt = new MarkdownIt({
...@@ -29,10 +32,10 @@ const initMarkdownIt = new MarkdownIt({ ...@@ -29,10 +32,10 @@ const initMarkdownIt = new MarkdownIt({
} }
}) })
const handleRenderMd = () => { const handleRenderMd = () => {
mdHtml.value = initMarkdownIt.render(props.content || '') mdHtml.value = initMarkdownIt.render(props.content || '')
} }
watch( () => props.content, () => { watch( () => props.content, () => {
handleRenderMd() handleRenderMd()
}, { immediate: true }) }, { immediate: true })
</script> </script>
<style> <style>
......
<template> <template>
<div class="w-full relative" style="aspect-ratio: 2/1"> <div class="w-full relative" style="aspect-ratio: 2/1">
<canvas ref="refChart" :aria-label="title"></canvas> <canvas ref="refChart" :aria-label="title"/>
</div> </div>
</template> </template>
<script setup> <script setup>
import { Chart } from 'chart.js/auto'; import { Chart } from 'chart.js/auto';
const colorMode = useColorMode() const colorMode = useColorMode()
const props = defineProps({ const props = defineProps({
type: { type: {
type: String, type: String,
default: 'line' default: 'line'
}, },
title: { title: {
type: String, type: String,
default: '' default: ''
}, },
labels: { labels: {
type: Array, type: Array,
default: () => [] default: () => []
}, },
data: { data: {
type: Array, type: Array,
default: () => [] default: () => []
}, },
simple: { simple: {
type: Boolean, type: Boolean,
default: false default: false
} }
}) })
const refChart = ref(null) const refChart = ref(null)
let chart let chart
function init () { function init () {
Chart.defaults.datasets.line.fill = true Chart.defaults.datasets.line.fill = true
if (props.simple) { if (props.simple) {
Chart.defaults.plugins.legend.display = false Chart.defaults.plugins.legend.display = false
} }
chart = new Chart(refChart.value, { chart = new Chart(refChart.value, {
type: props.type, type: props.type,
data: { data: {
labels: props.labels, labels: props.labels,
datasets: props.data.map(item => { datasets: props.data.map(item => {
// item.backgroundColor = ['rgba(54, 162, 235, 0.2)', 'rgba(255, 99, 132, 0.2)', 'rgba(255, 159, 64, 0.2)', 'rgba(255, 205, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(201, 203, 207, 0.2)'] // item.backgroundColor = ['rgba(54, 162, 235, 0.2)', 'rgba(255, 99, 132, 0.2)', 'rgba(255, 159, 64, 0.2)', 'rgba(255, 205, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(201, 203, 207, 0.2)']
// item.borderColor = ['rgb(54, 162, 235)', 'rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)', 'rgb(201, 203, 207)'] // item.borderColor = ['rgb(54, 162, 235)', 'rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)', 'rgb(201, 203, 207)']
item.backgroundColor = ['rgba(54, 162, 235, 0.2)'] item.backgroundColor = ['rgba(54, 162, 235, 0.2)']
item.borderColor = ['rgb(54, 162, 235)'] item.borderColor = ['rgb(54, 162, 235)']
item.borderWidth = 1 item.borderWidth = 1
return item return item
}) })
},
options: {
maintainAspectRatio: false,
plugins: {
title: {
display: !props.simple,
text: props.title,
font: {
size: 14,
weight: 'normal'
}
}
},
scales: {
x: {
display: !props.simple,
grid: {
display: false
}
}, },
options: { y: {
maintainAspectRatio: false, display: !props.simple,
plugins: { grid: {
title: { display: false
display: !props.simple, },
text: props.title, stacked: true
font: {
size: 14,
weight: 'normal'
}
}
},
scales: {
x: {
display: !props.simple,
grid: {
display: false
}
},
y: {
display: !props.simple,
grid: {
display: false
},
stacked: true
}
},
radius: 0,
interaction: {
intersect: false,
}
} }
}) },
radius: 0,
interaction: {
intersect: false,
}
}
})
} }
function destroy () { function destroy () {
if (chart) { if (chart) {
chart.destroy() chart.destroy()
chart = null chart = null
} }
} }
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
init() init()
}) })
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
destroy() destroy()
}) })
watch(()=> colorMode.value, (value) => { watch(()=> colorMode.value, (value) => {
let color = '#e5e7eb' let color = '#e5e7eb'
if (value === 'dark') color = '#1f2937' if (value === 'dark') color = '#1f2937'
Chart.defaults.borderColor = color Chart.defaults.borderColor = color
if (chart) { if (chart) {
destroy() destroy()
init() init()
} }
}, { immediate: true, deep: true }) }, { immediate: true, deep: true })
</script> </script>
<template> <template>
<UTable <UTable
:rows="data" :rows="data"
:columns="columns" :columns="columns"
:ui="config" :ui="config"
:sort-button="sortButton" :sort-button="sortButton"
> >
<template #repo_name-data="{ row }"> <template #repo_name-data="{ row }">
<ULink :to="'https://github.com/' + row.repo_name" target="_blank">{{ row.repo_name }}</ULink> <ULink :to="'https://github.com/' + row.repo_name" target="_blank">{{ row.repo_name }}</ULink>
</template> </template>
<template #repo_url-data="{ row }"> <template #repo_url-data="{ row }">
<ULink :to="row.repo_url" target="_blank">{{ row.repo_url }}</ULink> <ULink :to="row.repo_url" target="_blank">{{ row.repo_url }}</ULink>
</template> </template>
</UTable> </UTable>
</template> </template>
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
data: { data: {
type: Array, type: Array,
default: () => [] default: () => []
} }
}) })
const columns = computed(() => { const columns = computed(() => {
const columns = [] const columns = []
for (const key in props.data[0]) { for (const key in props.data[0]) {
columns.push({ columns.push({
key, key,
label: key, label: key,
sortable: true sortable: true
}) })
} }
return columns return columns
}) })
const config = { const config = {
base: 'table-auto', base: 'table-auto',
td: { td: {
base: 'max-w-96 whitespace-normal' base: 'max-w-96 whitespace-normal'
} }
} }
const sortButton = { const sortButton = {
icon: 'i-heroicons-chevron-up-down-20-solid' icon: 'i-heroicons-chevron-up-down-20-solid'
} }
</script> </script>
\ No newline at end of file
<template> <template>
<NuxtLink <NuxtLink
:href="href" :href="href"
:target="target" :target="target"
> >
<slot /> <slot />
</NuxtLink> </NuxtLink>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { PropType } from 'vue' import type { PropType } from 'vue'
defineProps({ defineProps({
href: { href: {
type: String, type: String,
default: '' default: ''
}, },
target: { target: {
type: String as PropType<'_blank' | '_parent' | '_self' | '_top' | (string & object) | null | undefined>, type: String as PropType<'_blank' | '_parent' | '_self' | '_top' | (string & object) | null | undefined>,
default: '_blank', default: '_blank',
required: false required: false
} }
}) })
</script> </script>
\ No newline at end of file
<template> <template>
<UCard :ui="cardUI"> <UCard :ui="cardUI">
<pre class="flex justify-between items-center m-0 p-1 pl-4 pr-1 rounded-none dark"> <pre class="flex justify-between items-center m-0 p-1 pl-4 pr-1 rounded-none dark">
<div>{{ language }}</div> <div>{{ language }}</div>
<UButton <UButton
leading-icon="i-heroicons-document-duplicate-20-solid" leading-icon="i-heroicons-document-duplicate-20-solid"
...@@ -9,9 +9,9 @@ ...@@ -9,9 +9,9 @@
@click="handleCopy" @click="handleCopy"
/> />
</pre> </pre>
<UDivider :ui="{ border: { base: 'border-gray-700' } }" /> <UDivider :ui="{ border: { base: 'border-gray-700' } }" />
<pre :class="$props.class" class="m-0 rounded-none"><code v-html="codeBlock"></code></pre> <pre :class="$props.class" class="m-0 rounded-none"><code v-html="codeBlock"/></pre>
</UCard> </UCard>
</template> </template>
<script setup> <script setup>
...@@ -19,54 +19,54 @@ import hljs from 'highlight.js' ...@@ -19,54 +19,54 @@ import hljs from 'highlight.js'
import 'highlight.js/styles/stackoverflow-dark.css' import 'highlight.js/styles/stackoverflow-dark.css'
const toast = useToast() const toast = useToast()
const props = defineProps({ const props = defineProps({
code: { code: {
type: String, type: String,
default: '' default: ''
}, },
language: { language: {
type: String, type: String,
default: null default: null
}, },
filename: { filename: {
type: String, type: String,
default: null default: null
}, },
highlights: { highlights: {
type: Array, type: Array,
default: () => [] default: () => []
}, },
meta: { meta: {
type: String, type: String,
default: null default: null
}, },
class: { class: {
type: String, type: String,
default: null default: null
} }
}) })
const cardUI = { const cardUI = {
body: { body: {
padding: 'p-0 sm:p-0' padding: 'p-0 sm:p-0'
}, },
base: 'overflow-hidden mt-5 mb-5', base: 'overflow-hidden mt-5 mb-5',
ring: 'ring-0 dark:ring-1' ring: 'ring-0 dark:ring-1'
} }
const handleCopy = () => { const handleCopy = () => {
useCopyToClipboard().copy(props.code) useCopyToClipboard().copy(props.code)
toast.add({ toast.add({
icon: 'i-heroicons-information-circle-20-solid', icon: 'i-heroicons-information-circle-20-solid',
timeout: 1000, timeout: 1000,
title: '复制成功' title: '复制成功'
}) })
} }
const codeBlock = ref(null) const codeBlock = ref(null)
const handleRender = () => { const handleRender = () => {
const language = props.language || 'html' const language = props.language || 'html'
const lang = language.startsWith('vue') ? 'html' : language const lang = language.startsWith('vue') ? 'html' : language
codeBlock.value = hljs.highlight(props.code, { language: lang }).value codeBlock.value = hljs.highlight(props.code, { language: lang }).value
} }
watch(()=> props.code, () => { watch(()=> props.code, () => {
handleRender(); handleRender();
}, { immediate: true }) }, { immediate: true })
</script> </script>
......
export default () => { export default () => {
const { $getCollection } = useLibraryStore() const { $getCollection } = useLibraryStore()
// 创建及修改收藏夹 // 创建及修改收藏夹
const setOrUpdateCollection = async (body) => { const setOrUpdateCollection = async (body) => {
/* /*
* id number 非必须 有ID参数是修改,没有ID则为新增 * id number 非必须 有ID参数是修改,没有ID则为新增
* name string 非必须 * name string 非必须
* is_public number 非必须 * is_public number 非必须
* description string * description string
* *
*/ */
const {data, error} = await useRequest('/v1/collection/merge', { const {data, error} = await useRequest('/v1/collection/merge', {
method: 'post', method: 'post',
body body
}) })
return { data, error } return { data, error }
} }
// 删除收藏夹 // 删除收藏夹
const deleteCollection = async (collection_id) => { const deleteCollection = async (collection_id) => {
const {data, error} = await useRequest(`/v1/collection/${collection_id}/remove`, { const {data, error} = await useRequest(`/v1/collection/${collection_id}/remove`, {
method: 'post' method: 'post'
}) })
return { data, error } return { data, error }
} }
// 将会话添加到收藏夹 // 将会话添加到收藏夹
const saveCollection = async (body) => { const saveCollection = async (body) => {
// collection_id number 收藏夹ID // collection_id number 收藏夹ID
// c_id string 会话ID // c_id string 会话ID
const {data, error} = await useRequest(`/v1/collection/item/add`, { const {data, error} = await useRequest(`/v1/collection/item/add`, {
method: 'post', method: 'post',
body body
}) })
return { data, error } return { data, error }
} }
// 查询收藏夹会话列表 // 查询收藏夹会话列表
const findCollection = async (collection_id) => { const findCollection = async (collection_id) => {
const {data, error} = await useRequest(`/v1/collection/${collection_id}/items`) const {data, error} = await useRequest(`/v1/collection/${collection_id}/items`)
if (error.value) { if (error.value) {
return [] return []
}
return data.value.data || []
}
// 删除收藏夹会话
const deleteCollectionRecord = async (collection_id, c_id) => {
const {data, error} = await useRequest(`/v1/collection/item/delete`, {
method: 'post',
body: { collection_id, c_id }
})
$getCollection()
return { data, error }
}
// 查询会话是否被收藏
const findRecordCollection = async (c_id) => {
const {data, error} = await useRequest(`/v1/collection/item/check/${c_id}`)
return { data, error }
} }
return data.value.data || []
}
// 删除收藏夹会话
const deleteCollectionRecord = async (collection_id, c_id) => {
const {data, error} = await useRequest(`/v1/collection/item/delete`, {
method: 'post',
body: { collection_id, c_id }
})
$getCollection()
return { data, error }
}
// 查询会话是否被收藏
const findRecordCollection = async (c_id) => {
const {data, error} = await useRequest(`/v1/collection/item/check/${c_id}`)
return { data, error }
}
const deleteThread = async (ids) => { const deleteThread = async (ids) => {
const {data, error} = await useRequest('/v1/chat/completion/remove', { const {data, error} = await useRequest('/v1/chat/completion/remove', {
method: 'post', method: 'post',
body: ids body: ids
}) })
return { data, error } return { data, error }
} }
const getRepoStars = async (params) => { const getRepoStars = async (params) => {
/* /*
url githuburl 多个使用逗号分割 url githuburl 多个使用逗号分割
per 数据统计粒度,day,week,month,默认是用week per 数据统计粒度,day,week,month,默认是用week
day_period 查询周期天,默认365 day_period 查询周期天,默认365
*/ */
const { data, error } = await useRequest('/v1/repo/stars', { params }) const { data, error } = await useRequest('/v1/repo/stars', { params })
if (error.value) return [] if (error.value) return []
return data.value.data return data.value.data
} }
return { return {
setOrUpdateCollection, setOrUpdateCollection,
deleteCollection, deleteCollection,
saveCollection, saveCollection,
findCollection, findCollection,
deleteCollectionRecord, deleteCollectionRecord,
findRecordCollection, findRecordCollection,
deleteThread, deleteThread,
getRepoStars getRepoStars
} }
} }
\ No newline at end of file
export default () => { export default () => {
// 查询主题列表 // 查询主题列表
const getThreadsList = async (c_ids) => { const getThreadsList = async (c_ids) => {
// c_ids => c_id,c_id // c_ids => c_id,c_id
let query = c_ids ? `?c_ids=${c_ids}` : '' let query = c_ids ? `?c_ids=${c_ids}` : ''
const { data, error } = await useRequest(`/v1/chat/completion/list${query}`) const { data, error } = await useRequest(`/v1/chat/completion/list${query}`)
if (error.value) { if (error.value) {
return [] return []
}
return data.value.data || []
}
return {
getThreadsList
} }
return data.value.data || []
}
return {
getThreadsList
}
} }
\ No newline at end of file
const BASE_URL = 'https://gpu-pod656e861afe3d944d6b3ce77e-7862.node.inscode.run' const BASE_URL = 'https://gpu-pod656e861afe3d944d6b3ce77e-7862.node.inscode.run'
const request = async (url, options = {}) => { const request = async (url, options = {}) => {
const token = useCookie('token') const token = useCookie('token')
const fullUrl = BASE_URL + url const fullUrl = BASE_URL + url
const config = { const config = {
method: options.method || 'get', method: options.method || 'get',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': token 'Authorization': token
}, },
onRequest({ request, options }) { onRequest() {
// 设置请求头 // 设置请求头
}, },
onRequestError({ request, options, error }) { onRequestError({ error }) {
// 处理请求错误 // 处理请求错误
error && console.error(error) error && console.error(error)
}, },
onResponse({ request, response, options }) { onResponse() {
}, },
onResponseError({ request, response, options }) { onResponseError({ request, response }) {
const status = response.status const status = response.status
useRequestError(status, response._data.message) useRequestError(status, response._data.message)
// 处理响应错误 // 处理响应错误
console.log('[ResponseError]', request) console.log('[ResponseError]', request)
}
} }
if (options && options.headers) { }
Object.assign(config.headers, options.headers) if (options && options.headers) {
delete options.headers Object.assign(config.headers, options.headers)
} delete options.headers
return useFetch(fullUrl, Object.assign(config, options)); }
return useFetch(fullUrl, Object.assign(config, options));
} }
export default request export default request
export default function (status, message) { export default function (status, message) {
if (process.client && [400, 401, 403].includes(status)) { if (import.meta.client && [400, 401, 403].includes(status)) {
// 全局弹提示 // 全局弹提示
let title let title
if (status === 400) title = message if (status === 400) title = message
else if (status === 401) title = '抱歉,您尚未登录' else if (status === 401) title = '抱歉,您尚未登录'
else if (status === 403) title = '抱歉,您没有权限' else if (status === 403) title = '抱歉,您没有权限'
nextTick(() => { nextTick(() => {
const toast = useToast() const toast = useToast()
toast.add({ toast.add({
icon: 'i-heroicons-exclamation-triangle-20-solid', icon: 'i-heroicons-exclamation-triangle-20-solid',
timeout: 3000, timeout: 3000,
title: title, title: title,
color: 'red' color: 'red'
}) })
}) })
// 全局弹登录 // 全局弹登录
const { $isSignIn } = storeToRefs(useUserStore()) const { $isSignIn } = storeToRefs(useUserStore())
const { $openSign } = useUserStore() const { $openSign } = useUserStore()
if (status === 401 && !$isSignIn.value) { if (status === 401 && !$isSignIn.value) {
$openSign() $openSign()
}
} }
}
} }
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册