初始化

This commit is contained in:
2026-02-22 18:56:10 +08:00
commit 26677972a6
3112 changed files with 255972 additions and 0 deletions

View File

@@ -0,0 +1,344 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus';
import type { BotInfo } from '@easyflow/types';
import type { ActionButton } from '#/components/page/CardList.vue';
import { computed, markRaw, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@easyflow/locales';
import { Delete, Edit, Plus, Setting } from '@element-plus/icons-vue';
import {
ElButton,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
} from 'element-plus';
import { tryit } from 'radash';
import { removeBotFromId } from '#/api';
import { api } from '#/api/request';
import defaultAvatar from '#/assets/ai/bot/defaultBotAvatar.png';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import CardList from '#/components/page/CardList.vue';
import PageData from '#/components/page/PageData.vue';
import PageSide from '#/components/page/PageSide.vue';
import { useDictStore } from '#/store';
import Modal from './modal.vue';
interface FieldDefinition {
// 字段名称
prop: string;
// 字段标签
label: string;
// 字段类型input, number, select, radio, checkbox, switch, date, datetime
type?: 'input' | 'number';
// 是否必填
required?: boolean;
// 占位符
placeholder?: string;
}
onMounted(() => {
initDict();
getSideList();
});
const router = useRouter();
const pageDataRef = ref();
const modalRef = ref<InstanceType<typeof Modal>>();
const dictStore = useDictStore();
// 操作按钮配置
const headerButtons = [
{
key: 'create',
text: `${$t('button.create')}${$t('bot.chatAssistant')}`,
icon: markRaw(Plus),
type: 'primary',
data: { action: 'create' },
permission: '/api/v1/documentCollection/save',
},
];
const actions: ActionButton[] = [
{
icon: Edit,
text: $t('button.edit'),
className: '',
permission: '',
onClick(row: BotInfo) {
modalRef.value?.open('edit', row);
},
},
{
icon: Setting,
text: $t('button.setting'),
className: '',
permission: '',
onClick(row: BotInfo) {
router.push({ path: `/ai/bots/setting/${row.id}` });
},
},
{
icon: Delete,
text: $t('button.delete'),
className: 'item-danger',
permission: '/api/v1/bot/remove',
onClick(row: BotInfo) {
removeBot(row);
},
},
];
const removeBot = async (bot: BotInfo) => {
const [action] = await tryit(ElMessageBox.confirm)(
$t('message.deleteAlert'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
},
);
if (!action) {
const [err, res] = await tryit(removeBotFromId)(bot.id);
if (!err && res.errorCode === 0) {
ElMessage.success($t('message.deleteOkMessage'));
pageDataRef.value.setQuery({});
}
}
};
const handleSearch = (params: string) => {
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
};
const handleButtonClick = () => {
modalRef.value?.open('create');
};
const fieldDefinitions = ref<FieldDefinition[]>([
{
prop: 'categoryName',
label: $t('aiWorkflowCategory.categoryName'),
type: 'input',
required: true,
placeholder: $t('aiWorkflowCategory.categoryName'),
},
{
prop: 'sortNo',
label: $t('aiWorkflowCategory.sortNo'),
type: 'number',
required: false,
placeholder: $t('aiWorkflowCategory.sortNo'),
},
]);
const formData = ref<any>({});
const dialogVisible = ref(false);
const formRef = ref<FormInstance>();
const saveLoading = ref(false);
const sideList = ref<any[]>([]);
const controlBtns = [
{
icon: Edit,
label: $t('button.edit'),
onClick(row: any) {
showControlDialog(row);
},
},
{
type: 'danger',
icon: Delete,
label: $t('button.delete'),
onClick(row: any) {
removeCategory(row);
},
},
];
const footerButton = {
icon: Plus,
label: $t('button.add'),
onClick() {
showControlDialog({});
},
};
const formRules = computed(() => {
const rules: Record<string, any[]> = {};
fieldDefinitions.value.forEach((field) => {
const fieldRules = [];
if (field.required) {
fieldRules.push({
required: true,
message: `${$t('message.required')}`,
trigger: 'blur',
});
}
if (fieldRules.length > 0) {
rules[field.prop] = fieldRules;
}
});
return rules;
});
function initDict() {
dictStore.fetchDictionary('dataStatus');
}
function changeCategory(category: any) {
pageDataRef.value.setQuery({ categoryId: category.id });
}
function showControlDialog(item: any) {
formRef.value?.resetFields();
formData.value = { ...item };
dialogVisible.value = true;
}
function removeCategory(row: any) {
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
api
.post('/api/v1/botCategory/remove', { id: row.id })
.then((res) => {
instance.confirmButtonLoading = false;
if (res.errorCode === 0) {
ElMessage.success(res.message);
done();
getSideList();
}
})
.catch(() => {
instance.confirmButtonLoading = false;
});
} else {
done();
}
},
}).catch(() => {});
}
function handleSubmit() {
formRef.value?.validate((valid) => {
if (valid) {
saveLoading.value = true;
const url = formData.value.id
? '/api/v1/botCategory/update'
: '/api/v1/botCategory/save';
api.post(url, formData.value).then((res) => {
saveLoading.value = false;
if (res.errorCode === 0) {
ElMessage.success(res.message);
dialogVisible.value = false;
getSideList();
}
});
}
});
}
const getSideList = async () => {
const [, res] = await tryit(api.get)('/api/v1/botCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
});
if (res && res.errorCode === 0) {
sideList.value = [
{
id: '',
categoryName: $t('common.allCategories'),
},
...res.data,
];
}
};
</script>
<template>
<div class="flex h-full flex-col gap-6 p-6">
<HeaderSearch
:buttons="headerButtons"
@search="handleSearch"
@button-click="handleButtonClick"
/>
<div class="flex flex-1 gap-6">
<PageSide
label-key="categoryName"
value-key="id"
:menus="sideList"
:control-btns="controlBtns"
:footer-button="footerButton"
@change="changeCategory"
/>
<div class="h-[calc(100vh-192px)] flex-1 overflow-auto">
<PageData
ref="pageDataRef"
page-url="/api/v1/bot/page"
:page-sizes="[12, 18, 24]"
:page-size="12"
>
<template #default="{ pageList }">
<CardList
:default-icon="defaultAvatar"
:data="pageList"
:actions="actions"
/>
</template>
</PageData>
</div>
</div>
<!-- 创建&编辑Bot弹窗 -->
<Modal ref="modalRef" @success="pageDataRef.setQuery({})" />
<ElDialog
v-model="dialogVisible"
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
:close-on-click-modal="false"
>
<ElForm
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
>
<!-- 动态生成表单项 -->
<ElFormItem
v-for="field in fieldDefinitions"
:key="field.prop"
:label="field.label"
:prop="field.prop"
>
<ElInput
v-if="!field.type || field.type === 'input'"
v-model="formData[field.prop]"
:placeholder="field.placeholder"
/>
<ElInputNumber
v-else-if="field.type === 'number'"
v-model="formData[field.prop]"
:placeholder="field.placeholder"
style="width: 100%"
/>
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="dialogVisible = false">
{{ $t('button.cancel') }}
</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="saveLoading">
{{ $t('button.confirm') }}
</ElButton>
</template>
</ElDialog>
</div>
</template>

