提交 d5f82065 编写于 作者: D DebugIsFalse

fix: eslint

上级 ddccb0a3
......@@ -22,4 +22,6 @@ logs
.env
.env.*
!.env.example
yarn.lock
\ No newline at end of file
yarn.lock
.eslintcache
.vscode/
\ No newline at end of file
<template>
<NuxtPwaManifest />
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<UNotifications />
<UModals />
<NuxtPwaManifest />
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<UNotifications />
<UModals />
</template>
<script setup>
const { $updateUserInfo } = useUserStore()
useHead({
title: 'GitBot AI'
title: 'GitBot AI'
})
nextTick(() => {
$updateUserInfo()
$updateUserInfo()
})
</script>
<template>
<UCard :ui="{ body: { padding: 'p-4 sm:p-4' } }">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2 text-lg">
<UIcon name="i-heroicons-sparkles-20-solid" />
搜索过程
</div>
<UButton
size="md"
color="gray"
variant="ghost"
:icon="openCollapse ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'"
:ui="{ rounded: 'rounded-full' }"
@click="handleToggleCollapse"
/>
</div>
<ICollapse :open="openCollapse" class="mt-2">
<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">
<UIcon name="i-heroicons-inbox-arrow-down" />
理解问题
</div>
<template v-if="item.desLoading">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<IMdMdc v-else :content="item.description" />
<template v-if="item.searchLoading !== undefined">
<div class="text-base flex items-center gap-1">
<UIcon name="i-heroicons-magnifying-glass" />
搜索项目
</div>
<template v-if="item.searchLoading">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<div class="text-xs pl-5" v-else>找到 {{ item.source && item.source.length || 0 }} 条来源</div>
</template>
<div class="text-base flex items-center gap-1" v-if="item.ansLoading !== undefined">
<UIcon name="i-heroicons-pencil-square" />
整理答案
</div>
</div>
</ICollapse>
</UCard>
<UCard :ui="{ body: { padding: 'p-4 sm:p-4' } }">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2 text-lg">
<UIcon name="i-heroicons-sparkles-20-solid" />
搜索过程
</div>
<UButton
size="md"
color="gray"
variant="ghost"
:icon="openCollapse ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'"
:ui="{ rounded: 'rounded-full' }"
@click="handleToggleCollapse"
/>
</div>
<ICollapse :open="openCollapse" class="mt-2">
<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">
<UIcon name="i-heroicons-inbox-arrow-down" />
理解问题
</div>
<template v-if="item.desLoading">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<IMdMdc v-else :content="item.description" />
<template v-if="item.searchLoading !== undefined">
<div class="text-base flex items-center gap-1">
<UIcon name="i-heroicons-magnifying-glass" />
搜索项目
</div>
<template v-if="item.searchLoading">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<div v-else class="text-xs pl-5">找到 {{ item.source && item.source.length || 0 }} 条来源</div>
</template>
<div v-if="item.ansLoading !== undefined" class="text-base flex items-center gap-1">
<UIcon name="i-heroicons-pencil-square" />
整理答案
</div>
</div>
</ICollapse>
</UCard>
</template>
<script setup>
const props = defineProps({
type: {
type: String,
default: 'search'
},
item: {
type: Object,
default: (() => {})
},
collapse: {
type: Boolean,
default: true
}
type: {
type: String,
default: 'search'
},
item: {
type: Object,
default: (() => {})
},
collapse: {
type: Boolean,
default: true
}
})
const openCollapse = ref(props.collapse)
function handleToggleCollapse () {
openCollapse.value = !openCollapse.value
openCollapse.value = !openCollapse.value
}
function handleCollapse (state) {
openCollapse.value = state
openCollapse.value = state
}
watch(() => props.collapse, () => {
if (props.collapse) {
handleCollapse(true)
} else {
setTimeout(() => {
handleCollapse(false)
}, 500)
}
if (props.collapse) {
handleCollapse(true)
} else {
setTimeout(() => {
handleCollapse(false)
}, 500)
}
}, { immediate: true})
defineExpose({ handleCollapse })
</script>
<template>
<div class="flex flex-col overflow-hidden">
<div class="flex justify-between">
<UButton
class="flex-grow"
leading-icon="i-heroicons-magnifying-glass"
color="gray"
variant="ghost"
size="md"
label="搜索记录"
@click="handleOpen"
/>
<UButton
v-if="$isSignIn"
label="清空"
size="md"
variant="link"
@click="handleClear"
/>
</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">
<template v-for="(item, index) in $searchHistory" :key="index">
<UButton
class="flex group text-gray-400"
color="gray"
variant="ghost"
size="xs"
long
:to="`/search/${item.c_id}`"
>
<div class="flex-grow truncate">{{ item.title }}</div>
<UButton
class="hidden group-hover:flex"
color="red"
variant="ghost"
size="xs"
:padded="false"
leading-icon="i-heroicons-x-mark-20-solid"
@click.stop.prevent="handleRemoveRecordItem(item.c_id)"
/>
</UButton>
</template>
</div>
</div>
<UModal v-model="isOpenHistory" :ui="{ width: 'w-full sm:max-w-screen-md' }">
<div class="flex items-center p-2">
<UInput
class="w-full"
v-model="query"
:padded="false"
variant="none"
leading-icon="i-heroicons-magnifying-glass-20-solid"
placeholder="输入关键字搜索..."
/>
<UButton
leading-icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="handleClose"
/>
</div>
<UDivider />
<div class="flex flex-col p-2">
<template v-for="(item, index) in $searchHistory" :key="index">
<UButton
class="flex group"
color="gray"
variant="ghost"
long
leading-icon="i-heroicons-document-text"
:to="`/search/${item.c_id}`"
@click="handleClose"
>
<div class="flex-grow truncate font-light">{{ item.title }}</div>
<UButton
class="hidden group-hover:flex"
color="red"
variant="ghost"
:padded="false"
leading-icon="i-heroicons-x-mark-20-solid"
@click.stop.prevent="handleRemoveRecordItem(item.c_id)"
/>
</UButton>
</template>
</div>
</UModal>
<div class="flex flex-col overflow-hidden">
<div class="flex justify-between">
<UButton
class="flex-grow"
leading-icon="i-heroicons-magnifying-glass"
color="gray"
variant="ghost"
size="md"
label="搜索记录"
@click="handleOpen"
/>
<UButton
v-if="$isSignIn"
label="清空"
size="md"
variant="link"
@click="handleClear"
/>
</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">
<template v-for="(item, index) in $searchHistory" :key="index">
<UButton
class="flex group text-gray-400"
color="gray"
variant="ghost"
size="xs"
long
:to="`/search/${item.c_id}`"
>
<div class="flex-grow truncate">{{ item.title }}</div>
<UButton
class="hidden group-hover:flex"
color="red"
variant="ghost"
size="xs"
:padded="false"
leading-icon="i-heroicons-x-mark-20-solid"
@click.stop.prevent="handleRemoveRecordItem(item.c_id)"
/>
</UButton>
</template>
</div>
</div>
<UModal v-model="isOpenHistory" :ui="{ width: 'w-full sm:max-w-screen-md' }">
<div class="flex items-center p-2">
<UInput
v-model="query"
class="w-full"
:padded="false"
variant="none"
leading-icon="i-heroicons-magnifying-glass-20-solid"
placeholder="输入关键字搜索..."
/>
<UButton
leading-icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="handleClose"
/>
</div>
<UDivider />
<div class="flex flex-col p-2">
<template v-for="(item, index) in $searchHistory" :key="index">
<UButton
class="flex group"
color="gray"
variant="ghost"
long
leading-icon="i-heroicons-document-text"
:to="`/search/${item.c_id}`"
@click="handleClose"
>
<div class="flex-grow truncate font-light">{{ item.title }}</div>
<UButton
class="hidden group-hover:flex"
color="red"
variant="ghost"
:padded="false"
leading-icon="i-heroicons-x-mark-20-solid"
@click.stop.prevent="handleRemoveRecordItem(item.c_id)"
/>
</UButton>
</template>
</div>
</UModal>
</template>
<script setup>
import { IConfirm } from '#components'
......@@ -95,45 +95,45 @@ const { $getSearchHistory } = useSearchStore()
const isOpenHistory = ref(false)
const query = ref('')
function handleClear () {
modal.open(IConfirm, {
title: '清空确认',
description: '确定要清空全部搜索记录吗?',
onSuccess () {
modal.close()
emits('clear')
handleRemoveRecords()
},
onCancel () {
modal.close()
}
})
modal.open(IConfirm, {
title: '清空确认',
description: '确定要清空全部搜索记录吗?',
onSuccess () {
modal.close()
emits('clear')
handleRemoveRecords()
},
onCancel () {
modal.close()
}
})
}
function handleRemoveRecordItem (id) {
handleRemoveRecords([id])
handleRemoveRecords([id])
}
async function handleRemoveRecords (ids) {
if (!ids) {
ids = $searchHistory.value.map(item => item.c_id)
}
const { data} = await useRequest('/v1/chat/completion/remove', {
method: 'post',
body: ids
})
if (data.value) {
$getSearchHistory()
navigateTo('/')
}
if (!ids) {
ids = $searchHistory.value.map(item => item.c_id)
}
const { data} = await useRequest('/v1/chat/completion/remove', {
method: 'post',
body: ids
})
if (data.value) {
$getSearchHistory()
navigateTo('/')
}
}
function handleOpen () {
if (!$isSignIn) emits('sign')
else {
isOpenHistory.value = true
}
if (!$isSignIn) emits('sign')
else {
isOpenHistory.value = true
}
}
function handleClose () {
isOpenHistory.value = false
isOpenHistory.value = false
}
nextTick(() => {
$getSearchHistory()
$getSearchHistory()
})
</script>
<template>
<UPopover :popper="{ strategy: 'absolute' }" :ui="{ width: 'w-[156px]' }">
<template #default="{ open }">
<UButton
color="gray"
variant="ghost"
square
:class="[open && 'bg-gray-50 dark:bg-gray-800']"
icon="i-heroicons-swatch-16-solid"
:ui="{ icon: { base: 'text-primary-500 dark:text-primary-400' } }"
aria-label="Color picker"
/>
</template>
<template #panel>
<div class="flex flex-col p-2 gap-2">
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in primaryColors" :key="color.value" :color="color" :selected="primary" @select="primary = color" />
</div>
<UDivider />
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in grayColors" :key="color.value" :color="color" :selected="gray" @select="gray = color" />
</div>
</div>
</template>
</UPopover>
<UPopover :popper="{ strategy: 'absolute' }" :ui="{ width: 'w-[156px]' }">
<template #default="{ open }">
<UButton
color="gray"
variant="ghost"
square
:class="[open && 'bg-gray-50 dark:bg-gray-800']"
icon="i-heroicons-swatch-16-solid"
:ui="{ icon: { base: 'text-primary-500 dark:text-primary-400' } }"
aria-label="Color picker"
/>
</template>
<template #panel>
<div class="flex flex-col p-2 gap-2">
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in primaryColors" :key="color.value" :color="color" :selected="primary" @select="primary = color" />
</div>
<UDivider />
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in grayColors" :key="color.value" :color="color" :selected="gray" @select="gray = color" />
</div>
</div>
</template>
</UPopover>
</template>
<script setup lang="ts">
......@@ -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 primary = computed({
get () {
return primaryColors.value.find(option => option.value === appConfig.ui.primary)
},
set (option) {
appConfig.ui.primary = option.value
window.localStorage.setItem('nuxt-ui-primary', appConfig.ui.primary)
}
get () {
return primaryColors.value.find(option => option.value === appConfig.ui.primary)
},
set (option) {
appConfig.ui.primary = option.value
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 gray = computed({
get () {
return grayColors.value.find(option => option.value === appConfig.ui.gray)
},
set (option) {
appConfig.ui.gray = option.value
window.localStorage.setItem('nuxt-ui-gray', appConfig.ui.gray)
}
get () {
return grayColors.value.find(option => option.value === appConfig.ui.gray)
},
set (option) {
appConfig.ui.gray = option.value
window.localStorage.setItem('nuxt-ui-gray', appConfig.ui.gray)
}
})
</script>
\ No newline at end of file
<template>
<UTooltip :text="color.value" class="capitalize" :open-delay="500">
<UButton
color="white"
square
:ui="{
color: {
white: {
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'
}
}
}"
:variant="color.value === selected.value ? 'solid' : 'ghost'"
@click.stop.prevent="$emit('select')"
>
<span class="inline-block w-3 h-3 rounded-full" :style="{ backgroundColor: color.hex }" />
</UButton>
</UTooltip>
<UTooltip :text="color.value" class="capitalize" :open-delay="500">
<UButton
color="white"
square
:ui="{
color: {
white: {
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'
}
}
}"
:variant="color.value === selected.value ? 'solid' : 'ghost'"
@click.stop.prevent="$emit('select')"
>
<span class="inline-block w-3 h-3 rounded-full" :style="{ backgroundColor: color.hex }" />
</UButton>
</UTooltip>
</template>
<script setup lang="ts">
......
<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="p-4 h-full flex flex-col">
<ILogo class="mt-2" />
<UButton
:ui="{ rounded: 'rounded-full' }"
class="flex w-full mt-6"
color="gray"
leading-icon="i-heroicons-plus-20-solid"
size="md"
@click="handleShowCreate"
>
<div class="flex flex-grow justify-between items-center">
<span>新主题</span>
<div class="flex items-center gap-0.5" v-if="$device.isDesktop">
<UKbd>{{ metaSymbol }}</UKbd>
<UKbd>K</UKbd>
</div>
</div>
</UButton>
<div class="flex flex-grow overflow-hidden mt-4">
<INav />
</div>
</div>
</div>
<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">
<IUserInfo />
</div>
<UModal v-model="isOpenCreate" :ui="{ width: 'w-full sm:max-w-screen-md' }">
<ICreate @search="handleCloseCreate" />
</UModal>
<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">
<ILogo class="mt-2" />
<UButton
:ui="{ rounded: 'rounded-full' }"
class="flex w-full mt-6"
color="gray"
leading-icon="i-heroicons-plus-20-solid"
size="md"
@click="handleShowCreate"
>
<div class="flex flex-grow justify-between items-center">
<span>新主题</span>
<div v-if="$device.isDesktop" class="flex items-center gap-0.5">
<UKbd>{{ metaSymbol }}</UKbd>
<UKbd>K</UKbd>
</div>
</div>
</UButton>
<div class="flex flex-grow overflow-hidden mt-4">
<INav />
</div>
</div>
</div>
<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">
<IUserInfo />
</div>
<UModal v-model="isOpenCreate" :ui="{ width: 'w-full sm:max-w-screen-md' }">
<ICreate @search="handleCloseCreate" />
</UModal>
</template>
<script setup>
const { metaSymbol } = useShortcuts()
const isOpenCreate = ref(false)
const handleShowCreate = () => {
isOpenCreate.value = true
isOpenCreate.value = true
}
const handleCloseCreate = () => {
isOpenCreate.value = false
isOpenCreate.value = false
}
defineShortcuts({
meta_k: {
handler: () => {
handleShowCreate()
}
meta_k: {
handler: () => {
handleShowCreate()
}
}
})
</script>
<template>
<Transition
@enter="onEnter"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
@leave="onLeave"
>
<div v-show="open" class="flex transition-[height] overflow-hidden"><slot /></div>
</Transition>
<Transition
@enter="onEnter"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
@leave="onLeave"
>
<div v-show="open" class="flex transition-[height] overflow-hidden"><slot /></div>
</Transition>
</template>
<script setup>
const props = defineProps({
open: {
type: Boolean,
default: true
}
defineProps({
open: {
type: Boolean,
default: true
}
})
function onEnter(_el, done) {
const el = _el
el.style.height = '0'
el.offsetHeight
el.style.height = el.scrollHeight + 'px'
el.addEventListener('transitionend', done, { once: true })
const el = _el
el.style.height = '0'
el.offsetHeight
el.style.height = el.scrollHeight + 'px'
el.addEventListener('transitionend', done, { once: true })
}
function onBeforeLeave(_el) {
const el = _el
el.style.height = el.scrollHeight + 'px'
el.offsetHeight
const el = _el
el.style.height = el.scrollHeight + 'px'
el.offsetHeight
}
function onAfterEnter(_el) {
const el = _el
el.style.height = 'auto'
const el = _el
el.style.height = 'auto'
}
function onLeave(_el, done) {
const el = _el
el.style.height = '0'
el.addEventListener('transitionend', done, { once: true })
const el = _el
el.style.height = '0'
el.addEventListener('transitionend', done, { once: true })
}
</script>
<template>
<UModal :ui="{ width: 'w-96 sm:max-w-screen-md' }">
<div class="flex p-6 gap-4">
<div class="flex">
<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" />
</div>
</div>
<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="mt-2 text-sm text-gray-500">{{ description }}</div>
</div>
</div>
<div class="p-4 flex justify-end gap-2">
<UButton color="white" @click="handleCancel">取消</UButton>
<UButton color="red" @click="handleSuccess">确定</UButton>
</div>
</UModal>
<UModal :ui="{ width: 'w-96 sm:max-w-screen-md' }">
<div class="flex p-6 gap-4">
<div class="flex">
<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" />
</div>
</div>
<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="mt-2 text-sm text-gray-500">{{ description }}</div>
</div>
</div>
<div class="p-4 flex justify-end gap-2">
<UButton color="white" @click="handleCancel">取消</UButton>
<UButton color="red" @click="handleSuccess">确定</UButton>
</div>
</UModal>
</template>
<script setup>
defineProps({
title: {
type: String,
default: ''
},
description: {
type: String,
default: '确认要全部清空吗?'
},
async: {
type: Boolean,
default: false
}
title: {
type: String,
default: ''
},
description: {
type: String,
default: '确认要全部清空吗?'
},
async: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['success', 'cancel'])
function handleCancel () {
emits('cancel')
emits('cancel')
}
function handleSuccess () {
emits('success')
emits('success')
}
</script>
<template>
<div class="max-w-screen-md w-full flex flex-col space-y-4 p-6">
<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"
:ui="cardUI"
>
<UTextarea
name="createInput"
v-model="query"
autoresize
placeholder="输入搜索内容..."
:rows="5"
variant="none"
:padded="false"
maxlength="2000"
/>
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<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>
<UButton
:ui="{ rounded: 'rounded-full' }"
:icon="isPro ? 'i-heroicons-sparkles-20-solid' : 'i-heroicons-sparkles-20-solid'"
color="gray"
variant="ghost"
@click="handleTogglePro"
/>
</UTooltip>
<USelectMenu
:ui-menu="menuUI"
v-model="selectedRepo"
:options="$repos"
placeholder="选择 GitHub 项目"
value-attribute="label"
option-attribute="label"
>
<UTooltip :text="isPro ? '选择 GitHub 项目' : '选择 GitHub 项目(需开启专家搜索)'" v-if="!selectedRepo">
<UButton
:ui="{ rounded: 'rounded-full' }"
icon="i-simple-icons-github"
color="gray"
variant="ghost"
:disabled="!isPro"
/>
</UTooltip>
<div class="max-w-screen-md w-full flex flex-col space-y-4 p-6">
<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"
:ui="cardUI"
>
<UTextarea
v-model="query"
name="createInput"
autoresize
placeholder="输入搜索内容..."
:rows="5"
variant="none"
:padded="false"
maxlength="2000"
/>
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<UTooltip class="relative" :text="isPro ? '已开启专家搜索' : '已关闭专家搜索'" :shortcuts="[metaSymbol, 'O']">
<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
:ui="{ rounded: 'rounded-full' }"
:icon="isPro ? 'i-heroicons-sparkles-20-solid' : 'i-heroicons-sparkles-20-solid'"
color="gray"
variant="ghost"
@click="handleTogglePro"
/>
</UTooltip>
<USelectMenu
v-model="selectedRepo"
:ui-menu="menuUI"
:options="$repos"
placeholder="选择 GitHub 项目"
value-attribute="label"
option-attribute="label"
>
<UTooltip v-if="!selectedRepo" :text="isPro ? '选择 GitHub 项目' : '选择 GitHub 项目(需开启专家搜索)'">
<UButton
:ui="{ rounded: 'rounded-full' }"
icon="i-simple-icons-github"
color="gray"
variant="ghost"
:disabled="!isPro"
/>
</UTooltip>
<UButton v-else color="gray" variant="ghost" :class="{ 'group': selectedRepo }">
<UIcon name="i-simple-icons-github" />
<span>{{ selectedRepo }}</span>
<UIcon name="i-heroicons-chevron-down-20-solid" class="text-xl flex group-hover:hidden" />
<UButton
v-if="selectedRepo"
class="hidden group-hover:flex"
@click.stop.prevent="handleClearRepo"
icon="i-heroicons-x-mark-20-solid"
:padded="false"
color="gray"
variant="link"
/>
</UButton>
</USelectMenu>
</div>
<UTooltip text="搜索" :shortcuts="[metaSymbol, '↵']">
<UButton
:ui="{ rounded: 'rounded-full' }"
icon="i-heroicons-chevron-right-20-solid"
@click="handleSearch"
:loading="loading"
:disabled="query === ''"
size="md"
/>
</UTooltip>
</div>
</UCard>
</div>
<UButton v-else color="gray" variant="ghost" :class="{ 'group': selectedRepo }">
<UIcon name="i-simple-icons-github" />
<span>{{ selectedRepo }}</span>
<UIcon name="i-heroicons-chevron-down-20-solid" class="text-xl flex group-hover:hidden" />
<UButton
v-if="selectedRepo"
class="hidden group-hover:flex"
icon="i-heroicons-x-mark-20-solid"
:padded="false"
color="gray"
variant="link"
@click.stop.prevent="handleClearRepo"
/>
</UButton>
</USelectMenu>
</div>
<UTooltip text="搜索" :shortcuts="[metaSymbol, '↵']">
<UButton
:ui="{ rounded: 'rounded-full' }"
icon="i-heroicons-chevron-right-20-solid"
:loading="loading"
:disabled="query === ''"
size="md"
@click="handleSearch"
/>
</UTooltip>
</div>
</UCard>
</div>
</template>
<script setup>
const { $repos } = storeToRefs(useReposStore())
......@@ -84,74 +84,74 @@ const query = ref('')
const selectedRepo = ref('')
const loading = ref(false)
const cardUI = {
body: {
padding: 'p-3 sm:p-3'
},
rounded: 'rounded-xl'
body: {
padding: 'p-3 sm:p-3'
},
rounded: 'rounded-xl'
}
const menuUI = {
width: 'w-auto'
width: 'w-auto'
}
const isPro = ref(true)
const handleSearch = async () => {
if (loading.value || query.value === '') return
loading.value = true
const currentRepo = $repos.value.find(item => item.label === selectedRepo.value)
const repo_path = currentRepo ? currentRepo.url : ''
const body = {
title: query.value,
repo_path: repo_path || ''
}
const { data, error } = await useRequest('/v1/chat/completion/create', { method: 'post', body })
loading.value = false
if (error.value) return
// todo 临时存到pina给搜索使用
$setFirstRecordTitle(query.value)
navigateTo(`/search/${data.value.data.c_id}`)
emits('search')
if (loading.value || query.value === '') return
loading.value = true
const currentRepo = $repos.value.find(item => item.label === selectedRepo.value)
const repo_path = currentRepo ? currentRepo.url : ''
const body = {
title: query.value,
repo_path: repo_path || ''
}
const { data, error } = await useRequest('/v1/chat/completion/create', { method: 'post', body })
loading.value = false
if (error.value) return
// todo 临时存到pina给搜索使用
$setFirstRecordTitle(query.value)
navigateTo(`/search/${data.value.data.c_id}`)
emits('search')
}
function handleQuickSearch (title) {
query.value = title
handleSearch()
query.value = title
handleSearch()
}
defineExpose({
handleQuickSearch
handleQuickSearch
})
function handleClearRepo () {
selectedRepo.value = ''
selectedRepo.value = ''
}
nextTick(async () => {
if (!$repos.value.length) {
const { data } = await useRequest('/v1/chat/repository')
const repoData = data.value.data.map(item => {
return {
label: item.name,
url: item.path,
branch: item.branch
}
})
$setRepo(repoData)
}
if (!$repos.value.length) {
const { data } = await useRequest('/v1/chat/repository')
const repoData = data.value.data.map(item => {
return {
label: item.name,
url: item.path,
branch: item.branch
}
})
$setRepo(repoData)
}
})
defineShortcuts({
meta_enter: {
usingInput: 'createInput',
handler: () => {
handleSearch()
}
},
meta_o: {
usingInput: 'createInput',
handler: () => {
handleTogglePro()
}
meta_enter: {
usingInput: 'createInput',
handler: () => {
handleSearch()
}
},
meta_o: {
usingInput: 'createInput',
handler: () => {
handleTogglePro()
}
}
})
function handleTogglePro () {
isPro.value = !isPro.value
if (!isPro.value) handleClearRepo()
isPro.value = !isPro.value
if (!isPro.value) handleClearRepo()
}
</script>
<template>
<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" />
<div>暂无数据</div>
</div>
<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" />
<div>暂无数据</div>
</div>
</template>
<script setup>
defineProps({
size: {
type: String,
default: 'sm'
}
size: {
type: String,
default: 'sm'
}
})
</script>
\ No newline at end of file
<template>
<div class="grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8">
<div class="text-center">
<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>
<p class="mt-6 text-base leading-7">{{ errorDescription }}</p>
<div class="mt-10 flex items-center justify-center gap-x-6">
<UButton to="/" size="lg">返回首页</UButton>
</div>
</div>
</div>
<div class="grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8">
<div class="text-center">
<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>
<p class="mt-6 text-base leading-7">{{ errorDescription }}</p>
<div class="mt-10 flex items-center justify-center gap-x-6">
<UButton to="/" size="lg">返回首页</UButton>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
code: {
type: Number,
default: 404
},
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
}
code: {
type: Number,
default: 404
},
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
}
})
const errorTitle = computed(() => {
let title = props.title
if (!title) {
const code = props.code
if (code === 404) title = 'Not found'
else if (code === 500 || code === 502) title = 'Error'
else if (code === 403) title= 'Forbidden'
}
return title
let title = props.title
if (!title) {
const code = props.code
if (code === 404) title = 'Not found'
else if (code === 500 || code === 502) title = 'Error'
else if (code === 403) title= 'Forbidden'
}
return title
})
const errorDescription = computed(() => {
let description = props.description
if (!description) {
const code = props.code
if (code === 404) description = '抱歉,当前访问的内容不存在或被删除'
else if (code === 500 || code === 502) description = '抱歉,当前内容出现错误'
else if (code === 403) description= '抱歉,您没有权限访问当前内容'
}
return description
let description = props.description
if (!description) {
const code = props.code
if (code === 404) description = '抱歉,当前访问的内容不存在或被删除'
else if (code === 500 || code === 502) description = '抱歉,当前内容出现错误'
else if (code === 403) description= '抱歉,您没有权限访问当前内容'
}
return description
})
</script>
\ No newline at end of file
<template>
<div class="flex justify-center">
<NuxtLink to="/" class="font-mono text-2xl font-medium">GitBot.<span class="text-primary">AI</span></NuxtLink>
</div>
<div class="flex justify-center">
<NuxtLink to="/" class="font-mono text-2xl font-medium">GitBot.<span class="text-primary">AI</span></NuxtLink>
</div>
</template>
\ No newline at end of file
<template>
<div class="flex md:hidden flex-shrink-0">
<UButton
class="z-20"
:class="{ 'fixed top-2 left-2' : fixed }"
color="white"
trailing-icon="i-heroicons-bars-3-20-solid"
@click="handleToggleAside"
/>
<USlideover
class="w-64"
v-model="isOpenAside"
side="left"
:overlay="false"
>
<UButton
class="absolute top-2 right-2 z-20"
color="white"
trailing-icon="i-heroicons-x-mark-20-solid"
@click="handleToggleAside"
/>
<IAside />
</USlideover>
</div>
<div class="flex md:hidden flex-shrink-0">
<UButton
class="z-20"
:class="{ 'fixed top-2 left-2' : fixed }"
color="white"
trailing-icon="i-heroicons-bars-3-20-solid"
@click="handleToggleAside"
/>
<USlideover
v-model="isOpenAside"
class="w-64"
side="left"
:overlay="false"
>
<UButton
class="absolute top-2 right-2 z-20"
color="white"
trailing-icon="i-heroicons-x-mark-20-solid"
@click="handleToggleAside"
/>
<IAside />
</USlideover>
</div>
</template>
<script setup>
defineProps({
fixed: {
type: Boolean,
default: false
}
fixed: {
type: Boolean,
default: false
}
})
const isOpenAside = ref(false)
function handleToggleAside () {
isOpenAside.value = !isOpenAside.value
isOpenAside.value = !isOpenAside.value
}
</script>
\ No newline at end of file
<template>
<div class="flex flex-col w-full gap-2">
<UButton
leading-icon="i-heroicons-home"
color="gray"
variant="ghost"
size="md"
label="首页"
to="/"
/>
<UButton
leading-icon="i-heroicons-rectangle-stack"
color="gray"
variant="ghost"
size="md"
label="主题"
to="/library"
/>
<ISearchHistory @sign="$openSign" />
<ClientOnly>
<UButton
v-if="!$isSignIn"
class="flex gap-2 justify-center"
size="lg"
label="登录"
@click="$openSign"
/>
</ClientOnly>
</div>
<UModal v-model="$isOpenSign">
<ISign @close="$closeSign" />
</UModal>
<div class="flex flex-col w-full gap-2">
<UButton
leading-icon="i-heroicons-home"
color="gray"
variant="ghost"
size="md"
label="首页"
to="/"
/>
<UButton
leading-icon="i-heroicons-rectangle-stack"
color="gray"
variant="ghost"
size="md"
label="主题"
to="/library"
/>
<ISearchHistory @sign="$openSign" />
<ClientOnly>
<UButton
v-if="!$isSignIn"
class="flex gap-2 justify-center"
size="lg"
label="登录"
@click="$openSign"
/>
</ClientOnly>
</div>
<UModal v-model="$isOpenSign">
<ISign @close="$closeSign" />
</UModal>
</template>
<script setup>
const { $isSignIn, $isOpenSign } = storeToRefs(useUserStore())
......
<template>
<div class="flex flex-col overflow-hidden group">
<div class="flex justify-between">
<UButton
class="flex-grow cursor-default hover:bg-transparent dark:hover:bg-transparent"
leading-icon="i-heroicons-queue-list"
color="gray"
variant="ghost"
size="md"
label="浏览记录"
/>
<UButton
class="hidden group-hover:flex"
label="清空"
size="md"
variant="link"
@click="handleClear"
/>
</div>
<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">
<template v-for="item in $searchHistory.reverse()" :key="item.c_id">
<UButton
class="flex text-gray-400"
color="gray"
variant="ghost"
size="xs"
long
:to="`/search/${item.c_id}`"
>
<div class="flex-grow truncate" :title="item.title">{{ item.title }}</div>
</UButton>
</template>
</div>
</ClientOnly>
</div>
<div class="flex flex-col overflow-hidden group">
<div class="flex justify-between">
<UButton
class="flex-grow cursor-default hover:bg-transparent dark:hover:bg-transparent"
leading-icon="i-heroicons-queue-list"
color="gray"
variant="ghost"
size="md"
label="浏览记录"
/>
<UButton
class="hidden group-hover:flex"
label="清空"
size="md"
variant="link"
@click="handleClear"
/>
</div>
<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">
<template v-for="item in $searchHistory.reverse()" :key="item.c_id">
<UButton
class="flex text-gray-400"
color="gray"
variant="ghost"
size="xs"
long
:to="`/search/${item.c_id}`"
>
<div class="flex-grow truncate" :title="item.title">{{ item.title }}</div>
</UButton>
</template>
</div>
</ClientOnly>
</div>
</template>
<script setup>
const emits = defineEmits(['sign', 'clear'])
const modal = useModal()
const { $searchHistory } = storeToRefs(useSearchStore())
const { $clearSearchHistory } = useSearchStore()
const query = ref('')
function handleClear () {
$clearSearchHistory()
$clearSearchHistory()
}
</script>
<template>
<div class="flex flex-col items-start gap-4 p-4">
<div class="flex w-full justify-between">
<ILogo />
<UButton
color="gray"
variant="ghost"
leading-icon="i-heroicons-x-mark-20-solid"
@click="handleClose"
/>
</div>
<div>登录以继续使用</div>
<UButton
block
color="gray"
size="md"
@click="handleGetSignUrl('gitcode')"
>
<img src="~/assets/svg/logo-gitcode.svg" />
使用 GitCode 登录
</UButton>
<UButton
block
leading-icon="i-simple-icons-github"
label="使用 GitHub 登录"
color="gray"
size="md"
@click="handleGetSignUrl('github')"
/>
<UButton
block
leading-icon="i-simple-icons-google"
label="使用 Google 登录"
color="gray"
size="md"
disabled
/>
<!-- <UDivider label="或" />-->
<!-- <UInput-->
<!-- class="w-full"-->
<!-- v-model="email"-->
<!-- placeholder="输入邮箱地址..."-->
<!-- size="md"-->
<!-- />-->
<!-- <UButton-->
<!-- block-->
<!-- leading-icon="i-heroicons-envelope-20-solid"-->
<!-- label="邮箱登录"-->
<!-- size="md"-->
<!-- @click="handleSign"-->
<!-- />-->
</div>
<div class="flex flex-col items-start gap-4 p-4">
<div class="flex w-full justify-between">
<ILogo />
<UButton
color="gray"
variant="ghost"
leading-icon="i-heroicons-x-mark-20-solid"
@click="handleClose"
/>
</div>
<div>登录以继续使用</div>
<UButton
block
color="gray"
size="md"
@click="handleGetSignUrl('gitcode')"
>
<img src="~/assets/svg/logo-gitcode.svg" >
使用 GitCode 登录
</UButton>
<UButton
block
leading-icon="i-simple-icons-github"
label="使用 GitHub 登录"
color="gray"
size="md"
@click="handleGetSignUrl('github')"
/>
<UButton
block
leading-icon="i-simple-icons-google"
label="使用 Google 登录"
color="gray"
size="md"
disabled
/>
<!-- <UDivider label="或" />-->
<!-- <UInput-->
<!-- class="w-full"-->
<!-- v-model="email"-->
<!-- placeholder="输入邮箱地址..."-->
<!-- size="md"-->
<!-- />-->
<!-- <UButton-->
<!-- block-->
<!-- leading-icon="i-heroicons-envelope-20-solid"-->
<!-- label="邮箱登录"-->
<!-- size="md"-->
<!-- @click="handleSign"-->
<!-- />-->
</div>
</template>
<script setup>
const emits = defineEmits(['close', 'signIn'])
const email = ref('')
// const email = ref('')
function handleClose () {
emits('close')
}
function handleSign () {
emits('signIn')
emits('close')
}
// function handleSign () {
// emits('signIn')
// }
async function handleGetSignUrl (source) {
let url
if (source === 'github') {
const { data } = await useRequest('/v1/user/github/authorize_url')
url = data.value.data.url
} else if (source === 'gitcode') {
const { data } = await useRequest('/v1/user/gitcode/authorize_url')
url = data.value.data.url
}
window.location.href = url
let url
if (source === 'github') {
const { data } = await useRequest('/v1/user/github/authorize_url')
url = data.value.data.url
} else if (source === 'gitcode') {
const { data } = await useRequest('/v1/user/gitcode/authorize_url')
url = data.value.data.url
}
window.location.href = url
}
</script>
<template>
<div class="flex flex-col w-full">
<ClientOnly>
<template v-if="$isSignIn">
<div class="flex flex-grow justify-between items-center p-4">
<UDropdown class="flex flex-grow" :items="items">
<UButton class="flex flex-grow" color="gray" variant="ghost">
<div class="flex flex-grow items-center gap-2">
<UAvatar :src="$info.avatar_url" />
<div>{{ $info.nickname }}</div>
<UBadge v-if="$info.pro" size="xs" color="yellow" variant="soft" label="PRO" />
<UBadge v-else size="xs" color="gray" variant="soft" label="FREE" />
</div>
<UIcon name="i-heroicons-chevron-down-20-solid" class="text-lg" />
</UButton>
</UDropdown>
</div>
<UDivider />
</template>
</ClientOnly>
<div class="flex justify-between gap-2 p-4">
<UButton
color="gray"
variant="ghost"
label="问题反馈"
/>
<ClientOnly>
<div class="flex">
<ColorPicker />
<USelectMenu
v-model="colorMode.preference"
:ui-menu="{ width: 'w-32' }"
:options="themeItems"
value-attribute="value"
>
<UButton
color="gray"
variant="ghost"
square
:icon="colorMode.value === 'dark' ? 'i-heroicons-moon-16-solid' : 'i-heroicons-sun-16-solid'"
aria-label="Theme"
/>
</USelectMenu>
<USelectMenu
v-if="false"
:ui-menu="{ width: 'w-32' }"
:model-value="$lang"
:options="$langOptions"
value-attribute="value"
>
<UButton
icon="i-heroicons-language-16-solid"
color="gray"
variant="ghost"
/>
</USelectMenu>
</div>
</ClientOnly>
</div>
</div>
<div class="flex flex-col w-full">
<ClientOnly>
<template v-if="$isSignIn">
<div class="flex flex-grow justify-between items-center p-4">
<UDropdown class="flex flex-grow" :items="items">
<UButton class="flex flex-grow" color="gray" variant="ghost">
<div class="flex flex-grow items-center gap-2">
<UAvatar :src="$info.avatar_url" />
<div>{{ $info.nickname }}</div>
<UBadge v-if="$info.pro" size="xs" color="yellow" variant="soft" label="PRO" />
<UBadge v-else size="xs" color="gray" variant="soft" label="FREE" />
</div>
<UIcon name="i-heroicons-chevron-down-20-solid" class="text-lg" />
</UButton>
</UDropdown>
</div>
<UDivider />
</template>
</ClientOnly>
<div class="flex justify-between gap-2 p-4">
<UButton
color="gray"
variant="ghost"
label="问题反馈"
/>
<ClientOnly>
<div class="flex">
<ColorPicker />
<USelectMenu
v-model="colorMode.preference"
:ui-menu="{ width: 'w-32' }"
:options="themeItems"
value-attribute="value"
>
<UButton
color="gray"
variant="ghost"
square
:icon="colorMode.value === 'dark' ? 'i-heroicons-moon-16-solid' : 'i-heroicons-sun-16-solid'"
aria-label="Theme"
/>
</USelectMenu>
<USelectMenu
v-if="false"
:ui-menu="{ width: 'w-32' }"
:model-value="$lang"
:options="$langOptions"
value-attribute="value"
>
<UButton
icon="i-heroicons-language-16-solid"
color="gray"
variant="ghost"
/>
</USelectMenu>
</div>
</ClientOnly>
</div>
</div>
</template>
<script setup>
const { $signOut } = useUserStore()
......@@ -65,41 +65,41 @@ const { $isSignIn, $info } = storeToRefs(useUserStore())
const { $lang, $langOptions } = useI18nStore()
const colorMode = useColorMode()
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: '亮色模式',
value: 'light',
icon: 'i-heroicons-sun'
label: '账号信息',
icon: 'i-heroicons-user'
},
{
label: '深色模式',
value: 'dark',
icon: 'i-heroicons-moon'
},
label: '用户反馈',
icon: 'i-heroicons-chat-bubble-oval-left-ellipsis'
}
],
[
{
label: '跟随系统',
value: 'system',
icon: 'i-heroicons-computer-desktop'
label: '退出登录',
icon: 'i-heroicons-power',
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>
<template>
<UDropdown class="flex flex-grow" :items="actionItems">
<UButton
color="gray"
variant="ghost"
square
icon="i-heroicons-ellipsis-horizontal"
/>
</UDropdown>
<ILibraryEdit :id="id" ref="refEdit" />
<UDropdown class="flex flex-grow" :items="actionItems">
<UButton
color="gray"
variant="ghost"
square
icon="i-heroicons-ellipsis-horizontal"
/>
</UDropdown>
<ILibraryEdit :id="id" ref="refEdit" />
</template>
<script setup>
import { IConfirm } from '#components'
const { deleteCollection } = useCollectionRequest()
const modal = useModal()
const props = defineProps({
id: {
type: [String, Number],
default: ''
}
id: {
type: [String, Number],
default: ''
}
})
const actionItems = [
[
{
label: '编辑合集',
icon: 'i-heroicons-pencil-square',
click: () => {
handleOpen()
}
},
{
label: '删除合集',
icon: 'i-heroicons-trash',
click: () => {
handleOpenDelete()
}
}
]
[
{
label: '编辑合集',
icon: 'i-heroicons-pencil-square',
click: () => {
handleOpen()
}
},
{
label: '删除合集',
icon: 'i-heroicons-trash',
click: () => {
handleOpenDelete()
}
}
]
]
const refEdit = ref(null)
function handleOpen () {
refEdit.value.open()
refEdit.value.open()
}
function handleOpenDelete () {
modal.open(IConfirm, {
title: '删除确认',
description: '确定要删除该合集吗?',
async onSuccess() {
await deleteCollection(props.id)
modal.close()
navigateTo('/library')
},
onCancel () {
modal.close()
}
})
modal.open(IConfirm, {
title: '删除确认',
description: '确定要删除该合集吗?',
async onSuccess() {
await deleteCollection(props.id)
modal.close()
navigateTo('/library')
},
onCancel () {
modal.close()
}
})
}
</script>
<template>
<UDropdown class="flex flex-grow" :items="actionItems">
<UButton
color="gray"
variant="ghost"
:size="size"
square
icon="i-heroicons-ellipsis-horizontal"
/>
</UDropdown>
<UDropdown class="flex flex-grow" :items="actionItems">
<UButton
color="gray"
variant="ghost"
:size="size"
square
icon="i-heroicons-ellipsis-horizontal"
/>
</UDropdown>
</template>
<script setup>
import { IConfirm } from '#components'
......@@ -16,73 +16,73 @@ const { deleteCollectionRecord, deleteThread } = useCollectionRequest()
const Layout = inject('Layout')
const modal = useModal()
const props = defineProps({
collection_id: {
type: [String, Number],
default: ''
},
c_id: {
type: [String, Number],
default: ''
},
size: {
type: String,
default: 'sm'
}
collectionId: {
type: [String, Number],
default: ''
},
cId: {
type: [String, Number],
default: ''
},
size: {
type: String,
default: 'sm'
}
})
const emit = defineEmits(['delete'])
const actionItems = computed(() => {
let items
if (props.collection_id && props.c_id) {
items = [
{
label: '更改合集',
icon: 'i-heroicons-squares-plus',
click: () => {
$openLibrarySelect(props.c_id, [props.collection_id])
}
},
{
label: '从收藏中移除',
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',
let items
if (props.collection_id && props.c_id) {
items = [
{
label: '更改合集',
icon: 'i-heroicons-squares-plus',
click: () => {
$openLibrarySelect(props.c_id, [props.collection_id])
}
},
{
label: '从收藏中移除',
icon: 'i-heroicons-x-mark',
click: async () => {
modal.open(IConfirm, {
title: '删除确认',
description: '确定要删除该主题吗?',
async onSuccess() {
modal.close()
await deleteThread([props.c_id])
$getCollection()
emit('delete')
},
onCancel () {
modal.close()
}
})
await deleteCollectionRecord(props.collection_id, props.c_id)
Layout.handleRemoveCollectData({
collection_id: props.collection_id,
c_id: props.c_id
})
}
})
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>
<template>
<ULink :to="`/library/${item.id}`">
<UCard :ui="cardUI">
<div class="flex flex-col gap-1">
<div>{{ item.name }}</div>
<div class="flex">
<div class="flex items-center text-gray-500 text-sm gap-0.5">
<UIcon name="i-heroicons-square-3-stack-3d" />
<span>{{ item.record_count }}</span>
</div>
</div>
</div>
</UCard>
</ULink>
<ULink :to="`/library/${item.id}`">
<UCard :ui="cardUI">
<div class="flex flex-col gap-1">
<div>{{ item.name }}</div>
<div class="flex">
<div class="flex items-center text-gray-500 text-sm gap-0.5">
<UIcon name="i-heroicons-square-3-stack-3d" />
<span>{{ item.record_count }}</span>
</div>
</div>
</div>
</UCard>
</ULink>
</template>
<script setup>
defineProps({
item: {
type: Object,
default: (() => {})()
}
item: {
type: Object,
default: (() => {})()
}
})
const cardUI = {
body: {
padding: 'sm:p-3 p-3',
base: 'h-full'
},
background: 'transition bg-gray-50 hover:bg-gray-100 dark:hover:bg-gray-800'
body: {
padding: 'sm:p-3 p-3',
base: 'h-full'
},
background: 'transition bg-gray-50 hover:bg-gray-100 dark:hover:bg-gray-800'
}
</script>
\ No newline at end of file
<template>
<UModal
:model-value="!id ? $isLibraryCreateOpen : updateVisible"
@update:modelValue="handleCloseModal"
:ui="{ width: 'w-96 sm:max-w-screen-md' }"
>
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<div class="text-xl" v-if="!id">创建合集</div>
<div class="text-xl" v-else>更新合集</div>
<UButton
leading-icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="handleClose"
/>
</div>
</template>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-1">
<div class="flex content-center items-center justify-between text-sm">
<label class="block font-medium text-gray-700 dark:text-gray-200">标题</label>
</div>
<UInput
v-model="title"
placeholder="合集标题"
autofocus
/>
</div>
<div class="flex flex-col gap-1">
<div class="flex content-center items-center justify-between text-sm">
<label class="block font-medium text-gray-700 dark:text-gray-200">
描述
<span class="text-gray-400 dark:text-gray-100">(可选)</span>
</label>
</div>
<UTextarea
v-model="description"
placeholder="合集描述"
/>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<UButton
v-if="!id"
size="md"
label="创建"
:loading="loading"
:disabled="!title"
@click="handleCreate"
/>
<UButton
v-else
size="md"
label="更新"
:loading="loading"
:disabled="!title"
@click="handleUpdate"
/>
</div>
</template>
</UCard>
</UModal>
<UModal
:model-value="!id ? $isLibraryCreateOpen : updateVisible"
: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' }">
<template #header>
<div class="flex items-center justify-between">
<div v-if="!id" class="text-xl">创建合集</div>
<div v-else class="text-xl">更新合集</div>
<UButton
leading-icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="handleClose"
/>
</div>
</template>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-1">
<div class="flex content-center items-center justify-between text-sm">
<label class="block font-medium text-gray-700 dark:text-gray-200">标题</label>
</div>
<UInput
v-model="title"
placeholder="合集标题"
autofocus
/>
</div>
<div class="flex flex-col gap-1">
<div class="flex content-center items-center justify-between text-sm">
<label class="block font-medium text-gray-700 dark:text-gray-200">
描述
<span class="text-gray-400 dark:text-gray-100">(可选)</span>
</label>
</div>
<UTextarea
v-model="description"
placeholder="合集描述"
/>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<UButton
v-if="!id"
size="md"
label="创建"
:loading="loading"
:disabled="!title"
@click="handleCreate"
/>
<UButton
v-else
size="md"
label="更新"
:loading="loading"
:disabled="!title"
@click="handleUpdate"
/>
</div>
</template>
</UCard>
</UModal>
</template>
<script setup>
const { $isLibraryCreateOpen, $collection } = storeToRefs(useLibraryStore())
const { $closeLibraryCreate, $getCollection } = useLibraryStore()
const { setOrUpdateCollection } = useCollectionRequest()
const props = defineProps({
id: {
type: [String, Number],
default: ''
}
id: {
type: [String, Number],
default: ''
}
})
const updateVisible = ref(false)
const title = ref('')
const description = ref('')
const loading = ref(false)
function handleClose () {
if (!props.id) $closeLibraryCreate()
else updateVisible.value = false
if (!props.id) $closeLibraryCreate()
else updateVisible.value = false
}
async function handleCreate () {
if (loading.value) return
loading.value = true
const { error } = await setOrUpdateCollection({
name: title.value,
description: description.value
})
loading.value = false
if (error.value) return
title.value = ''
description.value = ''
$closeLibraryCreate()
$getCollection()
if (loading.value) return
loading.value = true
const { error } = await setOrUpdateCollection({
name: title.value,
description: description.value
})
loading.value = false
if (error.value) return
title.value = ''
description.value = ''
$closeLibraryCreate()
$getCollection()
}
function handleCloseModal () {
if (!props.id) $isLibraryCreateOpen.value = false
else updateVisible.value = false
if (!props.id) $isLibraryCreateOpen.value = false
else updateVisible.value = false
}
async function handleUpdate () {
if (loading.value) return
loading.value = true
const { error } = await setOrUpdateCollection({
id: Number(props.id),
name: title.value,
description: description.value
})
loading.value = false
if (error.value) return
handleCloseModal()
$getCollection()
if (loading.value) return
loading.value = true
const { error } = await setOrUpdateCollection({
id: Number(props.id),
name: title.value,
description: description.value
})
loading.value = false
if (error.value) return
handleCloseModal()
$getCollection()
}
function handleFillInfo () {
const collection =$collection.value
const id = Number(props.id)
const { name, description:currentDes } = collection.find(item => item.id === id)
if (name) {
title.value = name
description.value = currentDes
}
const collection =$collection.value
const id = Number(props.id)
const { name, description:currentDes } = collection.find(item => item.id === id)
if (name) {
title.value = name
description.value = currentDes
}
}
function openUpdate () {
updateVisible.value = true
// todo 获取合集数据
if (props.id) {
handleFillInfo()
}
updateVisible.value = true
// todo 获取合集数据
if (props.id) {
handleFillInfo()
}
}
defineExpose({
openUpdate
openUpdate
})
</script>
<template>
<ILibraryCreate :id="id" ref="refCreate" />
<ILibraryCreate :id="id" ref="refCreate" />
</template>
<script setup>
defineProps({
id: {
type: [String, Number],
default: ''
}
id: {
type: [String, Number],
default: ''
}
})
const refCreate = ref(null)
function open () {
refCreate.value.openUpdate()
refCreate.value.openUpdate()
}
defineExpose({
open
open
})
</script>
\ No newline at end of file
<template>
<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="flex justify-between items-center gap-4">
<div class="flex gap-4">
<IMenuSider />
<div class="flex flex-shrink-0 items-center text-xl gap-2" v-if="!collect">
<UIcon name="i-heroicons-rectangle-stack-20-solid" />
<div>主题</div>
</div>
<template v-else>
<UButton
icon="i-heroicons-chevron-left"
color="gray"
variant="ghost"
to="/library"
/>
<div class="flex items-center text-xl gap-2">
<UIcon name="i-heroicons-squares-2x2" />
<div>{{ collect }}</div>
<UBadge color="gray" variant="soft" :label="count" />
</div>
</template>
</div>
<div class="flex flex-grow justify-end items-center gap-4">
<div class="flex">
<IActionCollect :id="collectId" v-if="collect" />
</div>
<div class="flex" v-if="!collect">
<UInput
name="queryInput"
:ui="{ icon: { trailing: { pointer: '' } } }"
v-model="searchQuery"
:loading="searchLoading"
icon="i-heroicons-magnifying-glass-20-solid"
placeholder="搜索你的主题..."
size="md"
>
<template #trailing>
<UButton
v-show="searchQuery !== ''"
color="gray"
variant="link"
icon="i-heroicons-x-mark-20-solid"
:padded="false"
@click="handleClear"
/>
</template>
</UInput>
</div>
</div>
</div>
<div v-if="description" class="text-gray-500 ml-12 mt-2">{{ description }}</div>
</div>
<UDivider />
<div v-if="showTabs" class="w-full p-6 block lg:hidden">
<UTabs :model-value="tab" @update:modelValue="handleChangeTab" :items="tabs" />
</div>
</div>
<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="flex justify-between items-center gap-4">
<div class="flex gap-4">
<IMenuSider />
<div v-if="!collect" class="flex flex-shrink-0 items-center text-xl gap-2">
<UIcon name="i-heroicons-rectangle-stack-20-solid" />
<div>主题</div>
</div>
<template v-else>
<UButton
icon="i-heroicons-chevron-left"
color="gray"
variant="ghost"
to="/library"
/>
<div class="flex items-center text-xl gap-2">
<UIcon name="i-heroicons-squares-2x2" />
<div>{{ collect }}</div>
<UBadge color="gray" variant="soft" :label="count" />
</div>
</template>
</div>
<div class="flex flex-grow justify-end items-center gap-4">
<div class="flex">
<IActionCollect v-if="collect" :id="collectId" />
</div>
<div v-if="!collect" class="flex">
<UInput
v-model="searchQuery"
name="queryInput"
:ui="{ icon: { trailing: { pointer: '' } } }"
:loading="searchLoading"
icon="i-heroicons-magnifying-glass-20-solid"
placeholder="搜索你的主题..."
size="md"
>
<template #trailing>
<UButton
v-show="searchQuery !== ''"
color="gray"
variant="link"
icon="i-heroicons-x-mark-20-solid"
:padded="false"
@click="handleClear"
/>
</template>
</UInput>
</div>
</div>
</div>
<div v-if="description" class="text-gray-500 ml-12 mt-2">{{ description }}</div>
</div>
<UDivider />
<div v-if="showTabs" class="w-full p-6 block lg:hidden">
<UTabs :model-value="tab" :items="tabs" @update:model-value="handleChangeTab" />
</div>
</div>
</template>
<script setup>
defineProps({
collect: {
type: String,
default: ''
},
description: {
type: String,
default: ''
},
count: {
type: Number,
default: 0
},
collectId: {
type: [String, Number],
default: ''
},
showTabs: {
type: Boolean,
default: false
},
tab: {
type: Number,
default: 0
}
collect: {
type: String,
default: ''
},
description: {
type: String,
default: ''
},
count: {
type: Number,
default: 0
},
collectId: {
type: [String, Number],
default: ''
},
showTabs: {
type: Boolean,
default: false
},
tab: {
type: Number,
default: 0
}
})
const emit = defineEmits(['search', 'clear', 'change-tab'])
const searchQuery = ref('')
const searchLoading = ref(false)
defineShortcuts({
enter: {
usingInput: 'queryInput',
handler: async () => {
searchLoading.value = true
const { data } = await useRequest(`/v1/chat/completion/list?keyword=${searchQuery.value}`)
searchLoading.value = false
emit('search', data.value.data)
}
enter: {
usingInput: 'queryInput',
handler: async () => {
searchLoading.value = true
const { data } = await useRequest(`/v1/chat/completion/list?keyword=${searchQuery.value}`)
searchLoading.value = false
emit('search', data.value.data)
}
}
})
function handleClear () {
searchQuery.value = ''
emit('clear')
searchQuery.value = ''
emit('clear')
}
const tabs = [
{
label: '全部主题',
icon: 'i-heroicons-square-3-stack-3d'
},
{
label: '合集'
}
{
label: '全部主题',
icon: 'i-heroicons-square-3-stack-3d'
},
{
label: '合集'
}
]
function handleChangeTab (index) {
emit('change-tab', index)
emit('change-tab', index)
}
</script>
<template>
<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' }">
<template #header>
<div class="flex items-center justify-between">
<div class="text-xl">选择合集</div>
<UButton
leading-icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="handleClose"
/>
</div>
</template>
<div class="flex flex-col gap-4">
<UButton
leading-icon="i-heroicons-plus-20-solid"
label="创建新合集"
color="gray"
@click="handleOpenCreate"
/>
<div class="flex flex-col gap-2">
<UButton
v-for="item in $collection"
color="white"
size="md"
class="flex"
:key="item.id"
@click="handleSelected(item.id)"
>
<div class="flex flex-grow justify-between items-center">
<div>{{ item.name }}</div>
<UIcon v-if="selected.includes(item.id)" name="i-heroicons-check-circle-20-solid" class="text-primary text-lg" />
</div>
</UButton>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<UButton
size="md"
label="保存"
:loading="loading"
@click="handleSave"
/>
</div>
</template>
</UCard>
</UModal>
<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' }">
<template #header>
<div class="flex items-center justify-between">
<div class="text-xl">选择合集</div>
<UButton
leading-icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="handleClose"
/>
</div>
</template>
<div class="flex flex-col gap-4">
<UButton
leading-icon="i-heroicons-plus-20-solid"
label="创建新合集"
color="gray"
@click="handleOpenCreate"
/>
<div class="flex flex-col gap-2">
<UButton
v-for="item in $collection"
:key="item.id"
color="white"
size="md"
class="flex"
@click="handleSelected(item.id)"
>
<div class="flex flex-grow justify-between items-center">
<div>{{ item.name }}</div>
<UIcon v-if="selected.includes(item.id)" name="i-heroicons-check-circle-20-solid" class="text-primary text-lg" />
</div>
</UButton>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<UButton
size="md"
label="保存"
:loading="loading"
@click="handleSave"
/>
</div>
</template>
</UCard>
</UModal>
</template>
<script setup>
const { $isLibrarySelectOpen, $selectCollectionId, $selectThreadId, $collection } = storeToRefs(useLibraryStore())
......@@ -57,43 +57,43 @@ const emits = defineEmits(['success'])
const selected = ref([])
const loading = ref(false)
function handleClose () {
$closeLibrarySelect()
$closeLibrarySelect()
}
function handleOpenCreate () {
handleClose()
$openLibraryCreate()
handleClose()
$openLibraryCreate()
}
async function handleSelected(id) {
selected.value = [id]
selected.value = [id]
}
async function handleSave() {
if (loading.value) return
loading.value = true
// 取消合集
const hasSelected = $selectCollectionId.value
if (hasSelected.length) {
const { error } = await deleteCollectionRecord(hasSelected[0], $selectThreadId.value)
!error.value && $setSelectCollectionId([])
}
// 添加合集
if (selected.value.length) {
const selectedItem = selected.value[0]
const { error } = await saveCollection({ collection_id: selectedItem, c_id: $selectThreadId.value })
!error.value && $setSelectCollectionId([selectedItem])
}
loading.value = false
handleClose()
$getCollection()
emits('success', {
c_id: $selectThreadId.value,
collection_id: selected.value[0],
collection_name: $collection.value.find(i => i.id === selected.value[0]).name
})
if (loading.value) return
loading.value = true
// 取消合集
const hasSelected = $selectCollectionId.value
if (hasSelected.length) {
const { error } = await deleteCollectionRecord(hasSelected[0], $selectThreadId.value)
!error.value && $setSelectCollectionId([])
}
// 添加合集
if (selected.value.length) {
const selectedItem = selected.value[0]
const { error } = await saveCollection({ collection_id: selectedItem, c_id: $selectThreadId.value })
!error.value && $setSelectCollectionId([selectedItem])
}
loading.value = false
handleClose()
$getCollection()
emits('success', {
c_id: $selectThreadId.value,
collection_id: selected.value[0],
collection_name: $collection.value.find(i => i.id === selected.value[0]).name
})
}
watch(() => $isLibrarySelectOpen.value, () => {
selected.value = [...$selectCollectionId.value]
if (!$collection.value.length) {
$getCollection()
}
selected.value = [...$selectCollectionId.value]
if (!$collection.value.length) {
$getCollection()
}
})
</script>
<template>
<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="break-word text-balance line-clamp-2 font-sans text-sm" :class="textColor" v-if="false">
{{ thread.description }}
</div>
</ULink>
<div class="flex justify-between items-center">
<div class="flex gap-4">
<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'" />
<span>{{ thread.is_public ? '公开' : '私有' }}</span>
</UTooltip>
<!-- <div class="flex items-center text-sm gap-0.5" :class="textColor">-->
<!-- <UIcon name="i-heroicons-eye" />-->
<!-- <span>1</span>-->
<!-- </div>-->
<div class="flex" :class="textColor">
<UTooltip class="flex items-center text-sm gap-0.5">
<UIcon name="i-heroicons-clock" />
<span>{{ useTime(thread.create_time) }}</span>
<template #text>
{{ toValue(useDateFormat(thread.create_time, 'YYYY年M月D日 HH:mm')) }}
</template>
</UTooltip>
</div>
</div>
<div class="flex gap-4">
<template v-for="collect in thread.collections" :key="collect.collection_id">
<UButton
:ui="{ rounded: 'rounded-full' }"
:to="`/library/${collect.collection_id}`"
color="white"
size="2xs"
:label="collect.collection_name"
/>
</template>
<UTooltip text="添加到收藏" v-if="!thread.collections.length">
<UButton
color="gray"
variant="ghost"
size="2xs"
square
icon="i-heroicons-plus"
@click="handleOpenSelect"
/>
</UTooltip>
<IActionThread
:collection_id="thread.collections.length ? thread.collections[0].collection_id : ''"
:c_id="item.c_id"
size="2xs"
@delete="handleDeletedThread"
/>
</div>
</div>
<UDivider />
<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 v-if="false" class="break-word text-balance line-clamp-2 font-sans text-sm" :class="textColor">
{{ thread.description }}
</div>
</ULink>
<div class="flex justify-between items-center">
<div class="flex gap-4">
<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'" />
<span>{{ thread.is_public ? '公开' : '私有' }}</span>
</UTooltip>
<!-- <div class="flex items-center text-sm gap-0.5" :class="textColor">-->
<!-- <UIcon name="i-heroicons-eye" />-->
<!-- <span>1</span>-->
<!-- </div>-->
<div class="flex" :class="textColor">
<UTooltip class="flex items-center text-sm gap-0.5">
<UIcon name="i-heroicons-clock" />
<span>{{ useTime(thread.create_time) }}</span>
<template #text>
{{ toValue(useDateFormat(thread.create_time, 'YYYY年M月D日 HH:mm')) }}
</template>
</UTooltip>
</div>
</div>
<div class="flex gap-4">
<template v-for="collect in thread.collections" :key="collect.collection_id">
<UButton
:ui="{ rounded: 'rounded-full' }"
:to="`/library/${collect.collection_id}`"
color="white"
size="2xs"
:label="collect.collection_name"
/>
</template>
<UTooltip v-if="!thread.collections.length" text="添加到收藏">
<UButton
color="gray"
variant="ghost"
size="2xs"
square
icon="i-heroicons-plus"
@click="handleOpenSelect"
/>
</UTooltip>
<IActionThread
:collection_id="thread.collections.length ? thread.collections[0].collection_id : ''"
:c_id="item.c_id"
size="2xs"
@delete="handleDeletedThread"
/>
</div>
</div>
<UDivider />
</template>
<script setup>
const Layout = inject('Layout')
const { $openLibrarySelect } = useLibraryStore()
const textColor = 'text-gray-500 dark:text-gray-400'
const props = defineProps({
item: {
type: Object,
default: (() => {})()
},
isItem: {
type: Boolean,
default: false
}
item: {
type: Object,
default: (() => {})()
},
isItem: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['delete'])
const thread = computed(() => {
if (!props.isItem) return props.item
else {
const item = props.item
item.title = props.item.c_title
item.collections = []
if (props.item.collection_id && props.item.collection_name) {
item.collections = [
{
collection_id: props.item.collection_id,
collection_name: props.item.collection_name
}
]
if (!props.isItem) return props.item
else {
const item = props.item
item.title = props.item.c_title
item.collections = []
if (props.item.collection_id && props.item.collection_name) {
item.collections = [
{
collection_id: props.item.collection_id,
collection_name: props.item.collection_name
}
return item
]
}
return item
}
})
function handleOpenSelect () {
$openLibrarySelect(props.item.c_id)
$openLibrarySelect(props.item.c_id)
}
function handleUpdateCollect (data) {
const { c_id } = data.value
if (c_id === thread.value.c_id) {
thread.value.collections = [data.value]
Layout.handleClearCollectData()
}
const { c_id } = data.value
if (c_id === thread.value.c_id) {
thread.value.collections = [data.value]
Layout.handleClearCollectData()
}
}
function handleRemoveCollect (data) {
const { c_id } = data.value
if (c_id === thread.value.c_id) {
thread.value.collections = []
Layout.handleClearRemoveCollectData()
}
const { c_id } = data.value
if (c_id === thread.value.c_id) {
thread.value.collections = []
Layout.handleClearRemoveCollectData()
}
}
watch(()=> Layout.selectCollectData, (data) => {
if (data.value !== null) handleUpdateCollect(data)
if (data.value !== null) handleUpdateCollect(data)
}, { deep: true })
watch(()=> Layout.removeCollectData, (data) => {
if (data.value !== null) handleRemoveCollect(data)
if (data.value !== null) handleRemoveCollect(data)
}, { deep: true })
function handleDeletedThread () {
emit('delete', thread.value.c_id)
emit('delete', thread.value.c_id)
}
</script>
\ No newline at end of file
<template>
<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" />
</div>
<div class="grid grid-cols-1">
<MDC v-if="content" class="prose dark:prose-invert max-w-none" :class="'prose-' + size" :value="content" tag="article" />
</div>
</template>
<script setup>
defineProps({
content: {
type: String,
default: ''
},
size: {
type: String,
default: 'base'
}
content: {
type: String,
default: ''
},
size: {
type: String,
default: 'base'
}
})
</script>
\ No newline at end of file
<template>
<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="grid">
<slot name="title" />
</div>
<slot />
</div>
<div class="flex flex-col w-full lg:w-64 flex-shrink-0 gap-6" v-if="$slots.extra">
<slot name="extra" />
</div>
</div>
<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="grid">
<slot name="title" />
</div>
<slot />
</div>
<div v-if="$slots.extra" class="flex flex-col w-full lg:w-64 flex-shrink-0 gap-6">
<slot name="extra" />
</div>
</div>
</template>
<script setup>
......
<template>
<div class="flex w-full justify-center">
<UCard
class="hover:ring-2 has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-500 dark:has-[textarea:focus]:ring-primary-400"
:ui="cardUI"
>
<UTextarea
ref="queryInput"
class="flex-grow"
name="queryInput"
:rows="1"
:maxrows="10"
autoresize
v-model="continueQuestion"
:placeholder="placeholder"
size="xl"
:padded="false"
variant="none"
maxlength="2000"
@focus="handleInputFocus"
@blur="handleInputBlur"
/>
<div class="flex flex-shrink-0 gap-2">
<UButton
v-if="!asking"
:ui="{ rounded: 'rounded-full' }"
:disabled="!continueQuestion"
trailing-icon="i-heroicons-chevron-right-20-solid"
size="xl"
@click.stop="handleAsk"
/>
<UTooltip v-else text="停止生成">
<UButton
:ui="{ rounded: 'rounded-full' }"
color="red"
trailing-icon="i-heroicons-stop-20-solid"
size="xl"
variant="ghost"
@click.stop="handleStop"
/>
</UTooltip>
</div>
</UCard>
</div>
<div class="flex w-full justify-center">
<UCard
class="hover:ring-2 has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-500 dark:has-[textarea:focus]:ring-primary-400"
:ui="cardUI"
>
<UTextarea
ref="queryInput"
v-model="continueQuestion"
class="flex-grow"
name="queryInput"
:rows="1"
:maxrows="10"
autoresize
:placeholder="placeholder"
size="xl"
:padded="false"
variant="none"
maxlength="2000"
@focus="handleInputFocus"
@blur="handleInputBlur"
/>
<div class="flex flex-shrink-0 gap-2">
<UButton
v-if="!asking"
:ui="{ rounded: 'rounded-full' }"
:disabled="!continueQuestion"
trailing-icon="i-heroicons-chevron-right-20-solid"
size="xl"
@click.stop="handleAsk"
/>
<UTooltip v-else text="停止生成">
<UButton
:ui="{ rounded: 'rounded-full' }"
color="red"
trailing-icon="i-heroicons-stop-20-solid"
size="xl"
variant="ghost"
@click.stop="handleStop"
/>
</UTooltip>
</div>
</UCard>
</div>
</template>
<script setup>
const { metaSymbol } = useShortcuts()
const { isDesktop } = useDevice()
const placeholder = computed(() => `提出后续问题${isDesktop ? '' + metaSymbol.value + 'L)' : ''}`)
const props = defineProps({
asking: {
type: Boolean,
default: false
}
asking: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['ask', 'stop'])
const isFocus = ref(false)
const cardUI = computed(() => {
const base = {
body: {
padding: '',
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',
rounded: 'rounded has-[textarea[rows="1"]]:rounded-full'
}
if (isFocus.value) {
base.base = 'transition-[width] w-full'
} else {
base.base = 'transition-[width] w-full sm:w-3/5'
}
return base
const base = {
body: {
padding: '',
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',
rounded: 'rounded has-[textarea[rows="1"]]:rounded-full'
}
if (isFocus.value) {
base.base = 'transition-[width] w-full'
} else {
base.base = 'transition-[width] w-full sm:w-3/5'
}
return base
})
defineShortcuts({
meta_enter: {
usingInput: 'queryInput',
handler: () => {
handleAsk()
}
},
escape: {
usingInput: 'queryInput',
handler: () => {
handleBlur()
}
},
meta_l: {
handler: () => {
handleFocus()
}
meta_enter: {
usingInput: 'queryInput',
handler: () => {
handleAsk()
}
},
escape: {
usingInput: 'queryInput',
handler: () => {
handleBlur()
}
},
meta_l: {
handler: () => {
handleFocus()
}
}
})
function handleFocus () {
queryInput.value.textarea.focus()
queryInput.value.textarea.focus()
}
function handleBlur () {
queryInput.value.textarea.blur()
queryInput.value.textarea.blur()
}
const continueQuestion = ref('')
function handleStop () {
emits('stop')
emits('stop')
}
function handleAsk () {
if (props.asking) return
emits('ask', continueQuestion.value)
continueQuestion.value = ''
handleBlur()
if (props.asking) return
emits('ask', continueQuestion.value)
continueQuestion.value = ''
handleBlur()
}
const queryInput = ref(null)
function handleInputFocus () {
isFocus.value = true
isFocus.value = true
}
function handleInputBlur () {
setTimeout(() => {
isFocus.value = false
}, 100)
setTimeout(() => {
isFocus.value = false
}, 100)
}
</script>
<template>
<ISearchProcess :collapse="collapse" :actions="actions" :status="processStatus" />
<template v-if="item.source && item.source.length > 0">
<div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-link-20-solid" />
<span>来源</span>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<ISearchSource :source="item.source" />
</div>
</template>
<div class="text-xl flex items-center space-x-1" v-if="processStatus ==='finish'">
<UIcon name="i-heroicons-chat-bubble-left-right-20-solid" />
<span>{{ item.ansLoading ? '回答中' : '回答' }}</span>
</div>
<IMdMdc :content="item.article" />
<div class="space-x-2" v-if="item.showActions">
<UButton size="xs" color="gray" @click="handleCopyMD" leading-icon="i-heroicons-document-duplicate-20-solid" label="复制" />
<UButton size="xs" color="gray" @click="handleShare" leading-icon="i-heroicons-share-20-solid" label="分享" />
<UButton v-if="isLastIndex" size="xs" color="gray" @click="handleReGenerate" leading-icon="i-heroicons-arrow-path-rounded-square-20-solid" label="重写" />
</div>
<ISearchProcess :collapse="collapse" :actions="actions" :status="processStatus" />
<template v-if="item.source && item.source.length > 0">
<div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-link-20-solid" />
<span>来源</span>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<ISearchSource :source="item.source" />
</div>
</template>
<div v-if="processStatus ==='finish'" class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-chat-bubble-left-right-20-solid" />
<span>{{ item.ansLoading ? '回答中' : '回答' }}</span>
</div>
<IMdMdc :content="item.article" />
<div v-if="item.showActions" class="space-x-2">
<UButton size="xs" color="gray" leading-icon="i-heroicons-document-duplicate-20-solid" label="复制" @click="handleCopyMD" />
<UButton size="xs" color="gray" leading-icon="i-heroicons-share-20-solid" label="分享" @click="handleShare" />
<UButton v-if="isLastIndex" size="xs" color="gray" leading-icon="i-heroicons-arrow-path-rounded-square-20-solid" label="重写" @click="handleReGenerate" />
</div>
</template>
<script setup>
const Search = inject('Search')
const toast = useToast()
const props = defineProps({
item: {
type: Object,
default: (() => {})
},
asking: {
type: Boolean,
default: false
},
isLastIndex: {
type: Boolean,
default: false
},
index: {
type: Number,
default: 0
},
collapse: {
type: Boolean,
default: true
},
actions: {
type: Array,
default: () => []
},
processStatus: {
type: String,
default: '' // start | finish
}
item: {
type: Object,
default: (() => {})
},
asking: {
type: Boolean,
default: false
},
isLastIndex: {
type: Boolean,
default: false
},
index: {
type: Number,
default: 0
},
collapse: {
type: Boolean,
default: true
},
actions: {
type: Array,
default: () => []
},
processStatus: {
type: String,
default: '' // start | finish
}
})
const emits = defineEmits(['regenerate'])
function handleReGenerate () {
emits('regenerate', props.index)
emits('regenerate', props.index)
}
function handleCopyMD () {
useCopyToClipboard().copy(props.item.article)
toast.add({
icon: 'i-heroicons-information-circle-20-solid',
timeout: 1000,
title: '复制成功'
})
useCopyToClipboard().copy(props.item.article)
toast.add({
icon: 'i-heroicons-information-circle-20-solid',
timeout: 1000,
title: '复制成功'
})
}
function handleShare () {
const hash = props.index + 1;
const url = window.location.href + (hash ? '#' + hash : '')
Search.handleUpdateOpenState(url)
function handleShare() {
const hash = props.index + 1;
const url = window.location.href + (hash ? '#' + hash : '')
Search.handleUpdateOpenState(url)
}
</script>
\ No newline at end of file
<template>
<div class="flex flex-col gap-4 sticky top-16">
<div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-square-3-stack-3d-20-solid" />
<span>补充信息</span>
</div>
<template v-for="(item, index) in data" :key="index">
<template v-if="item.ready">
<template v-if="item.type === 'search_relate_repo'">
<template v-if="item.data.length">
<template v-for="(chart, cIndex) in item.data" :key="cIndex">
<UCard :ui="cardUI">
<template v-if="chart.info">
<div class="flex mb-2">
<div class="flex items-center gap-1 text-sm overflow-hidden">
<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>
<UIcon class="flex-shrink-0" name="i-heroicons-arrow-top-right-on-square" />
</div>
</div>
</template>
<ProseChart
type="line"
:labels="chart.labels"
:data="chart.data"
:info="chart.info"
simple
/>
<div class="text-xs text-center text-gray-500 dark:text-gray-400 mt-2">{{ item.title }}</div>
</UCard>
</template>
</template>
<template v-else>
<UCard :ui="cardUI">
<IEmpty size="xs"/>
</UCard>
</template>
</template>
</template>
<template v-else>
<UCard :ui="cardUI">
<div class="flex flex-col gap-1">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</div>
</UCard>
</template>
</template>
</div>
<div class="flex flex-col gap-4 sticky top-16">
<div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-square-3-stack-3d-20-solid" />
<span>补充信息</span>
</div>
<template v-for="(item, index) in data" :key="index">
<template v-if="item.ready">
<template v-if="item.type === 'search_relate_repo'">
<template v-if="item.data.length">
<template v-for="(chart, cIndex) in item.data" :key="cIndex">
<UCard :ui="cardUI">
<template v-if="chart.info">
<div class="flex mb-2">
<div class="flex items-center gap-1 text-sm overflow-hidden">
<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>
<UIcon class="flex-shrink-0" name="i-heroicons-arrow-top-right-on-square" />
</div>
</div>
</template>
<ProseChart
type="line"
:labels="chart.labels"
:data="chart.data"
:info="chart.info"
simple
/>
<div class="text-xs text-center text-gray-500 dark:text-gray-400 mt-2">{{ item.title }}</div>
</UCard>
</template>
</template>
<template v-else>
<UCard :ui="cardUI">
<IEmpty size="xs"/>
</UCard>
</template>
</template>
</template>
<template v-else>
<UCard :ui="cardUI">
<div class="flex flex-col gap-1">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</div>
</UCard>
</template>
</template>
</div>
</template>
<script setup>
defineProps({
data: {
type: Array,
default: () => []
}
data: {
type: Array,
default: () => []
}
})
const cardUI = {
body: {
padding: 'sm:p-2 p-2',
base: 'h-full'
}
body: {
padding: 'sm:p-2 p-2',
base: 'h-full'
}
}
</script>
<template>
<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">
<IMenuSider />
<div class="hidden md:flex">
<div class="flex items-center gap-2" v-if="repo">
<UIcon name="i-simple-icons-github" />{{ repo }}
</div>
</div>
<div class="flex-grow justify-center items-center space-x-2 hidden sm:flex">
<UTooltip text="点击修改标题" v-if="!isEditTitle">
<div @click="handleFocusTitle">{{ editTitle }}</div>
</UTooltip>
<UInput
v-else
ref="titleRef"
autofocus
:model-value="editTitle"
@blur="handleBlurTitle"
/>
</div>
<div class="flex gap-2">
<IActionThread
:collection_id="$selectCollectionId.length ? $selectCollectionId[0] : ''"
:c_id="state.id"
@delete="handleDeletedThread"
/>
<UButton
color="gray"
variant="ghost"
leading-icon="i-heroicons-plus-small"
:label="$selectCollectionId.length ? '已收藏' : '收藏'"
@click="handleOpenSelect"
/>
<UPopover v-model:open="isShareOpen">
<UButton
:leading-icon="isOpen ? 'i-heroicons-share-16-solid' : 'i-heroicons-lock-closed-16-solid'"
label="分享"
@click="handleSetOpenState"
/>
<template #panel>
<div class="flex flex-col p-3 gap-2 min-w-72">
<div>访问权限</div>
<div class="flex flex-col border dark:border-gray-800 rounded">
<div
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)"
>
<div class="flex-grow flex flex-col gap-1 text-sm">
<div class="flex items-center gap-1" :class="{ 'text-primary-500': !isOpen }">
<UIcon name="i-heroicons-lock-closed-16-solid" />
<span>私密</span>
</div>
<div class="text-xs text-gray-500">只有作者可以查看</div>
</div>
<div v-if="!isOpen">
<UIcon name="i-heroicons-check-circle-20-solid" class="text-primary-500 text-xl" />
</div>
</div>
<UDivider />
<div
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)"
>
<div class="flex-grow flex flex-col gap-1 text-sm" :class="{ 'text-primary-500': isOpen }">
<div class="flex items-center gap-1">
<UIcon name="i-heroicons-share-20-solid" />
<span>可分享的</span>
</div>
<div class="text-xs text-gray-500">任何持有链接的人都可以查看</div>
</div>
<div v-if="isOpen">
<UIcon name="i-heroicons-check-circle-20-solid" class="text-primary-500 text-xl" />
</div>
</div>
</div>
<div class="flex gap-1 items-center text-xs text-primary-500" v-if="isOpen">
<UIcon name="i-heroicons-clipboard-document-check" />
<span>链接已复制</span>
</div>
</div>
</template>
</UPopover>
</div>
</div>
<UDivider />
</header>
<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">
<IMenuSider />
<div class="hidden md:flex">
<div v-if="repo" class="flex items-center gap-2">
<UIcon name="i-simple-icons-github" />{{ repo }}
</div>
</div>
<div class="flex-grow justify-center items-center space-x-2 hidden sm:flex">
<UTooltip v-if="!isEditTitle" text="点击修改标题">
<div @click="handleFocusTitle">{{ editTitle }}</div>
</UTooltip>
<UInput
v-else
ref="titleRef"
autofocus
:model-value="editTitle"
@blur="handleBlurTitle"
/>
</div>
<div class="flex gap-2">
<IActionThread
:collection_id="$selectCollectionId.length ? $selectCollectionId[0] : ''"
:c_id="state.id"
@delete="handleDeletedThread"
/>
<UButton
color="gray"
variant="ghost"
leading-icon="i-heroicons-plus-small"
:label="$selectCollectionId.length ? '已收藏' : '收藏'"
@click="handleOpenSelect"
/>
<UPopover v-model:open="isShareOpen">
<UButton
:leading-icon="isOpen ? 'i-heroicons-share-16-solid' : 'i-heroicons-lock-closed-16-solid'"
label="分享"
@click="handleSetOpenState"
/>
<template #panel>
<div class="flex flex-col p-3 gap-2 min-w-72">
<div>访问权限</div>
<div class="flex flex-col border dark:border-gray-800 rounded">
<div
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)"
>
<div class="flex-grow flex flex-col gap-1 text-sm">
<div class="flex items-center gap-1" :class="{ 'text-primary-500': !isOpen }">
<UIcon name="i-heroicons-lock-closed-16-solid" />
<span>私密</span>
</div>
<div class="text-xs text-gray-500">只有作者可以查看</div>
</div>
<div v-if="!isOpen">
<UIcon name="i-heroicons-check-circle-20-solid" class="text-primary-500 text-xl" />
</div>
</div>
<UDivider />
<div
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)"
>
<div class="flex-grow flex flex-col gap-1 text-sm" :class="{ 'text-primary-500': isOpen }">
<div class="flex items-center gap-1">
<UIcon name="i-heroicons-share-20-solid" />
<span>可分享的</span>
</div>
<div class="text-xs text-gray-500">任何持有链接的人都可以查看</div>
</div>
<div v-if="isOpen">
<UIcon name="i-heroicons-check-circle-20-solid" class="text-primary-500 text-xl" />
</div>
</div>
</div>
<div v-if="isOpen" class="flex gap-1 items-center text-xs text-primary-500">
<UIcon name="i-heroicons-clipboard-document-check" />
<span>链接已复制</span>
</div>
</div>
</template>
</UPopover>
</div>
</div>
<UDivider />
</header>
</template>
<script setup>
const toast = useToast()
......@@ -94,18 +94,21 @@ const { $selectCollectionId } = storeToRefs(useLibraryStore())
const { $openLibrarySelect, $setSelectCollectionId } = useLibraryStore()
const { findRecordCollection } = useCollectionRequest()
const props = defineProps({
query: String,
isPublic: {
type: Boolean,
default: false
},
repo: {
type: String,
default: ''
}
query: {
type: String,
default: ''
},
isPublic: {
type: Boolean,
default: false
},
repo: {
type: String,
default: ''
}
})
const state = reactive({
id: route.params.id
id: route.params.id
})
const Layout = inject('Layout')
const emits = defineEmits(['update-query'])
......@@ -116,72 +119,72 @@ const isOpen = ref(false)
const isShareOpen = ref(false)
watch(()=> props.query, () => {
editTitle.value = props.query
editTitle.value = props.query
}, { immediate: true })
watch(() => props.isPublic, () => {
isOpen.value = props.isPublic
isOpen.value = props.isPublic
}, { immediate: true })
function handleFocusTitle () {
isEditTitle.value = true
isEditTitle.value = true
}
function handleBlurTitle () {
isEditTitle.value = false
emits('update-query', editTitle.value)
isEditTitle.value = false
emits('update-query', editTitle.value)
}
async function handleUpdateOpenState (s) {
const { data } = await useRequest(`/v1/chat/${state.id}/public/${s}`, { method: 'post' })
if (data.value) isOpen.value = s
const { data } = await useRequest(`/v1/chat/${state.id}/public/${s}`, { method: 'post' })
if (data.value) isOpen.value = s
}
function handleCopyLink (url) {
useCopyToClipboard().copy(url || window.location.href)
toast.add({
icon: 'i-heroicons-information-circle-20-solid',
timeout: 2000,
title: '链接已复制到剪贴板'
})
useCopyToClipboard().copy(url || window.location.href)
toast.add({
icon: 'i-heroicons-information-circle-20-solid',
timeout: 2000,
title: '链接已复制到剪贴板'
})
}
async function handleSetOpenState() {
if (isShareOpen.value) return
if (!isOpen.value) {
setTimeout(async () => {
await handleUpdateOpenState(true)
handleCopyLink()
}, 200)
} else {
await handleUpdateOpenState(true)
handleCopyLink()
}
if (isShareOpen.value) return
if (!isOpen.value) {
setTimeout(async () => {
await handleUpdateOpenState(true)
handleCopyLink()
}, 200)
} else {
await handleUpdateOpenState(true)
handleCopyLink()
}
}
function handleOpenSelect () {
$openLibrarySelect(state.id, $selectCollectionId.value)
$openLibrarySelect(state.id, $selectCollectionId.value)
}
defineExpose({
isOpen,
handleUpdateOpenState,
handleCopyLink
isOpen,
handleUpdateOpenState,
handleCopyLink
})
async function initData () {
const { data, error } = await findRecordCollection(state.id)
if (!error.value) {
const ids = data.value.data.map(item => item.collection_id)
$setSelectCollectionId(ids)
}
const { data, error } = await findRecordCollection(state.id)
if (!error.value) {
const ids = data.value.data.map(item => item.collection_id)
$setSelectCollectionId(ids)
}
}
// 初始化登录的时候判断是否已收藏
if ($isSignIn.value) {
await initData()
await initData()
}
function handleRemoveCollect (data) {
const { c_id } = data.value
if (c_id === state.id) {
$setSelectCollectionId([])
Layout.handleClearRemoveCollectData()
}
const { c_id } = data.value
if (c_id === state.id) {
$setSelectCollectionId([])
Layout.handleClearRemoveCollectData()
}
}
watch(()=> Layout.removeCollectData, (data) => {
if (data.value !== null) handleRemoveCollect(data)
if (data.value !== null) handleRemoveCollect(data)
}, { deep: true })
function handleDeletedThread () {
navigateTo('/')
navigateTo('/')
}
</script>
\ No newline at end of file
<template>
<UCard :ui="{ body: { padding: 'p-4 sm:p-4' } }">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2 text-lg">
<UIcon name="i-heroicons-sparkles-20-solid" />
搜索过程
</div>
<UButton
size="md"
color="gray"
variant="ghost"
:icon="openCollapse ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'"
:ui="{ rounded: 'rounded-full' }"
@click="handleToggleCollapse"
/>
</div>
<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">
<template v-if="status !== 'finish' && !actions.length">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<template v-for="(action, index) in actions" :key="action.action">
<ISearchProcessAction
:action="action"
:last="actions.length === index + 1"
:status="status"
/>
</template>
</div>
</ICollapse>
</UCard>
<UCard :ui="{ body: { padding: 'p-4 sm:p-4' } }">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2 text-lg">
<UIcon name="i-heroicons-sparkles-20-solid" />
搜索过程
</div>
<UButton
size="md"
color="gray"
variant="ghost"
:icon="openCollapse ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'"
:ui="{ rounded: 'rounded-full' }"
@click="handleToggleCollapse"
/>
</div>
<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">
<template v-if="status !== 'finish' && !actions.length">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<template v-for="(action, index) in actions" :key="action.action">
<ISearchProcessAction
:action="action"
:last="actions.length === index + 1"
:status="status"
/>
</template>
</div>
</ICollapse>
</UCard>
</template>
<script setup>
const props = defineProps({
type: {
type: String,
default: 'search'
},
collapse: {
type: Boolean,
default: true
},
actions: {
type: Array,
default: () => []
},
status: {
type: String,
default: ''
}
type: {
type: String,
default: 'search'
},
collapse: {
type: Boolean,
default: true
},
actions: {
type: Array,
default: () => []
},
status: {
type: String,
default: ''
}
})
const openCollapse = ref(props.collapse)
function handleToggleCollapse () {
openCollapse.value = !openCollapse.value
openCollapse.value = !openCollapse.value
}
function handleCollapse (state) {
openCollapse.value = state
openCollapse.value = state
}
watch(() => props.collapse, () => {
if (props.collapse) {
handleCollapse(true)
} else {
setTimeout(() => {
handleCollapse(false)
}, 1000)
}
if (props.collapse) {
handleCollapse(true)
} else {
setTimeout(() => {
handleCollapse(false)
}, 1000)
}
}, { immediate: true})
defineExpose({ handleCollapse })
</script>
<template>
<div class="text-base flex items-center gap-1">
<UIcon :name="item.icon" />
{{ item.name }}
</div>
<template v-if="['rephrase_question', 'tool_select'].includes((action.action))">
<div class="pl-5">
<IMdMdc :content="action.output" size="sm" />
</div>
</template>
<template v-else-if="['search_file', 'search_web'].includes(action.action)">
<div class="text-xs pl-5">找到 {{ action.output.length }} 条来源</div>
</template>
<template v-if="last && status !== 'finish'">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<div class="text-base flex items-center gap-1">
<UIcon :name="item.icon" />
{{ item.name }}
</div>
<template v-if="['rephrase_question', 'tool_select'].includes((action.action))">
<div class="pl-5">
<IMdMdc :content="action.output" size="sm" />
</div>
</template>
<template v-else-if="['search_file', 'search_web'].includes(action.action)">
<div class="text-xs pl-5">找到 {{ action.output.length }} 条来源</div>
</template>
<template v-if="last && status !== 'finish'">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
</template>
<script setup>
const props = defineProps({
action: {
type: Object,
default: () => {}
},
last: {
type: Boolean,
default: false
},
status: {
type: String,
default: ''
}
action: {
type: Object,
default: () => {}
},
last: {
type: Boolean,
default: false
},
status: {
type: String,
default: ''
}
})
const item = computed(() => {
const action = props.action.action
let name, icon
if (action === 'rephrase_question') {
name = '理解问题'
icon = 'i-heroicons-inbox-arrow-down'
}
else if (action === 'search_file') {
name = '搜索项目'
icon = 'i-heroicons-magnifying-glass'
}
else if (action === 'search_web') {
name = '搜索网页'
icon = 'i-heroicons-magnifying-glass'
}
else if (action === 'tool_select') {
name = '使用工具'
icon = 'i-heroicons-puzzle-piece'
}
else if (action === 'search_relate_repo') {
name = '查找相关项目'
icon = 'i-heroicons-rectangle-group'
}
return {
name,
icon
}
const action = props.action.action
let name, icon
if (action === 'rephrase_question') {
name = '理解问题'
icon = 'i-heroicons-inbox-arrow-down'
}
else if (action === 'search_file') {
name = '搜索项目'
icon = 'i-heroicons-magnifying-glass'
}
else if (action === 'search_web') {
name = '搜索网页'
icon = 'i-heroicons-magnifying-glass'
}
else if (action === 'tool_select') {
name = '使用工具'
icon = 'i-heroicons-puzzle-piece'
}
else if (action === 'search_relate_repo') {
name = '查找相关项目'
icon = 'i-heroicons-rectangle-group'
}
return {
name,
icon
}
})
</script>
\ No newline at end of file
<template>
<div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-rectangle-group-20-solid" />
<span>相关问题</span>
</div>
<div class="flex flex-col gap-2">
<UButton
v-for="item in recommendQuestions"
color="gray"
size="md"
variant="soft"
@click="handleClick(item.title)"
>
<div class="justify-between w-full flex items-center">
<div class="truncate">{{ item.title }}</div>
<UIcon name="i-heroicons-plus-20-solid" class="text-base" />
</div>
</UButton>
</div>
<div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-rectangle-group-20-solid" />
<span>相关问题</span>
</div>
<div class="flex flex-col gap-2">
<UButton
v-for="(item, index) in recommendQuestions"
:key="index"
color="gray"
size="md"
variant="soft"
@click="handleClick(item.title)"
>
<div class="justify-between w-full flex items-center">
<div class="truncate">{{ item.title }}</div>
<UIcon name="i-heroicons-plus-20-solid" class="text-base" />
</div>
</UButton>
</div>
</template>
<script setup>
defineProps({
recommendQuestions: {
type: Array,
default: () => []
}
recommendQuestions: {
type: Array,
default: () => []
}
})
const emits = defineEmits(['click'])
function handleClick(title) {
emits('click', title)
emits('click', title)
}
</script>
\ No newline at end of file
<template>
<ULink v-for="(item, index) in limitSource" :to="item.url" :title="item.url" target="_blank">
<UCard :ui="cardUI">
<div class="flex flex-col h-full gap-1">
<template v-if="getIconPath(item.url) === 'github'">
<div class="items-center flex gap-1">
<UIcon name="i-simple-icons-github" class="flex-shrink-0" />
<div class="flex flex-grow overflow-hidden">
<div class="truncate">{{ item.title }}</div>
</div>
<div class="text-gray-200">{{ index + 1 }}</div>
</div>
<div class="text-blue-500 line-clamp-1">{{ item.label }}</div>
</template>
<template v-else>
<div class="flex gap-1 h-full">
<div class="line-clamp-2 text-sm">{{ item.title }}</div>
</div>
<div class="items-center flex gap-1">
<UAvatar :src="getIconPath(item.url)" size="2xs" class="flex-shrink-0" />
<div class="flex flex-grow overflow-hidden">
<div class="truncate text-xs text-gray-500">{{ getDomain(item.url) }}</div>
</div>
<div class="text-gray-200 text-sm">{{ index + 1 }}</div>
</div>
</template>
</div>
</UCard>
</ULink>
<UCard class="cursor-pointer" :ui="cardUI" v-if="source.length > 6" @click="handleToggleShowAll">
<div class="flex items-center justify-center h-full gap-1">
<UIcon v-if="!showAllSource" name="i-heroicons-chevron-down-20-solid" />
<UIcon v-else name="i-heroicons-chevron-up-20-solid" />
<div v-if="!showAllSource">查看全部{{ source.length }}个来源</div>
<div v-else>收起</div>
</div>
</UCard>
<ULink v-for="(item, index) in limitSource" :key="index" :to="item.url" :title="item.url" target="_blank">
<UCard :ui="cardUI">
<div class="flex flex-col h-full gap-1">
<template v-if="getIconPath(item.url) === 'github'">
<div class="items-center flex gap-1">
<UIcon name="i-simple-icons-github" class="flex-shrink-0" />
<div class="flex flex-grow overflow-hidden">
<div class="truncate">{{ item.title }}</div>
</div>
<div class="text-gray-200">{{ index + 1 }}</div>
</div>
<div class="text-blue-500 line-clamp-1">{{ item.label }}</div>
</template>
<template v-else>
<div class="flex gap-1 h-full">
<div class="line-clamp-2 text-sm">{{ item.title }}</div>
</div>
<div class="items-center flex gap-1">
<UAvatar :src="getIconPath(item.url)" size="2xs" class="flex-shrink-0" />
<div class="flex flex-grow overflow-hidden">
<div class="truncate text-xs text-gray-500">{{ getDomain(item.url) }}</div>
</div>
<div class="text-gray-200 text-sm">{{ index + 1 }}</div>
</div>
</template>
</div>
</UCard>
</ULink>
<UCard v-if="source.length > 6" class="cursor-pointer" :ui="cardUI" @click="handleToggleShowAll">
<div class="flex items-center justify-center h-full gap-1">
<UIcon v-if="!showAllSource" name="i-heroicons-chevron-down-20-solid" />
<UIcon v-else name="i-heroicons-chevron-up-20-solid" />
<div v-if="!showAllSource">查看全部{{ source.length }}个来源</div>
<div v-else>收起</div>
</div>
</UCard>
</template>
<script setup>
const props = defineProps({
source: {
type: Array,
default: () => []
}
source: {
type: Array,
default: () => []
}
})
const cardUI = {
base: 'h-full',
body: {
padding: 'sm:p-2 p-2',
base: 'h-full'
},
background: 'transition hover:bg-gray-100 dark:hover:bg-gray-800'
base: 'h-full',
body: {
padding: 'sm:p-2 p-2',
base: 'h-full'
},
background: 'transition hover:bg-gray-100 dark:hover:bg-gray-800'
}
const showAllSource = ref(false)
const limitSource = computed(() => {
if (props.source.length <= 6) return props.source
else if (showAllSource.value) return props.source
else return props.source.slice(0, 5)
if (props.source.length <= 6) return props.source
else if (showAllSource.value) return props.source
else return props.source.slice(0, 5)
})
function getIconPath (url) {
if (!url || !url.startsWith('http')) return ''
const uri = new URL(url)
if (uri.origin.endsWith('github.com')) return 'github'
return `https://toolb.cn/favicon/${url}`
if (!url || !url.startsWith('http')) return ''
const uri = new URL(url)
if (uri.origin.endsWith('github.com')) return 'github'
return `https://toolb.cn/favicon/${url}`
}
function getDomain(url) {
// 使用正则表达式匹配协议和域名部分
const regex = /^(https?:\/\/)?([^\/]+)/
const match = url.match(regex)
// 使用正则表达式匹配协议和域名部分
const regex = /^(https?:\/\/)?([^/]+)/
const match = url.match(regex)
// 如果匹配不到,返回空字符串
if (!match) {
return ''
}
// 如果匹配不到,返回空字符串
if (!match) {
return ''
}
// 获取域名部分
const domain = match[2]
// 获取域名部分
const domain = match[2]
// 去除可能存在的端口号
// 返回域名
return domain.split(':')[0];
// 去除可能存在的端口号
// 返回域名
return domain.split(':')[0];
}
const handleToggleShowAll = () => {
showAllSource.value = !showAllSource.value
showAllSource.value = !showAllSource.value
}
</script>
<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>
<script setup>
const props = defineProps({
as: {
type: String,
default: 'h2'
},
title: {
type: String,
default: ''
},
id: {
type: [String, Number],
default: ''
}
as: {
type: String,
default: 'h2'
},
title: {
type: String,
default: ''
},
id: {
type: [String, Number],
default: ''
}
})
const titleTag = computed(() => {
let tag = props.as
const title = props.title
if (title.indexOf('\n') > -1) tag = 'div'
else if (title.length > 50) tag = 'div'
return tag
let tag = props.as
const title = props.title
if (title.indexOf('\n') > -1) tag = 'div'
else if (title.length > 50) tag = 'div'
return tag
})
</script>
<template>
<div class="grid grid-cols-1">
<article class="prose dark:prose-invert max-w-none" v-html="mdHtml"></article>
</div>
<div class="grid grid-cols-1">
<article class="prose dark:prose-invert max-w-none" v-html="mdHtml"/>
</div>
</template>
<script setup>
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'
const props = defineProps({
content: String
content: {
type: String,
default: ''
}
})
const mdHtml = ref('')
const initMarkdownIt = new MarkdownIt({
......@@ -29,10 +32,10 @@ const initMarkdownIt = new MarkdownIt({
}
})
const handleRenderMd = () => {
mdHtml.value = initMarkdownIt.render(props.content || '')
mdHtml.value = initMarkdownIt.render(props.content || '')
}
watch( () => props.content, () => {
handleRenderMd()
handleRenderMd()
}, { immediate: true })
</script>
<style>
......
<template>
<div class="w-full relative" style="aspect-ratio: 2/1">
<canvas ref="refChart" :aria-label="title"></canvas>
</div>
<div class="w-full relative" style="aspect-ratio: 2/1">
<canvas ref="refChart" :aria-label="title"/>
</div>
</template>
<script setup>
import { Chart } from 'chart.js/auto';
const colorMode = useColorMode()
const props = defineProps({
type: {
type: String,
default: 'line'
},
title: {
type: String,
default: ''
},
labels: {
type: Array,
default: () => []
},
data: {
type: Array,
default: () => []
},
simple: {
type: Boolean,
default: false
}
type: {
type: String,
default: 'line'
},
title: {
type: String,
default: ''
},
labels: {
type: Array,
default: () => []
},
data: {
type: Array,
default: () => []
},
simple: {
type: Boolean,
default: false
}
})
const refChart = ref(null)
let chart
function init () {
Chart.defaults.datasets.line.fill = true
Chart.defaults.datasets.line.fill = true
if (props.simple) {
Chart.defaults.plugins.legend.display = false
}
if (props.simple) {
Chart.defaults.plugins.legend.display = false
}
chart = new Chart(refChart.value, {
type: props.type,
data: {
labels: props.labels,
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.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.borderColor = ['rgb(54, 162, 235)']
item.borderWidth = 1
return item
})
chart = new Chart(refChart.value, {
type: props.type,
data: {
labels: props.labels,
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.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.borderColor = ['rgb(54, 162, 235)']
item.borderWidth = 1
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: {
maintainAspectRatio: false,
plugins: {
title: {
display: !props.simple,
text: props.title,
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,
}
y: {
display: !props.simple,
grid: {
display: false
},
stacked: true
}
})
},
radius: 0,
interaction: {
intersect: false,
}
}
})
}
function destroy () {
if (chart) {
chart.destroy()
chart = null
}
if (chart) {
chart.destroy()
chart = null
}
}
onMounted(() => {
nextTick(() => {
init()
})
nextTick(() => {
init()
})
})
onBeforeUnmount(() => {
destroy()
destroy()
})
watch(()=> colorMode.value, (value) => {
let color = '#e5e7eb'
if (value === 'dark') color = '#1f2937'
Chart.defaults.borderColor = color
if (chart) {
destroy()
init()
}
let color = '#e5e7eb'
if (value === 'dark') color = '#1f2937'
Chart.defaults.borderColor = color
if (chart) {
destroy()
init()
}
}, { immediate: true, deep: true })
</script>
<template>
<UTable
:rows="data"
:columns="columns"
:ui="config"
:sort-button="sortButton"
>
<template #repo_name-data="{ row }">
<ULink :to="'https://github.com/' + row.repo_name" target="_blank">{{ row.repo_name }}</ULink>
</template>
<template #repo_url-data="{ row }">
<ULink :to="row.repo_url" target="_blank">{{ row.repo_url }}</ULink>
</template>
</UTable>
<UTable
:rows="data"
:columns="columns"
:ui="config"
:sort-button="sortButton"
>
<template #repo_name-data="{ row }">
<ULink :to="'https://github.com/' + row.repo_name" target="_blank">{{ row.repo_name }}</ULink>
</template>
<template #repo_url-data="{ row }">
<ULink :to="row.repo_url" target="_blank">{{ row.repo_url }}</ULink>
</template>
</UTable>
</template>
<script setup>
const props = defineProps({
data: {
type: Array,
default: () => []
}
data: {
type: Array,
default: () => []
}
})
const columns = computed(() => {
const columns = []
for (const key in props.data[0]) {
columns.push({
key,
label: key,
sortable: true
})
}
const columns = []
for (const key in props.data[0]) {
columns.push({
key,
label: key,
sortable: true
})
}
return columns
return columns
})
const config = {
base: 'table-auto',
td: {
base: 'max-w-96 whitespace-normal'
}
base: 'table-auto',
td: {
base: 'max-w-96 whitespace-normal'
}
}
const sortButton = {
icon: 'i-heroicons-chevron-up-down-20-solid'
icon: 'i-heroicons-chevron-up-down-20-solid'
}
</script>
\ No newline at end of file
<template>
<NuxtLink
:href="href"
:target="target"
>
<slot />
</NuxtLink>
<NuxtLink
:href="href"
:target="target"
>
<slot />
</NuxtLink>
</template>
<script setup lang="ts">
import type { PropType } from 'vue'
defineProps({
href: {
type: String,
default: ''
},
target: {
type: String as PropType<'_blank' | '_parent' | '_self' | '_top' | (string & object) | null | undefined>,
default: '_blank',
required: false
}
href: {
type: String,
default: ''
},
target: {
type: String as PropType<'_blank' | '_parent' | '_self' | '_top' | (string & object) | null | undefined>,
default: '_blank',
required: false
}
})
</script>
\ No newline at end of file
<template>
<UCard :ui="cardUI">
<pre class="flex justify-between items-center m-0 p-1 pl-4 pr-1 rounded-none dark">
<UCard :ui="cardUI">
<pre class="flex justify-between items-center m-0 p-1 pl-4 pr-1 rounded-none dark">
<div>{{ language }}</div>
<UButton
leading-icon="i-heroicons-document-duplicate-20-solid"
......@@ -9,9 +9,9 @@
@click="handleCopy"
/>
</pre>
<UDivider :ui="{ border: { base: 'border-gray-700' } }" />
<pre :class="$props.class" class="m-0 rounded-none"><code v-html="codeBlock"></code></pre>
</UCard>
<UDivider :ui="{ border: { base: 'border-gray-700' } }" />
<pre :class="$props.class" class="m-0 rounded-none"><code v-html="codeBlock"/></pre>
</UCard>
</template>
<script setup>
......@@ -19,54 +19,54 @@ import hljs from 'highlight.js'
import 'highlight.js/styles/stackoverflow-dark.css'
const toast = useToast()
const props = defineProps({
code: {
type: String,
default: ''
},
language: {
type: String,
default: null
},
filename: {
type: String,
default: null
},
highlights: {
type: Array,
default: () => []
},
meta: {
type: String,
default: null
},
class: {
type: String,
default: null
}
code: {
type: String,
default: ''
},
language: {
type: String,
default: null
},
filename: {
type: String,
default: null
},
highlights: {
type: Array,
default: () => []
},
meta: {
type: String,
default: null
},
class: {
type: String,
default: null
}
})
const cardUI = {
body: {
padding: 'p-0 sm:p-0'
},
base: 'overflow-hidden mt-5 mb-5',
ring: 'ring-0 dark:ring-1'
body: {
padding: 'p-0 sm:p-0'
},
base: 'overflow-hidden mt-5 mb-5',
ring: 'ring-0 dark:ring-1'
}
const handleCopy = () => {
useCopyToClipboard().copy(props.code)
toast.add({
icon: 'i-heroicons-information-circle-20-solid',
timeout: 1000,
title: '复制成功'
})
useCopyToClipboard().copy(props.code)
toast.add({
icon: 'i-heroicons-information-circle-20-solid',
timeout: 1000,
title: '复制成功'
})
}
const codeBlock = ref(null)
const handleRender = () => {
const language = props.language || 'html'
const lang = language.startsWith('vue') ? 'html' : language
codeBlock.value = hljs.highlight(props.code, { language: lang }).value
const language = props.language || 'html'
const lang = language.startsWith('vue') ? 'html' : language
codeBlock.value = hljs.highlight(props.code, { language: lang }).value
}
watch(()=> props.code, () => {
handleRender();
handleRender();
}, { immediate: true })
</script>
......
export default () => {
const { $getCollection } = useLibraryStore()
// 创建及修改收藏夹
const setOrUpdateCollection = async (body) => {
/*
const { $getCollection } = useLibraryStore()
// 创建及修改收藏夹
const setOrUpdateCollection = async (body) => {
/*
* id number 非必须 有ID参数是修改,没有ID则为新增
* name string 非必须
* is_public number 非必须
* description string
*
*/
const {data, error} = await useRequest('/v1/collection/merge', {
method: 'post',
body
})
return { data, error }
}
// 删除收藏夹
const deleteCollection = async (collection_id) => {
const {data, error} = await useRequest(`/v1/collection/${collection_id}/remove`, {
method: 'post'
})
return { data, error }
}
// 将会话添加到收藏夹
const saveCollection = async (body) => {
// collection_id number 收藏夹ID
// c_id string 会话ID
const {data, error} = await useRequest(`/v1/collection/item/add`, {
method: 'post',
body
})
return { data, error }
}
// 查询收藏夹会话列表
const findCollection = async (collection_id) => {
const {data, error} = await useRequest(`/v1/collection/${collection_id}/items`)
if (error.value) {
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 }
const {data, error} = await useRequest('/v1/collection/merge', {
method: 'post',
body
})
return { data, error }
}
// 删除收藏夹
const deleteCollection = async (collection_id) => {
const {data, error} = await useRequest(`/v1/collection/${collection_id}/remove`, {
method: 'post'
})
return { data, error }
}
// 将会话添加到收藏夹
const saveCollection = async (body) => {
// collection_id number 收藏夹ID
// c_id string 会话ID
const {data, error} = await useRequest(`/v1/collection/item/add`, {
method: 'post',
body
})
return { data, error }
}
// 查询收藏夹会话列表
const findCollection = async (collection_id) => {
const {data, error} = await useRequest(`/v1/collection/${collection_id}/items`)
if (error.value) {
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 }
}
const deleteThread = async (ids) => {
const {data, error} = await useRequest('/v1/chat/completion/remove', {
method: 'post',
body: ids
})
return { data, error }
}
const getRepoStars = async (params) => {
/*
const deleteThread = async (ids) => {
const {data, error} = await useRequest('/v1/chat/completion/remove', {
method: 'post',
body: ids
})
return { data, error }
}
const getRepoStars = async (params) => {
/*
url githuburl 多个使用逗号分割
per 数据统计粒度,day,week,month,默认是用week
day_period 查询周期天,默认365
*/
const { data, error } = await useRequest('/v1/repo/stars', { params })
if (error.value) return []
return data.value.data
}
return {
setOrUpdateCollection,
deleteCollection,
saveCollection,
findCollection,
deleteCollectionRecord,
findRecordCollection,
deleteThread,
getRepoStars
}
const { data, error } = await useRequest('/v1/repo/stars', { params })
if (error.value) return []
return data.value.data
}
return {
setOrUpdateCollection,
deleteCollection,
saveCollection,
findCollection,
deleteCollectionRecord,
findRecordCollection,
deleteThread,
getRepoStars
}
}
\ No newline at end of file
export default () => {
// 查询主题列表
const getThreadsList = async (c_ids) => {
// c_ids => c_id,c_id
let query = c_ids ? `?c_ids=${c_ids}` : ''
const { data, error } = await useRequest(`/v1/chat/completion/list${query}`)
if (error.value) {
return []
}
return data.value.data || []
}
return {
getThreadsList
// 查询主题列表
const getThreadsList = async (c_ids) => {
// c_ids => c_id,c_id
let query = c_ids ? `?c_ids=${c_ids}` : ''
const { data, error } = await useRequest(`/v1/chat/completion/list${query}`)
if (error.value) {
return []
}
return data.value.data || []
}
return {
getThreadsList
}
}
\ No newline at end of file
const BASE_URL = 'https://gpu-pod656e861afe3d944d6b3ce77e-7862.node.inscode.run'
const request = async (url, options = {}) => {
const token = useCookie('token')
const fullUrl = BASE_URL + url
const config = {
method: options.method || 'get',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
onRequest({ request, options }) {
// 设置请求头
},
onRequestError({ request, options, error }) {
// 处理请求错误
error && console.error(error)
},
onResponse({ request, response, options }) {
},
onResponseError({ request, response, options }) {
const status = response.status
useRequestError(status, response._data.message)
// 处理响应错误
console.log('[ResponseError]', request)
}
const token = useCookie('token')
const fullUrl = BASE_URL + url
const config = {
method: options.method || 'get',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
onRequest() {
// 设置请求头
},
onRequestError({ error }) {
// 处理请求错误
error && console.error(error)
},
onResponse() {
},
onResponseError({ request, response }) {
const status = response.status
useRequestError(status, response._data.message)
// 处理响应错误
console.log('[ResponseError]', request)
}
if (options && options.headers) {
Object.assign(config.headers, options.headers)
delete options.headers
}
return useFetch(fullUrl, Object.assign(config, options));
}
if (options && options.headers) {
Object.assign(config.headers, options.headers)
delete options.headers
}
return useFetch(fullUrl, Object.assign(config, options));
}
export default request
export default function (status, message) {
if (process.client && [400, 401, 403].includes(status)) {
// 全局弹提示
let title
if (status === 400) title = message
else if (status === 401) title = '抱歉,您尚未登录'
else if (status === 403) title = '抱歉,您没有权限'
nextTick(() => {
const toast = useToast()
toast.add({
icon: 'i-heroicons-exclamation-triangle-20-solid',
timeout: 3000,
title: title,
color: 'red'
})
})
// 全局弹登录
const { $isSignIn } = storeToRefs(useUserStore())
const { $openSign } = useUserStore()
if (status === 401 && !$isSignIn.value) {
$openSign()
}
if (import.meta.client && [400, 401, 403].includes(status)) {
// 全局弹提示
let title
if (status === 400) title = message
else if (status === 401) title = '抱歉,您尚未登录'
else if (status === 403) title = '抱歉,您没有权限'
nextTick(() => {
const toast = useToast()
toast.add({
icon: 'i-heroicons-exclamation-triangle-20-solid',
timeout: 3000,
title: title,
color: 'red'
})
})
// 全局弹登录
const { $isSignIn } = storeToRefs(useUserStore())
const { $openSign } = useUserStore()
if (status === 401 && !$isSignIn.value) {
$openSign()
}
}
}
\ No newline at end of file
import { useTimeAgo } from '@vueuse/core'
export default function (time) {
const timeAgo = useTimeAgo(time)
let timeStr = timeAgo
if (timeAgo.value === 'just now') timeStr = '刚刚'
else if (timeAgo.value === 'yesterday') timeStr = '昨天'
else if (timeAgo.value === 'last week') timeStr = '上周'
else if (timeAgo.value === 'last month') timeStr = '上个月'
else if (timeAgo.value === 'last year') timeStr = '去年'
else if (timeAgo.value.indexOf('minute ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 分钟前'
else if (timeAgo.value.indexOf('minutes ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 分钟前'
else if (timeAgo.value.indexOf('hour ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 小时前'
else if (timeAgo.value.indexOf('hours ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 小时前'
else if (timeAgo.value.indexOf('days ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 天前'
else if (timeAgo.value.indexOf('weeks ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 周前'
else if (timeAgo.value.indexOf('months ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 个月前'
else if (timeAgo.value.indexOf('years ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 年前'
return timeStr
const timeAgo = useTimeAgo(time)
let timeStr = timeAgo
if (timeAgo.value === 'just now') timeStr = '刚刚'
else if (timeAgo.value === 'yesterday') timeStr = '昨天'
else if (timeAgo.value === 'last week') timeStr = '上周'
else if (timeAgo.value === 'last month') timeStr = '上个月'
else if (timeAgo.value === 'last year') timeStr = '去年'
else if (timeAgo.value.indexOf('minute ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 分钟前'
else if (timeAgo.value.indexOf('minutes ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 分钟前'
else if (timeAgo.value.indexOf('hour ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 小时前'
else if (timeAgo.value.indexOf('hours ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 小时前'
else if (timeAgo.value.indexOf('days ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 天前'
else if (timeAgo.value.indexOf('weeks ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 周前'
else if (timeAgo.value.indexOf('months ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 个月前'
else if (timeAgo.value.indexOf('years ago') > 0) timeStr = timeAgo.value.split(' ')[0] + ' 年前'
return timeStr
}
\ No newline at end of file
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt({
files: ['**/*.vue', '**/*.js'],
rules: {
'indent': [2, 2],
'vue/html-indent': ['off', 'tab', { }],
'no-empty': ['error', { 'allowEmptyCatch': true }],
'vue/no-v-html': 'off',
'no-useless-escape': 'error',
'vue/no-multiple-template-root': 'off',
'vue/multi-word-component-names': 0
}
})
<template>
<div class="flex min-h-screen">
<div class="fixed top-0 w-64 hidden md:flex flex-col h-screen">
<IAside />
</div>
<div class="w-full ml-0 md:ml-64 bg-white dark:bg-black flex flex-grow">
<slot />
</div>
</div>
<ILibraryCreate />
<ILibrarySelect @success="handleCollectSuccess" />
<div class="flex min-h-screen">
<div class="fixed top-0 w-64 hidden md:flex flex-col h-screen">
<IAside />
</div>
<div class="w-full ml-0 md:ml-64 bg-white dark:bg-black flex flex-grow">
<slot />
</div>
</div>
<ILibraryCreate />
<ILibrarySelect @success="handleCollectSuccess" />
</template>
<script setup>
const selectCollectData = ref(null)
const removeCollectData = ref(null)
function handleCollectSuccess (data) {
selectCollectData.value = data
selectCollectData.value = data
}
function handleClearCollectData () {
selectCollectData.value = null
selectCollectData.value = null
}
function handleRemoveCollectData (data) {
removeCollectData.value = data
removeCollectData.value = data
}
function handleClearRemoveCollectData () {
removeCollectData.value = null
removeCollectData.value = null
}
provide('Layout', {
selectCollectData,
handleClearCollectData,
removeCollectData,
handleRemoveCollectData,
handleClearRemoveCollectData
selectCollectData,
handleClearCollectData,
removeCollectData,
handleRemoveCollectData,
handleClearRemoveCollectData
})
</script>
<template>
<slot />
<slot />
</template>
......@@ -12,8 +12,14 @@ export default defineNuxtConfig({
'@nuxtjs/device',
'@vueuse/nuxt',
'@pinia/nuxt',
'@formkit/auto-animate/nuxt'
'@formkit/auto-animate/nuxt',
"@nuxt/eslint"
],
eslint: {
checker: {
configType: 'eslintrc'
}
},
ui: {
icons: ['simple-icons']
},
......
因为 它太大了无法显示 source diff 。你可以改为 查看blob
......@@ -9,7 +9,8 @@
"dev-sw": "SW=true nuxi dev --host",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
"postinstall": "nuxt prepare",
"lint": "eslint . --fix"
},
"dependencies": {
"@formkit/auto-animate": "^0.8.2",
......@@ -31,6 +32,8 @@
"vue-router": "^4.3.2"
},
"devDependencies": {
"@nuxtjs/device": "^3.1.1"
"@nuxtjs/device": "^3.1.1",
"vite-plugin-eslint2": "^4.4.0",
"@nuxt/eslint": "^0.3.13"
}
}
<template>
<div class="flex flex-col items-center justify-center w-full h-full">
<ILogo />
<div class="mt-2">开源搜索,直达结果</div>
<ICreate ref="refCreate" />
<div class="max-w-screen-md w-full p-6 grid grid-cols-1 lg:grid-cols-2 gap-4">
<template v-for="(item, index) in recommendQuestions" :key="index">
<UButton
class="flex justify-between"
color="gray"
variant="soft"
size="md"
:trailing-icon="item.c_id ? 'i-heroicons-chevron-right-20-solid' : 'i-heroicons-plus-20-solid'"
:label="item.title"
:to="item.c_id ? `/search/${item.c_id}` : ''"
@click="handleSearch(item)"
/>
</template>
</div>
</div>
<IMenuSider fixed />
<div class="flex flex-col items-center justify-center w-full h-full">
<ILogo />
<div class="mt-2">开源搜索,直达结果</div>
<ICreate ref="refCreate" />
<div class="max-w-screen-md w-full p-6 grid grid-cols-1 lg:grid-cols-2 gap-4">
<template v-for="(item, index) in recommendQuestions" :key="index">
<UButton
class="flex justify-between"
color="gray"
variant="soft"
size="md"
:trailing-icon="item.c_id ? 'i-heroicons-chevron-right-20-solid' : 'i-heroicons-plus-20-solid'"
:label="item.title"
:to="item.c_id ? `/search/${item.c_id}` : ''"
@click="handleSearch(item)"
/>
</template>
</div>
</div>
<IMenuSider fixed />
</template>
<script setup>
const recommendQuestions = [
{
title: '如何解决 CORS 问题',
c_id: 'c_CawfQuhqPxGy1Ny9'
},
{
title: 'Tailwind CSS 响应式设计指南',
c_id: 'c_8bRgiXlnjosuiTto'
},
{
title: '有哪些有效的方法可以应对 DDoS 攻击?'
},
{
title: '如何防止网站被爬'
}
{
title: '如何解决 CORS 问题',
c_id: 'c_CawfQuhqPxGy1Ny9'
},
{
title: 'Tailwind CSS 响应式设计指南',
c_id: 'c_8bRgiXlnjosuiTto'
},
{
title: '有哪些有效的方法可以应对 DDoS 攻击?'
},
{
title: '如何防止网站被爬'
}
]
const refCreate = ref(null)
function handleSearch (item) {
if (!item.c_id) refCreate.value.handleQuickSearch(item.title)
if (!item.c_id) refCreate.value.handleQuickSearch(item.title)
}
</script>
\ No newline at end of file
<template>
<div class="w-full items-center flex flex-col">
<ILibraryHeader
v-if="currentCollect"
:collect="currentCollect.name"
:description="currentCollect.description"
:count="currentCollect.record_count"
:collect-id="currentCollect.id"
/>
<div class="container max-w-screen-lg 2xl:max-w-screen-xl flex flex-col p-6">
<div class="flex gap-10">
<div class="flex flex-col flex-grow">
<div v-auto-animate class="flex flex-col gap-4">
<ILibraryThread
v-for="item in themesTagList"
:item="item"
:key="item.id"
is-item
@delete="handleDeletedThread"
/>
</div>
</div>
</div>
</div>
</div>
<div class="w-full items-center flex flex-col">
<ILibraryHeader
v-if="currentCollect"
:collect="currentCollect.name"
:description="currentCollect.description"
:count="currentCollect.record_count"
:collect-id="currentCollect.id"
/>
<div class="container max-w-screen-lg 2xl:max-w-screen-xl flex flex-col p-6">
<div class="flex gap-10">
<div class="flex flex-col flex-grow">
<div v-auto-animate class="flex flex-col gap-4">
<ILibraryThread
v-for="item in themesTagList"
:key="item.id"
:item="item"
is-item
@delete="handleDeletedThread"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
const route = useRoute()
......@@ -30,26 +30,26 @@ const { findCollection } = useCollectionRequest()
const { $collection } = storeToRefs(useLibraryStore())
const { $getCollection } = useLibraryStore()
const state = reactive({
id: Number(route.params.id)
id: Number(route.params.id)
})
const themesTagList = ref([])
const currentCollect = computed(() => $collection.value.find(i => i.id === state.id))
async function findCollectionData() {
const data = await findCollection(state.id)
themesTagList.value = data.map(item => {
return {
...item,
title: item.c_title
}
})
const data = await findCollection(state.id)
themesTagList.value = data.map(item => {
return {
...item,
title: item.c_title
}
})
}
await findCollectionData()
await $getCollection()
function handleDeletedThread (c_id) {
const findIndex = themesTagList.value.findIndex(i => i.c_id === c_id)
if (findIndex >= 0) themesTagList.value.splice(findIndex, 1)
const findIndex = themesTagList.value.findIndex(i => i.c_id === c_id)
if (findIndex >= 0) themesTagList.value.splice(findIndex, 1)
}
useHead({
title: `${currentCollect.value.name} - GitBot`
title: `${currentCollect.value.name} - GitBot`
})
</script>
\ No newline at end of file
<template>
<div class="w-full items-center flex flex-col">
<ILibraryHeader show-tabs :tab="tab" @change-tab="handleChangeTab" @search="handleSearch" @clear="getThreadData" />
<div class="container max-w-screen-lg 2xl:max-w-screen-xl flex flex-col p-6 pt-0 lg:pt-6">
<div class="flex gap-10" v-show="ready">
<div class="flex flex-col flex-grow" v-show="tab === 0 || tab === -1">
<div v-auto-animate class="flex flex-col gap-4">
<ILibraryThread
v-for="item in threads"
:item="item"
:key="item.c_id"
@delete="handleDeletedThread"
/>
<IEmpty v-if="!threads.length" />
</div>
</div>
<div class="flex flex-col flex-shrink-0 w-full lg:w-56" v-show="tab === 1 || tab === -1">
<div class="flex justify-between items-center">
<div class="flex items-center text-lg gap-2">
<UIcon name="i-heroicons-squares-2x2" />
<div>合集</div>
</div>
<UButton
icon="i-heroicons-plus-20-solid"
variant="soft"
@click="handleOpenCreateLibrary"
/>
</div>
<div v-auto-animate class="flex flex-col gap-4 mt-4">
<ILibraryCollect
v-for="item in $collection"
:item="item"
:key="item.id"
/>
</div>
</div>
</div>
</div>
</div>
<div class="w-full items-center flex flex-col">
<ILibraryHeader show-tabs :tab="tab" @change-tab="handleChangeTab" @search="handleSearch" @clear="getThreadData" />
<div class="container max-w-screen-lg 2xl:max-w-screen-xl flex flex-col p-6 pt-0 lg:pt-6">
<div v-show="ready" class="flex gap-10">
<div v-show="tab === 0 || tab === -1" class="flex flex-col flex-grow">
<div v-auto-animate class="flex flex-col gap-4">
<ILibraryThread
v-for="item in threads"
:key="item.c_id"
:item="item"
@delete="handleDeletedThread"
/>
<IEmpty v-if="!threads.length" />
</div>
</div>
<div v-show="tab === 1 || tab === -1" class="flex flex-col flex-shrink-0 w-full lg:w-56">
<div class="flex justify-between items-center">
<div class="flex items-center text-lg gap-2">
<UIcon name="i-heroicons-squares-2x2" />
<div>合集</div>
</div>
<UButton
icon="i-heroicons-plus-20-solid"
variant="soft"
@click="handleOpenCreateLibrary"
/>
</div>
<div v-auto-animate class="flex flex-col gap-4 mt-4">
<ILibraryCollect
v-for="item in $collection"
:key="item.id"
:item="item"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {LG} from '~/composables/useMQ.js';
......@@ -46,39 +46,39 @@ const { $openLibraryCreate, $getCollection } = useLibraryStore()
const { getThreadsList } = useLibraryRequest()
function handleOpenCreateLibrary () {
$openLibraryCreate()
$openLibraryCreate()
}
// 主题列表
const threads = ref([])
async function getThreadData() {
threads.value = await getThreadsList()
threads.value = await getThreadsList()
}
await getThreadData()
await $getCollection()
function handleDeletedThread (c_id) {
const findIndex = threads.value.findIndex(i => i.c_id === c_id)
if (findIndex >= 0) threads.value.splice(findIndex, 1)
const findIndex = threads.value.findIndex(i => i.c_id === c_id)
if (findIndex >= 0) threads.value.splice(findIndex, 1)
}
function handleSearch (data) {
threads.value = data
threads.value = data
}
const tab = ref(-1)
watch(()=> LG.value, (value) => {
if (value) tab.value = 0
else tab.value = -1
if (value) tab.value = 0
else tab.value = -1
})
const ready = ref(false)
onMounted(() => {
if (LG.value) tab.value = 0
ready.value = true
if (LG.value) tab.value = 0
ready.value = true
})
function handleChangeTab (index) {
tab.value = index
tab.value = index
}
useHead({
title: '全部主题 - GitBot'
title: '全部主题 - GitBot'
})
</script>
<template>
<div class="w-full p-6">
<IMdMdc :content="lineChart"/>
<UDivider />
<IMdMdc :content="tableChart"/>
</div>
<div class="w-full p-6">
<IMdMdc :content="lineChart"/>
<UDivider />
<IMdMdc :content="tableChart"/>
</div>
</template>
<script setup>
const lineChart = `
......@@ -12,23 +12,23 @@ const lineChart = `
const tableChart = `
::ProseGridTable{:data='[{"repo_id":"806709826","repo_name":"2noise/ChatTTS","primary_language":"Jupyter Notebook","description":"ChatTTS is a generative speech model for daily dialogue.","stars":"1451","forks":"167","pull_requests":"3","pushes":"","total_score":"6000.5405","contributor_logins":"zhouhao27,Huixxi,Thinking80s,bank010,lphkxd","collection_names":""}]'}
`
const table = `
| repo_id | repo_name | primary_language | description | stars | forks | pull_requests | pushes | total_score | contributor_logins | collection_names |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 806709826 | 2noise/ChatTTS | Jupyter Notebook | ChatTTS is a generative speech model for daily dialogue | 1451 | 167 | 3 | | 6000.5405 | zhouhao27,Huixxi,Thinking80s,bank010,lphkxd | |
`
const table2 = `
| 列1 | 列2 | 列3 | 列4 | 列5 | 列6 | 列7 | 列8 | 列9 | 列10 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 行1数据A | 数据B | 数据C | 数据D | 数据E | 数据F | 数据G | 数据H | 数据I | 数据J |
| 行2数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 行3数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 行4数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 行5数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 行6数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 行7数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 行8数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 行9数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 行10数据A | 数据B | 数据C | 数据D | 数据E | 数据F | 数据G | 数据H | 数据I | 数据J |
`
// const table = `
// | repo_id | repo_name | primary_language | description | stars | forks | pull_requests | pushes | total_score | contributor_logins | collection_names |
// | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
// | 806709826 | 2noise/ChatTTS | Jupyter Notebook | ChatTTS is a generative speech model for daily dialogue | 1451 | 167 | 3 | | 6000.5405 | zhouhao27,Huixxi,Thinking80s,bank010,lphkxd | |
// `
// const table2 = `
// | 列1 | 列2 | 列3 | 列4 | 列5 | 列6 | 列7 | 列8 | 列9 | 列10 |
// | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
// | 行1数据A | 数据B | 数据C | 数据D | 数据E | 数据F | 数据G | 数据H | 数据I | 数据J |
// | 行2数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
// | 行3数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
// | 行4数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
// | 行5数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
// | 行6数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
// | 行7数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
// | 行8数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
// | 行9数据A | ... | ... | ... | ... | ... | ... | ... | ... | ... |
// | 行10数据A | 数据B | 数据C | 数据D | 数据E | 数据F | 数据G | 数据H | 数据I | 数据J |
// `
</script>
\ No newline at end of file
<template>
<div class="w-full items-center flex flex-col">
<IException v-if="historyStatus !== 200" :code="historyStatus" />
<template v-else>
<iSearchHeader
ref="refHeader"
:query="state.title"
:is-public="state.isPublic"
:repo="state.repo"
@update-query="handleUpdateQuery"
/>
<div class="container min-h-svh max-w-screen-lg 2xl:max-w-screen-xl flex flex-col space-y-6 p-6 mb-6 pb-0" ref="scrollElement">
<template v-for="(item, index) in data" :key="index">
<ISearchArticle>
<template #title>
<ISearchTitle as="h2" :title="item.question" :id="index + 1" />
</template>
<ISearchContent
:item="item"
:asking="false"
:is-last-index="false"
:index="index"
:actions="item.actions"
:collapse="false"
processStatus="finish"
/>
<template v-if="item.extra && item.extra.length > 0" #extra>
<ISearchExtraInfo :data="item.extra" />
</template>
</ISearchArticle>
<UDivider v-if="data.length !== index + 1 || askingData.question" class="pt-3 pb-2" />
</template>
<template v-if="askingData.question">
<ISearchArticle>
<template #title>
<ISearchTitle #title as="h2" :title="askingData.question" :id="data.length + 1" />
</template>
<ISearchContent
:item="askingData"
:asking="asking"
:collapse="askingData.collapse"
:is-last-index="true"
:actions="askingData.actions"
@regenerate="handleReGenerate"
:processStatus="askingData.processStatus"
/>
<template v-if="askingSidebarCards.length > 0" #extra>
<ISearchExtraInfo :data="askingSidebarCards" />
</template>
</ISearchArticle>
</template>
<ISearchRecommendQuestion
v-if="recommendQuestions.length > 0 && !asking"
:recommend-questions="recommendQuestions"
@click="handleContinueAsk"
/>
</div>
<div class="container max-w-screen-lg 2xl:max-w-screen-xl sticky bottom-4 flex pl-6 pr-6">
<ISearchAsk :asking="asking" @stop="handleStopGenerate" @ask="handleContinueAsk" />
</div>
</template>
</div>
<div class="w-full items-center flex flex-col">
<IException v-if="historyStatus !== 200" :code="historyStatus" />
<template v-else>
<iSearchHeader
ref="refHeader"
:query="state.title"
:is-public="state.isPublic"
:repo="state.repo"
@update-query="handleUpdateQuery"
/>
<div ref="scrollElement" class="container min-h-svh max-w-screen-lg 2xl:max-w-screen-xl flex flex-col space-y-6 p-6 mb-6 pb-0">
<template v-for="(item, index) in data" :key="index">
<ISearchArticle>
<template #title>
<ISearchTitle :id="index + 1" as="h2" :title="item.question" />
</template>
<ISearchContent
:item="item"
:asking="false"
:is-last-index="false"
:index="index"
:actions="item.actions"
:collapse="false"
process-status="finish"
/>
<template v-if="item.extra && item.extra.length > 0" #extra>
<ISearchExtraInfo :data="item.extra" />
</template>
</ISearchArticle>
<UDivider v-if="data.length !== index + 1 || askingData.question" class="pt-3 pb-2" />
</template>
<template v-if="askingData.question">
<ISearchArticle>
<template #title>
<ISearchTitle :id="data.length + 1" as="h2" :title="askingData.question" />
</template>
<ISearchContent
:item="askingData"
:asking="asking"
:collapse="askingData.collapse"
:is-last-index="true"
:actions="askingData.actions"
:process-status="askingData.processStatus"
@regenerate="handleReGenerate"
/>
<template v-if="askingSidebarCards.length > 0" #extra>
<ISearchExtraInfo :data="askingSidebarCards" />
</template>
</ISearchArticle>
</template>
<ISearchRecommendQuestion
v-if="recommendQuestions.length > 0 && !asking"
:recommend-questions="recommendQuestions"
@click="handleContinueAsk"
/>
</div>
<div class="container max-w-screen-lg 2xl:max-w-screen-xl sticky bottom-4 flex pl-6 pr-6">
<ISearchAsk :asking="asking" @stop="handleStopGenerate" @ask="handleContinueAsk" />
</div>
</template>
</div>
</template>
<script setup>
const route = useRoute()
......@@ -84,295 +84,296 @@ const markedEnd = '[DONE]'
// 是否是第一次生成记录
const isFirstCreate = $firstRecordTitle.value !== ''
function fetchRepoStarsData (urls, query) {
urls = urls.join(',')
query = query || {}
const params = Object.assign({ urls, per: 'week', day_period: 365 }, query)
return getRepoStars(params)
urls = urls.join(',')
query = query || {}
const params = Object.assign({ urls, per: 'week', day_period: 365 }, query)
return getRepoStars(params)
}
function initSearchItemInfo (info, records) {
const { title, repo_name, repo_path, repo_branch, is_public } = info
Object.assign(state, { query: $firstRecordTitle.value || title, title, repo: repo_name, gitPath: repo_path, branch: repo_branch, isPublic: is_public })
// 单独处理query,兼容临时title的历史记录
if (records && records.length > 0) {
state.query = records[0].question
}
const { title, repo_name, repo_path, repo_branch, is_public } = info
Object.assign(state, { query: $firstRecordTitle.value || title, title, repo: repo_name, gitPath: repo_path, branch: repo_branch, isPublic: is_public })
// 单独处理query,兼容临时title的历史记录
if (records && records.length > 0) {
state.query = records[0].question
}
}
async function initSearchRecordsChart (repos) {
if (!process.client) return
repos.forEach((repo) => {
let {output, index} = repo
const {gits, urls} = handleFormatInfoData(output)
fetchRepoStarsData(urls).then((result) => {
const card = handleFormatRepoStars(result, gits)
const cardItem = data.value[index].extra.find(item => item.type === card.type)
if (!cardItem) return
Object.assign(cardItem, card)
})
if (!import.meta.client) return
repos.forEach((repo) => {
const {output, index} = repo
const {gits, urls} = handleFormatInfoData(output)
fetchRepoStarsData(urls).then((result) => {
const card = handleFormatRepoStars(result, gits)
const cardItem = data.value[index].extra.find(item => item.type === card.type)
if (!cardItem) return
Object.assign(cardItem, card)
})
})
}
function initSearchRecords (records) {
const repos = []
records = records.map((item, index) => {
let { answer, actions, question, answer_type } = item
answer = answer_type === 'json' ? handleFormatReports(answer) : answer
const historyItem = { article: answer, question, showActions: true}
actions = typeof actions === 'string' ? JSON.parse(actions) : actions
historyItem.extra = historyItem.extra || []
// 处理搜索过程
actions = actions.map(child => {
const { action } = child
if (action === 'search_file') {
child.output = historyItem.source = handleFormatSource(child.output, state)
} else if (action === 'search_web') {
const outputs = handleFormatWebSource(child.output)
child.output = historyItem.source = outputs
} else if (action === 'search_relate_repo') {
// 处理请求Loading使用使用
historyItem.extra.push({ ready: false, type: action })
repos.push({ output: child.output, index})
}
return child
})
historyItem.actions = actions
return historyItem
const repos = []
records = records.map((item, index) => {
let { answer, actions } = item
const { question, answer_type } = item
answer = answer_type === 'json' ? handleFormatReports(answer) : answer
const historyItem = { article: answer, question, showActions: true}
actions = typeof actions === 'string' ? JSON.parse(actions) : actions
historyItem.extra = historyItem.extra || []
// 处理搜索过程
actions = actions.map(child => {
const { action } = child
if (action === 'search_file') {
child.output = historyItem.source = handleFormatSource(child.output, state)
} else if (action === 'search_web') {
const outputs = handleFormatWebSource(child.output)
child.output = historyItem.source = outputs
} else if (action === 'search_relate_repo') {
// 处理请求Loading使用使用
historyItem.extra.push({ ready: false, type: action })
repos.push({ output: child.output, index})
}
return child
})
initSearchRecordsChart(repos)
data.value = records
historyItem.actions = actions
return historyItem
})
initSearchRecordsChart(repos)
data.value = records
}
async function init () {
const { data, error } = await useRequest(`/v1/chat/${state.id}/history`)
if (error.value) {
historyStatus.value = error.value.statusCode
return
}
const { info, records } = data.value && data.value.data
initSearchItemInfo(info, records)
initSearchRecords(records)
// 写入本地浏览记录
$setSearchHistory({ title: info.title, c_id: info.c_id, type: 'thread' })
const { data, error } = await useRequest(`/v1/chat/${state.id}/history`)
if (error.value) {
historyStatus.value = error.value.statusCode
return
}
const { info, records } = data.value && data.value.data
initSearchItemInfo(info, records)
initSearchRecords(records)
// 写入本地浏览记录
$setSearchHistory({ title: info.title, c_id: info.c_id, type: 'thread' })
}
await init()
function handleUpdateQuery(query) {
state.title = query
state.title = query
}
// 重置askingData
function resetAskingData (question) {
askingData.value = { question, collapse: true, actions: [], processStatus: 'start' }
askingData.value = { question, collapse: true, actions: [], processStatus: 'start' }
}
// 是否停止自动滚动到底部
const isAutoToBottom = ref(true)
watch(() => directions.top, () => {
if (directions.top) isAutoToBottom.value = false
if (directions.top) isAutoToBottom.value = false
})
watch(() => askingData.value.collapse, (newVal, oldVal) => {
if (!newVal && asking.value) resetAutoBottom()
watch(() => askingData.value.collapse, (newVal) => {
if (!newVal && asking.value) resetAutoBottom()
})
watch(() => directions.bottom, () => {
if (directions.bottom && asking.value) handleToBottom()
if (directions.bottom && asking.value) handleToBottom()
})
function handleToBottom () {
const $target = scrollElement.value
const scrollTop = window.innerHeight + window.scrollY + 30;
const height = $target.parentNode.clientHeight
if (scrollTop >= height) {
isAutoToBottom.value = true
scrollToView()
}
const $target = scrollElement.value
const scrollTop = window.innerHeight + window.scrollY + 30;
const height = $target.parentNode.clientHeight
if (scrollTop >= height) {
isAutoToBottom.value = true
scrollToView()
}
}
function scrollToView () {
if (!isAutoToBottom.value || !scrollElement.value) return
y.value = scrollElement.value.clientHeight
if (!isAutoToBottom.value || !scrollElement.value) return
y.value = scrollElement.value.clientHeight
}
function createGenerateInitItem (question, regenerate) {
if (askingData.value.question && !regenerate) {
const deepCopy = JSON.parse(JSON.stringify(askingData.value))
deepCopy.extra = [...askingSidebarCards.value]
data.value.push(deepCopy)
}
askingData.value.question = ''
askingSidebarCards.value = []
recommendQuestions.value = []
nextTick(() => {
resetAskingData(question)
})
if (askingData.value.question && !regenerate) {
const deepCopy = JSON.parse(JSON.stringify(askingData.value))
deepCopy.extra = [...askingSidebarCards.value]
data.value.push(deepCopy)
}
askingData.value.question = ''
askingSidebarCards.value = []
recommendQuestions.value = []
nextTick(() => {
resetAskingData(question)
})
}
function handleContinueAsk (question, regenerate) {
if (asking.value) return
asking.value = true
createGenerateInitItem(question, regenerate)
// 兼容再次提问问题
question = regenerate || question
generateFetchData(question)
setTimeout(() => {
isAutoToBottom.value = true
scrollToView()
})
if (asking.value) return
asking.value = true
createGenerateInitItem(question, regenerate)
// 兼容再次提问问题
question = regenerate || question
generateFetchData(question)
setTimeout(() => {
isAutoToBottom.value = true
scrollToView()
})
}
function handleReGenerate () {
const { question } = askingData.value
handleContinueAsk(question, `根据上述回答,对问题"${question}"重新生成`)
const { question } = askingData.value
handleContinueAsk(question, `根据上述回答,对问题"${question}"重新生成`)
}
function resetAutoBottom () {
nextTick(() => {
isAutoToBottom.value = true
})
nextTick(() => {
isAutoToBottom.value = true
})
}
function handleSetNoPermission () {
resetAskingData('')
useRequestError(403)
resetAskingData('')
useRequestError(403)
}
async function getRepoStarsData (output) {
askingSidebarCards.value.push({ ready: false, type: 'search_relate_repo' })
const { gits, urls } = handleFormatInfoData(output)
const data = await fetchRepoStarsData(urls)
const cardItem = handleFormatRepoStars(data, gits)
const hasRepoStarItem = askingSidebarCards.value.find(item => item.type === cardItem.type)
if (hasRepoStarItem) {
Object.assign(hasRepoStarItem, cardItem)
} else {
askingSidebarCards.value.push(cardItem)
}
askingSidebarCards.value.push({ ready: false, type: 'search_relate_repo' })
const { gits, urls } = handleFormatInfoData(output)
const data = await fetchRepoStarsData(urls)
const cardItem = handleFormatRepoStars(data, gits)
const hasRepoStarItem = askingSidebarCards.value.find(item => item.type === cardItem.type)
if (hasRepoStarItem) {
Object.assign(hasRepoStarItem, cardItem)
} else {
askingSidebarCards.value.push(cardItem)
}
}
function handleFormFetchData (fetchData) {
let message = {}
try {
message = JSON.parse(fetchData)
} catch(error) { }
if (Object.keys(message).length === 0) return
const { meta, choices, error, code } = message
// 处理403
if (error && code === 403) {
handleSetNoPermission()
}
// 不处理异常数据
if (!meta) {
handleStopGenerate()
}
if (meta.type === 'answer') {
let article = message.choices[0].message.content
article = Array.isArray(article) ? handleFormatReports(article) : article
Object.assign(askingData.value, { article, collapse: false, processStatus: 'finish', ansLoading: true })
nextTick(() => {
scrollToView()
})
return
}
if (meta.type !== 'log') return
const content = choices[0].message.content
switch (meta.action) {
case 'rephrase_question' :
askingData.value.actions.push({ action: meta.action, output: content})
break
case 'search_file' :
askingData.value.source = handleFormatSource(content, state)
askingData.value.actions.push({ action: meta.action, output: askingData.value.source })
break
case 'search_web' :
askingData.value.source = handleFormatWebSource(content)
askingData.value.actions.push({ action: meta.action, output: askingData.value.source })
break
case 'tool_select' :
askingData.value.actions.push({ action: meta.action, output: content })
break
case 'search_relate_repo' :
askingData.value.actions.push({ action: meta.action, output: '' })
getRepoStarsData(content)
break
}
resetAutoBottom()
let message = {}
try {
message = JSON.parse(fetchData)
} catch(error) { }
if (Object.keys(message).length === 0) return
const { meta, choices, error, code } = message
// 处理403
if (error && code === 403) {
handleSetNoPermission()
}
// 不处理异常数据
if (!meta) {
handleStopGenerate()
}
if (meta.type === 'answer') {
let article = message.choices[0].message.content
article = Array.isArray(article) ? handleFormatReports(article) : article
Object.assign(askingData.value, { article, collapse: false, processStatus: 'finish', ansLoading: true })
nextTick(() => {
scrollToView()
})
return
}
if (meta.type !== 'log') return
const content = choices[0].message.content
switch (meta.action) {
case 'rephrase_question' :
askingData.value.actions.push({ action: meta.action, output: content})
break
case 'search_file' :
askingData.value.source = handleFormatSource(content, state)
askingData.value.actions.push({ action: meta.action, output: askingData.value.source })
break
case 'search_web' :
askingData.value.source = handleFormatWebSource(content)
askingData.value.actions.push({ action: meta.action, output: askingData.value.source })
break
case 'tool_select' :
askingData.value.actions.push({ action: meta.action, output: content })
break
case 'search_relate_repo' :
askingData.value.actions.push({ action: meta.action, output: '' })
getRepoStarsData(content)
break
}
resetAutoBottom()
}
function handleCreateAiTitle () {
if (isFirstCreate) {
// todo 需要延迟2s获取title
setTimeout(async () => {
const records = await getThreadsList(state.id)
const currentCollection = records.find( item => item.c_id === state.id)
if (currentCollection) {
state.title = currentCollection.title
$updateSearchHistory(currentCollection)
}
}, 2000)
}
if (isFirstCreate) {
// todo 需要延迟2s获取title
setTimeout(async () => {
const records = await getThreadsList(state.id)
const currentCollection = records.find( item => item.c_id === state.id)
if (currentCollection) {
state.title = currentCollection.title
$updateSearchHistory(currentCollection)
}
}, 2000)
}
}
function handleMessage (event) {
if (event.data === markedEnd) {
Object.assign(askingData.value, { ansLoading: false, showActions: true })
handleCreateAiTitle()
handleStopGenerate()
return
}
handleFormFetchData(event.data)
if (event.data === markedEnd) {
Object.assign(askingData.value, { ansLoading: false, showActions: true })
handleCreateAiTitle()
handleStopGenerate()
return
}
handleFormFetchData(event.data)
}
function handleStopGenerate () {
asking.value = false
aiChatController && aiChatController.abort()
asking.value = false
aiChatController && aiChatController.abort()
}
function handleError (event) {
console.log(`error:`, event)
resetAskingData('')
handleStopGenerate()
if (event) throw event
console.log(`error:`, event)
resetAskingData('')
handleStopGenerate()
if (event) throw event
}
function handleOpen (status) {
if ([401, 403].includes(status)) resetAskingData('')
if ([401, 403].includes(status)) resetAskingData('')
}
async function fetchLinkedQuestion (query) {
const { gitPath, id } = state
const messages = [{ role: 'user', content: query }]
const { data } = await useRequest('/v1/chat/recomend_question', {
method: 'post',
body: { repo_path: gitPath || '', c_id: id, messages }
})
recommendQuestions.value = data.value.data.items.map(item => {
return { title: typeof item === 'string' ? item : item.question }
})
const { gitPath, id } = state
const messages = [{ role: 'user', content: query }]
const { data } = await useRequest('/v1/chat/recomend_question', {
method: 'post',
body: { repo_path: gitPath || '', c_id: id, messages }
})
recommendQuestions.value = data.value.data.items.map(item => {
return { title: typeof item === 'string' ? item : item.question }
})
}
function generateFetchData (query) {
const { gitPath, id } = state
const messages = []
messages.push({ role: 'user', content: query })
aiChatController = new AbortController()
asking.value = true
const params = { c_id: id, stream: true, messages }
if (gitPath) {
params.repo_path = gitPath
}
fetchEventSource('/v1/chat/completions', {
params ,
onopen: handleOpen,
onmessage: handleMessage,
onerror: handleError,
controller: aiChatController
})
// 重新生成相关问题
fetchLinkedQuestion(query)
const { gitPath, id } = state
const messages = []
messages.push({ role: 'user', content: query })
aiChatController = new AbortController()
asking.value = true
const params = { c_id: id, stream: true, messages }
if (gitPath) {
params.repo_path = gitPath
}
fetchEventSource('/v1/chat/completions', {
params ,
onopen: handleOpen,
onmessage: handleMessage,
onerror: handleError,
controller: aiChatController
})
// 重新生成相关问题
fetchLinkedQuestion(query)
}
const refHeader = ref(null)
async function handleUpdateOpenState(url) {
const isOpen = refHeader.value.isOpen
if (isOpen) refHeader.value.handleCopyLink(url)
else {
await refHeader.value.handleUpdateOpenState(true)
refHeader.value.handleCopyLink(url)
}
const isOpen = refHeader.value.isOpen
if (isOpen) refHeader.value.handleCopyLink(url)
else {
await refHeader.value.handleUpdateOpenState(true)
refHeader.value.handleCopyLink(url)
}
}
provide('Search', {
handleUpdateOpenState
handleUpdateOpenState
})
useHead({
title: `${state.title} - GitBot`
title: `${state.title} - GitBot`
})
nextTick( () => {
const query = $firstRecordTitle.value || state.query
// 清除临时缓存提问问题
$setFirstRecordTitle('')
if (data.value.length === 0 && query) {
Object.assign(askingData.value, { question: query })
generateFetchData(query)
} else {
asking.value = false;
}
const query = $firstRecordTitle.value || state.query
// 清除临时缓存提问问题
$setFirstRecordTitle('')
if (data.value.length === 0 && query) {
Object.assign(askingData.value, { question: query })
generateFetchData(query)
} else {
asking.value = false;
}
})
</script>
\ No newline at end of file
<template>
</template>
<script setup>
definePageMeta({
layout: 'empty'
layout: 'empty'
})
const route = useRoute()
const state = reactive({
source: route.query.source || '',
code: route.query.code || ''
source: route.query.source || '',
code: route.query.code || ''
})
let url
if (state.source === 'github') url = '/v1/user/github/login'
else if (state.source === 'gitcode') url = '/v1/user/gitcode/login'
const { data } = await useRequest(url, {
query: { code: state.code },
server: false,
onResponse({ response }) {
const token = useCookie('token', {
maxAge: 86400 * 28
})
const userInfo = useCookie('user-info', {
maxAge: 86400 * 28
})
await useRequest(url, {
query: { code: state.code },
server: false,
onResponse({ response }) {
const token = useCookie('token', {
maxAge: 86400 * 28
})
const userInfo = useCookie('user-info', {
maxAge: 86400 * 28
})
token.value = response._data.data.access_token
userInfo.value = response._data.data.user_info
navigateTo('/')
}
token.value = response._data.data.access_token
userInfo.value = response._data.data.user_info
navigateTo('/')
}
})
</script>
export default defineEventHandler(async (event) => {
// const config = useRuntimeConfig(event)
const cookie = event.headers.get('cookie')
// console.log(`config:`, event.headers.get('cookie'))
const repo = await $fetch('https://ieditor-ai.inscode.cc/ai/md', {
method: 'POST',
headers: {
cookie
}
})
return repo
})
\ No newline at end of file
......@@ -3,9 +3,9 @@ import { createRouter, defineEventHandler, useBase } from 'h3'
const router = createRouter()
router.get('/test', defineEventHandler(() => {
const code1 = `在Vue 3中,通过\`setup()\`函数与Composition API的结合,你可以实现更加高效和模块化的组件开发。下面提供一个综合示例,该示例展示了如何使用\`ref\`创建响应式数据、定义方法以及如何在模板中使用这些功能,同时还包括条件渲染和事件处理。\n\n**组件代码示例:**\n\n\`\`\`vue\n<template>\n <div>\n <!-- 显示响应式消息 -->\n <p v-if=\"showMessage\">{{ displayedMessage }}</p>\n \n <!-- 按钮控制消息的显示与切换 -->\n <button @click=\"toggleMessage\">切换消息</button>\n <button @click=\"hideShowMessage\">隐藏/显示消息</button>\n </div>\n</template>\n\n<script>\nimport { ref, computed } from 'vue';\n\nexport default {\n setup() {\n // 使用ref创建响应式数据\n const originalMessage = ref('这是原始消息');\n \n // 计算属性用于展示的消息,基于原始消息动态变化\n const displayedMessage = computed(() => {\n if (showMessage.value) {\n return originalMessage.value;\n }\n return '';\n });\n \n // 控制消息显示与否的响应式数据\n const showMessage = ref(true);\n \n // 切换消息的内容(假设为一种简单切换)\n function toggleMessage() {\n originalMessage.value = originalMessage.value === '这是原始消息' ?\n '现在消息已改变!' :\n '这是原始消息';\n }\n\n // 控制消息的显示与隐藏\n function hideShowMessage() {\n showMessage.value = !showMessage.value;\n }\n\n // 返回给模板的数据和方法\n return {\n displayedMessage,\n toggleMessage,\n hideShowMessage,\n showMessage, // 如果需要在模板中直接控制显隐,可以返回\n };\n },\n};\n</script>\n\`\`\`\n\n在这个示例中:\n- 使用\`ref\`创建了两个响应式变量\`originalMessage\`\`showMessage\`,分别用于存储消息内容和控制是否显示消息。\n- 利用\`computed\`创建\`displayedMessage\`来根据\`originalMessage\`和显示标志动态展现消息。\n- 定义了两个方法\`toggleMessage\`\`hideShowMessage\`,分别用来切换消息内容和控制消息的显示与隐藏。\n- 模板中通过\`v-if\`指令和事件监听来呈现动态效果,实现了用户交互和数据反应性的完美融合,体现了Composition API的优势。`
const code2 = `在Vue 3中,两种编写组件的方式是通过\`setup()\`函数和使用\`defineComponent\`。下面分别展示这两种方法的基本用法。\n\n### 使用 \`setup()\` 函数\n\n\`setup()\` 是Vue 3 Composition API的一部分,它提供了一个独立的作用域来设置组件的逻辑,不直接混入到模板或数据属性中。此函数不接受任何选项对象,而是直接访问setupContext的属性(如props和attrs)。\n\n\`\`\`vue\n<script>\nimport { defineComponent, ref } from 'vue';\n\nexport default defineComponent({\n setup(props) {\n // 定义响应式数据\n const count = ref(0);\n\n // 方法\n const increment = () => {\n count.value++;\n };\n\n return { count, increment };\n },\n template: \`\n <button @click=\"increment\">{{ count }}</button>\n \`,\n});\n</script>\n\`\`\`\n\n### 使用 \`defineComponent\` 结合选项API\n\n虽然\`setup()\`更偏向于Composition API,但如果你想混合使用Options API与一些Composition API特性,可以直接在其选项结构中使用\`setup()\`,或者完全使用\`defineComponent\`包裹传统的Options风格代码。\n\n\`\`\`vue\n<script>\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n data() {\n return {\n count: 0,\n };\n },\n methods: {\n increment() {\n this.count++;\n },\n },\n template: \`\n <button @click=\"increment\">{{ count }}</button>\n \`,\n});\n</script>\n\`\`\`\n\n在这个例子中,我们展示了如何在Vue 3中定义一个简单的组件,其中一个是使用了Composition API的\`setup()\`函数,另一个则是传统风格,通过\`data\`, \`methods\`, 和 \`template\` 属性来定义。`
return code1 + code2
const code1 = ''// `在Vue 3中,通过\`setup()\`函数与Composition API的结合,你可以实现更加高效和模块化的组件开发。下面提供一个综合示例,该示例展示了如何使用\`ref\`创建响应式数据、定义方法以及如何在模板中使用这些功能,同时还包括条件渲染和事件处理。\n\n**组件代码示例:**\n\n\`\`\`vue\n<template>\n <div>\n <!-- 显示响应式消息 -->\n <p v-if=\"showMessage\">{{ displayedMessage }}</p>\n \n <!-- 按钮控制消息的显示与切换 -->\n <button @click=\"toggleMessage\">切换消息</button>\n <button @click=\"hideShowMessage\">隐藏/显示消息</button>\n </div>\n</template>\n\n<script>\nimport { ref, computed } from 'vue';\n\nexport default {\n setup() {\n // 使用ref创建响应式数据\n const originalMessage = ref('这是原始消息');\n \n // 计算属性用于展示的消息,基于原始消息动态变化\n const displayedMessage = computed(() => {\n if (showMessage.value) {\n return originalMessage.value;\n }\n return '';\n });\n \n // 控制消息显示与否的响应式数据\n const showMessage = ref(true);\n \n // 切换消息的内容(假设为一种简单切换)\n function toggleMessage() {\n originalMessage.value = originalMessage.value === '这是原始消息' ?\n '现在消息已改变!' :\n '这是原始消息';\n }\n\n // 控制消息的显示与隐藏\n function hideShowMessage() {\n showMessage.value = !showMessage.value;\n }\n\n // 返回给模板的数据和方法\n return {\n displayedMessage,\n toggleMessage,\n hideShowMessage,\n showMessage, // 如果需要在模板中直接控制显隐,可以返回\n };\n },\n};\n</script>\n\`\`\`\n\n在这个示例中:\n- 使用\`ref\`创建了两个响应式变量\`originalMessage\`和\`showMessage\`,分别用于存储消息内容和控制是否显示消息。\n- 利用\`computed\`创建\`displayedMessage\`来根据\`originalMessage\`和显示标志动态展现消息。\n- 定义了两个方法\`toggleMessage\`和\`hideShowMessage\`,分别用来切换消息内容和控制消息的显示与隐藏。\n- 模板中通过\`v-if\`指令和事件监听来呈现动态效果,实现了用户交互和数据反应性的完美融合,体现了Composition API的优势。`
const code2 = ''// `在Vue 3中,两种编写组件的方式是通过\`setup()\`函数和使用\`defineComponent\`。下面分别展示这两种方法的基本用法。\n\n### 使用 \`setup()\` 函数\n\n\`setup()\` 是Vue 3 Composition API的一部分,它提供了一个独立的作用域来设置组件的逻辑,不直接混入到模板或数据属性中。此函数不接受任何选项对象,而是直接访问setupContext的属性(如props和attrs)。\n\n\`\`\`vue\n<script>\nimport { defineComponent, ref } from 'vue';\n\nexport default defineComponent({\n setup(props) {\n // 定义响应式数据\n const count = ref(0);\n\n // 方法\n const increment = () => {\n count.value++;\n };\n\n return { count, increment };\n },\n template: \`\n <button @click=\"increment\">{{ count }}</button>\n \`,\n});\n</script>\n\`\`\`\n\n### 使用 \`defineComponent\` 结合选项API\n\n虽然\`setup()\`更偏向于Composition API,但如果你想混合使用Options API与一些Composition API特性,可以直接在其选项结构中使用\`setup()\`,或者完全使用\`defineComponent\`包裹传统的Options风格代码。\n\n\`\`\`vue\n<script>\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n data() {\n return {\n count: 0,\n };\n },\n methods: {\n increment() {\n this.count++;\n },\n },\n template: \`\n <button @click=\"increment\">{{ count }}</button>\n \`,\n});\n</script>\n\`\`\`\n\n在这个例子中,我们展示了如何在Vue 3中定义一个简单的组件,其中一个是使用了Composition API的\`setup()\`函数,另一个则是传统风格,通过\`data\`, \`methods\`, 和 \`template\` 属性来定义。`
return code1 + code2
}))
export default useBase('/api/mock', router.handler)
import { defineStore } from 'pinia'
export const useI18nStore = defineStore('i18n', () => {
const $lang = ref('zh-CN')
const $lang = ref('zh-CN')
const $langOptions = ref([
{
label: '简体中文',
value: 'zh-CN'
},
{
label: 'English',
value: 'en-US'
}
])
const $langOptions = ref([
{
label: '简体中文',
value: 'zh-CN'
},
{
label: 'English',
value: 'en-US'
}
])
return { $lang, $langOptions }
return { $lang, $langOptions }
})
import { defineStore } from 'pinia'
export const useLibraryStore = defineStore('library', () => {
const $isLibraryCreateOpen = ref(false)
const $isLibrarySelectOpen = ref(false)
const $selectThreadId = ref('')
const $selectCollectionId = ref([])
const $isLibraryCreateOpen = ref(false)
const $isLibrarySelectOpen = ref(false)
const $selectThreadId = ref('')
const $selectCollectionId = ref([])
const $collection = ref([])
const $collection = ref([])
function $openLibraryCreate () {
$isLibraryCreateOpen.value = true
}
function $closeLibraryCreate () {
$isLibraryCreateOpen.value = false
}
function $openLibrarySelect (id, collectionId) {
$isLibrarySelectOpen.value = true
if (id) $selectThreadId.value = id
if (collectionId) {
$selectCollectionId.value = collectionId
} else {
$selectCollectionId.value = []
}
}
function $closeLibrarySelect () {
$isLibrarySelectOpen.value = false
}
function $setSelectCollectionId (ids) {
$selectCollectionId.value = ids
function $openLibraryCreate () {
$isLibraryCreateOpen.value = true
}
function $closeLibraryCreate () {
$isLibraryCreateOpen.value = false
}
function $openLibrarySelect (id, collectionId) {
$isLibrarySelectOpen.value = true
if (id) $selectThreadId.value = id
if (collectionId) {
$selectCollectionId.value = collectionId
} else {
$selectCollectionId.value = []
}
}
function $closeLibrarySelect () {
$isLibrarySelectOpen.value = false
}
function $setSelectCollectionId (ids) {
$selectCollectionId.value = ids
}
async function $getCollection () {
const { data } = await useRequest('/v1/collection/list')
$collection.value = data && data.value && data.value.data || []
}
return {
$selectThreadId,
$isLibraryCreateOpen,
$isLibrarySelectOpen,
$openLibraryCreate,
$closeLibraryCreate,
$openLibrarySelect,
$closeLibrarySelect,
$selectCollectionId,
$setSelectCollectionId,
$collection,
$getCollection
}
async function $getCollection () {
const { data } = await useRequest('/v1/collection/list')
$collection.value = data && data.value && data.value.data || []
}
return {
$selectThreadId,
$isLibraryCreateOpen,
$isLibrarySelectOpen,
$openLibraryCreate,
$closeLibraryCreate,
$openLibrarySelect,
$closeLibrarySelect,
$selectCollectionId,
$setSelectCollectionId,
$collection,
$getCollection
}
})
import { defineStore } from 'pinia'
export const useReposStore = defineStore('repos', () => {
const $repos = ref([])
function $setRepo (data) {
$repos.value = data || []
}
return { $repos, $setRepo }
const $repos = ref([])
function $setRepo (data) {
$repos.value = data || []
}
return { $repos, $setRepo }
})
......@@ -6,39 +6,39 @@ const searchHistory = useStorage('search-history', '')
const maxLength = 50
export const useSearchStore = defineStore('search', () => {
const $searchHistory = computed(() => searchHistory.value ? JSON.parse(searchHistory.value) : [])
function $setSearchHistory (item) {
const history = searchHistory.value ? JSON.parse(searchHistory.value) : []
const findIndex = history.findIndex(i => i.c_id === item.c_id)
const $searchHistory = computed(() => searchHistory.value ? JSON.parse(searchHistory.value) : [])
function $setSearchHistory (item) {
const history = searchHistory.value ? JSON.parse(searchHistory.value) : []
const findIndex = history.findIndex(i => i.c_id === item.c_id)
if (history.length >= maxLength) history.splice(0, 1)
if (history.length >= maxLength) history.splice(0, 1)
if (findIndex >= 0) history.splice(findIndex, 1)
history.push(item)
searchHistory.value = JSON.stringify(history)
}
function $updateSearchHistory (item) {
const history = searchHistory.value ? JSON.parse(searchHistory.value) : []
const historyItem = history.find(i => i.c_id === item.c_id)
if (historyItem) {
Object.assign(historyItem, item)
}
searchHistory.value = JSON.stringify(history)
}
function $clearSearchHistory () {
searchHistory.value = ''
if (findIndex >= 0) history.splice(findIndex, 1)
history.push(item)
searchHistory.value = JSON.stringify(history)
}
function $updateSearchHistory (item) {
const history = searchHistory.value ? JSON.parse(searchHistory.value) : []
const historyItem = history.find(i => i.c_id === item.c_id)
if (historyItem) {
Object.assign(historyItem, item)
}
searchHistory.value = JSON.stringify(history)
}
function $clearSearchHistory () {
searchHistory.value = ''
}
const $firstRecordTitle = ref('')
function $setFirstRecordTitle (article) {
$firstRecordTitle.value = article
}
return {
$searchHistory,
$setSearchHistory,
$updateSearchHistory,
$clearSearchHistory,
$firstRecordTitle,
$setFirstRecordTitle
}
const $firstRecordTitle = ref('')
function $setFirstRecordTitle (article) {
$firstRecordTitle.value = article
}
return {
$searchHistory,
$setSearchHistory,
$updateSearchHistory,
$clearSearchHistory,
$firstRecordTitle,
$setFirstRecordTitle
}
})
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const token = useCookie('token')
const userInfo = useCookie('user-info')
const token = useCookie('token')
const userInfo = useCookie('user-info')
const $info = computed(() => userInfo.value ? userInfo.value : {})
const $isSignIn = computed(() => !!token.value)
const $isOpenSign = ref(false)
const $info = computed(() => userInfo.value ? userInfo.value : {})
const $isSignIn = computed(() => !!token.value)
const $isOpenSign = ref(false)
function $signOut () {
token.value = null
userInfo.value = null
}
function $signOut () {
token.value = null
userInfo.value = null
}
async function $updateUserInfo () {
if (!token.value) return
const { data } = await useRequest('/v1/user/user_info')
userInfo.value = data.value.data
}
async function $updateUserInfo () {
if (!token.value) return
const { data } = await useRequest('/v1/user/user_info')
userInfo.value = data.value.data
}
function $openSign () {
$isOpenSign.value = true
}
function $closeSign () {
$isOpenSign.value = false
}
function $openSign () {
$isOpenSign.value = true
}
function $closeSign () {
$isOpenSign.value = false
}
return { $info, $isSignIn, $isOpenSign, $signOut, $updateUserInfo, $openSign, $closeSign }
return { $info, $isSignIn, $isOpenSign, $signOut, $updateUserInfo, $openSign, $closeSign }
})
import type { Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
export default <Partial<Config>>{
theme: {
......
......@@ -2,31 +2,31 @@ import { fetchEventSource } from '@microsoft/fetch-event-source'
const BASE_URL = 'https://gpu-pod656e861afe3d944d6b3ce77e-7862.node.inscode.run'
// class PermissionError extends Error { }
export default (url, { onmessage, onerror, controller, params, onopen }) => {
const token = useCookie('token')
fetchEventSource(`${BASE_URL}${url}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': '*/*',
'Authorization': token.value
},
body: JSON.stringify(params),
onopen (response) {
const status = response.status
useRequestError(status)
onopen(status)
// todo
// if ([401, 403].includes(status)) {
// throw new PermissionError();
// }
},
onmessage (info) {
onmessage(info)
},
onerror (error) {
onerror(error)
},
openWhenHidden: true,
signal: controller && controller.signal
})
const token = useCookie('token')
fetchEventSource(`${BASE_URL}${url}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': '*/*',
'Authorization': token.value
},
body: JSON.stringify(params),
onopen (response) {
const status = response.status
useRequestError(status)
onopen(status)
// todo
// if ([401, 403].includes(status)) {
// throw new PermissionError();
// }
},
onmessage (info) {
onmessage(info)
},
onerror (error) {
onerror(error)
},
openWhenHidden: true,
signal: controller && controller.signal
})
}
\ No newline at end of file
export default () => {
return [
{ url: 'https://github.com/allwefantasy/byzer-llm.git', label: 'byzer-llm', branch: 'master' },
{ url: 'https://github.com/keycloak/keycloak.git', label: 'keycloak', branch: 'master' }
]
return [
{ url: 'https://github.com/allwefantasy/byzer-llm.git', label: 'byzer-llm', branch: 'master' },
{ url: 'https://github.com/keycloak/keycloak.git', label: 'keycloak', branch: 'master' }
]
}
\ No newline at end of file
export function handleFormatReportsDatasets (sources, columns) {
const labels = []
const datas = []
const dataMap = {}
sources.forEach(item => {
labels.push(item.date)
for (let i in item) {
if (i !== 'date') {
if (!dataMap[i]) {
dataMap[i] = { label: columns[i], data: [] }
}
dataMap[i].data.push(Number(item[i]))
}
const labels = []
const datas = []
const dataMap = {}
sources.forEach(item => {
labels.push(item.date)
for (let i in item) {
if (i !== 'date') {
if (!dataMap[i]) {
dataMap[i] = { label: columns[i], data: [] }
}
})
Object.keys(dataMap).forEach(item => {
datas.push(dataMap[item])
})
return { labels, datas }
dataMap[i].data.push(Number(item[i]))
}
}
})
Object.keys(dataMap).forEach(item => {
datas.push(dataMap[item])
})
return { labels, datas }
}
export function handleFormatReports (content) {
let chartStart = '::ProseChart'
let tableStart = '::ProseGridTable'
let result = ''
const reg = /\'/g
const replaceStr = '&#39;'
// ::ProseChart{type='line' title='langchain-ai/langchain [2023-06-05,2024-06-04] Star Data' :labels='["2023-06-05","2023-06-12"]' :data='[{"label": "star数量", "data": [43071,44946]}]' }
content.forEach(item => {
let { title, data, format, columns } = item
title = title.replace(reg, replaceStr)
if (format === 'timeline') {
const { labels, datas } = handleFormatReportsDatasets(data, columns)
const datasStr = JSON.stringify(datas)
const labelsStr = JSON.stringify(labels).replace(reg, replaceStr)
result += `${chartStart}{title='${title}' :labels='${labelsStr}' :data='${datasStr}'}`
} else if ( format === 'table' ) {
const dataStr = JSON.stringify(data).replace(reg, replaceStr)
result += `${tableStart}{:data='${dataStr}'}`
}
})
return result
let chartStart = '::ProseChart'
let tableStart = '::ProseGridTable'
let result = ''
const reg = /'/g
const replaceStr = '&#39;'
// ::ProseChart{type='line' title='langchain-ai/langchain [2023-06-05,2024-06-04] Star Data' :labels='["2023-06-05","2023-06-12"]' :data='[{"label": "star数量", "data": [43071,44946]}]' }
content.forEach(item => {
let { title, data, format, columns } = item
title = title.replace(reg, replaceStr)
if (format === 'timeline') {
const { labels, datas } = handleFormatReportsDatasets(data, columns)
const datasStr = JSON.stringify(datas)
const labelsStr = JSON.stringify(labels).replace(reg, replaceStr)
result += `${chartStart}{title='${title}' :labels='${labelsStr}' :data='${datasStr}'}`
} else if ( format === 'table' ) {
const dataStr = JSON.stringify(data).replace(reg, replaceStr)
result += `${tableStart}{:data='${dataStr}'}`
}
})
return result
}
export function handleFormatWebSource (source) {
if ( Array.isArray(source)) return source
let sources = []
try {
sources = JSON.parse(source)
} catch (error) {}
return sources
if ( Array.isArray(source)) return source
let sources = []
try {
sources = JSON.parse(source)
} catch (error) {}
return sources
}
export function gitToLabel(url) {
// 使用正则表达式匹配GitHub URL的项目部分
const regex = /https?:\/\/github.com\/[^/]+\/(.+?)\.git/;
const match = url.match(regex);
// 使用正则表达式匹配GitHub URL的项目部分
const regex = /https?:\/\/github.com\/[^/]+\/(.+?)\.git/;
const match = url.match(regex);
// 如果匹配不到,返回空字符串
if (!match) {
return "";
}
// 如果匹配不到,返回空字符串
if (!match) {
return "";
}
// 返回匹配到的项目名称
return match[1];
// 返回匹配到的项目名称
return match[1];
}
export function handleFormatRepoStars (repoObj, gits) {
const card = {
type: 'search_relate_repo',
title: 'Star 数量',
ready: true,
data: []
}
const gitsMap = {}
gits.forEach(item => {
gitsMap[item.url] = item
const card = {
type: 'search_relate_repo',
title: 'Star 数量',
ready: true,
data: []
}
const gitsMap = {}
gits.forEach(item => {
gitsMap[item.url] = item
})
Object.keys(repoObj).forEach(child => {
const data = repoObj[child]
const cardDataItem = { data: [{ data: [], label: 'Star 数量' }], labels: [], info: gitsMap[child] }
data.forEach(dataItem => {
const { date, stargazers } = dataItem
cardDataItem.labels.push(date)
cardDataItem.data[0].data.push(stargazers)
})
Object.keys(repoObj).forEach(child => {
const data = repoObj[child]
const cardDataItem = { data: [{ data: [], label: 'Star 数量' }], labels: [], info: gitsMap[child] }
data.forEach(dataItem => {
const { date, stargazers } = dataItem
cardDataItem.labels.push(date)
cardDataItem.data[0].data.push(stargazers)
})
if (cardDataItem.data[0].data.length > 0) {
card.data.push(cardDataItem)
}
})
return card
if (cardDataItem.data[0].data.length > 0) {
card.data.push(cardDataItem)
}
})
return card
}
export function baseGitUrl(gitPath) {
const endWidthGit = gitPath.endsWith('.git')
return endWidthGit ? gitPath.slice(0, gitPath.length - 4) : gitPath
const endWidthGit = gitPath.endsWith('.git')
return endWidthGit ? gitPath.slice(0, gitPath.length - 4) : gitPath
}
export function handleFormatSource (source, params) {
const {gitPath, branch,repo} = params
if (Array.isArray(source)) return source
const gitUrl = baseGitUrl(gitPath)
return source.split('\n').map((item) => {
const isLink = item.indexOf('http') === 0
return {
title: repo,
url: isLink ? item : `${gitUrl}/blob/${branch}/${item}`,
label: item,
// todo 后期处理
description: ''
}
})
const {gitPath, branch,repo} = params
if (Array.isArray(source)) return source
const gitUrl = baseGitUrl(gitPath)
return source.split('\n').map((item) => {
const isLink = item.indexOf('http') === 0
return {
title: repo,
url: isLink ? item : `${gitUrl}/blob/${branch}/${item}`,
label: item,
// todo 后期处理
description: ''
}
})
}
export function handleFormatInfoData (output) {
const info = {
gits: [],
urls: []
}
if (!output) {
return info
}
if (typeof output === 'string') {
try {
info.gits = JSON.parse(output)
} catch(error) {}
} else {
info.gits = output
}
info.urls = info.gits.map(item => item.url)
return info
const info = {
gits: [],
urls: []
}
if (!output) {
return info
}
if (typeof output === 'string') {
try {
info.gits = JSON.parse(output)
} catch(error) {}
} else {
info.gits = output
}
info.urls = info.gits.map(item => item.url)
return info
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册