mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-13 01:30:57 +08:00
refactor: 重构AIChat组织
This commit is contained in:
+12
-12
@@ -1,20 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ConversationList from './ConversationList.vue'
|
||||
import DataSourcePanel from './DataSourcePanel.vue'
|
||||
import ChatMessage from './ChatMessage.vue'
|
||||
import AIChatInput from './AIChatInput.vue'
|
||||
import AIThinkingIndicator from './AIThinkingIndicator.vue'
|
||||
import ChatStatusBar from './ChatStatusBar.vue'
|
||||
import ConversationList from './chat/ConversationList.vue'
|
||||
import DataSourcePanel from './chat/DataSourcePanel.vue'
|
||||
import ChatMessage from './chat/ChatMessage.vue'
|
||||
import AIChatInput from './input/AIChatInput.vue'
|
||||
import AIThinkingIndicator from './chat/AIThinkingIndicator.vue'
|
||||
import ChatStatusBar from './chat/ChatStatusBar.vue'
|
||||
import { useAIChat } from '@/composables/useAIChat'
|
||||
import CaptureButton from '@/components/common/CaptureButton.vue'
|
||||
import AssistantSelector from './AssistantSelector.vue'
|
||||
import AssistantConfigModal from './AssistantConfigModal.vue'
|
||||
import AssistantMarketModal from './AssistantMarketModal.vue'
|
||||
import SkillMarketModal from './SkillMarketModal.vue'
|
||||
import SkillConfigModal from './SkillConfigModal.vue'
|
||||
import PresetQuestions from './PresetQuestions.vue'
|
||||
import AssistantSelector from './assistant/AssistantSelector.vue'
|
||||
import AssistantConfigModal from './assistant/AssistantConfigModal.vue'
|
||||
import AssistantMarketModal from './assistant/AssistantMarketModal.vue'
|
||||
import SkillMarketModal from './skill/SkillMarketModal.vue'
|
||||
import SkillConfigModal from './skill/SkillConfigModal.vue'
|
||||
import PresetQuestions from './input/PresetQuestions.vue'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useAssistantStore } from '@/stores/assistant'
|
||||
@@ -0,0 +1,2 @@
|
||||
// AIChat 组件统一导出,外部优先通过目录入口引用,降低后续目录调整的影响。
|
||||
export { default as ChatExplorer } from './ChatExplorer.vue'
|
||||
@@ -1,67 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
status?: 'ready' | 'submitted' | 'streaming' | 'error'
|
||||
}>()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
send: [content: string]
|
||||
stop: []
|
||||
}>()
|
||||
|
||||
// 输入内容
|
||||
const inputValue = ref('')
|
||||
|
||||
// 计算 status
|
||||
const chatStatus = computed(() => {
|
||||
if (props.disabled) {
|
||||
return props.status || 'submitted'
|
||||
}
|
||||
return 'ready'
|
||||
})
|
||||
|
||||
// 发送消息
|
||||
function handleSubmit() {
|
||||
if (!inputValue.value.trim() || props.disabled) return
|
||||
|
||||
emit('send', inputValue.value.trim())
|
||||
inputValue.value = ''
|
||||
}
|
||||
|
||||
// 停止生成
|
||||
function handleStop() {
|
||||
emit('stop')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="shrink-0 border-t border-gray-200 pt-4 pb-2 dark:border-gray-800">
|
||||
<div class="w-full">
|
||||
<UChatPrompt
|
||||
v-model="inputValue"
|
||||
:placeholder="placeholder || t('ai.chat.input.placeholder')"
|
||||
:disabled="disabled"
|
||||
variant="subtle"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<UButton
|
||||
v-if="chatStatus === 'streaming'"
|
||||
icon="i-heroicons-stop-20-solid"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
class="rounded-full"
|
||||
@click="handleStop"
|
||||
/>
|
||||
<UChatPromptSubmit v-else :status="chatStatus" class="rounded-full" color="primary" @stop="handleStop" />
|
||||
</UChatPrompt>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,55 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SkillSummary } from '@/stores/skill'
|
||||
|
||||
defineProps<{
|
||||
skill: SkillSummary
|
||||
selected?: boolean
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [id: string]
|
||||
}>()
|
||||
|
||||
function getChatScopeIcon(scope: string): string {
|
||||
if (scope === 'group') return 'i-heroicons-user-group'
|
||||
if (scope === 'private') return 'i-heroicons-user'
|
||||
return 'i-heroicons-globe-alt'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="group relative cursor-pointer rounded-lg border px-3 py-2 transition-all duration-150"
|
||||
:class="[
|
||||
disabled
|
||||
? 'cursor-not-allowed border-gray-200 bg-gray-50 opacity-50 dark:border-gray-700 dark:bg-gray-800/50'
|
||||
: selected
|
||||
? 'border-primary-500 bg-primary-50 shadow-sm dark:border-primary-400 dark:bg-primary-950/30'
|
||||
: 'border-gray-200 bg-white hover:border-primary-300 hover:shadow-sm dark:border-gray-700 dark:bg-gray-800 dark:hover:border-primary-600',
|
||||
]"
|
||||
@click="!disabled && emit('select', skill.id)"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<UIcon :name="getChatScopeIcon(skill.chatScope)" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-gray-400" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<h4 class="truncate text-xs font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ skill.name }}
|
||||
</h4>
|
||||
<p class="mt-0.5 line-clamp-1 text-[11px] leading-relaxed text-gray-500 dark:text-gray-400">
|
||||
{{ skill.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tags -->
|
||||
<div v-if="skill.tags.length" class="mt-1.5 flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="tag in skill.tags.slice(0, 3)"
|
||||
:key="tag"
|
||||
class="rounded-full bg-gray-100 px-1.5 py-0.5 text-[10px] text-gray-500 dark:bg-gray-700 dark:text-gray-400"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,101 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSkillStore } from '@/stores/skill'
|
||||
import SkillCard from './SkillCard.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
chatType: 'group' | 'private'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
manage: []
|
||||
}>()
|
||||
|
||||
const skillStore = useSkillStore()
|
||||
const { compatibleSkills, activeSkillId, isLoaded } = storeToRefs(skillStore)
|
||||
|
||||
watch(
|
||||
() => props.chatType,
|
||||
(chatType) => {
|
||||
skillStore.setFilterContext(chatType)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
if (!isLoaded.value) {
|
||||
await skillStore.loadSkills()
|
||||
}
|
||||
})
|
||||
|
||||
function handleSelectSkill(id: string) {
|
||||
if (activeSkillId.value === id) {
|
||||
skillStore.activateSkill(null)
|
||||
} else {
|
||||
skillStore.activateSkill(id)
|
||||
}
|
||||
}
|
||||
|
||||
function handleFreeChat() {
|
||||
skillStore.activateSkill(null)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-3 rounded-xl border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800">
|
||||
<!-- 标题 + 管理入口 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-600 dark:text-gray-300">{{ t('ai.skill.selector.title') }}</span>
|
||||
<button
|
||||
class="text-[11px] text-gray-400 transition-colors hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400"
|
||||
@click="emit('manage')"
|
||||
>
|
||||
{{ t('ai.skill.selector.manage') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 自由对话选项 -->
|
||||
<div
|
||||
class="cursor-pointer rounded-lg border px-3 py-2 transition-all duration-150"
|
||||
:class="[
|
||||
!activeSkillId
|
||||
? 'border-primary-500 bg-primary-50 dark:border-primary-400 dark:bg-primary-950/30'
|
||||
: 'border-gray-200 hover:border-primary-300 dark:border-gray-700 dark:hover:border-primary-600',
|
||||
]"
|
||||
@click="handleFreeChat"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-chat-bubble-left-right" class="h-3.5 w-3.5 text-gray-500" />
|
||||
<div>
|
||||
<span class="text-xs font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ t('ai.skill.selector.freeChat') }}
|
||||
</span>
|
||||
<p class="text-[11px] text-gray-400 dark:text-gray-500">
|
||||
{{ t('ai.skill.selector.freeChatDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 技能列表 -->
|
||||
<div v-if="compatibleSkills.length > 0" class="max-h-48 space-y-1.5 overflow-y-auto">
|
||||
<SkillCard
|
||||
v-for="skill in compatibleSkills"
|
||||
:key="skill.id"
|
||||
:skill="skill"
|
||||
:selected="activeSkillId === skill.id"
|
||||
@select="handleSelectSkill"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 无可用技能 -->
|
||||
<div v-else-if="isLoaded" class="py-3 text-center">
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">{{ t('ai.skill.selector.noSkills') }}</p>
|
||||
<p class="mt-1 text-[11px] text-gray-400 dark:text-gray-500">{{ t('ai.skill.selector.noSkillsHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { SubTabs } from '@/components/UI'
|
||||
import ChatExplorer from './AIChat/ChatExplorer.vue'
|
||||
import { ChatExplorer } from '../AIChat'
|
||||
import SQLLabTab from './SQLLabTab.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
Reference in New Issue
Block a user