View File

@@ -0,0 +1,120 @@
<script setup lang="ts">
import type { BotInfo } from '@easyflow/types';
import type { SaveBotParams, UpdateBotParams } from '#/api/ai/bot';
import { ref } from 'vue';
import { $t } from '@easyflow/locales';
import {
ElButton,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElMessage,
} from 'element-plus';
import { tryit } from 'radash';
import { saveBot, updateBotApi } from '#/api/ai/bot';
import DictSelect from '#/components/dict/DictSelect.vue';
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
const emit = defineEmits(['success']);
const initialFormData = {
icon: '',
title: '',
alias: '',
description: '',
categoryId: '',
status: 1,
};
const dialogVisible = ref(false);
const dialogType = ref<'create' | 'edit'>('create');
const formRef = ref<InstanceType<typeof ElForm>>();
const formData = ref<SaveBotParams | UpdateBotParams>(initialFormData);
const rules = {
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
alias: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
};
const loading = ref(false);
const handleSubmit = async () => {
loading.value = true;
const [err, res] = await tryit(
dialogType.value === 'create' ? saveBot : updateBotApi,
)(formData.value as any);
if (!err && res.errorCode === 0) {
emit('success');
ElMessage.success($t('message.saveOkMessage'));
dialogVisible.value = false;
}
loading.value = false;
};
defineExpose({
open(type: typeof dialogType.value, bot?: BotInfo) {
formData.value = bot
? {
id: bot.id,
icon: bot.icon,
title: bot.title,
alias: bot.alias,
description: bot.description,
categoryId: bot.categoryId,
status: bot.status,
}
: initialFormData;
dialogType.value = type;
dialogVisible.value = true;
},
});
</script>
<template>
<ElDialog
v-model="dialogVisible"
:title="`${$t(`button.${dialogType}`)}${$t('bot.chatAssistant')}`"
draggable
align-center
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="150px">
<ElFormItem :label="$t('common.avatar')" prop="icon">
<UploadAvatar v-model="formData.icon" />
</ElFormItem>
<ElFormItem prop="categoryId" :label="$t('aiWorkflow.categoryId')">
<DictSelect v-model="formData.categoryId" dict-code="aiBotCategory" />
</ElFormItem>
<ElFormItem :label="$t('aiWorkflow.title')" prop="title">
<ElInput v-model="formData.title" />
</ElFormItem>
<ElFormItem :label="$t('plugin.alias')" prop="alias">
<ElInput v-model="formData.alias" />
</ElFormItem>
<ElFormItem :label="$t('plugin.description')" prop="description">
<ElInput type="textarea" :rows="3" v-model="formData.description" />
</ElFormItem>
<ElFormItem prop="status" :label="$t('aiWorkflow.status')">
<DictSelect v-model="formData.status" dict-code="showOrNot" />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="dialogVisible = false">
{{ $t('button.cancel') }}
</ElButton>
<ElButton
type="primary"
:loading="loading"
:disabled="loading"
@click="handleSubmit"
>
{{ $t('button.save') }}
</ElButton>
</template>
</ElDialog>
</template>

