paragraph

This commit is contained in:
wangdan-fit2cloud 2025-06-06 16:32:03 +08:00
parent 7d898d870c
commit 1390ddb36e
29 changed files with 1009 additions and 239 deletions

View File

@ -20,6 +20,7 @@
"axios": "^1.8.4",
"dingtalk-jsapi": "^3.1.0",
"element-plus": "^2.9.10",
"md-editor-v3": "^5.6.1",
"nprogress": "^0.2.0",
"pinia": "^3.0.1",
"use-element-plus-theme": "^0.0.5",

View File

@ -3,9 +3,4 @@
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.custom-slider {
.el-input-number.is-without-controls .el-input__wrapper {
padding: 0 !important;
}
}
</style>

View File

@ -16,6 +16,9 @@ import InfiniteScroll from './infinite-scroll/index.vue'
import ModelSelect from './model-select/index.vue'
import ReadWrite from './read-write/index.vue'
import AutoTooltip from './auto-tooltip/index.vue'
import MdEditor from './markdown/MdEditor.vue'
import MdPreview from './markdown/MdPreview.vue'
import MdEditorMagnify from './markdown/MdEditorMagnify.vue'
export default {
install(app: App) {
app.component('LogoFull', LogoFull)
@ -35,5 +38,8 @@ export default {
app.component('ModelSelect', ModelSelect)
app.component('ReadWrite', ReadWrite)
app.component('AutoTooltip', AutoTooltip)
app.component('MdPreview', MdPreview)
app.component('MdEditor', MdEditor)
app.component('MdEditorMagnify', MdEditorMagnify)
},
}

View File

@ -0,0 +1,119 @@
<template>
<div class="charts-container">
<div ref="chartsRef" :style="style" v-resize="changeChartSize"></div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, nextTick, watch, onBeforeUnmount, ref } from 'vue'
import * as echarts from 'echarts'
const tmp = ref()
const props = defineProps<{ option: string }>()
const chartsRef = ref()
const style = ref({
height: '220px',
width: '100%'
})
function initChart() {
if (chartsRef.value) {
let myChart = echarts?.getInstanceByDom(chartsRef.value)
if (myChart === null || myChart === undefined) {
myChart = echarts.init(chartsRef.value)
}
const option = JSON.parse(props.option)
if (option.actionType === 'EVAL') {
myChart.setOption(evalParseOption(option), true)
} else {
myChart.setOption(jsonParseOption(option), true)
}
}
}
function jsonParseOption(option: any) {
if (option.style) {
style.value = option.style
}
if (option.option) {
//
return option.option
}
return option
}
function evalParseOption(option_json: any) {
if (option_json.style) {
style.value = option_json.style
}
let option = {}
echarts
tmp.value = echarts
eval(option_json.option)
return option
}
function changeChartSize() {
echarts?.getInstanceByDom(chartsRef.value)?.resize()
}
watch(
() => props.option,
(val) => {
if (val) {
nextTick(() => {
initChart()
})
}
}
)
onMounted(() => {
nextTick(() => {
initChart()
})
})
onBeforeUnmount(() => {
echarts.getInstanceByDom(chartsRef.value)?.dispose()
})
</script>
<style lang="scss" scoped>
.charts-container {
overflow-x: auto;
}
.charts-container::-webkit-scrollbar-track-piece {
background-color: rgba(0, 0, 0, 0);
border-left: 1px solid rgba(0, 0, 0, 0);
}
.charts-container::-webkit-scrollbar {
width: 5px;
height: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.charts-container::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.5);
background-clip: padding-box;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
min-height: 28px;
}
.charts-container::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.5);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<div>
<DynamicsForm
:disabled="is_submit || disabled"
label-position="top"
require-asterisk-position="right"
ref="dynamicsFormRef"
:render_data="form_field_list"
label-suffix=":"
v-model="form_data"
:model="form_data"
></DynamicsForm>
<el-button
:type="is_submit ? 'info' : 'primary'"
:disabled="is_submit || disabled"
@click="submit"
>{{$t('common.submit')}}</el-button
>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import DynamicsForm from '@/components/dynamics-form/index.vue'
const props = withDefaults(
defineProps<{
form_setting: string
disabled?: boolean
sendMessage?: (question: string, type: 'old' | 'new', other_params_data?: any) => void
child_node?: any
chat_record_id?: string
runtime_node_id?: string
}>(),
{
disabled: false
}
)
const form_setting_data = computed(() => {
if (props.form_setting) {
return JSON.parse(props.form_setting)
} else {
return {}
}
})
const _submit = ref<boolean>(false)
/**
* 表单字段列表
*/
const form_field_list = computed(() => {
if (form_setting_data.value.form_field_list) {
return form_setting_data.value.form_field_list
}
return []
})
const is_submit = computed(() => {
if (_submit.value) {
return true
}
if (form_setting_data.value.is_submit) {
return form_setting_data.value.is_submit
} else {
return false
}
})
const _form_data = ref<any>({})
const form_data = computed({
get: () => {
if (form_setting_data.value.is_submit) {
return form_setting_data.value.form_data
} else {
return _form_data.value
}
},
set: (v) => {
_form_data.value = v
}
})
const dynamicsFormRef = ref<InstanceType<typeof DynamicsForm>>()
const submit = () => {
dynamicsFormRef.value?.validate().then(() => {
_submit.value = true
if (props.sendMessage) {
props.sendMessage('', 'old', {
child_node: props.child_node,
runtime_node_id: props.runtime_node_id,
chat_record_id: props.chat_record_id,
node_data: form_data.value
})
}
})
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,36 @@
<template>
<div ref="htmlRef" :innerHTML="source"></div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const htmlRef = ref<HTMLElement>()
const props = withDefaults(
defineProps<{
source?: string
script_exec?: boolean
}>(),
{
source: '',
script_exec: true
}
)
onMounted(() => {
if (htmlRef.value && props.script_exec) {
const range = document.createRange()
range.selectNode(htmlRef.value)
const scripts = htmlRef.value.getElementsByTagName('script')
if (scripts) {
var documentFragment = range.createContextualFragment(
[...scripts]
.map((item: HTMLElement) => {
htmlRef.value?.removeChild(item)
return item.outerHTML
})
.join('\n')
)
htmlRef.value.appendChild(documentFragment)
}
}
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,26 @@
<template>
<MdEditor :language="language" noIconfont noPrettier v-bind="$attrs">
<template #defFooters>
<slot name="defFooters"> </slot>
</template>
</MdEditor>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { MdEditor, config } from 'md-editor-v3'
import { getBrowserLang } from '@/locales/index'
import './assets/markdown-iconfont.js'
//
import ZH_TW from '@vavt/cm-extension/dist/locale/zh-TW'
defineOptions({ name: 'MdEditor' })
const language = computed(() => localStorage.getItem('MaxKB-locale') || getBrowserLang() || '')
config({
editorConfig: {
languageUserDefined: {
'zh-Hant': ZH_TW
}
}
})
</script>

View File

@ -0,0 +1,68 @@
<template>
<MdEditor
v-bind="$attrs"
v-model="data"
:preview="false"
:toolbars="[]"
class="magnify-md-editor"
:footers="footers"
>
<template #defFooters>
<el-button text type="info" @click="openDialog">
<AppIcon class="color-secondary" iconName="app-magnify" style="font-size: 16px"></AppIcon>
</el-button>
</template>
</MdEditor>
<!-- 回复内容弹出层 -->
<el-dialog v-model="dialogVisible" :title="title" append-to-body align-center>
<MdEditor v-model="cloneContent" :preview="false" :toolbars="[]" :footers="[]"></MdEditor>
<template #footer>
<div class="dialog-footer mt-24">
<el-button type="primary" @click="submitDialog"> {{ $t('common.confirm') }}</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
defineOptions({ name: 'MdEditorMagnify' })
const props = defineProps<{
title: String
modelValue: any
}>()
const emit = defineEmits(['update:modelValue', 'submitDialog'])
const data = computed({
set: (value) => {
emit('update:modelValue', value)
},
get: () => {
return props.modelValue
}
})
const dialogVisible = ref(false)
watch(dialogVisible, (bool) => {
if (!bool) {
emit('submitDialog', cloneContent.value)
}
})
const cloneContent = ref('')
const footers: any = [null, '=', 0]
function openDialog() {
cloneContent.value = props.modelValue
dialogVisible.value = true
}
function submitDialog() {
emit('submitDialog', cloneContent.value)
dialogVisible.value = false
}
</script>
<style lang="scss" scoped>
.magnify-md-editor {
:deep(.md-editor-footer) {
border: none !important;
}
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<MdPreview :language="language" noIconfont noPrettier :codeFoldable="false" v-bind="$attrs" />
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { MdPreview, config } from 'md-editor-v3'
import { getBrowserLang } from '@/locales/index'
import useStore from '@/stores'
//
import ZH_TW from '@vavt/cm-extension/dist/locale/zh-TW'
defineOptions({ name: 'MdPreview' })
const { user } = useStore()
const language = computed(() => user.getLanguage() || getBrowserLang() || '')
config({
editorConfig: {
languageUserDefined: {
'zh-Hant': ZH_TW
}
}
})
</script>

View File

@ -0,0 +1,256 @@
<template>
<div>
<!-- 推理过程组件 -->
<ReasoningRander :content="reasoning_content" v-if="reasoning_content?.trim()" />
<template v-for="(item, index) in md_view_list" :key="index">
<div
v-if="item.type === 'question'"
@click="sendMessage ? sendMessage(item.content, 'new') : (content: string) => {}"
class="problem-button mt-4 mb-4 flex"
:class="sendMessage ? 'cursor' : 'disabled'"
>
<el-icon class="mr-8" style="margin-top: 2px;">
<EditPen />
</el-icon>
{{ item.content }}
</div>
<HtmlRander v-else-if="item.type === 'html_rander'" :source="item.content"></HtmlRander>
<EchartsRander
v-else-if="item.type === 'echarts_rander'"
:option="item.content"
></EchartsRander>
<FormRander
:chat_record_id="chat_record_id"
:runtime_node_id="runtime_node_id"
:child_node="child_node"
:disabled="disabled"
:send-message="sendMessage"
v-else-if="item.type === 'form_rander'"
:form_setting="item.content"
></FormRander>
<MdPreview
v-else
ref="editorRef"
editorId="preview-only"
:modelValue="item.content"
:key="index"
class="maxkb-md"
/>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { config } from 'md-editor-v3'
import HtmlRander from './HtmlRander.vue'
import EchartsRander from './EchartsRander.vue'
import FormRander from './FormRander.vue'
import ReasoningRander from './ReasoningRander.vue'
config({
markdownItConfig(md) {
md.renderer.rules.image = (tokens, idx, options, env, self) => {
tokens[idx].attrSet('style', 'display:inline-block;min-height:33px;padding:0;margin:0')
if (tokens[idx].content) {
tokens[idx].attrSet('title', tokens[idx].content)
}
tokens[idx].attrSet(
'onerror',
'this.src="/ui/assets/load_error.png";this.onerror=null;this.height="33px"'
)
return md.renderer.renderToken(tokens, idx, options)
}
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
tokens[idx].attrSet('target', '_blank')
return md.renderer.renderToken(tokens, idx, options)
}
document.appendChild
}
})
const props = withDefaults(
defineProps<{
source?: string
reasoning_content?: string
inner_suffix?: boolean
sendMessage?: (question: string, type: 'old' | 'new', other_params_data?: any) => void
child_node?: any
chat_record_id?: string
runtime_node_id?: string
disabled?: boolean
}>(),
{
source: '',
disabled: false
}
)
const editorRef = ref()
const md_view_list = computed(() => {
const temp_source = props.source
return split_form_rander(
split_echarts_rander(split_html_rander(split_quick_question([temp_source])))
)
})
const split_quick_question = (result: Array<string>) => {
return result
.map((item) => split_quick_question_(item))
.reduce((x: any, y: any) => {
return [...x, ...y]
}, [])
}
const split_quick_question_ = (source: string) => {
const temp_md_quick_question_list = source.match(/<quick_question>[\d\D]*?<\/quick_question>/g)
const md_quick_question_list = temp_md_quick_question_list
? temp_md_quick_question_list.filter((i) => i)
: []
const split_quick_question_value = source
.split(/<quick_question>[\d\D]*?<\/quick_question>/g)
.filter((item) => item !== undefined)
.filter((item) => !md_quick_question_list?.includes(item))
const result = Array.from(
{ length: md_quick_question_list.length + split_quick_question_value.length },
(v, i) => i
).map((index) => {
if (index % 2 == 0) {
return { type: 'md', content: split_quick_question_value[Math.floor(index / 2)] }
} else {
return {
type: 'question',
content: md_quick_question_list[Math.floor(index / 2)]
.replace('<quick_question>', '')
.replace('</quick_question>', '')
}
}
})
return result
}
const split_html_rander = (result: Array<any>) => {
return result
.map((item) => split_html_rander_(item.content, item.type))
.reduce((x: any, y: any) => {
return [...x, ...y]
}, [])
}
const split_html_rander_ = (source: string, type: string) => {
const temp_md_quick_question_list = source.match(/<html_rander>[\d\D]*?<\/html_rander>/g)
const md_quick_question_list = temp_md_quick_question_list
? temp_md_quick_question_list.filter((i) => i)
: []
const split_quick_question_value = source
.split(/<html_rander>[\d\D]*?<\/html_rander>/g)
.filter((item) => item !== undefined)
.filter((item) => !md_quick_question_list?.includes(item))
const result = Array.from(
{ length: md_quick_question_list.length + split_quick_question_value.length },
(v, i) => i
).map((index) => {
if (index % 2 == 0) {
return { type: type, content: split_quick_question_value[Math.floor(index / 2)] }
} else {
return {
type: 'html_rander',
content: md_quick_question_list[Math.floor(index / 2)]
.replace('<html_rander>', '')
.replace('</html_rander>', '')
}
}
})
return result
}
const split_echarts_rander = (result: Array<any>) => {
return result
.map((item) => split_echarts_rander_(item.content, item.type))
.reduce((x: any, y: any) => {
return [...x, ...y]
}, [])
}
const split_echarts_rander_ = (source: string, type: string) => {
const temp_md_quick_question_list = source.match(/<echarts_rander>[\d\D]*?<\/echarts_rander>/g)
const md_quick_question_list = temp_md_quick_question_list
? temp_md_quick_question_list.filter((i) => i)
: []
const split_quick_question_value = source
.split(/<echarts_rander>[\d\D]*?<\/echarts_rander>/g)
.filter((item) => item !== undefined)
.filter((item) => !md_quick_question_list?.includes(item))
const result = Array.from(
{ length: md_quick_question_list.length + split_quick_question_value.length },
(v, i) => i
).map((index) => {
if (index % 2 == 0) {
return { type: type, content: split_quick_question_value[Math.floor(index / 2)] }
} else {
return {
type: 'echarts_rander',
content: md_quick_question_list[Math.floor(index / 2)]
.replace('<echarts_rander>', '')
.replace('</echarts_rander>', '')
}
}
})
return result
}
const split_form_rander = (result: Array<any>) => {
return result
.map((item) => split_form_rander_(item.content, item.type))
.reduce((x: any, y: any) => {
return [...x, ...y]
}, [])
}
const split_form_rander_ = (source: string, type: string) => {
const temp_md_quick_question_list = source.match(/<form_rander>[\d\D]*?<\/form_rander>/g)
const md_quick_question_list = temp_md_quick_question_list
? temp_md_quick_question_list.filter((i) => i)
: []
const split_quick_question_value = source
.split(/<form_rander>[\d\D]*?<\/form_rander>/g)
.filter((item) => item !== undefined)
.filter((item) => !md_quick_question_list?.includes(item))
const result = Array.from(
{ length: md_quick_question_list.length + split_quick_question_value.length },
(v, i) => i
).map((index) => {
if (index % 2 == 0) {
return { type: type, content: split_quick_question_value[Math.floor(index / 2)] }
} else {
return {
type: 'form_rander',
content: md_quick_question_list[Math.floor(index / 2)]
.replace('<form_rander>', '')
.replace('</form_rander>', '')
}
}
})
return result
}
</script>
<style lang="scss" scoped>
.problem-button {
width: 100%;
border: none;
border-radius: 8px;
background: var(--app-layout-bg-color);
padding: 12px;
box-sizing: border-box;
color: var(--el-text-color-regular);
word-break: break-all;
&:hover {
background: var(--el-color-primary-light-9);
}
&.disabled {
&:hover {
background: var(--app-layout-bg-color);
}
}
:deep(.el-icon) {
color: var(--el-color-primary);
}
}
</style>

View File

@ -0,0 +1,35 @@
<template>
<div class="reasoning">
<el-button text @click="showThink = !showThink" class="reasoning-button">
{{ $t('views.applicationWorkflow.nodes.aiChatNode.think') }}
<el-icon class="ml-4" :class="showThink ? 'rotate-180' : ''"><ArrowDownBold /> </el-icon>
</el-button>
<el-collapse-transition>
<div class="border-l mt-8" v-show="showThink">
<MdPreview
ref="editorRef"
editorId="preview-only"
:modelValue="content"
class="reasoning-md"
/>
</div>
</el-collapse-transition>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const props = defineProps<{ content?: string }>()
const showThink = ref<boolean>(true)
</script>
<style lang="scss" scoped>
.reasoning {
.reasoning-button {
font-size: 14px;
color: var(--app-text-color-secondary) !important;
}
.reasoning-md {
padding-left: 8px;
--md-color: var(--app-text-color-secondary) !important;
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@ -80,4 +80,5 @@ export default {
uploadImagePrompt: 'Please upload an image',
},
info: 'Base Information',
otherSetting: 'Other Settings',
}

View File

@ -84,4 +84,5 @@ export default {
uploadImagePrompt: '请上传一张图片',
},
info: '基本信息',
otherSetting: '其他设置',
}

View File

@ -10,10 +10,9 @@ import application from './application'
import problem from './problem'
import applicationOverview from './application-overview'
import applicationWorkflow from './application-workflow'
import paragraph from './paragraph'
// import notFound from './404'
// import paragraph from './paragraph'
// import log from './log'
// import operateLog from './operate-log'
@ -30,8 +29,8 @@ export default {
problem,
applicationOverview,
applicationWorkflow,
paragraph,
// notFound,
// paragraph,
// log,

View File

@ -64,6 +64,8 @@ export default {
label: '选择器',
placeholder: '默认为 body可输入 .classname/#idname/tagname',
},
},
ResultSuccess: {

View File

@ -0,0 +1,32 @@
export default {
title: '段落',
paragraph_count: '段落',
editParagraph: '编辑分段',
addParagraph: '添加分段',
paragraphDetail: '分段详情',
character_count: '个字符',
setting: {
batchSelected: '批量选择',
cancelSelected: '取消选择'
},
delete: {
confirmTitle: '是否删除段落:',
confirmMessage: '删除后无法恢复,请谨慎操作。'
},
relatedProblem: {
title: '关联问题',
placeholder: '请选择问题'
},
form: {
paragraphTitle: {
label: '分段标题',
placeholder: '请输入分段标题'
},
content: {
label: '分段内容',
placeholder: '请输入分段内容',
requiredMessage1: '请输入分段内容',
requiredMessage2: '内容最多不超过 100000 个字'
}
}
}

View File

@ -80,4 +80,5 @@ export default {
uploadImagePrompt: '請上傳一張圖片',
},
info: '使用者資訊',
otherSetting: '其他設定',
}

View File

@ -11,6 +11,40 @@ import router from '@/router'
import i18n from '@/locales'
import Components from '@/components'
import directives from '@/directives'
import { config } from 'md-editor-v3'
import screenfull from 'screenfull'
import katex from 'katex'
import 'katex/dist/katex.min.css'
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
import mermaid from 'mermaid'
import highlight from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'
config({
editorExtensions: {
highlight: {
instance: highlight
},
screenfull: {
instance: screenfull
},
katex: {
instance: katex
},
cropper: {
instance: Cropper
},
mermaid: {
instance: mermaid
}
}
})
const app = createApp(App)
app.use(createPinia())
for (const [key, component] of Object.entries(ElementPlusIcons)) {

View File

@ -7,7 +7,7 @@ const ParagraphRouter = {
children: [
{
path: '/paragraph/:id/:documentId',
name: 'Paragraph1',
name: 'ParagraphIndex',
meta: { activeMenu: '/knowledge' },
component: () => import('@/views/paragraph/index.vue'),
},

View File

@ -93,3 +93,9 @@
}
}
}
.custom-slider {
.el-input-number.is-without-controls .el-input__wrapper {
padding: 0 !important;
}
}

View File

@ -23,9 +23,13 @@
// card
.el-card {
--el-card-padding: calc(var(--app-base-px) * 2);
--el-card-border-radius: 8px;
--el-card-border-radius: 6px;
box-shadow: 0px 2px 4px 0px rgba(31, 35, 41, 0.12) !important;
border: none;
&.is-never-shadow {
border: 1px solid var(--el-card-border-color);
box-shadow: none !important;
}
}
// tree

View File

@ -4,3 +4,5 @@
@use './app.scss';
@use './component.scss';
@import 'nprogress/nprogress.css';
@import 'md-editor-v3/lib/style.css';
@import './md-editor.scss';

View File

@ -0,0 +1,46 @@
.md-editor {
font-weight: 400;
}
.md-editor-preview {
padding: 0;
margin: 0;
font-size: inherit;
word-break: break-word;
table {
display: block;
}
p {
padding: 0 !important;
}
.md-editor-admonition {
margin: 0;
padding: 0;
}
img {
border: 0 !important;
max-width: 360px !important;
}
}
@media only screen and (max-width: 768px) {
.md-editor-preview {
img {
max-width: 100% !important;
}
}
}
.md-editor-preview-wrapper {
padding: 0;
}
.md-editor-footer {
height: auto !important;
}
.ͼ1 .cm-placeholder {
color: var(--app-input-color-placeholder);
font-size: 14px;
font-weight: 400;
}

View File

@ -502,12 +502,6 @@
</el-table-column>
</app-table>
</div>
<ImportDocumentDialog ref="ImportDocumentDialogRef" :title="title" @refresh="refresh" />
<SyncWebDialog ref="SyncWebDialogRef" @refresh="refresh" />
<!-- 选择知识库 -->
<SelectDatasetDialog ref="SelectDatasetDialogRef" @refresh="refreshMigrate" />
<GenerateRelatedDialog ref="GenerateRelatedDialogRef" @refresh="getList" />
</div>
</el-card>
<div class="mul-operation w-full flex" v-if="multipleSelection.length !== 0">
@ -525,7 +519,14 @@
{{ $t('common.clear') }}
</el-button>
</div>
<EmbeddingContentDialog ref="embeddingContentDialogRef"></EmbeddingContentDialog>
<ImportDocumentDialog ref="ImportDocumentDialogRef" :title="title" @refresh="refresh" />
<SyncWebDialog ref="SyncWebDialogRef" @refresh="refresh" />
<!-- 选择知识库 -->
<SelectDatasetDialog ref="SelectDatasetDialogRef" @refresh="refreshMigrate" />
<GenerateRelatedDialog ref="GenerateRelatedDialogRef" @refresh="getList" />
</div>
</template>
<script setup lang="ts">
@ -768,7 +769,7 @@ function rowClickHandle(row: any, column: any) {
return
}
router.push({ path: `/knowledge/${id}/${row.id}` })
router.push({ path: `/paragraph/${id}/${row.id}` })
}
/*
@ -930,7 +931,7 @@ function getList(bool?: boolean) {
folder_id: folderId,
}
documentApi
.getDocument( id as string, paginationConfig.value, param, bool ? undefined : loading)
.getDocument(id as string, paginationConfig.value, param, bool ? undefined : loading)
.then((res) => {
documentData.value = res.data.records
paginationConfig.value.total = res.data.total
@ -938,7 +939,7 @@ function getList(bool?: boolean) {
}
function getDetail() {
knowledge.asyncGetDatasetDetail( id, loading).then((res: any) => {
knowledge.asyncGetDatasetDetail(id, loading).then((res: any) => {
datasetDetail.value = res.data
})
}

View File

@ -8,7 +8,7 @@
<h4 class="title-decoration-1 mb-16">
{{ $t('common.info') }}
</h4>
<BaseForm ref="BaseFormRef" :data="detail"/>
<BaseForm ref="BaseFormRef" :data="detail" />
<el-form
ref="webFormRef"
@ -18,23 +18,33 @@
require-asterisk-position="right"
>
<el-form-item :label="$t('views.knowledge.knowledgeType.label')" required>
<el-card shadow="never" class="mb-8" style="width: 50%" v-if="detail.type === 0">
<el-card
shadow="never"
class="mb-8 w-full"
style="line-height: 22px"
v-if="detail.type === 0"
>
<div class="flex align-center">
<el-avatar class="mr-8 avatar-blue" shape="square" :size="32">
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt=""/>
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
</el-avatar>
<div>
<div>{{ $t('views.knowledge.knowledgeType.generalKnowledge') }}</div>
<el-text type="info"
>{{ $t('views.knowledge.knowledgeType.generalInfo') }}
>{{ $t('views.knowledge.knowledgeType.generalInfo') }}
</el-text>
</div>
</div>
</el-card>
<el-card shadow="never" class="mb-8" style="width: 50%" v-if="detail?.type === 1">
<el-card
shadow="never"
class="mb-8 w-full"
style="line-height: 22px"
v-if="detail?.type === 1"
>
<div class="flex align-center">
<el-avatar class="mr-8 avatar-purple" shape="square" :size="32">
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt=""/>
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
</el-avatar>
<div>
<div>{{ $t('views.knowledge.knowledgeType.webKnowledge') }}</div>
@ -44,17 +54,22 @@
</div>
</div>
</el-card>
<el-card shadow="never" class="mb-8" style="width: 50%" v-if="detail?.type === 2">
<el-card
shadow="never"
class="mb-8 w-full"
style="line-height: 22px"
v-if="detail?.type === 2"
>
<div class="flex align-center">
<el-avatar shape="square" :size="32" style="background: none">
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt=""/>
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
</el-avatar>
<div>
<p>
<el-text>{{ $t('views.knowledge.knowledgeType.larkKnowledge') }}</el-text>
</p>
<el-text type="info"
>{{ $t('views.knowledge.knowledgeType.larkInfo') }}
>{{ $t('views.knowledge.knowledgeType.larkInfo') }}
</el-text>
</div>
</div>
@ -107,48 +122,23 @@
"
/>
</el-form-item>
</el-form>
<div v-if="application_id_list.length > 0">
<h4 class="title-decoration-1 mb-16">
{{ $t('views.dataset.relatedApplications') }}
</h4>
<el-row :gutter="12">
<el-col
:span="12"
v-for="(item, index) in application_list.filter((obj: any) =>
application_id_list.some((v: any) => v === obj?.id),
)"
:key="index"
class="mb-16"
>
<el-card shadow="never">
<div class="flex-between">
<div class="flex align-center">
<el-avatar
v-if="isAppIcon(item?.icon)"
shape="square"
:size="32"
style="background: none"
class="mr-12"
>
<img :src="item?.icon" alt=""/>
</el-avatar>
<el-avatar
v-else-if="item?.name"
:name="item?.name"
pinyinColor
shape="square"
:size="32"
class="mr-12"
/>
{{ item.name }}
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<div v-if="detail.type === 0">
<h4 class="title-decoration-1 mb-16">
{{ $t('common.otherSetting') }}
</h4>
</div>
<el-form-item :label="$t('上传的每个文档最大限制')">
<el-slider
v-model="form.max_paragraph_char_number"
show-input
:show-input-controls="false"
:min="500"
:max="100000"
class="custom-slider"
/>
</el-form-item>
</el-form>
<div class="text-right">
<el-button @click="submit" type="primary"> {{ $t('common.save') }}</el-button>
</div>
@ -159,22 +149,22 @@
</div>
</template>
<script setup lang="ts">
import {ref, onMounted, reactive} from 'vue'
import {useRoute} from 'vue-router'
import { ref, onMounted, reactive } from 'vue'
import { useRoute } from 'vue-router'
import BaseForm from '@/views/knowledge/component/BaseForm.vue'
import KnowledgeApi from '@/api/knowledge/knowledge'
import type {ApplicationFormType} from '@/api/type/application'
import {MsgSuccess, MsgConfirm} from '@/utils/message'
import {isAppIcon} from '@/utils/common'
import type { ApplicationFormType } from '@/api/type/application'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { isAppIcon } from '@/utils/common'
import useStore from '@/stores'
import {t} from '@/locales'
import { t } from '@/locales'
const route = useRoute()
const {
params: {id},
params: { id },
} = route as any
const {knowledge} = useStore()
const { knowledge } = useStore()
const webFormRef = ref()
const BaseFormRef = ref()
const loading = ref(false)
@ -229,14 +219,14 @@ async function submit() {
const obj =
detail.value.type === '1' || detail.value.type === '2'
? {
application_id_list: application_id_list.value,
meta: form.value,
...BaseFormRef.value.form,
}
application_id_list: application_id_list.value,
meta: form.value,
...BaseFormRef.value.form,
}
: {
application_id_list: application_id_list.value,
...BaseFormRef.value.form,
}
application_id_list: application_id_list.value,
...BaseFormRef.value.form,
}
if (cloneModelId.value !== BaseFormRef.value.form.embedding_mode_id) {
MsgConfirm(t('common.tip'), t('views.knowledge.tip.updateModeMessage'), {
@ -257,8 +247,7 @@ async function submit() {
})
}
})
.catch(() => {
})
.catch(() => {})
} else {
if (detail.value.type === 2) {
KnowledgeApi.putLarkDataset(id, obj, loading).then((res) => {

View File

@ -0,0 +1,85 @@
<template>
<el-card shadow="hover" class="paragraph-box" @mouseenter="cardEnter()" @mouseleave="cardLeave()">
<div class="card-header">
<h2>{{ 1111 }}</h2>
</div>
<MdPreview
ref="editorRef"
editorId="preview-only"
:modelValue="form.content"
class="maxkb-md"
/>
</el-card>
</template>
<script setup lang="ts">
import { ref, useSlots } from 'vue'
import { t } from '@/locales'
defineOptions({ name: 'CardBox' })
const props = withDefaults(
defineProps<{
/**
* 标题
*/
title?: string
/**
* 描述
*/
description?: string
/**
* 是否展示icon
*/
showIcon?: boolean
}>(),
{ title: t('common.title'), description: '', showIcon: true, border: true },
)
const show = ref(false)
// carddropdown
const subHovered = ref(false)
function cardEnter() {
show.value = true
subHovered.value = false
}
function cardLeave() {
show.value = subHovered.value
}
function subHoveredEnter() {
subHovered.value = true
}
</script>
<style lang="scss" scoped>
.card-box {
font-size: 14px;
position: relative;
min-height: var(--card-min-height);
min-width: var(--card-min-width);
.card-header {
margin-top: -5px;
}
.description {
line-height: 22px;
font-weight: 400;
min-height: 70px;
.content {
display: -webkit-box;
height: var(--app-card-box-description-height, 40px);
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
.card-footer {
position: absolute;
bottom: 8px;
left: 0;
min-height: 30px;
font-weight: 400;
padding: 0 16px;
width: 100%;
box-sizing: border-box;
}
}
</style>

View File

@ -1,39 +1,34 @@
<template>
<LayoutContainer back-to="-1" class="document-detail">
<template #header>
<div style="width: 78%">
<h3 style="display: inline-block">{{ documentDetail?.name }}</h3>
<el-text type="info" v-if="documentDetail?.type === '1'"
>{{ $t('views.document.form.source_url.label') }}<el-link
:href="documentDetail?.meta?.source_url"
target="_blank"
>
<span class="break-all">{{ documentDetail?.meta?.source_url }} </span></el-link
>
</el-text>
</div>
<div class="header-button">
<el-button @click="batchSelectedHandle(true)" v-if="isBatch === false">
{{ $t('views.paragraph.setting.batchSelected') }}
</el-button>
<el-button @click="batchSelectedHandle(false)" v-if="isBatch === true">
{{ $t('views.paragraph.setting.cancelSelected') }}
</el-button>
<el-button
@click="addParagraph"
type="primary"
:disabled="loading"
v-if="isBatch === false"
<div class="paragraph p-12-24">
<div class="flex align-center" style="width: 78%">
<back-button to="-1" style="margin-left: -4px"></back-button>
<h3 style="display: inline-block">{{ documentDetail?.name }}</h3>
<el-text type="info" v-if="documentDetail?.type === '1'"
>{{ $t('views.document.form.source_url.label') }}<el-link
:href="documentDetail?.meta?.source_url"
target="_blank"
>
{{ $t('views.paragraph.addParagraph') }}
</el-button>
</div>
</template>
<div
class="document-detail__main p-16"
<span class="break-all">{{ documentDetail?.meta?.source_url }} </span></el-link
>
</el-text>
</div>
<div class="header-button">
<el-button @click="batchSelectedHandle(true)" v-if="isBatch === false">
{{ $t('views.paragraph.setting.batchSelected') }}
</el-button>
<el-button @click="batchSelectedHandle(false)" v-if="isBatch === true">
{{ $t('views.paragraph.setting.cancelSelected') }}
</el-button>
<el-button @click="addParagraph" type="primary" :disabled="loading" v-if="isBatch === false">
{{ $t('views.paragraph.addParagraph') }}
</el-button>
</div>
<el-card
style="--el-card-padding: 0"
class="paragraph-detail__main mt-16"
v-loading="(paginationConfig.current_page === 1 && loading) || changeStateloading"
>
<div class="flex-between p-8">
<div class="flex-between p-12-16 border-b">
<span>{{ paginationConfig.total }} {{ $t('views.paragraph.paragraph_count') }}</span>
<el-input
v-model="search"
@ -51,111 +46,23 @@
</template>
</el-input>
</div>
<el-scrollbar>
<div class="document-detail-height">
<el-empty v-if="paragraphDetail.length == 0" :description="$t('common.noData')" />
<el-empty v-if="paragraphDetail.length == 0" :description="$t('common.noData')" />
<div v-else>
<el-scrollbar>
<div class="paragraph-detail-height">
<InfiniteScroll
:size="paragraphDetail.length"
:total="paginationConfig.total"
:page_size="paginationConfig.page_size"
v-model:current_page="paginationConfig.current_page"
@load="getParagraphList"
:loading="loading"
>
<InfiniteScroll
v-else
:size="paragraphDetail.length"
:total="paginationConfig.total"
:page_size="paginationConfig.page_size"
v-model:current_page="paginationConfig.current_page"
@load="getParagraphList"
:loading="loading"
>
<el-row>
<el-col
:xs="24"
:sm="12"
:md="8"
:lg="6"
:xl="6"
v-for="(item, index) in paragraphDetail"
:key="index"
class="p-8"
>
<!-- 批量操作card -->
<CardBox
v-if="isBatch === true"
shadow="hover"
:title="item.title || '-'"
:description="item.content"
class="document-card cursor"
:class="multipleSelection.includes(item.id) ? 'selected' : ''"
:showIcon="false"
@click="selectHandle(item.id)"
>
<div class="active-button" @click.stop></div>
<template #footer>
<div class="footer-content flex-between">
<span>
{{ numberFormat(item?.content.length) || 0 }}
{{ $t('views.paragraph.character_count') }}
</span>
</div>
</template>
</CardBox>
<!-- 非批量操作card -->
<CardBox
v-else
shadow="hover"
:title="item.title || '-'"
:description="item.content"
class="document-card cursor"
:class="item.is_active ? '' : 'disabled'"
:showIcon="false"
@click="editParagraph(item)"
>
<div class="active-button" @click.stop>
<el-switch
:loading="loading"
v-model="item.is_active"
:before-change="() => changeState(item)"
size="small"
/>
</div>
<template #footer>
<div class="footer-content flex-between">
<span>
{{ numberFormat(item?.content.length) || 0 }}
{{ $t('views.paragraph.character_count') }}
</span>
<span @click.stop>
<el-dropdown trigger="click">
<el-button text>
<el-icon><MoreFilled /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openGenerateDialog(item)">
<el-icon><Connection /></el-icon>
{{
$t('views.document.generateQuestion.title')
}}</el-dropdown-item
>
<el-dropdown-item @click="openSelectDocumentDialog(item)">
<AppIcon iconName="app-migrate"></AppIcon>
{{ $t('views.document.setting.migration') }}</el-dropdown-item
>
<el-dropdown-item icon="Delete" @click.stop="deleteParagraph(item)">{{
$t('common.delete')
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
</template>
</CardBox>
</el-col>
</el-row>
</InfiniteScroll>
</div>
</el-scrollbar>
</InfiniteScroll>
</div>
</el-scrollbar>
</div>
<div class="mul-operation border-t w-full" v-if="isBatch === true">
<el-button :disabled="multipleSelection.length === 0" @click="openGenerateDialog()">
@ -173,11 +80,11 @@
{{ $t('views.document.items') }}
</span>
</div>
</div>
</el-card>
<ParagraphDialog ref="ParagraphDialogRef" :title="title" @refresh="refresh" />
<SelectDocumentDialog ref="SelectDocumentDialogRef" @refresh="refreshMigrateParagraph" />
<GenerateRelatedDialog ref="GenerateRelatedDialogRef" @refresh="refresh" />
</LayoutContainer>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, computed } from 'vue'
@ -194,7 +101,7 @@ import { t } from '@/locales'
const { paragraph } = useStore()
const route = useRoute()
const {
params: { id, documentId }
params: { id, documentId },
} = route as any
const SelectDocumentDialogRef = ref()
@ -214,12 +121,12 @@ const multipleSelection = ref<any[]>([])
const paginationConfig = reactive({
current_page: 1,
page_size: 30,
total: 0
total: 0,
})
function refreshMigrateParagraph() {
paragraphDetail.value = paragraphDetail.value.filter(
(v) => !multipleSelection.value.includes(v.id)
(v) => !multipleSelection.value.includes(v.id),
)
multipleSelection.value = []
MsgSuccess(t('views.document.tip.migrationSuccess'))
@ -237,15 +144,15 @@ function deleteMulParagraph() {
t('views.paragraph.delete.confirmMessage'),
{
confirmButtonText: t('common.confirm'),
confirmButtonClass: 'danger'
}
confirmButtonClass: 'danger',
},
)
.then(() => {
paragraphApi
.delMulParagraph(id, documentId, multipleSelection.value, changeStateloading)
.then(() => {
paragraphDetail.value = paragraphDetail.value.filter(
(v) => !multipleSelection.value.includes(v.id)
(v) => !multipleSelection.value.includes(v.id),
)
multipleSelection.value = []
MsgSuccess(t('views.document.delete.successMessage'))
@ -275,7 +182,7 @@ function searchHandle() {
function changeState(row: any) {
const obj = {
is_active: !row.is_active
is_active: !row.is_active,
}
paragraph
.asyncPutParagraph(id, documentId, row.id, obj, changeStateloading)
@ -295,8 +202,8 @@ function deleteParagraph(row: any) {
t('views.paragraph.delete.confirmMessage'),
{
confirmButtonText: t('common.confirm'),
confirmButtonClass: 'danger'
}
confirmButtonClass: 'danger',
},
)
.then(() => {
paragraph.asyncDelParagraph(id, documentId, row.id, loading).then(() => {
@ -337,7 +244,7 @@ function getParagraphList() {
documentId,
paginationConfig,
search.value && { [searchType.value]: search.value },
loading
loading,
)
.then((res) => {
paragraphDetail.value = [...paragraphDetail.value, ...res.data.records]
@ -378,16 +285,18 @@ onMounted(() => {
})
</script>
<style lang="scss" scoped>
.document-detail {
.paragraph {
position: relative;
.header-button {
position: absolute;
right: calc(var(--app-base-px) * 3);
top: calc(var(--app-base-px) + 4px);
}
.document-detail-height {
.paragraph-detail-height {
height: calc(var(--app-main-height) - 75px);
}
.document-card {
.paragraph-card {
height: 210px;
background: var(--app-layout-bg-color);
border: 1px solid var(--app-layout-bg-color);

View File

@ -141,7 +141,7 @@
</el-icon>
{{ $t('common.edit') }}
</el-dropdown-item>
<!-- <el-dropdown-item
<el-dropdown-item
:disabled="!canEdit(item)"
v-if="!item.template_id"
@click.stop="copytool(item)"
@ -172,7 +172,7 @@
>
<el-icon><Delete /></el-icon>
{{ $t('common.delete') }}
</el-dropdown-item> -->
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>