Merge pull request #79 from SuanmoSuanyangTechnology/feature/workflow_zy
Feature/workflow zy
This commit is contained in:
@@ -2262,6 +2262,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
context_details: 'Preference Details',
|
context_details: 'Preference Details',
|
||||||
supporting_evidence: 'Preference Source',
|
supporting_evidence: 'Preference Source',
|
||||||
specific_examples: 'Source',
|
specific_examples: 'Source',
|
||||||
|
wordEmpty: 'Click on a node in the left chart to view preference details'
|
||||||
},
|
},
|
||||||
shortTermDetail: {
|
shortTermDetail: {
|
||||||
title: 'Short-term memory is the "workbench" of the AI system, connecting instant conversations with long-term knowledge bases. Through real-time capture, deep retrieval, intelligent extraction and filtering transformation, temporary unstructured information is converted into valuable long-term knowledge.',
|
title: 'Short-term memory is the "workbench" of the AI system, connecting instant conversations with long-term knowledge bases. Through real-time capture, deep retrieval, intelligent extraction and filtering transformation, temporary unstructured information is converted into valuable long-term knowledge.',
|
||||||
|
|||||||
@@ -2361,6 +2361,7 @@ export const zh = {
|
|||||||
context_details: '偏好详情',
|
context_details: '偏好详情',
|
||||||
supporting_evidence: '偏好来源',
|
supporting_evidence: '偏好来源',
|
||||||
specific_examples: '来源',
|
specific_examples: '来源',
|
||||||
|
wordEmpty: '点击左侧图表中的节点查看偏好详情'
|
||||||
},
|
},
|
||||||
shortTermDetail: {
|
shortTermDetail: {
|
||||||
title: '短期记忆是AI系统的"工作台",连接即时对话与长期知识库。通过实时捕获、深度检索、智能提取和筛选转化,将临时的非结构化信息转化为有价值的长期知识。',
|
title: '短期记忆是AI系统的"工作台",连接即时对话与长期知识库。通过实时捕获、深度检索、智能提取和筛选转化,将临时的非结构化信息转化为有价值的长期知识。',
|
||||||
|
|||||||
198
web/src/utils/event.md
Normal file
198
web/src/utils/event.md
Normal file
File diff suppressed because one or more lines are too long
@@ -23,10 +23,20 @@ export function parseSSEToJSON(sseString: string) {
|
|||||||
currentEvent.event = line.substring(6).trim()
|
currentEvent.event = line.substring(6).trim()
|
||||||
} else if (line.startsWith('data:')) {
|
} else if (line.startsWith('data:')) {
|
||||||
const dataStr = line.substring(5).trim()
|
const dataStr = line.substring(5).trim()
|
||||||
try {
|
if (dataStr) {
|
||||||
currentEvent.data = JSON.parse(dataStr.replace(/"/g, '"'))
|
try {
|
||||||
} catch {
|
// 尝试解析为 JSON
|
||||||
currentEvent.data = dataStr
|
currentEvent.data = JSON.parse(dataStr)
|
||||||
|
} catch {
|
||||||
|
// JSON 解析失败时,检查是否是被转义的 JSON 字符串
|
||||||
|
try {
|
||||||
|
const unescaped = dataStr.replace(/"/g, '"').replace(/&/g, '&')
|
||||||
|
currentEvent.data = JSON.parse(unescaped)
|
||||||
|
} catch {
|
||||||
|
// 如果仍然失败,保存为原始字符串
|
||||||
|
currentEvent.data = dataStr
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Card from './components/Card'
|
|||||||
import { Form, Space, Row, Col, Button, Flex, App, Select } from 'antd'
|
import { Form, Space, Row, Col, Button, Flex, App, Select } from 'antd'
|
||||||
import Tag, { type TagProps } from './components/Tag'
|
import Tag, { type TagProps } from './components/Tag'
|
||||||
import CustomSelect from '@/components/CustomSelect';
|
import CustomSelect from '@/components/CustomSelect';
|
||||||
import { getMultiAgentConfig, saveMultiAgentConfig } from '@/api/application';
|
import { getMultiAgentConfig, saveMultiAgentConfig, getApplicationList } from '@/api/application';
|
||||||
import type {
|
import type {
|
||||||
Config,
|
Config,
|
||||||
SubAgentModalRef,
|
SubAgentModalRef,
|
||||||
@@ -21,6 +21,7 @@ import Empty from '@/components/Empty'
|
|||||||
import RadioGroupCard from '@/components/RadioGroupCard'
|
import RadioGroupCard from '@/components/RadioGroupCard'
|
||||||
import { getModelListUrl } from '@/api/models'
|
import { getModelListUrl } from '@/api/models'
|
||||||
import ModelConfigModal from './components/ModelConfigModal'
|
import ModelConfigModal from './components/ModelConfigModal'
|
||||||
|
import type { Application } from '@/views/ApplicationManagement/types'
|
||||||
|
|
||||||
|
|
||||||
const tagColors = ['processing', 'warning', 'default']
|
const tagColors = ['processing', 'warning', 'default']
|
||||||
@@ -91,7 +92,27 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
|||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
...response,
|
...response,
|
||||||
})
|
})
|
||||||
setSubAgents(response.sub_agents || [])
|
let sub_agents = response.sub_agents || []
|
||||||
|
if (sub_agents.length > 0) {
|
||||||
|
console.log({ ids: sub_agents?.map(item => item.agent_id) })
|
||||||
|
getApplicationList({ ids: sub_agents?.map(item => item.agent_id).join(',')})
|
||||||
|
.then(res => {
|
||||||
|
const applicationList = (res as Application[]) || []
|
||||||
|
setSubAgents(sub_agents.map(vo => {
|
||||||
|
const filterVO = applicationList.find(item => item.id === vo.agent_id)
|
||||||
|
if (filterVO) {
|
||||||
|
return {
|
||||||
|
...vo,
|
||||||
|
name: filterVO.name,
|
||||||
|
is_active: filterVO.is_active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vo
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setSubAgents(sub_agents)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const handleSubAgentModal = (agent?: SubAgentItem) => {
|
const handleSubAgentModal = (agent?: SubAgentItem) => {
|
||||||
@@ -171,7 +192,12 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
|||||||
{agent.name?.[0]}
|
{agent.name?.[0]}
|
||||||
</div>
|
</div>
|
||||||
<div className="rb:flex rb:flex-col rb:justify-center rb:max-w-[calc(100%-60px)]">
|
<div className="rb:flex rb:flex-col rb:justify-center rb:max-w-[calc(100%-60px)]">
|
||||||
{agent.name}
|
|
||||||
|
<div>{agent.name}
|
||||||
|
<Tag color={agent.is_active ? 'success' : 'warning'} className="rb:ml-2">
|
||||||
|
{agent.is_active ? t('common.enable') : t('common.deleted')}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
{agent.role && <div className="rb:font-regular rb:leading-5 rb:text-[#5B6167] rb:mt-1.5">{agent.role || '-'}</div>}
|
{agent.role && <div className="rb:font-regular rb:leading-5 rb:text-[#5B6167] rb:mt-1.5">{agent.role || '-'}</div>}
|
||||||
{agent.capabilities && <Flex wrap gap={8} className="rb:mt-4">{agent.capabilities.map((tag, tagIndex) => <Tag key={tagIndex} color={tagColors[tagIndex % tagColors.length] as TagProps['color']}>{tag}</Tag>)}</Flex>}
|
{agent.capabilities && <Flex wrap gap={8} className="rb:mt-4">{agent.capabilities.map((tag, tagIndex) => <Tag key={tagIndex} color={tagColors[tagIndex % tagColors.length] as TagProps['color']}>{tag}</Tag>)}</Flex>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ const SubAgentModal = forwardRef<SubAgentModalRef, SubAgentModalProps>(({
|
|||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
form.validateFields().then(() => {
|
form.validateFields().then(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
refresh(values)
|
refresh({
|
||||||
|
...values,
|
||||||
|
is_active: true
|
||||||
|
})
|
||||||
handleClose()
|
handleClose()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ export interface SubAgentItem {
|
|||||||
name: string;
|
name: string;
|
||||||
role: string;
|
role: string;
|
||||||
capabilities: string[];
|
capabilities: string[];
|
||||||
|
is_active?: boolean;
|
||||||
}
|
}
|
||||||
export interface SubAgentModalRef {
|
export interface SubAgentModalRef {
|
||||||
handleOpen: (agent?: SubAgentItem) => void;
|
handleOpen: (agent?: SubAgentItem) => void;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const ExplicitDetailModal = forwardRef<ExplicitDetailModalRef>((_props, ref) =>
|
|||||||
onCancel={handleClose}
|
onCancel={handleClose}
|
||||||
>
|
>
|
||||||
{loading ? <Skeleton active />
|
{loading ? <Skeleton active />
|
||||||
: <Descriptions column={data.memory_type === 'semantic' ? 1 : 2} classNames={{ label: 'rb:w-20' }}>
|
: <Descriptions column={1} classNames={{ label: 'rb:w-20' }}>
|
||||||
{data.emotion && <Descriptions.Item label={t('explicitDetail.emotion')}>
|
{data.emotion && <Descriptions.Item label={t('explicitDetail.emotion')}>
|
||||||
<div className="rb:flex rb:items-center rb:gap-2">
|
<div className="rb:flex rb:items-center rb:gap-2">
|
||||||
<div className="rb:w-3 rb:h-3 rb:rounded-full" style={{ backgroundColor: getEmotionColor(data.emotion) }}></div>
|
<div className="rb:w-3 rb:h-3 rb:rounded-full" style={{ backgroundColor: getEmotionColor(data.emotion) }}></div>
|
||||||
@@ -88,7 +88,7 @@ const ExplicitDetailModal = forwardRef<ExplicitDetailModalRef>((_props, ref) =>
|
|||||||
{data.created_at && <Descriptions.Item label={t('explicitDetail.created_at')}>
|
{data.created_at && <Descriptions.Item label={t('explicitDetail.created_at')}>
|
||||||
{formatDateTime(data.created_at)}
|
{formatDateTime(data.created_at)}
|
||||||
</Descriptions.Item>}
|
</Descriptions.Item>}
|
||||||
{data.content && <Descriptions.Item span="filled" label={t('explicitDetail.content')}>
|
{data.content && <Descriptions.Item label={t('explicitDetail.content')}>
|
||||||
{data.content}
|
{data.content}
|
||||||
</Descriptions.Item>}
|
</Descriptions.Item>}
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'echarts-wordcloud'
|
|||||||
import Empty from '@/components/Empty'
|
import Empty from '@/components/Empty'
|
||||||
import RbCard from '@/components/RbCard/Card'
|
import RbCard from '@/components/RbCard/Card'
|
||||||
import { getImplicitPreferences } from '@/api/memory'
|
import { getImplicitPreferences } from '@/api/memory'
|
||||||
|
import detailEmpty from '@/assets/images/userMemory/detail_empty.png'
|
||||||
|
|
||||||
interface PreferenceItem {
|
interface PreferenceItem {
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
@@ -164,7 +165,12 @@ const Preferences: FC = () => {
|
|||||||
bodyClassName='rb:p-3! rb:h-[326px]'
|
bodyClassName='rb:p-3! rb:h-[326px]'
|
||||||
>
|
>
|
||||||
{selectedWord === null
|
{selectedWord === null
|
||||||
? <Empty size={88} className="rb:h-full!" />
|
? <Empty
|
||||||
|
url={detailEmpty}
|
||||||
|
subTitle={t('implicitDetail.wordEmpty')}
|
||||||
|
className="rb:h-full rb:mx-10 rb:text-center"
|
||||||
|
size={[197.81, 150]}
|
||||||
|
/>
|
||||||
: <>
|
: <>
|
||||||
<div className="rb:leading-5 rb:mb-1 rb:font-medium">{t('implicitDetail.context_details')}</div>
|
<div className="rb:leading-5 rb:mb-1 rb:font-medium">{t('implicitDetail.context_details')}</div>
|
||||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">{data[selectedWord].context_details}</div>
|
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">{data[selectedWord].context_details}</div>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const TAG_COLORS: Record<string, "processing" | "success" | "warning" | "error"
|
|||||||
learning: "warning",
|
learning: "warning",
|
||||||
decision: "warning",
|
decision: "warning",
|
||||||
important_event: "error",
|
important_event: "error",
|
||||||
|
default: 'default'
|
||||||
}
|
}
|
||||||
const BG_COLORS: Record<string, string> = {
|
const BG_COLORS: Record<string, string> = {
|
||||||
conversation: "rb:bg-[#155EEF]",
|
conversation: "rb:bg-[#155EEF]",
|
||||||
@@ -46,10 +47,12 @@ const BG_COLORS: Record<string, string> = {
|
|||||||
learning: "rb:bg-[#FF5D34]",
|
learning: "rb:bg-[#FF5D34]",
|
||||||
decision: "rb:bg-[#FF5D34]",
|
decision: "rb:bg-[#FF5D34]",
|
||||||
important_event: "rb:bg-[#5B6167]",
|
important_event: "rb:bg-[#5B6167]",
|
||||||
|
default: 'rb:bg-[#F0F3F8] rb:text-[#5B6167]!'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map display types to internal keys
|
// Map display types to internal keys
|
||||||
const getTypeKey = (type: string): string => {
|
const getTypeKey = (type: string): string => {
|
||||||
|
if (!type) return 'default'
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
'Learning': 'learning',
|
'Learning': 'learning',
|
||||||
'Project/Work': 'project_work',
|
'Project/Work': 'project_work',
|
||||||
@@ -176,6 +179,7 @@ const EpisodicDetail: FC = () => {
|
|||||||
<RbCard
|
<RbCard
|
||||||
title={<>{t('episodicDetail.curResult')}<span className="rb:text-[#5B6167] rb:font-regular!"> ({data.total || 0}{t('episodicDetail.unix')})</span></>}
|
title={<>{t('episodicDetail.curResult')}<span className="rb:text-[#5B6167] rb:font-regular!"> ({data.total || 0}{t('episodicDetail.unix')})</span></>}
|
||||||
headerType="borderless"
|
headerType="borderless"
|
||||||
|
bodyClassName="rb:h-[calc(100vh-349px)] rb:overflow-y-auto"
|
||||||
>
|
>
|
||||||
{loading
|
{loading
|
||||||
? <Skeleton active />
|
? <Skeleton active />
|
||||||
@@ -192,9 +196,12 @@ const EpisodicDetail: FC = () => {
|
|||||||
})}
|
})}
|
||||||
onClick={() => setSelected(vo)}
|
onClick={() => setSelected(vo)}
|
||||||
>
|
>
|
||||||
<div className={clsx("rb:bg-[#369F21] rb:rounded-lg rb:text-[#FFFFFF] rb:size-6 rb:text-[12px] rb:leading-6 rb:text-center rb:mr-3", BG_COLORS[getTypeKey(vo.type)])}>{index + 1}</div>
|
<div className={clsx("rb:rounded-lg rb:text-[#FFFFFF] rb:size-6 rb:text-[12px] rb:leading-6 rb:text-center rb:mr-3", BG_COLORS[getTypeKey(vo.type)])}>{index + 1}</div>
|
||||||
<div className="rb:flex-1">
|
<div className="rb:flex-1 rb:w-[calc(100%-36px)]">
|
||||||
<div className="rb:flex rb:items-center rb:justify-between">{vo.title} <Tag color={TAG_COLORS[getTypeKey(vo.type)]}>{t(`episodicDetail.${getTypeKey(vo.type)}`)}</Tag></div>
|
<div className="rb:flex rb:items-center rb:justify-between">
|
||||||
|
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1">{vo.title}</div>
|
||||||
|
{vo.type && <Tag color={TAG_COLORS[getTypeKey(vo.type)]}>{t(`episodicDetail.${getTypeKey(vo.type)}`)}</Tag>}
|
||||||
|
</div>
|
||||||
<div className="rb:text-[#5B6167] rb:text-[12px]">{formatDateTime(vo.created_at)}</div>
|
<div className="rb:text-[#5B6167] rb:text-[12px]">{formatDateTime(vo.created_at)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,13 +209,13 @@ const EpisodicDetail: FC = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
</RbCard>
|
</RbCard>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={16}>
|
<Col span={16}>
|
||||||
<RbCard
|
<RbCard
|
||||||
title={selected?.title}
|
title={selected?.title}
|
||||||
headerType="borderless"
|
headerType="borderless"
|
||||||
|
bodyClassName="rb:h-[calc(100vh-349px)] rb:overflow-y-auto"
|
||||||
>
|
>
|
||||||
{detailLoading
|
{detailLoading
|
||||||
? <Skeleton active />
|
? <Skeleton active />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type FC, useEffect, useState, useRef } from 'react'
|
import { type FC, useEffect, useState, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { List, Skeleton, Row, Col } from 'antd'
|
import { Skeleton, Row, Col } from 'antd'
|
||||||
import RbCard from '@/components/RbCard/Card'
|
import RbCard from '@/components/RbCard/Card'
|
||||||
import {
|
import {
|
||||||
getExplicitMemory,
|
getExplicitMemory,
|
||||||
@@ -65,7 +65,7 @@ const ExplicitDetail: FC = () => {
|
|||||||
{loading ?
|
{loading ?
|
||||||
<Skeleton active />
|
<Skeleton active />
|
||||||
: data.episodic_memories?.length > 0 ? (
|
: data.episodic_memories?.length > 0 ? (
|
||||||
<Row gutter={16}>
|
<Row gutter={[16, 16]}>
|
||||||
{data.episodic_memories.map(item => (
|
{data.episodic_memories.map(item => (
|
||||||
<Col key={item.id} span={6}>
|
<Col key={item.id} span={6}>
|
||||||
<RbCard
|
<RbCard
|
||||||
@@ -85,7 +85,7 @@ const ExplicitDetail: FC = () => {
|
|||||||
{loading ?
|
{loading ?
|
||||||
<Skeleton active />
|
<Skeleton active />
|
||||||
: data.semantic_memories?.length > 0 ? (
|
: data.semantic_memories?.length > 0 ? (
|
||||||
<Row gutter={16}>
|
<Row gutter={[16, 16]}>
|
||||||
{data.semantic_memories.map(item => (
|
{data.semantic_memories.map(item => (
|
||||||
<Col key={item.id} span={6}>
|
<Col key={item.id} span={6}>
|
||||||
<RbCard
|
<RbCard
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ const WorkingDetail: FC = () => {
|
|||||||
<div className={clsx("rb:p-[8px_13px] rb:rounded-lg rb:leading-5 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
<div className={clsx("rb:p-[8px_13px] rb:rounded-lg rb:leading-5 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
||||||
'rb:bg-[#FFFFFF] rb:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)] rb:font-medium rb:hover:bg-[#FFFFFF]!': item.id === selected?.id,
|
'rb:bg-[#FFFFFF] rb:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)] rb:font-medium rb:hover:bg-[#FFFFFF]!': item.id === selected?.id,
|
||||||
})}
|
})}
|
||||||
onClick={() => getDetail(item.id)}
|
onClick={() => setSelected(item)}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ const Detail: FC = () => {
|
|||||||
{type === 'FORGETTING_MANAGEMENT' && <ForgetDetail />}
|
{type === 'FORGETTING_MANAGEMENT' && <ForgetDetail />}
|
||||||
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail />}
|
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail />}
|
||||||
{type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />}
|
{type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />}
|
||||||
{type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />} {/** TODO */}
|
{type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />}
|
||||||
{type === 'EPISODIC_MEMORY' && <EpisodicDetail />}
|
{type === 'EPISODIC_MEMORY' && <EpisodicDetail />}
|
||||||
{type === 'WORKING_MEMORY' && <WorkingDetail />} {/** TODO */}
|
{type === 'WORKING_MEMORY' && <WorkingDetail />}
|
||||||
{type === 'EXPLICIT_MEMORY' && <ExplicitDetail />} {/** TODO */}
|
{type === 'EXPLICIT_MEMORY' && <ExplicitDetail />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import AutocompletePlugin, { type Suggestion } from './plugin/AutocompletePlugin
|
|||||||
import CharacterCountPlugin from './plugin/CharacterCountPlugin'
|
import CharacterCountPlugin from './plugin/CharacterCountPlugin'
|
||||||
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
||||||
import CommandPlugin from './plugin/CommandPlugin';
|
import CommandPlugin from './plugin/CommandPlugin';
|
||||||
|
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
||||||
|
import LineNumberPlugin from './plugin/LineNumberPlugin';
|
||||||
import { VariableNode } from './nodes/VariableNode'
|
import { VariableNode } from './nodes/VariableNode'
|
||||||
|
|
||||||
interface LexicalEditorProps {
|
interface LexicalEditorProps {
|
||||||
@@ -88,6 +90,35 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
.editor-paragraph:has-text('[') .editor-text {
|
.editor-paragraph:has-text('[') .editor-text {
|
||||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
||||||
}
|
}
|
||||||
|
.editor-with-line-numbers {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.line-numbers {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-right: 1px solid #e1e4e8;
|
||||||
|
color: #656d76;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.line-numbers > div {
|
||||||
|
min-height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.editor-content-with-numbers {
|
||||||
|
flex: 1;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.editor-content-with-numbers p {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
@@ -117,25 +148,49 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<RichTextPlugin
|
<RichTextPlugin
|
||||||
contentEditable={
|
contentEditable={
|
||||||
<ContentEditable
|
enableJinja2 ? (
|
||||||
style={{
|
<div className="editor-with-line-numbers" style={{
|
||||||
minHeight: `${height}px`,
|
|
||||||
padding: variant === 'borderless' ? '0' : '4px 11px',
|
|
||||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
outline: 'none',
|
minHeight: `${height}px`,
|
||||||
resize: 'none',
|
}}>
|
||||||
fontSize: '14px',
|
<div className="line-numbers">
|
||||||
lineHeight: '20px',
|
<div>1</div>
|
||||||
}}
|
</div>
|
||||||
/>
|
<ContentEditable
|
||||||
|
className="editor-content-with-numbers"
|
||||||
|
style={{
|
||||||
|
minHeight: `${height}px`,
|
||||||
|
padding: '4px 11px',
|
||||||
|
outline: 'none',
|
||||||
|
resize: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '20px',
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ContentEditable
|
||||||
|
style={{
|
||||||
|
minHeight: `${height}px`,
|
||||||
|
padding: variant === 'borderless' ? '0' : '4px 11px',
|
||||||
|
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||||
|
borderRadius: '6px',
|
||||||
|
outline: 'none',
|
||||||
|
resize: 'none',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '20px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
placeholder={
|
placeholder={
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: variant === 'borderless' ? '0' : '6px',
|
top: variant === 'borderless' ? '0' : '6px',
|
||||||
left: variant === 'borderless' ? '0' : '11px',
|
left: enableJinja2 ? '59px' : (variant === 'borderless' ? '0' : '11px'),
|
||||||
color: '#5B6167',
|
color: '#5B6167',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
lineHeight: '20px',
|
lineHeight: '20px',
|
||||||
@@ -149,6 +204,8 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
/>
|
/>
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<CommandPlugin />
|
<CommandPlugin />
|
||||||
|
{enableJinja2 && <Jinja2HighlightPlugin />}
|
||||||
|
{enableJinja2 && <LineNumberPlugin />}
|
||||||
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
||||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||||
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
||||||
|
|||||||
@@ -36,64 +36,68 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
|||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const root = $getRoot();
|
const root = $getRoot();
|
||||||
root.clear();
|
root.clear();
|
||||||
const paragraph = $createParagraphNode();
|
|
||||||
|
|
||||||
const parts = value.split(/(\{\{[^}]+\}\})/);
|
const parts = value.split(/(\{\{[^}]+\}\})/);
|
||||||
|
|
||||||
parts.forEach(part => {
|
if (enableJinja2) {
|
||||||
const match = part.match(/^\{\{([^.]+)\.([^}]+)\}\}$/);
|
// Handle newlines properly in Jinja2 mode
|
||||||
const contextMatch = part.match(/^\{\{context\}\}$/);
|
const lines = value.split('\n');
|
||||||
const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/);
|
lines.forEach((line) => {
|
||||||
|
const paragraph = $createParagraphNode();
|
||||||
|
paragraph.append($createTextNode(line));
|
||||||
|
root.append(paragraph);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const paragraph = $createParagraphNode();
|
||||||
|
parts.forEach(part => {
|
||||||
|
const match = part.match(/^\{\{([^.]+)\.([^}]+)\}\}$/);
|
||||||
|
const contextMatch = part.match(/^\{\{context\}\}$/);
|
||||||
|
const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/);
|
||||||
|
|
||||||
if (enableJinja2) {
|
if (contextMatch) {
|
||||||
paragraph.append($createTextNode(part));
|
const contextSuggestion = options.find(s => s.isContext && s.label === 'context');
|
||||||
return;
|
if (contextSuggestion) {
|
||||||
}
|
paragraph.append($createVariableNode(contextSuggestion));
|
||||||
|
} else {
|
||||||
if (contextMatch) {
|
paragraph.append($createTextNode(part));
|
||||||
const contextSuggestion = options.find(s => s.isContext && s.label === 'context');
|
|
||||||
if (contextSuggestion) {
|
|
||||||
paragraph.append($createVariableNode(contextSuggestion));
|
|
||||||
} else {
|
|
||||||
paragraph.append($createTextNode(part));
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conversationMatch) {
|
|
||||||
const [_, variableName] = conversationMatch;
|
|
||||||
const conversationSuggestion = options.find(s =>
|
|
||||||
s.group === 'CONVERSATION' && s.label === variableName
|
|
||||||
);
|
|
||||||
if (conversationSuggestion) {
|
|
||||||
paragraph.append($createVariableNode(conversationSuggestion));
|
|
||||||
} else {
|
|
||||||
paragraph.append($createTextNode(part));
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const [_, nodeId, label] = match;
|
|
||||||
|
|
||||||
const suggestion = options.find(s => {
|
|
||||||
if (nodeId === 'sys') {
|
|
||||||
return s.nodeData.type === 'start' && s.label === `sys.${label}`
|
|
||||||
}
|
}
|
||||||
return s.nodeData.id === nodeId && s.label === label
|
return
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (conversationMatch) {
|
||||||
|
const [_, variableName] = conversationMatch;
|
||||||
|
const conversationSuggestion = options.find(s =>
|
||||||
|
s.group === 'CONVERSATION' && s.label === variableName
|
||||||
|
);
|
||||||
|
if (conversationSuggestion) {
|
||||||
|
paragraph.append($createVariableNode(conversationSuggestion));
|
||||||
|
} else {
|
||||||
|
paragraph.append($createTextNode(part));
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const [_, nodeId, label] = match;
|
||||||
|
|
||||||
if (suggestion) {
|
const suggestion = options.find(s => {
|
||||||
paragraph.append($createVariableNode(suggestion));
|
if (nodeId === 'sys') {
|
||||||
} else {
|
return s.nodeData.type === 'start' && s.label === `sys.${label}`
|
||||||
|
}
|
||||||
|
return s.nodeData.id === nodeId && s.label === label
|
||||||
|
});
|
||||||
|
|
||||||
|
if (suggestion) {
|
||||||
|
paragraph.append($createVariableNode(suggestion));
|
||||||
|
} else {
|
||||||
|
paragraph.append($createTextNode(part));
|
||||||
|
}
|
||||||
|
} else if (part) {
|
||||||
paragraph.append($createTextNode(part));
|
paragraph.append($createTextNode(part));
|
||||||
}
|
}
|
||||||
} else if (part) {
|
});
|
||||||
paragraph.append($createTextNode(part));
|
root.append(paragraph);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
root.append(paragraph);
|
|
||||||
}, { discrete: true });
|
}, { discrete: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
import { TextNode, $createTextNode } from 'lexical';
|
||||||
|
|
||||||
|
const Jinja2HighlightPlugin = () => {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||||
|
const text = textNode.getTextContent();
|
||||||
|
|
||||||
|
if (containsJinja2Patterns(text)) {
|
||||||
|
const parent = textNode.getParent();
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
const tokens = tokenizeJinja2(text);
|
||||||
|
const newNodes = tokens.map(token => {
|
||||||
|
const newNode = $createTextNode(token.text);
|
||||||
|
|
||||||
|
switch (token.type) {
|
||||||
|
case 'number':
|
||||||
|
newNode.setStyle('color: #005cc5; font-weight: 500;');
|
||||||
|
break;
|
||||||
|
case 'header-0':
|
||||||
|
case 'header-1':
|
||||||
|
case 'header-2':
|
||||||
|
case 'header-3':
|
||||||
|
case 'header-4':
|
||||||
|
case 'header-5':
|
||||||
|
newNode.setStyle('color: #008000');
|
||||||
|
break;
|
||||||
|
case 'brace-0':
|
||||||
|
newNode.setStyle('color: #d73a49; font-family: monospace; font-weight: bold;');
|
||||||
|
break;
|
||||||
|
case 'brace-1':
|
||||||
|
newNode.setStyle('color: #0366d6; font-family: monospace; font-weight: bold;');
|
||||||
|
break;
|
||||||
|
case 'brace-2':
|
||||||
|
newNode.setStyle('color: #28a745; font-family: monospace; font-weight: bold;');
|
||||||
|
break;
|
||||||
|
case 'brace-3':
|
||||||
|
newNode.setStyle('color: #6f42c1; font-family: monospace; font-weight: bold;');
|
||||||
|
break;
|
||||||
|
case 'expression-0':
|
||||||
|
case 'expression-1':
|
||||||
|
case 'expression-2':
|
||||||
|
case 'expression-3':
|
||||||
|
case 'statement-0':
|
||||||
|
case 'statement-1':
|
||||||
|
case 'statement-2':
|
||||||
|
case 'statement-3':
|
||||||
|
// Jinja2 delimiters use same color as braces
|
||||||
|
break;
|
||||||
|
case 'comment-0':
|
||||||
|
case 'comment-1':
|
||||||
|
case 'comment-2':
|
||||||
|
case 'comment-3':
|
||||||
|
newNode.setStyle('color: #721c24; font-family: monospace;');
|
||||||
|
break;
|
||||||
|
case 'variable':
|
||||||
|
newNode.setStyle('color: #0969da; font-weight: 500;');
|
||||||
|
break;
|
||||||
|
case 'filter':
|
||||||
|
newNode.setStyle('color: #8250df; font-weight: 500;');
|
||||||
|
break;
|
||||||
|
case 'keyword':
|
||||||
|
newNode.setStyle('color: #cf222e; font-weight: 600;');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newNodes.length > 1) {
|
||||||
|
textNode.replace(newNodes[0]);
|
||||||
|
for (let i = 1; i < newNodes.length; i++) {
|
||||||
|
newNodes[i - 1].insertAfter(newNodes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function containsJinja2Patterns(text: string): boolean {
|
||||||
|
return /[{}#\d]/.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenizeJinja2(text: string): Array<{text: string, type: string}> {
|
||||||
|
const tokens: Array<{text: string, type: string}> = [];
|
||||||
|
let i = 0;
|
||||||
|
let braceLevel = 0;
|
||||||
|
|
||||||
|
while (i < text.length) {
|
||||||
|
// Check for markdown headers (at start or after whitespace)
|
||||||
|
if (text[i] === '#' && (i === 0 || /\s/.test(text[i - 1]))) {
|
||||||
|
let headerLevel = 0;
|
||||||
|
let start = i;
|
||||||
|
while (i < text.length && text[i] === '#') {
|
||||||
|
headerLevel++;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
// Skip space after #
|
||||||
|
if (i < text.length && text[i] === ' ') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
// Get the rest of the header text
|
||||||
|
while (i < text.length && text[i] !== '\n' && !/[{}]/.test(text[i])) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
tokens.push({ text: text.slice(start, i), type: `header-${Math.min(headerLevel - 1, 5)}` });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for numbers
|
||||||
|
if (/\d/.test(text[i])) {
|
||||||
|
let start = i;
|
||||||
|
while (i < text.length && /[\d.]/.test(text[i])) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
tokens.push({ text: text.slice(start, i), type: 'number' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text[i] === '{') {
|
||||||
|
tokens.push({ text: '{', type: `brace-${braceLevel % 4}` });
|
||||||
|
braceLevel++;
|
||||||
|
i++;
|
||||||
|
} else if (text[i] === '}') {
|
||||||
|
braceLevel = Math.max(0, braceLevel - 1);
|
||||||
|
tokens.push({ text: '}', type: `brace-${braceLevel % 4}` });
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
let start = i;
|
||||||
|
while (i < text.length && text[i] !== '{' && text[i] !== '}' &&
|
||||||
|
!(text[i] === '#' && (i === 0 || /\s/.test(text[i - 1]))) &&
|
||||||
|
!/\d/.test(text[i])) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (start < i) {
|
||||||
|
tokens.push({ text: text.slice(start, i), type: 'text' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Jinja2HighlightPlugin;
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
||||||
import { $getRoot, $getSelection, $isRangeSelection, TextNode, $createTextNode } from 'lexical';
|
|
||||||
|
|
||||||
const JsonHighlightPlugin = () => {
|
|
||||||
const [editor] = useLexicalComposerContext();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
|
||||||
const text = textNode.getTextContent();
|
|
||||||
|
|
||||||
// Check if text contains JSON-like patterns
|
|
||||||
if (containsJsonPatterns(text)) {
|
|
||||||
const parent = textNode.getParent();
|
|
||||||
if (!parent) return;
|
|
||||||
|
|
||||||
// Split text into tokens and create new nodes with appropriate classes
|
|
||||||
const tokens = tokenizeJson(text);
|
|
||||||
const newNodes = tokens.map(token => {
|
|
||||||
const newNode = $createTextNode(token.text);
|
|
||||||
|
|
||||||
// Set format based on token type
|
|
||||||
switch (token.type) {
|
|
||||||
case 'string':
|
|
||||||
newNode.setFormat('code');
|
|
||||||
newNode.setStyle('color: #032f62');
|
|
||||||
break;
|
|
||||||
case 'number':
|
|
||||||
newNode.setFormat('code');
|
|
||||||
newNode.setStyle('color: #005cc5');
|
|
||||||
break;
|
|
||||||
case 'boolean':
|
|
||||||
newNode.setFormat('code');
|
|
||||||
newNode.setStyle('color: #d73a49');
|
|
||||||
break;
|
|
||||||
case 'null':
|
|
||||||
newNode.setFormat('code');
|
|
||||||
newNode.setStyle('color: #6f42c1');
|
|
||||||
break;
|
|
||||||
case 'key':
|
|
||||||
newNode.setFormat('code');
|
|
||||||
newNode.setStyle('color: #22863a; font-weight: bold');
|
|
||||||
break;
|
|
||||||
case 'punctuation':
|
|
||||||
newNode.setFormat('code');
|
|
||||||
newNode.setStyle('color: #24292e');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newNode;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Replace the original text node with the new highlighted nodes
|
|
||||||
if (newNodes.length > 1) {
|
|
||||||
textNode.replace(newNodes[0]);
|
|
||||||
for (let i = 1; i < newNodes.length; i++) {
|
|
||||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [editor]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function containsJsonPatterns(text: string): boolean {
|
|
||||||
// Check for JSON-like patterns
|
|
||||||
return /[{}\[\]:,]/.test(text) ||
|
|
||||||
/"[^"]*"/.test(text) ||
|
|
||||||
/\b\d+(\.\d+)?\b/.test(text) ||
|
|
||||||
/\b(true|false|null)\b/.test(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenizeJson(text: string): Array<{text: string, type: string}> {
|
|
||||||
const tokens: Array<{text: string, type: string}> = [];
|
|
||||||
const regex = /("[^"]*")|([{}\[\]:,])|(\b\d+(?:\.\d+)?\b)|(\b(?:true|false|null)\b)|(\s+)|([^\s{}\[\]:,"]+)/g;
|
|
||||||
|
|
||||||
let match;
|
|
||||||
while ((match = regex.exec(text)) !== null) {
|
|
||||||
const [fullMatch, string, punctuation, number, boolean, whitespace, other] = match;
|
|
||||||
|
|
||||||
if (string) {
|
|
||||||
// Check if it's a key (followed by colon)
|
|
||||||
const afterMatch = text.slice(match.index + fullMatch.length).trim();
|
|
||||||
if (afterMatch.startsWith(':')) {
|
|
||||||
tokens.push({ text: fullMatch, type: 'key' });
|
|
||||||
} else {
|
|
||||||
tokens.push({ text: fullMatch, type: 'string' });
|
|
||||||
}
|
|
||||||
} else if (punctuation) {
|
|
||||||
tokens.push({ text: fullMatch, type: 'punctuation' });
|
|
||||||
} else if (number) {
|
|
||||||
tokens.push({ text: fullMatch, type: 'number' });
|
|
||||||
} else if (boolean) {
|
|
||||||
if (fullMatch === 'null') {
|
|
||||||
tokens.push({ text: fullMatch, type: 'null' });
|
|
||||||
} else {
|
|
||||||
tokens.push({ text: fullMatch, type: 'boolean' });
|
|
||||||
}
|
|
||||||
} else if (whitespace || other) {
|
|
||||||
tokens.push({ text: fullMatch, type: 'text' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default JsonHighlightPlugin;
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
import { $getRoot } from 'lexical';
|
||||||
|
|
||||||
|
const LineNumberPlugin = () => {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
const [lineCount, setLineCount] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return editor.registerUpdateListener(({ editorState }) => {
|
||||||
|
editorState.read(() => {
|
||||||
|
const root = $getRoot();
|
||||||
|
const paragraphCount = root.getChildren().length;
|
||||||
|
const lines = Math.max(1, paragraphCount);
|
||||||
|
setLineCount(lines);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateLineNumbers = () => {
|
||||||
|
const lineNumbersElement = document.querySelector('.line-numbers');
|
||||||
|
const editorElement = document.querySelector('.editor-content-with-numbers');
|
||||||
|
|
||||||
|
if (lineNumbersElement && editorElement) {
|
||||||
|
const paragraphs = editorElement.querySelectorAll('p');
|
||||||
|
|
||||||
|
// Clear existing line numbers
|
||||||
|
lineNumbersElement.innerHTML = '';
|
||||||
|
|
||||||
|
// Create line numbers positioned at each paragraph
|
||||||
|
paragraphs.forEach((paragraph, index) => {
|
||||||
|
const lineNumber = document.createElement('div');
|
||||||
|
lineNumber.textContent = (index + 1).toString();
|
||||||
|
lineNumber.style.position = 'absolute';
|
||||||
|
lineNumber.style.top = paragraph.offsetTop + 'px';
|
||||||
|
lineNumber.style.right = '8px';
|
||||||
|
lineNumber.style.height = '20px';
|
||||||
|
lineNumber.style.lineHeight = '20px';
|
||||||
|
lineNumbersElement.appendChild(lineNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set line numbers container to relative positioning
|
||||||
|
(lineNumbersElement as HTMLElement).style.position = 'relative';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update line numbers after content changes
|
||||||
|
const timer = setTimeout(updateLineNumbers, 100);
|
||||||
|
|
||||||
|
// Also update on window resize
|
||||||
|
window.addEventListener('resize', updateLineNumbers);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
window.removeEventListener('resize', updateLineNumbers);
|
||||||
|
};
|
||||||
|
}, [lineCount]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LineNumberPlugin;
|
||||||
@@ -56,7 +56,8 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
|||||||
graph.addEdge({
|
graph.addEdge({
|
||||||
source: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'right')?.id || 'right' },
|
source: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'right')?.id || 'right' },
|
||||||
target: { cell: edge.getTargetCellId(), port: targetPortId },
|
target: { cell: edge.getTargetCellId(), port: targetPortId },
|
||||||
attrs: edge.getAttrs()
|
attrs: edge.getAttrs(),
|
||||||
|
zIndex: 3
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
zIndex: 3
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,6 +128,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
zIndex: 3
|
||||||
}
|
}
|
||||||
|
|
||||||
graph.addEdge(edgeConfig)
|
graph.addEdge(edgeConfig)
|
||||||
|
|||||||
@@ -101,8 +101,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
|||||||
<EditableTable
|
<EditableTable
|
||||||
parentName="headers"
|
parentName="headers"
|
||||||
title="HEADERS"
|
title="HEADERS"
|
||||||
options={options}
|
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||||
filterBooleanType={true}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -110,8 +109,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
|||||||
<EditableTable
|
<EditableTable
|
||||||
parentName="params"
|
parentName="params"
|
||||||
title="PARAMS"
|
title="PARAMS"
|
||||||
options={options}
|
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||||
filterBooleanType={true}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -134,8 +132,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
|||||||
<Form.Item name={['body', 'data']} noStyle>
|
<Form.Item name={['body', 'data']} noStyle>
|
||||||
<EditableTable
|
<EditableTable
|
||||||
parentName={['body', 'data']}
|
parentName={['body', 'data']}
|
||||||
options={options}
|
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||||
filterBooleanType={true}
|
|
||||||
typeOptions={[
|
typeOptions={[
|
||||||
{ label: 'text', value: 'text' },
|
{ label: 'text', value: 'text' },
|
||||||
{ label: 'file', value: 'file' }
|
{ label: 'file', value: 'file' }
|
||||||
@@ -168,7 +165,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
|||||||
<MessageEditor
|
<MessageEditor
|
||||||
key="raw"
|
key="raw"
|
||||||
parentName={['body', 'data']}
|
parentName={['body', 'data']}
|
||||||
options={options}
|
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||||
isArray={false}
|
isArray={false}
|
||||||
title="RAW TEXT"
|
title="RAW TEXT"
|
||||||
/>
|
/>
|
||||||
@@ -177,7 +174,8 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
|||||||
{values?.body?.content_type === 'binary' &&
|
{values?.body?.content_type === 'binary' &&
|
||||||
<Form.Item name={['body', 'data']}>
|
<Form.Item name={['body', 'data']}>
|
||||||
<VariableSelect
|
<VariableSelect
|
||||||
options={options}
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={options.filter(vo => vo.dataType.includes('file'))}
|
||||||
filterBooleanType={true}
|
filterBooleanType={true}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSyncingRef.current || lastSyncSourceRef.current === 'mapping' || selectedNode?.data?.type !== 'jinja-render' || !values?.mapping || !values?.template) return
|
if (isSyncingRef.current || lastSyncSourceRef.current === 'mapping' || selectedNode?.data?.type !== 'jinja-render' || !values?.mapping || !values?.template) return
|
||||||
|
|
||||||
const currentMappingNames = Array.isArray(values.mapping) ? values.mapping.map((item: any) => item.name).filter(Boolean) : []
|
const currentMappingNames = Array.isArray(values.mapping) ? values.mapping.filter(item => item && item.name).map((item: any) => item.name) : []
|
||||||
const prevNames = prevMappingNamesRef.current
|
const prevNames = prevMappingNamesRef.current
|
||||||
|
|
||||||
if (prevNames.length === 0) {
|
if (prevNames.length === 0) {
|
||||||
@@ -121,8 +121,8 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedMapping = Array.isArray(values.mapping) ? [...values.mapping] : []
|
const updatedMapping = Array.isArray(values.mapping) ? [...values.mapping.filter(item => item)] : []
|
||||||
const existingNames = updatedMapping.map(item => item.name)
|
const existingNames = updatedMapping.filter(item => item && item.name).map(item => item.name)
|
||||||
let updatedTemplate = String(values.template)
|
let updatedTemplate = String(values.template)
|
||||||
|
|
||||||
if (prevTemplateVarsRef.current.length > 0) {
|
if (prevTemplateVarsRef.current.length > 0) {
|
||||||
@@ -157,7 +157,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
|
|
||||||
isSyncingRef.current = true
|
isSyncingRef.current = true
|
||||||
lastSyncSourceRef.current = 'template'
|
lastSyncSourceRef.current = 'template'
|
||||||
prevMappingNamesRef.current = finalMapping.map((item: any) => item.name).filter(Boolean)
|
prevMappingNamesRef.current = finalMapping.filter(item => item && item.name).map((item: any) => item.name)
|
||||||
prevTemplateVarsRef.current = templateVars
|
prevTemplateVarsRef.current = templateVars
|
||||||
|
|
||||||
if (JSON.stringify(finalMapping) !== JSON.stringify(values.mapping)) {
|
if (JSON.stringify(finalMapping) !== JSON.stringify(values.mapping)) {
|
||||||
@@ -391,6 +391,54 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if parent loop/iteration is connected to http-request via ERROR connection
|
||||||
|
if (parentData.type === 'loop' || parentData.type === 'iteration') {
|
||||||
|
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
|
||||||
|
parentPreviousNodeIds.forEach(prevNodeId => {
|
||||||
|
const prevNode = nodes.find(n => n.id === prevNodeId);
|
||||||
|
if (!prevNode) return;
|
||||||
|
|
||||||
|
const prevNodeData = prevNode.getData();
|
||||||
|
if (prevNodeData.type === 'http-request') {
|
||||||
|
// Check if connected via ERROR connection point
|
||||||
|
const errorEdges = edges.filter(edge => {
|
||||||
|
return edge.getTargetCellId() === parentLoopNode.id &&
|
||||||
|
edge.getSourceCellId() === prevNodeId &&
|
||||||
|
edge.getSourcePortId() === 'ERROR'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errorEdges.length > 0) {
|
||||||
|
const errorMessageKey = `${prevNodeData.id}_error_message`;
|
||||||
|
const errorTypeKey = `${prevNodeData.id}_error_type`;
|
||||||
|
|
||||||
|
if (!addedKeys.has(errorMessageKey)) {
|
||||||
|
addedKeys.add(errorMessageKey);
|
||||||
|
variableList.push({
|
||||||
|
key: errorMessageKey,
|
||||||
|
label: 'error_message',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'string',
|
||||||
|
value: `${prevNodeData.id}.error_message`,
|
||||||
|
nodeData: prevNodeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!addedKeys.has(errorTypeKey)) {
|
||||||
|
addedKeys.add(errorTypeKey);
|
||||||
|
variableList.push({
|
||||||
|
key: errorTypeKey,
|
||||||
|
label: 'error_type',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'string',
|
||||||
|
value: `${prevNodeData.id}.error_type`,
|
||||||
|
nodeData: prevNodeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add variables from nodes preceding the parent loop/iteration node
|
// Add variables from nodes preceding the parent loop/iteration node
|
||||||
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
|
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
|
||||||
allRelevantNodeIds.push(...parentPreviousNodeIds);
|
allRelevantNodeIds.push(...parentPreviousNodeIds);
|
||||||
@@ -455,15 +503,15 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'knowledge-retrieval':
|
case 'knowledge-retrieval':
|
||||||
const knowledgeKey = `${dataNodeId}_message`;
|
const knowledgeKey = `${dataNodeId}_output`;
|
||||||
if (!addedKeys.has(knowledgeKey)) {
|
if (!addedKeys.has(knowledgeKey)) {
|
||||||
addedKeys.add(knowledgeKey);
|
addedKeys.add(knowledgeKey);
|
||||||
variableList.push({
|
variableList.push({
|
||||||
key: knowledgeKey,
|
key: knowledgeKey,
|
||||||
label: 'message',
|
label: 'output',
|
||||||
type: 'variable',
|
type: 'variable',
|
||||||
dataType: 'array[object]',
|
dataType: 'array[object]',
|
||||||
value: `${dataNodeId}.message`,
|
value: `${dataNodeId}.output`,
|
||||||
nodeData: nodeData,
|
nodeData: nodeData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -571,6 +619,42 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
nodeData: nodeData,
|
nodeData: nodeData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if connected via ERROR connection point
|
||||||
|
const errorEdges = edges.filter(edge =>
|
||||||
|
edge.getTargetCellId() === selectedNode.id &&
|
||||||
|
edge.getSourceCellId() === nodeId &&
|
||||||
|
edge.getSourcePortId() === 'ERROR'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (errorEdges.length > 0) {
|
||||||
|
const errorMessageKey = `${dataNodeId}_error_message`;
|
||||||
|
const errorTypeKey = `${dataNodeId}_error_type`;
|
||||||
|
|
||||||
|
if (!addedKeys.has(errorMessageKey)) {
|
||||||
|
addedKeys.add(errorMessageKey);
|
||||||
|
variableList.push({
|
||||||
|
key: errorMessageKey,
|
||||||
|
label: 'error_message',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'string',
|
||||||
|
value: `${dataNodeId}.error_message`,
|
||||||
|
nodeData: nodeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!addedKeys.has(errorTypeKey)) {
|
||||||
|
addedKeys.add(errorTypeKey);
|
||||||
|
variableList.push({
|
||||||
|
key: errorTypeKey,
|
||||||
|
label: 'error_type',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'string',
|
||||||
|
value: `${dataNodeId}.error_type`,
|
||||||
|
nodeData: nodeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'jinja-render':
|
case 'jinja-render':
|
||||||
const jinjaOutputKey = `${dataNodeId}_output`;
|
const jinjaOutputKey = `${dataNodeId}_output`;
|
||||||
@@ -613,7 +697,6 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'iteration':
|
case 'iteration':
|
||||||
console.log('iteration addedKeys', addedKeys)
|
|
||||||
const iterationOutputKey = `${dataNodeId}_output`;
|
const iterationOutputKey = `${dataNodeId}_output`;
|
||||||
const iterationItemKey = `${dataNodeId}_item`;
|
const iterationItemKey = `${dataNodeId}_item`;
|
||||||
if (!addedKeys.has(iterationOutputKey)) {
|
if (!addedKeys.has(iterationOutputKey)) {
|
||||||
@@ -651,18 +734,21 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
break
|
break
|
||||||
case 'loop':
|
case 'loop':
|
||||||
const cycleVars = nodeData.config.cycle_vars.defaultValue || [];
|
const cycleVars = nodeData.config.cycle_vars.defaultValue || [];
|
||||||
|
console.log('cycleVars', cycleVars)
|
||||||
cycleVars.forEach((cycleVar: any) => {
|
cycleVars.forEach((cycleVar: any) => {
|
||||||
const cycleVarKey = `${dataNodeId}_cycle_${cycleVar.name}`;
|
const cycleVarKey = `${dataNodeId}_cycle_${cycleVar.name}`;
|
||||||
if (!addedKeys.has(cycleVarKey)) {
|
if (!addedKeys.has(cycleVarKey)) {
|
||||||
addedKeys.add(cycleVarKey);
|
addedKeys.add(cycleVarKey);
|
||||||
variableList.push({
|
if (cycleVar.name && cycleVar.name.trim() !== '') {
|
||||||
key: cycleVarKey,
|
variableList.push({
|
||||||
label: cycleVar.name,
|
key: cycleVarKey,
|
||||||
type: 'variable',
|
label: cycleVar.name,
|
||||||
dataType: cycleVar.type || 'string',
|
type: 'variable',
|
||||||
value: `${dataNodeId}.${cycleVar.name}`,
|
dataType: cycleVar.type || 'string',
|
||||||
nodeData: nodeData,
|
value: `${dataNodeId}.${cycleVar.name}`,
|
||||||
});
|
nodeData: nodeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break
|
break
|
||||||
@@ -818,7 +904,11 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item key={key} name={key}>
|
<Form.Item key={key} name={key}>
|
||||||
<MessageEditor key={key} options={contextVariableList} parentName={key} />
|
<MessageEditor
|
||||||
|
key={key}
|
||||||
|
options={contextVariableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
|
||||||
|
parentName={key}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1010,39 +1100,25 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
? <ConditionList
|
? <ConditionList
|
||||||
parentName={key}
|
parentName={key}
|
||||||
options={(() => {
|
options={(() => {
|
||||||
// For loop nodes, add cycle_vars to condition options
|
const cycleVars = values?.cycle_vars || [];
|
||||||
if (selectedNode?.data?.type === 'loop') {
|
const cycleVarSuggestions: Suggestion[] = cycleVars.filter(vo => vo.name && vo.name.trim() !== '').map((cycleVar: any) => ({
|
||||||
const cycleVars = values?.cycle_vars || [];
|
key: `${selectedNode.id}_cycle_${cycleVar.name}`,
|
||||||
const cycleVarSuggestions: Suggestion[] = cycleVars.map((cycleVar: any) => ({
|
label: cycleVar.name,
|
||||||
key: `${selectedNode.id}_cycle_${cycleVar.name}`,
|
type: 'variable',
|
||||||
label: cycleVar.name,
|
dataType: cycleVar.type || 'String',
|
||||||
type: 'variable',
|
value: `${selectedNode.getData().id}.${cycleVar.name}`,
|
||||||
dataType: cycleVar.type || 'String',
|
nodeData: selectedNode.getData(),
|
||||||
value: `${selectedNode.getData().id}.${cycleVar.name}`,
|
}));
|
||||||
nodeData: selectedNode.getData(),
|
return [...variableList.filter(variable => {
|
||||||
}));
|
|
||||||
return [...getFilteredVariableList(selectedNode?.data?.type).filter(variable => {
|
|
||||||
// Keep conversation variables
|
|
||||||
if (variable.group === 'CONVERSATION') return true;
|
|
||||||
// Keep sys variables from start nodes
|
|
||||||
if (variable.nodeData?.type === 'start' && variable.value?.startsWith('sys.')) return true;
|
|
||||||
// Keep variables from non-start nodes
|
|
||||||
if (variable.nodeData?.type !== 'start') return true;
|
|
||||||
// Filter out custom variables from start nodes
|
|
||||||
return false;
|
|
||||||
}), ...cycleVarSuggestions];
|
|
||||||
}
|
|
||||||
// Filter options for condition list: only sys variables from start nodes and conversation variables
|
|
||||||
return getFilteredVariableList(selectedNode?.data?.type).filter(variable => {
|
|
||||||
// Keep conversation variables
|
// Keep conversation variables
|
||||||
if (variable.group === 'CONVERSATION') return true;
|
if (variable.group === 'CONVERSATION') return true;
|
||||||
// Keep sys variables from start nodes
|
// Keep sys variables from start nodes
|
||||||
if (variable.nodeData?.type === 'start' && variable.value?.startsWith('sys.')) return true;
|
if (variable.nodeData?.type === 'start' && variable.value?.startsWith('sys.')) return true;
|
||||||
// Keep variables from non-start nodes
|
// Keep variables from non-start nodes
|
||||||
if (variable.nodeData?.type !== 'start') return true;
|
if (variable.nodeData?.type !== 'start' && variable.nodeData?.type !== 'http-request' && variable.dataType !== 'boolean') return true;
|
||||||
// Filter out custom variables from start nodes
|
// Filter out custom variables from start nodes
|
||||||
return false;
|
return false;
|
||||||
});
|
}), ...cycleVarSuggestions];
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
selectedNode={selectedNode}
|
selectedNode={selectedNode}
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export const useWorkflowGraph = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
zIndex: 0
|
zIndex: targetCell.getData()?.cycle ? 3 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return edgeConfig
|
return edgeConfig
|
||||||
@@ -434,13 +434,13 @@ export const useWorkflowGraph = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
// 显示/隐藏连接桩
|
// 显示/隐藏连接桩
|
||||||
const showPorts = (show: boolean) => {
|
// const showPorts = (show: boolean) => {
|
||||||
const container = containerRef.current!;
|
// const container = containerRef.current!;
|
||||||
const ports = container.querySelectorAll('.x6-port-body') as NodeListOf<SVGElement>;
|
// const ports = container.querySelectorAll('.x6-port-body') as NodeListOf<SVGElement>;
|
||||||
for (let i = 0, len = ports.length; i < len; i += 1) {
|
// for (let i = 0, len = ports.length; i < len; i += 1) {
|
||||||
ports[i].style.visibility = show ? 'visible' : 'hidden';
|
// ports[i].style.visibility = show ? 'visible' : 'hidden';
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
// 节点选择事件
|
// 节点选择事件
|
||||||
const nodeClick = ({ node }: { node: Node }) => {
|
const nodeClick = ({ node }: { node: Node }) => {
|
||||||
// 忽略 add-node 类型的节点点击
|
// 忽略 add-node 类型的节点点击
|
||||||
|
|||||||
Reference in New Issue
Block a user