View File

@@ -0,0 +1,159 @@
<script setup lang="ts">
import type { BotInfo, Session } from '@easyflow/types';
import { onMounted, ref, watchEffect } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { IconifyIcon } from '@easyflow/icons';
import { preferences } from '@easyflow/preferences';
import { uuid } from '@easyflow/utils';
import {
ElAside,
ElButton,
ElContainer,
ElEmpty,
ElMain,
ElSpace,
} from 'element-plus';
import { tryit } from 'radash';
import { getBotDetails, getSessionList } from '#/api';
import BotAvatar from '#/components/botAvatar/botAvatar.vue';
import Chat from '#/components/chat/chat.vue';
import { $t } from '#/locales';
const route = useRoute();
const router = useRouter();
const bot = ref<BotInfo>();
const sessionList = ref<Session[]>([]);
const sessionId = ref<string>(route.params.sessionId as string);
watchEffect(() => {
sessionId.value = route.params.sessionId as string;
});
// 内置菜单点击方法
// function handleMenuCommand(
// command: ConversationMenuCommand,
// item: ConversationItem,
// ) {
// console.warn('内置菜单点击事件:', command, item);
// // 直接修改 item 是否生效
// if (command === 'delete') {
// const index = menuTestItems.value.findIndex(
// (itemSlef) => itemSlef.key === item.key,
// );
// if (index !== -1) {
// menuTestItems.value.splice(index, 1);
// console.warn('删除成功');
// ElMessage.success('删除成功');
// }
// }
// if (command === 'rename') {
// item.label = '已修改';
// console.warn('重命名成功');
// ElMessage.success('重命名成功');
// }
// }
onMounted(() => {
if (route.params.botId) {
fetchBotDetail(route.params.botId as string);
fetchSessionList(route.params.botId as string);
}
});
const fetchBotDetail = async (id: string) => {
const [, res] = await tryit(getBotDetails)(id);
if (res?.errorCode === 0) {
bot.value = res.data;
}
};
const fetchSessionList = async (id: string) => {
const [, res] = await tryit(getSessionList)({
botId: id,
tempUserId: uuid().toString() + id,
});
if (res?.errorCode === 0) {
sessionList.value = res.data.cons;
}
};
const updateActive = (_sessionId?: number | string) => {
sessionId.value = `${_sessionId ?? ''}`;
router.push(
`/ai/bots/run/${bot.value?.id}${_sessionId ? `/${_sessionId}` : ''}`,
);
};
</script>
<template>
<ElContainer class="h-screen" v-if="bot">
<ElAside width="240px" class="flex flex-col items-center bg-[#f5f5f580]">
<ElSpace class="py-7">
<BotAvatar :src="bot.icon" :size="40" />
<span class="text-base font-medium text-black/85">{{ bot.title }}</span>
</ElSpace>
<ElButton
type="primary"
class="!h-10 w-full max-w-[208px]"
plain
@click="updateActive()"
>
<template #icon>
<IconifyIcon icon="mdi:chat-outline" />
</template>
{{ $t('button.newConversation') }}
</ElButton>
<span class="self-start p-6 pb-2 text-sm text-[#969799]">{{
$t('common.history')
}}</span>
<div class="w-full max-w-[208px] flex-1 overflow-hidden">
<ElConversations
v-show="sessionList.length > 0"
class="!w-full !shadow-none"
v-model:active="sessionId"
:items="sessionList"
:label-max-width="120"
:show-tooltip="true"
row-key="sessionId"
label-key="title"
tooltip-placement="right"
:tooltip-offset="35"
show-to-top-btn
show-built-in-menu
show-built-in-menu-type="hover"
@update:active="updateActive"
/>
<ElEmpty
:image="`/empty${preferences.theme.mode === 'dark' ? '-dark' : ''}.png`"
v-show="sessionList.length === 0"
/>
</div>
</ElAside>
<ElMain>
<Chat :session-id="sessionId" :bot="bot" />
</ElMain>
</ElContainer>
</template>
<style lang="css" scoped>
.conversations-container :deep(.conversations-list) {
width: 100% !important;
padding: 0 !important;
background: none !important;
}
.conversations-container :deep(.conversation-item) {
margin: 0;
}
.conversations-container :deep(.conversation-label) {
color: #1a1a1a;
}
</style>

View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
import { ref } from 'vue';
import { $t } from '@easyflow/locales';
import { ElButton, ElDialog, ElForm, ElFormItem, ElInput } from 'element-plus';
import { sseClient } from '#/api/request';
const emit = defineEmits(['success']);
const dialogVisible = ref(false);
const formRef = ref<InstanceType<typeof ElForm>>();
const formData = ref();
const rules = {
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
};
const loading = ref(false);
const handleSubmit = async () => {
loading.value = true;
const data = {
botId: formData.value.botId,
prompt: formData.value.prompt,
};
formData.value.prompt = '';
sseClient.post('/api/v1/bot/prompt/chore/chat', data, {
onMessage(message) {
const event = message.event;
// done
if (event === 'done') {
loading.value = false;
return;
}
if (!message.data) {
return;
}
const sseData = JSON.parse(message.data);
const delta = sseData.payload?.delta;
formData.value.prompt += delta;
},
});
};
const handleReplace = () => {
emit('success', formData.value.prompt);
dialogVisible.value = false;
};
defineExpose({
open(botId: string, systemPrompt: string) {
formData.value = {
botId,
prompt: systemPrompt,
};
dialogVisible.value = true;
handleSubmit();
},
});
</script>
<template>
<ElDialog
v-model="dialogVisible"
:title="$t('bot.aiOptimizedPrompts')"
draggable
align-center
width="550px"
>
<ElForm ref="formRef" :model="formData" :rules="rules">
<ElFormItem prop="prompt">
<ElInput type="textarea" :rows="20" v-model="formData.prompt" />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="dialogVisible = false">
{{ $t('button.cancel') }}
</ElButton>
<ElButton type="primary" @click="handleReplace" v-if="!loading">
{{ $t('button.replace') }}
</ElButton>
<ElButton
type="primary"
:loading="loading"
:disabled="loading"
@click="handleSubmit"
>
{{ loading ? $t('button.optimizing') : $t('button.regenerate') }}
</ElButton>
</template>
</ElDialog>
</template>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import type { BotInfo } from '@easyflow/types';
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { tryit } from 'radash';
import { getBotDetails } from '#/api';
import { hasPermission } from '#/api/common/hasPermission';
import Config from './config.vue';
import Preview from './preview.vue';
import Prompt from './prompt.vue';
const route = useRoute();
const hasSavePermission = computed(() =>
hasPermission(['/api/v1/bot/save', '/api/v1/bot/updateLlmId']),
);
const bot = ref<BotInfo>();
onMounted(() => {
if (route.params.id) {
fetchBotDetail(route.params.id as string);
}
});
const fetchBotDetail = async (id: string) => {
const [, res] = await tryit(getBotDetails)(id);
if (res?.errorCode === 0) {
bot.value = res.data;
}
};
</script>
<template>
<div class="settings-container">
<div class="row-container">
<div class="row-item">
<Prompt :bot="bot" :has-save-permission="hasSavePermission" />
</div>
<div class="row-item">
<Config :bot="bot" :has-save-permission="hasSavePermission" />
</div>
<div class="row-item">
<Preview :bot="bot" />
</div>
</div>
</div>
</template>
<style scoped>
.settings-container {
height: calc(100vh - 90px);
padding: 20px;
}
.row-container {
height: 100%;
display: flex;
gap: 20px;
}
.row-item {
height: 100%;
flex: 1;
}
</style>

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Brush } from '@element-plus/icons-vue';
import { ElButton, ElIcon } from 'element-plus';
import Chat from '#/components/chat/chat.vue';
const chatRef = ref();
const handleClear = () => {
chatRef.value.clear();
};
</script>
<template>
<div
class="bg-background dark:border-border flex h-full flex-col gap-3 rounded-lg p-3 dark:border"
>
<div class="flex justify-between">
<h1 class="text-base font-medium">
{{ $t('button.preview') }}
</h1>
<ElButton text @click="handleClear">
<ElIcon class="rotate-180"><Brush /></ElIcon>
</ElButton>
</div>
<div class="relative flex-1">
<Chat
ref="chatRef"
class="absolute inset-0"
:ishow-chat-conversations="false"
/>
</div>
</div>
</template>

View File

@@ -0,0 +1,102 @@
<script setup lang="ts">
import type { BotInfo } from '@easyflow/types';
import { ref, watch } from 'vue';
import { useDebounceFn } from '@vueuse/core';
import { ElIcon, ElInput } from 'element-plus';
import { updateLlmOptions } from '#/api';
import MagicStaffIcon from '#/components/icons/MagicStaffIcon.vue';
import { $t } from '#/locales';
import PromptChoreChatModal from '#/views/ai/bots/pages/setting/PromptChoreChatModal.vue';
const props = defineProps<{
bot?: BotInfo;
hasSavePermission?: boolean;
}>();
const systemPrompt = ref($t('bot.placeholder.prompt'));
const promptChoreChatModalRef = ref();
watch(
() => props.bot?.modelOptions.systemPrompt,
(newPrompt) => {
if (newPrompt) {
systemPrompt.value = newPrompt;
}
},
{ immediate: true },
);
const handleInput = useDebounceFn((value: string) => {
updateLlmOptions({
id: props.bot?.id || '',
llmOptions: {
systemPrompt: value,
},
});
}, 1000);
const handelReplacePrompt = (value: string) => {
systemPrompt.value = value;
handleInput(value);
};
</script>
<template>
<div
class="bg-background dark:border-border flex h-full flex-col gap-2 rounded-lg p-3 dark:border"
>
<div class="flex justify-between">
<h1 class="text-base font-medium">
{{ $t('bot.systemPrompt') }}
</h1>
<button
@click="promptChoreChatModalRef.open(props.bot?.id, systemPrompt)"
type="button"
class="flex items-center gap-0.5 rounded-lg bg-[#f7f7f7] px-3 py-1"
>
<ElIcon size="16"><MagicStaffIcon /></ElIcon>
<span
class="bg-[linear-gradient(106.75666073298856deg,#F17E47,#D85ABF,#717AFF)] bg-clip-text text-sm text-transparent"
>
{{ $t('bot.aiOptimization') }}
</span>
</button>
</div>
<ElInput
class="flex-1"
type="textarea"
resize="none"
v-model="systemPrompt"
:title="!hasSavePermission ? $t('bot.placeholder.permission') : ''"
:disabled="!hasSavePermission"
@input="handleInput"
/>
<!--系统提示词优化模态框-->
<PromptChoreChatModal
ref="promptChoreChatModalRef"
@success="handelReplacePrompt"
/>
</div>
</template>
<style lang="css" scoped>
.el-textarea :deep(.el-textarea__inner) {
--el-input-bg-color: #f7f7f7;
height: 100%;
padding: 12px;
font-size: 14px;
font-weight: 500;
line-height: 1.25;
border-radius: 8px;
box-shadow: none;
}
.dark .el-textarea :deep(.el-textarea__inner) {
--el-input-bg-color: hsl(var(--background-deep));
border: 1px solid hsl(var(--border));
}
</style>