Remove deprecated files and modules from the txt2graph tool, including the main application, graph builder, text extractor, and associated resources. This cleanup streamlines the project structure and prepares for future enhancements.

This commit is contained in:
666ghj 2025-11-28 17:36:54 +08:00
parent 08f417f3b7
commit 3156f9453d
20 changed files with 0 additions and 3112 deletions

View file

@ -1,3 +0,0 @@
# Zep Cloud API Key
# 从 https://app.getzep.com 获取
ZEP_API_KEY=your_zep_api_key_here

View file

@ -1,114 +0,0 @@
# txt2graph
将文本文件PDF/Markdown/TXT转换为知识图谱的工具。
## 功能特点
- 支持多种文件格式PDF、Markdown、TXT
- 基于 Zep Cloud 的知识图谱构建
- 自动提取真实存在的实体(人物、公司、组织、地点、产品、事件、媒体)
- 交互式图谱可视化界面
- 实体和关系的详细展示
## 实体类型
本工具只提取现实生活中真实存在的、可以有行动的实体:
| 类型 | 说明 | 示例 |
|------|------|------|
| Person | 真实的人物 | 马化腾、Elon Musk |
| Company | 注册的公司 | 腾讯、Apple Inc. |
| Organization | 组织机构 | 武汉大学、联合国 |
| Location | 地理位置 | 北京、硅谷 |
| Product | 具体产品/服务 | iPhone、微信 |
| Event | 真实事件 | 2024年巴黎奥运会 |
| Media | 媒体机构 | 人民日报、CNN |
## 安装
### 1. 激活conda环境
```bash
conda activate MiroFish
```
### 2. 安装依赖
```bash
cd txt2graph
pip install -r requirements.txt
```
### 3. 配置环境变量
复制 `.env.example``.env` 并填入你的 Zep API Key
```bash
cp .env.example .env
# 编辑 .env 文件,填入 ZEP_API_KEY
```
获取 API Key: https://app.getzep.com
## 使用方法
### 方式1: Web界面推荐
启动 Streamlit 应用:
```bash
streamlit run app.py
```
然后在浏览器中打开显示的URL通常是 http://localhost:8501
### 方式2: 命令行
```python
from text_extractor import extract_text
from graph_builder import build_graph_from_text
# 从文件提取文本
text = extract_text("your_document.pdf")
# 构建知识图谱
graph_data = build_graph_from_text(
text=text,
graph_name="我的知识图谱",
progress_callback=print
)
# 查看结果
print(f"节点数: {len(graph_data.nodes)}")
print(f"边数: {len(graph_data.edges)}")
```
## 项目结构
```
txt2graph/
├── app.py # Streamlit Web应用
├── text_extractor.py # 文本提取模块
├── graph_builder.py # 图谱构建模块
├── ontology.py # 实体类型定义
├── requirements.txt # 依赖列表
├── .env.example # 环境变量示例
└── README.md # 说明文档
```
## 注意事项
1. **处理时间**:知识图谱构建可能需要几分钟,取决于文本长度
2. **API限制**Zep Cloud 有API调用限制大文件建议分批处理
3. **文本质量**:输入文本的质量直接影响实体提取效果
4. **费用**Zep Cloud 可能会产生API调用费用请查看其定价
## 技术栈
- [Zep Cloud](https://www.getzep.com/) - 知识图谱服务
- [Streamlit](https://streamlit.io/) - Web界面框架
- [PyVis](https://pyvis.readthedocs.io/) - 图可视化
- [PyMuPDF](https://pymupdf.readthedocs.io/) - PDF处理

View file

@ -1,497 +0,0 @@
"""
txt2graph 可视化界面
基于Streamlit和PyVis实现知识图谱可视化
"""
import os
import tempfile
import streamlit as st
from pathlib import Path
from pyvis.network import Network
import streamlit.components.v1 as components
from dotenv import load_dotenv
load_dotenv()
from text_extractor import extract_text, split_text_into_chunks
from graph_builder import ZepGraphBuilder, GraphData
# 页面配置
st.set_page_config(
page_title="txt2graph - 知识图谱生成器",
page_icon="🕸️",
layout="wide",
initial_sidebar_state="expanded"
)
# 自定义CSS样式
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&family=JetBrains+Mono&display=swap');
.main {
font-family: 'Noto Sans SC', sans-serif;
}
.stTitle {
font-weight: 700 !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stats-card {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px;
padding: 20px;
margin: 10px 0;
border: 1px solid rgba(102, 126, 234, 0.3);
}
.stats-number {
font-size: 2.5rem;
font-weight: 700;
color: #667eea;
font-family: 'JetBrains Mono', monospace;
}
.stats-label {
font-size: 0.9rem;
color: #a0a0a0;
text-transform: uppercase;
letter-spacing: 1px;
}
.entity-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
margin: 2px;
font-weight: 500;
}
.entity-Person { background: rgba(255, 107, 107, 0.2); color: #ff6b6b; border: 1px solid #ff6b6b; }
.entity-Company { background: rgba(78, 205, 196, 0.2); color: #4ecdc4; border: 1px solid #4ecdc4; }
.entity-Organization { background: rgba(69, 183, 209, 0.2); color: #45b7d1; border: 1px solid #45b7d1; }
.entity-Location { background: rgba(150, 206, 180, 0.2); color: #96ceb4; border: 1px solid #96ceb4; }
.entity-Product { background: rgba(255, 238, 173, 0.2); color: #ffeead; border: 1px solid #ffeead; }
.entity-Event { background: rgba(220, 198, 224, 0.2); color: #dcc6e0; border: 1px solid #dcc6e0; }
.entity-Media { background: rgba(255, 183, 77, 0.2); color: #ffb74d; border: 1px solid #ffb74d; }
.sidebar .stButton > button {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s ease;
}
.sidebar .stButton > button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
}
</style>
""", unsafe_allow_html=True)
# 实体类型对应的颜色
ENTITY_COLORS = {
"Person": "#ff6b6b",
"Company": "#4ecdc4",
"Organization": "#45b7d1",
"Location": "#96ceb4",
"Product": "#ffeead",
"Event": "#dcc6e0",
"Media": "#ffb74d",
}
def create_pyvis_graph(graph_data: GraphData) -> str:
"""
创建PyVis图并返回HTML
"""
# 创建网络图
net = Network(
height="700px",
width="100%",
bgcolor="#0e1117",
font_color="white",
directed=True,
select_menu=True,
filter_menu=True,
)
# 配置物理引擎
net.set_options("""
{
"nodes": {
"font": {
"size": 14,
"face": "Noto Sans SC, Arial"
},
"borderWidth": 2,
"shadow": true
},
"edges": {
"color": {
"inherit": false,
"color": "#555555",
"highlight": "#667eea"
},
"arrows": {
"to": {
"enabled": true,
"scaleFactor": 0.5
}
},
"smooth": {
"type": "continuous",
"roundness": 0.2
},
"font": {
"size": 10,
"color": "#888888",
"face": "Noto Sans SC, Arial"
}
},
"physics": {
"enabled": true,
"barnesHut": {
"gravitationalConstant": -5000,
"centralGravity": 0.3,
"springLength": 150,
"springConstant": 0.04,
"damping": 0.09
},
"stabilization": {
"enabled": true,
"iterations": 200
}
},
"interaction": {
"hover": true,
"tooltipDelay": 100,
"navigationButtons": true,
"keyboard": true
}
}
""")
# 构建节点UUID到名称的映射
node_map = {node.uuid: node for node in graph_data.nodes}
# 添加节点
for node in graph_data.nodes:
# 确定节点类型和颜色
node_type = node.labels[0] if node.labels else "Unknown"
color = ENTITY_COLORS.get(node_type, "#888888")
# 构建工具提示
title = f"<b>{node.name}</b><br>"
title += f"<i>类型: {node_type}</i><br><br>"
if node.summary:
title += f"{node.summary[:200]}{'...' if len(node.summary) > 200 else ''}"
# 根据节点类型调整大小
size = 25 if node_type == "Person" else 30 if node_type in ["Company", "Organization"] else 20
net.add_node(
node.uuid,
label=node.name,
title=title,
color=color,
size=size,
shape="dot",
)
# 添加边
for edge in graph_data.edges:
if edge.source_node_uuid in node_map and edge.target_node_uuid in node_map:
# 构建边的工具提示
title = edge.fact if edge.fact else edge.name
net.add_edge(
edge.source_node_uuid,
edge.target_node_uuid,
title=title,
label=edge.name[:20] if edge.name else "",
)
# 生成HTML
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
net.save_graph(f.name)
with open(f.name, 'r', encoding='utf-8') as html_file:
html_content = html_file.read()
os.unlink(f.name)
return html_content
def display_stats(graph_data: GraphData):
"""显示图谱统计信息"""
col1, col2, col3 = st.columns(3)
with col1:
st.markdown(f"""
<div class="stats-card">
<div class="stats-number">{len(graph_data.nodes)}</div>
<div class="stats-label">实体节点</div>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown(f"""
<div class="stats-card">
<div class="stats-number">{len(graph_data.edges)}</div>
<div class="stats-label">关系边</div>
</div>
""", unsafe_allow_html=True)
# 统计实体类型分布
type_counts = {}
for node in graph_data.nodes:
node_type = node.labels[0] if node.labels else "Unknown"
type_counts[node_type] = type_counts.get(node_type, 0) + 1
with col3:
st.markdown(f"""
<div class="stats-card">
<div class="stats-number">{len(type_counts)}</div>
<div class="stats-label">实体类型</div>
</div>
""", unsafe_allow_html=True)
def display_entity_list(graph_data: GraphData):
"""显示实体列表"""
st.subheader("实体列表")
# 按类型分组
entities_by_type = {}
for node in graph_data.nodes:
node_type = node.labels[0] if node.labels else "Unknown"
if node_type not in entities_by_type:
entities_by_type[node_type] = []
entities_by_type[node_type].append(node)
# 创建标签页
if entities_by_type:
tabs = st.tabs(list(entities_by_type.keys()))
for tab, (entity_type, entities) in zip(tabs, entities_by_type.items()):
with tab:
for entity in entities:
with st.expander(f"{entity.name}", expanded=False):
if entity.summary:
st.write(entity.summary)
if entity.attributes:
st.json(entity.attributes)
def main():
# 标题
st.title("txt2graph")
st.markdown("*将文本转化为知识图谱*")
# 侧边栏
with st.sidebar:
st.header("配置")
# API Key
api_key = st.text_input(
"Zep API Key",
type="password",
value=os.environ.get("ZEP_API_KEY", ""),
help="从 https://app.getzep.com 获取API Key"
)
if api_key:
os.environ["ZEP_API_KEY"] = api_key
st.divider()
# 文件上传
st.header("上传文件")
uploaded_file = st.file_uploader(
"支持 .txt, .md, .pdf 文件",
type=["txt", "md", "pdf"],
help="上传要转换为知识图谱的文本文件"
)
# 或者直接输入文本
st.divider()
st.header("或直接输入文本")
text_input = st.text_area(
"输入文本内容",
height=150,
placeholder="在此输入或粘贴文本..."
)
st.divider()
# 高级设置
with st.expander("高级设置"):
chunk_size = st.slider(
"文本分块大小",
min_value=500,
max_value=4000,
value=2000,
step=500,
help="较小的块处理更稳定,较大的块包含更多上下文"
)
graph_name = st.text_input(
"图谱名称",
value="Knowledge Graph",
help="为生成的图谱命名"
)
st.divider()
# 生成按钮
generate_btn = st.button("生成知识图谱", type="primary", use_container_width=True)
# 主内容区
if "graph_data" not in st.session_state:
st.session_state.graph_data = None
if generate_btn:
if not api_key:
st.error("请先配置 Zep API Key")
return
# 获取文本内容
text_content = None
if uploaded_file:
with st.spinner("正在提取文本..."):
# 保存上传的文件到临时位置
with tempfile.NamedTemporaryFile(delete=False, suffix=Path(uploaded_file.name).suffix) as tmp:
tmp.write(uploaded_file.getvalue())
tmp_path = tmp.name
try:
text_content = extract_text(tmp_path)
finally:
os.unlink(tmp_path)
elif text_input:
text_content = text_input
else:
st.warning("请上传文件或输入文本")
return
if text_content:
st.info(f"提取了 {len(text_content)} 个字符的文本")
# 进度显示
progress_bar = st.progress(0)
status_text = st.empty()
try:
# 创建图谱构建器
builder = ZepGraphBuilder(api_key=api_key)
# 创建图谱
status_text.text("创建图谱...")
progress_bar.progress(10)
graph_id = builder.create_graph(name=graph_name)
# 设置本体
status_text.text("配置实体类型...")
progress_bar.progress(20)
builder.set_ontology(graph_id)
# 分块
status_text.text("分割文本...")
progress_bar.progress(30)
chunks = split_text_into_chunks(text_content, max_chunk_size=chunk_size)
st.info(f"文本已分为 {len(chunks)} 个块")
# 添加到图谱
status_text.text("正在发送数据到Zep...")
progress_bar.progress(40)
def update_progress(msg):
status_text.text(msg)
# 分批发送数据
task_ids = builder.add_text_to_graph(
graph_id=graph_id,
text_chunks=chunks,
batch_size=3,
progress_callback=update_progress
)
# 等待处理完成
progress_bar.progress(60)
status_text.text("等待Zep处理数据...")
if task_ids:
builder.wait_for_tasks(
task_ids,
timeout=600,
progress_callback=update_progress
)
# 获取图数据
status_text.text("获取图谱数据...")
progress_bar.progress(90)
st.session_state.graph_data = builder.get_graph_data(graph_id)
st.session_state.graph_id = graph_id
progress_bar.progress(100)
status_text.text("完成!")
st.success(f"知识图谱生成成功! Graph ID: {graph_id}")
except Exception as e:
st.error(f"生成图谱时出错: {str(e)}")
import traceback
st.code(traceback.format_exc())
# 显示图谱
if st.session_state.graph_data:
graph_data = st.session_state.graph_data
# 统计信息
display_stats(graph_data)
st.divider()
# 图谱可视化
st.subheader("知识图谱可视化")
if graph_data.nodes:
with st.spinner("渲染图谱..."):
html_content = create_pyvis_graph(graph_data)
components.html(html_content, height=750, scrolling=True)
else:
st.warning("图谱中没有节点")
st.divider()
# 实体列表
col1, col2 = st.columns([1, 1])
with col1:
display_entity_list(graph_data)
with col2:
st.subheader("关系列表")
if graph_data.edges:
for edge in graph_data.edges[:50]: # 只显示前50条
st.markdown(f"- **{edge.fact}**" if edge.fact else f"- {edge.name}")
if len(graph_data.edges) > 50:
st.caption(f"...还有 {len(graph_data.edges) - 50} 条关系")
else:
st.info("暂无关系数据")
if __name__ == "__main__":
main()

View file

@ -1,415 +0,0 @@
"""
Zep图谱构建模块
负责与Zep云服务交互构建知识图谱
"""
import os
import time
import uuid
from typing import Optional, Callable
from dataclasses import dataclass
from zep_cloud.client import Zep
from zep_cloud import EpisodeData, EntityEdgeSourceTarget
from ontology import ENTITY_TYPES, EDGE_TYPES
@dataclass
class GraphNode:
"""图节点数据结构"""
uuid: str
name: str
summary: str
labels: list[str]
attributes: dict
@dataclass
class GraphEdge:
"""图边数据结构"""
uuid: str
name: str
fact: str
source_node_uuid: str
target_node_uuid: str
attributes: dict
@dataclass
class GraphData:
"""完整图数据"""
graph_id: str
nodes: list[GraphNode]
edges: list[GraphEdge]
class ZepGraphBuilder:
"""Zep知识图谱构建器"""
def __init__(self, api_key: Optional[str] = None):
"""
初始化图谱构建器
Args:
api_key: Zep API密钥如果不提供则从环境变量ZEP_API_KEY读取
"""
self.api_key = api_key or os.environ.get("ZEP_API_KEY")
if not self.api_key:
raise ValueError("需要提供ZEP_API_KEY可以通过参数传入或设置环境变量")
self.client = Zep(api_key=self.api_key)
def create_graph(self, graph_id: Optional[str] = None, name: str = "Knowledge Graph") -> str:
"""
创建新的图谱
Args:
graph_id: 图谱ID如果不提供则自动生成
name: 图谱名称
Returns:
图谱ID
"""
if graph_id is None:
graph_id = f"graph_{uuid.uuid4().hex[:16]}"
self.client.graph.create(
graph_id=graph_id,
name=name,
description="Knowledge graph generated by txt2graph"
)
return graph_id
def set_ontology(self, graph_id: str):
"""
为图谱设置自定义本体实体和边类型
Args:
graph_id: 图谱ID
"""
# 构建边类型的源目标映射
edge_definitions = {}
# WORKS_FOR: Person -> Organization/Company
edge_definitions["WORKS_FOR"] = (
EDGE_TYPES["WORKS_FOR"],
[
EntityEdgeSourceTarget(source="Person", target="Organization"),
EntityEdgeSourceTarget(source="Person", target="Company"),
]
)
# LOCATED_IN: 多种实体 -> Location
edge_definitions["LOCATED_IN"] = (
EDGE_TYPES["LOCATED_IN"],
[
EntityEdgeSourceTarget(source="Person", target="Location"),
EntityEdgeSourceTarget(source="Organization", target="Location"),
EntityEdgeSourceTarget(source="Company", target="Location"),
EntityEdgeSourceTarget(source="Event", target="Location"),
]
)
# PART_OF: Organization -> Organization, Company -> Company
edge_definitions["PART_OF"] = (
EDGE_TYPES["PART_OF"],
[
EntityEdgeSourceTarget(source="Organization", target="Organization"),
EntityEdgeSourceTarget(source="Company", target="Company"),
]
)
# PRODUCES: Company -> Product
edge_definitions["PRODUCES"] = (
EDGE_TYPES["PRODUCES"],
[
EntityEdgeSourceTarget(source="Company", target="Product"),
EntityEdgeSourceTarget(source="Organization", target="Product"),
]
)
# PARTICIPATES_IN: Person/Organization/Company -> Event
edge_definitions["PARTICIPATES_IN"] = (
EDGE_TYPES["PARTICIPATES_IN"],
[
EntityEdgeSourceTarget(source="Person", target="Event"),
EntityEdgeSourceTarget(source="Organization", target="Event"),
EntityEdgeSourceTarget(source="Company", target="Event"),
]
)
# COLLABORATES: 各种实体之间的合作
edge_definitions["COLLABORATES"] = (
EDGE_TYPES["COLLABORATES"],
[
EntityEdgeSourceTarget(source="Person", target="Person"),
EntityEdgeSourceTarget(source="Company", target="Company"),
EntityEdgeSourceTarget(source="Organization", target="Organization"),
EntityEdgeSourceTarget(source="Company", target="Organization"),
]
)
# COMPETES: 公司之间的竞争
edge_definitions["COMPETES"] = (
EDGE_TYPES["COMPETES"],
[
EntityEdgeSourceTarget(source="Company", target="Company"),
]
)
# REPORTS: Media报道相关实体
edge_definitions["REPORTS"] = (
EDGE_TYPES["REPORTS"],
[
EntityEdgeSourceTarget(source="Media", target="Person"),
EntityEdgeSourceTarget(source="Media", target="Company"),
EntityEdgeSourceTarget(source="Media", target="Organization"),
EntityEdgeSourceTarget(source="Media", target="Event"),
]
)
# 设置本体
self.client.graph.set_ontology(
graph_ids=[graph_id],
entities=ENTITY_TYPES,
edges=edge_definitions,
)
def add_text_to_graph(
self,
graph_id: str,
text_chunks: list[str],
batch_size: int = 3,
progress_callback: Optional[Callable] = None
) -> list[str]:
"""
将文本块分批添加到图谱中
Args:
graph_id: 图谱ID
text_chunks: 文本块列表
batch_size: 每批发送的块数量
progress_callback: 进度回调函数
Returns:
任务ID列表
"""
task_ids = []
total_chunks = len(text_chunks)
# 分批处理
for i in range(0, total_chunks, batch_size):
batch_chunks = text_chunks[i:i + batch_size]
batch_num = i // batch_size + 1
total_batches = (total_chunks + batch_size - 1) // batch_size
if progress_callback:
progress_callback(f"发送第 {batch_num}/{total_batches} 批数据 ({len(batch_chunks)} 块)...")
# 构建episode数据
episodes = [
EpisodeData(data=chunk, type="text")
for chunk in batch_chunks
]
try:
# 批量添加
batch_result = self.client.graph.add_batch(
graph_id=graph_id,
episodes=episodes
)
if batch_result and batch_result[0].task_id:
task_ids.append(batch_result[0].task_id)
# 短暂等待,避免请求过快
time.sleep(1)
except Exception as e:
if progress_callback:
progress_callback(f"批次 {batch_num} 发送失败: {str(e)}")
raise
return task_ids
def wait_for_tasks(
self,
task_ids: list[str],
timeout: int = 600,
progress_callback: Optional[Callable] = None
):
"""
等待所有任务完成
Args:
task_ids: 任务ID列表
timeout: 超时时间
progress_callback: 进度回调
"""
if not task_ids:
return
start_time = time.time()
pending_tasks = set(task_ids)
completed_tasks = set()
while pending_tasks:
if time.time() - start_time > timeout:
if progress_callback:
progress_callback(f"警告: 部分任务超时,已完成 {len(completed_tasks)}/{len(task_ids)}")
break
for task_id in list(pending_tasks):
try:
task = self.client.task.get(task_id=task_id)
if task.status == "completed":
pending_tasks.remove(task_id)
completed_tasks.add(task_id)
elif task.status == "failed":
pending_tasks.remove(task_id)
if progress_callback:
progress_callback(f"任务失败: {task.error}")
except Exception as e:
if progress_callback:
progress_callback(f"检查任务状态出错: {str(e)}")
if pending_tasks:
if progress_callback:
elapsed = int(time.time() - start_time)
progress_callback(f"等待处理中... 已完成 {len(completed_tasks)}/{len(task_ids)} ({elapsed}秒)")
time.sleep(3)
if progress_callback:
progress_callback(f"所有任务处理完成: {len(completed_tasks)}/{len(task_ids)}")
def get_graph_data(self, graph_id: str) -> GraphData:
"""
获取图谱的完整数据
Args:
graph_id: 图谱ID
Returns:
GraphData对象包含所有节点和边
"""
# 获取所有节点
raw_nodes = self.client.graph.node.get_by_graph_id(graph_id=graph_id)
nodes = [
GraphNode(
uuid=node.uuid_,
name=node.name,
summary=node.summary or "",
labels=node.labels or [],
attributes=node.attributes or {}
)
for node in raw_nodes
]
# 获取所有边
raw_edges = self.client.graph.edge.get_by_graph_id(graph_id=graph_id)
edges = [
GraphEdge(
uuid=edge.uuid_,
name=edge.name or "",
fact=edge.fact or "",
source_node_uuid=edge.source_node_uuid,
target_node_uuid=edge.target_node_uuid,
attributes=edge.attributes or {}
)
for edge in raw_edges
]
return GraphData(
graph_id=graph_id,
nodes=nodes,
edges=edges
)
def delete_graph(self, graph_id: str):
"""删除图谱"""
self.client.graph.delete(graph_id=graph_id)
def build_graph_from_text(
text: str,
graph_name: str = "Knowledge Graph",
api_key: Optional[str] = None,
chunk_size: int = 2000,
progress_callback: Optional[Callable] = None
) -> GraphData:
"""
便捷函数从文本构建知识图谱
Args:
text: 输入文本
graph_name: 图谱名称
api_key: Zep API密钥
chunk_size: 文本分块大小默认2000字符
progress_callback: 进度回调
Returns:
GraphData对象
"""
from text_extractor import split_text_into_chunks
builder = ZepGraphBuilder(api_key=api_key)
# 创建图谱
graph_id = builder.create_graph(name=graph_name)
if progress_callback:
progress_callback(f"创建图谱: {graph_id}")
# 设置本体
builder.set_ontology(graph_id)
if progress_callback:
progress_callback("设置实体类型...")
# 分块处理文本
chunks = split_text_into_chunks(text, max_chunk_size=chunk_size)
if progress_callback:
progress_callback(f"文本分为 {len(chunks)} 个块")
# 分批添加到图谱
task_ids = builder.add_text_to_graph(
graph_id=graph_id,
text_chunks=chunks,
batch_size=3,
progress_callback=progress_callback
)
# 等待所有任务完成
if task_ids:
builder.wait_for_tasks(task_ids, progress_callback=progress_callback)
# 获取并返回图数据
return builder.get_graph_data(graph_id)
if __name__ == "__main__":
# 测试
from dotenv import load_dotenv
load_dotenv()
test_text = """
武汉大学是中国著名的高等学府位于湖北省武汉市
该校的樱花季每年吸引大量游客
马化腾是腾讯公司的创始人腾讯总部位于深圳
"""
result = build_graph_from_text(
text=test_text,
graph_name="测试图谱",
progress_callback=print
)
print(f"\n节点数: {len(result.nodes)}")
for node in result.nodes:
print(f" - {node.name} ({node.labels})")
print(f"\n边数: {len(result.edges)}")
for edge in result.edges:
print(f" - {edge.fact}")

File diff suppressed because one or more lines are too long

View file

@ -1,189 +0,0 @@
function neighbourhoodHighlight(params) {
// console.log("in nieghbourhoodhighlight");
allNodes = nodes.get({ returnType: "Object" });
// originalNodes = JSON.parse(JSON.stringify(allNodes));
// if something is selected:
if (params.nodes.length > 0) {
highlightActive = true;
var i, j;
var selectedNode = params.nodes[0];
var degrees = 2;
// mark all nodes as hard to read.
for (let nodeId in allNodes) {
// nodeColors[nodeId] = allNodes[nodeId].color;
allNodes[nodeId].color = "rgba(200,200,200,0.5)";
if (allNodes[nodeId].hiddenLabel === undefined) {
allNodes[nodeId].hiddenLabel = allNodes[nodeId].label;
allNodes[nodeId].label = undefined;
}
}
var connectedNodes = network.getConnectedNodes(selectedNode);
var allConnectedNodes = [];
// get the second degree nodes
for (i = 1; i < degrees; i++) {
for (j = 0; j < connectedNodes.length; j++) {
allConnectedNodes = allConnectedNodes.concat(
network.getConnectedNodes(connectedNodes[j])
);
}
}
// all second degree nodes get a different color and their label back
for (i = 0; i < allConnectedNodes.length; i++) {
// allNodes[allConnectedNodes[i]].color = "pink";
allNodes[allConnectedNodes[i]].color = "rgba(150,150,150,0.75)";
if (allNodes[allConnectedNodes[i]].hiddenLabel !== undefined) {
allNodes[allConnectedNodes[i]].label =
allNodes[allConnectedNodes[i]].hiddenLabel;
allNodes[allConnectedNodes[i]].hiddenLabel = undefined;
}
}
// all first degree nodes get their own color and their label back
for (i = 0; i < connectedNodes.length; i++) {
// allNodes[connectedNodes[i]].color = undefined;
allNodes[connectedNodes[i]].color = nodeColors[connectedNodes[i]];
if (allNodes[connectedNodes[i]].hiddenLabel !== undefined) {
allNodes[connectedNodes[i]].label =
allNodes[connectedNodes[i]].hiddenLabel;
allNodes[connectedNodes[i]].hiddenLabel = undefined;
}
}
// the main node gets its own color and its label back.
// allNodes[selectedNode].color = undefined;
allNodes[selectedNode].color = nodeColors[selectedNode];
if (allNodes[selectedNode].hiddenLabel !== undefined) {
allNodes[selectedNode].label = allNodes[selectedNode].hiddenLabel;
allNodes[selectedNode].hiddenLabel = undefined;
}
} else if (highlightActive === true) {
// console.log("highlightActive was true");
// reset all nodes
for (let nodeId in allNodes) {
// allNodes[nodeId].color = "purple";
allNodes[nodeId].color = nodeColors[nodeId];
// delete allNodes[nodeId].color;
if (allNodes[nodeId].hiddenLabel !== undefined) {
allNodes[nodeId].label = allNodes[nodeId].hiddenLabel;
allNodes[nodeId].hiddenLabel = undefined;
}
}
highlightActive = false;
}
// transform the object into an array
var updateArray = [];
if (params.nodes.length > 0) {
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
// console.log(allNodes[nodeId]);
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
} else {
// console.log("Nothing was selected");
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
// console.log(allNodes[nodeId]);
// allNodes[nodeId].color = {};
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
}
}
function filterHighlight(params) {
allNodes = nodes.get({ returnType: "Object" });
// if something is selected:
if (params.nodes.length > 0) {
filterActive = true;
let selectedNodes = params.nodes;
// hiding all nodes and saving the label
for (let nodeId in allNodes) {
allNodes[nodeId].hidden = true;
if (allNodes[nodeId].savedLabel === undefined) {
allNodes[nodeId].savedLabel = allNodes[nodeId].label;
allNodes[nodeId].label = undefined;
}
}
for (let i=0; i < selectedNodes.length; i++) {
allNodes[selectedNodes[i]].hidden = false;
if (allNodes[selectedNodes[i]].savedLabel !== undefined) {
allNodes[selectedNodes[i]].label = allNodes[selectedNodes[i]].savedLabel;
allNodes[selectedNodes[i]].savedLabel = undefined;
}
}
} else if (filterActive === true) {
// reset all nodes
for (let nodeId in allNodes) {
allNodes[nodeId].hidden = false;
if (allNodes[nodeId].savedLabel !== undefined) {
allNodes[nodeId].label = allNodes[nodeId].savedLabel;
allNodes[nodeId].savedLabel = undefined;
}
}
filterActive = false;
}
// transform the object into an array
var updateArray = [];
if (params.nodes.length > 0) {
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
} else {
for (let nodeId in allNodes) {
if (allNodes.hasOwnProperty(nodeId)) {
updateArray.push(allNodes[nodeId]);
}
}
nodes.update(updateArray);
}
}
function selectNode(nodes) {
network.selectNodes(nodes);
neighbourhoodHighlight({ nodes: nodes });
return nodes;
}
function selectNodes(nodes) {
network.selectNodes(nodes);
filterHighlight({nodes: nodes});
return nodes;
}
function highlightFilter(filter) {
let selectedNodes = []
let selectedProp = filter['property']
if (filter['item'] === 'node') {
let allNodes = nodes.get({ returnType: "Object" });
for (let nodeId in allNodes) {
if (allNodes[nodeId][selectedProp] && filter['value'].includes((allNodes[nodeId][selectedProp]).toString())) {
selectedNodes.push(nodeId)
}
}
}
else if (filter['item'] === 'edge'){
let allEdges = edges.get({returnType: 'object'});
// check if the selected property exists for selected edge and select the nodes connected to the edge
for (let edge in allEdges) {
if (allEdges[edge][selectedProp] && filter['value'].includes((allEdges[edge][selectedProp]).toString())) {
selectedNodes.push(allEdges[edge]['from'])
selectedNodes.push(allEdges[edge]['to'])
}
}
}
selectNodes(selectedNodes)
}

View file

@ -1,356 +0,0 @@
/**
* Tom Select v2.0.0-rc.4
* Licensed under the Apache License, Version 2.0 (the "License");
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).TomSelect=t()}(this,(function(){"use strict"
function e(e,t){e.split(/\s+/).forEach((e=>{t(e)}))}class t{constructor(){this._events={}}on(t,i){e(t,(e=>{this._events[e]=this._events[e]||[],this._events[e].push(i)}))}off(t,i){var s=arguments.length
0!==s?e(t,(e=>{if(1===s)return delete this._events[e]
e in this._events!=!1&&this._events[e].splice(this._events[e].indexOf(i),1)})):this._events={}}trigger(t,...i){var s=this
e(t,(e=>{if(e in s._events!=!1)for(let t of s._events[e])t.apply(s,i)}))}}var i
const s="[̀-ͯ·ʾ]",n=new RegExp(s,"g")
var o
const r={"æ":"ae","ⱥ":"a","ø":"o"},l=new RegExp(Object.keys(r).join("|"),"g"),a=[[67,67],[160,160],[192,438],[452,652],[961,961],[1019,1019],[1083,1083],[1281,1289],[1984,1984],[5095,5095],[7429,7441],[7545,7549],[7680,7935],[8580,8580],[9398,9449],[11360,11391],[42792,42793],[42802,42851],[42873,42897],[42912,42922],[64256,64260],[65313,65338],[65345,65370]],c=e=>e.normalize("NFKD").replace(n,"").toLowerCase().replace(l,(function(e){return r[e]})),d=(e,t="|")=>{if(1==e.length)return e[0]
var i=1
return e.forEach((e=>{i=Math.max(i,e.length)})),1==i?"["+e.join("")+"]":"(?:"+e.join(t)+")"},p=e=>{if(1===e.length)return[[e]]
var t=[]
return p(e.substring(1)).forEach((function(i){var s=i.slice(0)
s[0]=e.charAt(0)+s[0],t.push(s),(s=i.slice(0)).unshift(e.charAt(0)),t.push(s)})),t},u=e=>{void 0===o&&(o=(()=>{var e={}
a.forEach((t=>{for(let s=t[0];s<=t[1];s++){let t=String.fromCharCode(s),n=c(t)
if(n!=t.toLowerCase()){n in e||(e[n]=[n])
var i=new RegExp(d(e[n]),"iu")
t.match(i)||e[n].push(t)}}}))
var t=Object.keys(e)
t=t.sort(((e,t)=>t.length-e.length)),i=new RegExp("("+d(t)+"[̀-ͯ·ʾ]*)","g")
var s={}
return t.sort(((e,t)=>e.length-t.length)).forEach((t=>{var i=p(t).map((t=>(t=t.map((t=>e.hasOwnProperty(t)?d(e[t]):t)),d(t,""))))
s[t]=d(i)})),s})())
return e.normalize("NFKD").toLowerCase().split(i).map((e=>{if(""==e)return""
const t=c(e)
if(o.hasOwnProperty(t))return o[t]
const i=e.normalize("NFC")
return i!=e?d([e,i]):e})).join("")},h=(e,t)=>{if(e)return e[t]},g=(e,t)=>{if(e){for(var i,s=t.split(".");(i=s.shift())&&(e=e[i]););return e}},f=(e,t,i)=>{var s,n
return e?-1===(n=(e+="").search(t.regex))?0:(s=t.string.length/e.length,0===n&&(s+=.5),s*i):0},v=e=>(e+"").replace(/([\$\(-\+\.\?\[-\^\{-\}])/g,"\\$1"),m=(e,t)=>{var i=e[t]
if("function"==typeof i)return i
i&&!Array.isArray(i)&&(e[t]=[i])},y=(e,t)=>{if(Array.isArray(e))e.forEach(t)
else for(var i in e)e.hasOwnProperty(i)&&t(e[i],i)},O=(e,t)=>"number"==typeof e&&"number"==typeof t?e>t?1:e<t?-1:0:(e=c(e+"").toLowerCase())>(t=c(t+"").toLowerCase())?1:t>e?-1:0
class b{constructor(e,t){this.items=e,this.settings=t||{diacritics:!0}}tokenize(e,t,i){if(!e||!e.length)return[]
const s=[],n=e.split(/\s+/)
var o
return i&&(o=new RegExp("^("+Object.keys(i).map(v).join("|")+"):(.*)$")),n.forEach((e=>{let i,n=null,r=null
o&&(i=e.match(o))&&(n=i[1],e=i[2]),e.length>0&&(r=v(e),this.settings.diacritics&&(r=u(r)),t&&(r="\\b"+r)),s.push({string:e,regex:r?new RegExp(r,"iu"):null,field:n})})),s}getScoreFunction(e,t){var i=this.prepareSearch(e,t)
return this._getScoreFunction(i)}_getScoreFunction(e){const t=e.tokens,i=t.length
if(!i)return function(){return 0}
const s=e.options.fields,n=e.weights,o=s.length,r=e.getAttrFn
if(!o)return function(){return 1}
const l=1===o?function(e,t){const i=s[0].field
return f(r(t,i),e,n[i])}:function(e,t){var i=0
if(e.field){const s=r(t,e.field)
!e.regex&&s?i+=1/o:i+=f(s,e,1)}else y(n,((s,n)=>{i+=f(r(t,n),e,s)}))
return i/o}
return 1===i?function(e){return l(t[0],e)}:"and"===e.options.conjunction?function(e){for(var s,n=0,o=0;n<i;n++){if((s=l(t[n],e))<=0)return 0
o+=s}return o/i}:function(e){var s=0
return y(t,(t=>{s+=l(t,e)})),s/i}}getSortFunction(e,t){var i=this.prepareSearch(e,t)
return this._getSortFunction(i)}_getSortFunction(e){var t,i,s
const n=this,o=e.options,r=!e.query&&o.sort_empty?o.sort_empty:o.sort,l=[],a=[]
if("function"==typeof r)return r.bind(this)
const c=function(t,i){return"$score"===t?i.score:e.getAttrFn(n.items[i.id],t)}
if(r)for(t=0,i=r.length;t<i;t++)(e.query||"$score"!==r[t].field)&&l.push(r[t])
if(e.query){for(s=!0,t=0,i=l.length;t<i;t++)if("$score"===l[t].field){s=!1
break}s&&l.unshift({field:"$score",direction:"desc"})}else for(t=0,i=l.length;t<i;t++)if("$score"===l[t].field){l.splice(t,1)
break}for(t=0,i=l.length;t<i;t++)a.push("desc"===l[t].direction?-1:1)
const d=l.length
if(d){if(1===d){const e=l[0].field,t=a[0]
return function(i,s){return t*O(c(e,i),c(e,s))}}return function(e,t){var i,s,n
for(i=0;i<d;i++)if(n=l[i].field,s=a[i]*O(c(n,e),c(n,t)))return s
return 0}}return null}prepareSearch(e,t){const i={}
var s=Object.assign({},t)
if(m(s,"sort"),m(s,"sort_empty"),s.fields){m(s,"fields")
const e=[]
s.fields.forEach((t=>{"string"==typeof t&&(t={field:t,weight:1}),e.push(t),i[t.field]="weight"in t?t.weight:1})),s.fields=e}return{options:s,query:e.toLowerCase().trim(),tokens:this.tokenize(e,s.respect_word_boundaries,i),total:0,items:[],weights:i,getAttrFn:s.nesting?g:h}}search(e,t){var i,s,n=this
s=this.prepareSearch(e,t),t=s.options,e=s.query
const o=t.score||n._getScoreFunction(s)
e.length?y(n.items,((e,n)=>{i=o(e),(!1===t.filter||i>0)&&s.items.push({score:i,id:n})})):y(n.items,((e,t)=>{s.items.push({score:1,id:t})}))
const r=n._getSortFunction(s)
return r&&s.items.sort(r),s.total=s.items.length,"number"==typeof t.limit&&(s.items=s.items.slice(0,t.limit)),s}}const w=e=>{if(e.jquery)return e[0]
if(e instanceof HTMLElement)return e
if(e.indexOf("<")>-1){let t=document.createElement("div")
return t.innerHTML=e.trim(),t.firstChild}return document.querySelector(e)},_=(e,t)=>{var i=document.createEvent("HTMLEvents")
i.initEvent(t,!0,!1),e.dispatchEvent(i)},I=(e,t)=>{Object.assign(e.style,t)},C=(e,...t)=>{var i=A(t);(e=x(e)).map((e=>{i.map((t=>{e.classList.add(t)}))}))},S=(e,...t)=>{var i=A(t);(e=x(e)).map((e=>{i.map((t=>{e.classList.remove(t)}))}))},A=e=>{var t=[]
return y(e,(e=>{"string"==typeof e&&(e=e.trim().split(/[\11\12\14\15\40]/)),Array.isArray(e)&&(t=t.concat(e))})),t.filter(Boolean)},x=e=>(Array.isArray(e)||(e=[e]),e),k=(e,t,i)=>{if(!i||i.contains(e))for(;e&&e.matches;){if(e.matches(t))return e
e=e.parentNode}},F=(e,t=0)=>t>0?e[e.length-1]:e[0],L=(e,t)=>{if(!e)return-1
t=t||e.nodeName
for(var i=0;e=e.previousElementSibling;)e.matches(t)&&i++
return i},P=(e,t)=>{y(t,((t,i)=>{null==t?e.removeAttribute(i):e.setAttribute(i,""+t)}))},E=(e,t)=>{e.parentNode&&e.parentNode.replaceChild(t,e)},T=(e,t)=>{if(null===t)return
if("string"==typeof t){if(!t.length)return
t=new RegExp(t,"i")}const i=e=>3===e.nodeType?(e=>{var i=e.data.match(t)
if(i&&e.data.length>0){var s=document.createElement("span")
s.className="highlight"
var n=e.splitText(i.index)
n.splitText(i[0].length)
var o=n.cloneNode(!0)
return s.appendChild(o),E(n,s),1}return 0})(e):((e=>{if(1===e.nodeType&&e.childNodes&&!/(script|style)/i.test(e.tagName)&&("highlight"!==e.className||"SPAN"!==e.tagName))for(var t=0;t<e.childNodes.length;++t)t+=i(e.childNodes[t])})(e),0)
i(e)},V="undefined"!=typeof navigator&&/Mac/.test(navigator.userAgent)?"metaKey":"ctrlKey"
var j={options:[],optgroups:[],plugins:[],delimiter:",",splitOn:null,persist:!0,diacritics:!0,create:null,createOnBlur:!1,createFilter:null,highlight:!0,openOnFocus:!0,shouldOpen:null,maxOptions:50,maxItems:null,hideSelected:null,duplicates:!1,addPrecedence:!1,selectOnTab:!1,preload:null,allowEmptyOption:!1,loadThrottle:300,loadingClass:"loading",dataAttr:null,optgroupField:"optgroup",valueField:"value",labelField:"text",disabledField:"disabled",optgroupLabelField:"label",optgroupValueField:"value",lockOptgroupOrder:!1,sortField:"$order",searchField:["text"],searchConjunction:"and",mode:null,wrapperClass:"ts-wrapper",controlClass:"ts-control",dropdownClass:"ts-dropdown",dropdownContentClass:"ts-dropdown-content",itemClass:"item",optionClass:"option",dropdownParent:null,copyClassesToDropdown:!1,placeholder:null,hidePlaceholder:null,shouldLoad:function(e){return e.length>0},render:{}}
const q=e=>null==e?null:D(e),D=e=>"boolean"==typeof e?e?"1":"0":e+"",N=e=>(e+"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),z=(e,t)=>{var i
return function(s,n){var o=this
i&&(o.loading=Math.max(o.loading-1,0),clearTimeout(i)),i=setTimeout((function(){i=null,o.loadedSearches[s]=!0,e.call(o,s,n)}),t)}},R=(e,t,i)=>{var s,n=e.trigger,o={}
for(s in e.trigger=function(){var i=arguments[0]
if(-1===t.indexOf(i))return n.apply(e,arguments)
o[i]=arguments},i.apply(e,[]),e.trigger=n,o)n.apply(e,o[s])},H=(e,t=!1)=>{e&&(e.preventDefault(),t&&e.stopPropagation())},B=(e,t,i,s)=>{e.addEventListener(t,i,s)},K=(e,t)=>!!t&&(!!t[e]&&1===(t.altKey?1:0)+(t.ctrlKey?1:0)+(t.shiftKey?1:0)+(t.metaKey?1:0)),M=(e,t)=>{const i=e.getAttribute("id")
return i||(e.setAttribute("id",t),t)},Q=e=>e.replace(/[\\"']/g,"\\$&"),G=(e,t)=>{t&&e.append(t)}
function U(e,t){var i=Object.assign({},j,t),s=i.dataAttr,n=i.labelField,o=i.valueField,r=i.disabledField,l=i.optgroupField,a=i.optgroupLabelField,c=i.optgroupValueField,d=e.tagName.toLowerCase(),p=e.getAttribute("placeholder")||e.getAttribute("data-placeholder")
if(!p&&!i.allowEmptyOption){let t=e.querySelector('option[value=""]')
t&&(p=t.textContent)}var u,h,g,f,v,m,O={placeholder:p,options:[],optgroups:[],items:[],maxItems:null}
return"select"===d?(h=O.options,g={},f=1,v=e=>{var t=Object.assign({},e.dataset),i=s&&t[s]
return"string"==typeof i&&i.length&&(t=Object.assign(t,JSON.parse(i))),t},m=(e,t)=>{var s=q(e.value)
if(null!=s&&(s||i.allowEmptyOption)){if(g.hasOwnProperty(s)){if(t){var a=g[s][l]
a?Array.isArray(a)?a.push(t):g[s][l]=[a,t]:g[s][l]=t}}else{var c=v(e)
c[n]=c[n]||e.textContent,c[o]=c[o]||s,c[r]=c[r]||e.disabled,c[l]=c[l]||t,c.$option=e,g[s]=c,h.push(c)}e.selected&&O.items.push(s)}},O.maxItems=e.hasAttribute("multiple")?null:1,y(e.children,(e=>{var t,i,s
"optgroup"===(u=e.tagName.toLowerCase())?((s=v(t=e))[a]=s[a]||t.getAttribute("label")||"",s[c]=s[c]||f++,s[r]=s[r]||t.disabled,O.optgroups.push(s),i=s[c],y(t.children,(e=>{m(e,i)}))):"option"===u&&m(e)}))):(()=>{const t=e.getAttribute(s)
if(t)O.options=JSON.parse(t),y(O.options,(e=>{O.items.push(e[o])}))
else{var r=e.value.trim()||""
if(!i.allowEmptyOption&&!r.length)return
const t=r.split(i.delimiter)
y(t,(e=>{const t={}
t[n]=e,t[o]=e,O.options.push(t)})),O.items=t}})(),Object.assign({},j,O,t)}var W=0
class J extends(function(e){return e.plugins={},class extends e{constructor(...e){super(...e),this.plugins={names:[],settings:{},requested:{},loaded:{}}}static define(t,i){e.plugins[t]={name:t,fn:i}}initializePlugins(e){var t,i
const s=this,n=[]
if(Array.isArray(e))e.forEach((e=>{"string"==typeof e?n.push(e):(s.plugins.settings[e.name]=e.options,n.push(e.name))}))
else if(e)for(t in e)e.hasOwnProperty(t)&&(s.plugins.settings[t]=e[t],n.push(t))
for(;i=n.shift();)s.require(i)}loadPlugin(t){var i=this,s=i.plugins,n=e.plugins[t]
if(!e.plugins.hasOwnProperty(t))throw new Error('Unable to find "'+t+'" plugin')
s.requested[t]=!0,s.loaded[t]=n.fn.apply(i,[i.plugins.settings[t]||{}]),s.names.push(t)}require(e){var t=this,i=t.plugins
if(!t.plugins.loaded.hasOwnProperty(e)){if(i.requested[e])throw new Error('Plugin has circular dependency ("'+e+'")')
t.loadPlugin(e)}return i.loaded[e]}}}(t)){constructor(e,t){var i
super(),this.order=0,this.isOpen=!1,this.isDisabled=!1,this.isInvalid=!1,this.isValid=!0,this.isLocked=!1,this.isFocused=!1,this.isInputHidden=!1,this.isSetup=!1,this.ignoreFocus=!1,this.hasOptions=!1,this.lastValue="",this.caretPos=0,this.loading=0,this.loadedSearches={},this.activeOption=null,this.activeItems=[],this.optgroups={},this.options={},this.userOptions={},this.items=[],W++
var s=w(e)
if(s.tomselect)throw new Error("Tom Select already initialized on this element")
s.tomselect=this,i=(window.getComputedStyle&&window.getComputedStyle(s,null)).getPropertyValue("direction")
const n=U(s,t)
this.settings=n,this.input=s,this.tabIndex=s.tabIndex||0,this.is_select_tag="select"===s.tagName.toLowerCase(),this.rtl=/rtl/i.test(i),this.inputId=M(s,"tomselect-"+W),this.isRequired=s.required,this.sifter=new b(this.options,{diacritics:n.diacritics}),n.mode=n.mode||(1===n.maxItems?"single":"multi"),"boolean"!=typeof n.hideSelected&&(n.hideSelected="multi"===n.mode),"boolean"!=typeof n.hidePlaceholder&&(n.hidePlaceholder="multi"!==n.mode)
var o=n.createFilter
"function"!=typeof o&&("string"==typeof o&&(o=new RegExp(o)),o instanceof RegExp?n.createFilter=e=>o.test(e):n.createFilter=()=>!0),this.initializePlugins(n.plugins),this.setupCallbacks(),this.setupTemplates()
const r=w("<div>"),l=w("<div>"),a=this._render("dropdown"),c=w('<div role="listbox" tabindex="-1">'),d=this.input.getAttribute("class")||"",p=n.mode
var u
if(C(r,n.wrapperClass,d,p),C(l,n.controlClass),G(r,l),C(a,n.dropdownClass,p),n.copyClassesToDropdown&&C(a,d),C(c,n.dropdownContentClass),G(a,c),w(n.dropdownParent||r).appendChild(a),n.hasOwnProperty("controlInput"))n.controlInput?(u=w(n.controlInput),this.focus_node=u):(u=w("<input/>"),this.focus_node=l)
else{u=w('<input type="text" autocomplete="off" size="1" />')
y(["autocorrect","autocapitalize","autocomplete"],(e=>{s.getAttribute(e)&&P(u,{[e]:s.getAttribute(e)})})),u.tabIndex=-1,l.appendChild(u),this.focus_node=u}this.wrapper=r,this.dropdown=a,this.dropdown_content=c,this.control=l,this.control_input=u,this.setup()}setup(){const e=this,t=e.settings,i=e.control_input,s=e.dropdown,n=e.dropdown_content,o=e.wrapper,r=e.control,l=e.input,a=e.focus_node,c={passive:!0},d=e.inputId+"-ts-dropdown"
P(n,{id:d}),P(a,{role:"combobox","aria-haspopup":"listbox","aria-expanded":"false","aria-controls":d})
const p=M(a,e.inputId+"-ts-control"),u="label[for='"+(e=>e.replace(/['"\\]/g,"\\$&"))(e.inputId)+"']",h=document.querySelector(u),g=e.focus.bind(e)
if(h){B(h,"click",g),P(h,{for:p})
const t=M(h,e.inputId+"-ts-label")
P(a,{"aria-labelledby":t}),P(n,{"aria-labelledby":t})}if(o.style.width=l.style.width,e.plugins.names.length){const t="plugin-"+e.plugins.names.join(" plugin-")
C([o,s],t)}(null===t.maxItems||t.maxItems>1)&&e.is_select_tag&&P(l,{multiple:"multiple"}),e.settings.placeholder&&P(i,{placeholder:t.placeholder}),!e.settings.splitOn&&e.settings.delimiter&&(e.settings.splitOn=new RegExp("\\s*"+v(e.settings.delimiter)+"+\\s*")),t.load&&t.loadThrottle&&(t.load=z(t.load,t.loadThrottle)),e.control_input.type=l.type,B(s,"click",(t=>{const i=k(t.target,"[data-selectable]")
i&&(e.onOptionSelect(t,i),H(t,!0))})),B(r,"click",(t=>{var s=k(t.target,"[data-ts-item]",r)
s&&e.onItemSelect(t,s)?H(t,!0):""==i.value&&(e.onClick(),H(t,!0))})),B(i,"mousedown",(e=>{""!==i.value&&e.stopPropagation()})),B(a,"keydown",(t=>e.onKeyDown(t))),B(i,"keypress",(t=>e.onKeyPress(t))),B(i,"input",(t=>e.onInput(t))),B(a,"resize",(()=>e.positionDropdown()),c),B(a,"blur",(t=>e.onBlur(t))),B(a,"focus",(t=>e.onFocus(t))),B(a,"paste",(t=>e.onPaste(t)))
const f=t=>{const i=t.composedPath()[0]
if(!o.contains(i)&&!s.contains(i))return e.isFocused&&e.blur(),void e.inputState()
H(t,!0)}
var m=()=>{e.isOpen&&e.positionDropdown()}
B(document,"mousedown",f),B(window,"scroll",m,c),B(window,"resize",m,c),this._destroy=()=>{document.removeEventListener("mousedown",f),window.removeEventListener("sroll",m),window.removeEventListener("resize",m),h&&h.removeEventListener("click",g)},this.revertSettings={innerHTML:l.innerHTML,tabIndex:l.tabIndex},l.tabIndex=-1,l.insertAdjacentElement("afterend",e.wrapper),e.sync(!1),t.items=[],delete t.optgroups,delete t.options,B(l,"invalid",(t=>{e.isValid&&(e.isValid=!1,e.isInvalid=!0,e.refreshState())})),e.updateOriginalInput(),e.refreshItems(),e.close(!1),e.inputState(),e.isSetup=!0,l.disabled?e.disable():e.enable(),e.on("change",this.onChange),C(l,"tomselected","ts-hidden-accessible"),e.trigger("initialize"),!0===t.preload&&e.preload()}setupOptions(e=[],t=[]){this.addOptions(e),y(t,(e=>{this.registerOptionGroup(e)}))}setupTemplates(){var e=this,t=e.settings.labelField,i=e.settings.optgroupLabelField,s={optgroup:e=>{let t=document.createElement("div")
return t.className="optgroup",t.appendChild(e.options),t},optgroup_header:(e,t)=>'<div class="optgroup-header">'+t(e[i])+"</div>",option:(e,i)=>"<div>"+i(e[t])+"</div>",item:(e,i)=>"<div>"+i(e[t])+"</div>",option_create:(e,t)=>'<div class="create">Add <strong>'+t(e.input)+"</strong>&hellip;</div>",no_results:()=>'<div class="no-results">No results found</div>',loading:()=>'<div class="spinner"></div>',not_loading:()=>{},dropdown:()=>"<div></div>"}
e.settings.render=Object.assign({},s,e.settings.render)}setupCallbacks(){var e,t,i={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",item_select:"onItemSelect",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"}
for(e in i)(t=this.settings[i[e]])&&this.on(e,t)}sync(e=!0){const t=this,i=e?U(t.input,{delimiter:t.settings.delimiter}):t.settings
t.setupOptions(i.options,i.optgroups),t.setValue(i.items,!0),t.lastQuery=null}onClick(){var e=this
if(e.activeItems.length>0)return e.clearActiveItems(),void e.focus()
e.isFocused&&e.isOpen?e.blur():e.focus()}onMouseDown(){}onChange(){_(this.input,"input"),_(this.input,"change")}onPaste(e){var t=this
t.isFull()||t.isInputHidden||t.isLocked?H(e):t.settings.splitOn&&setTimeout((()=>{var e=t.inputValue()
if(e.match(t.settings.splitOn)){var i=e.trim().split(t.settings.splitOn)
y(i,(e=>{t.createItem(e)}))}}),0)}onKeyPress(e){var t=this
if(!t.isLocked){var i=String.fromCharCode(e.keyCode||e.which)
return t.settings.create&&"multi"===t.settings.mode&&i===t.settings.delimiter?(t.createItem(),void H(e)):void 0}H(e)}onKeyDown(e){var t=this
if(t.isLocked)9!==e.keyCode&&H(e)
else{switch(e.keyCode){case 65:if(K(V,e))return H(e),void t.selectAll()
break
case 27:return t.isOpen&&(H(e,!0),t.close()),void t.clearActiveItems()
case 40:if(!t.isOpen&&t.hasOptions)t.open()
else if(t.activeOption){let e=t.getAdjacent(t.activeOption,1)
e&&t.setActiveOption(e)}return void H(e)
case 38:if(t.activeOption){let e=t.getAdjacent(t.activeOption,-1)
e&&t.setActiveOption(e)}return void H(e)
case 13:return void(t.isOpen&&t.activeOption?(t.onOptionSelect(e,t.activeOption),H(e)):t.settings.create&&t.createItem()&&H(e))
case 37:return void t.advanceSelection(-1,e)
case 39:return void t.advanceSelection(1,e)
case 9:return void(t.settings.selectOnTab&&(t.isOpen&&t.activeOption&&(t.onOptionSelect(e,t.activeOption),H(e)),t.settings.create&&t.createItem()&&H(e)))
case 8:case 46:return void t.deleteSelection(e)}t.isInputHidden&&!K(V,e)&&H(e)}}onInput(e){var t=this
if(!t.isLocked){var i=t.inputValue()
t.lastValue!==i&&(t.lastValue=i,t.settings.shouldLoad.call(t,i)&&t.load(i),t.refreshOptions(),t.trigger("type",i))}}onFocus(e){var t=this,i=t.isFocused
if(t.isDisabled)return t.blur(),void H(e)
t.ignoreFocus||(t.isFocused=!0,"focus"===t.settings.preload&&t.preload(),i||t.trigger("focus"),t.activeItems.length||(t.showInput(),t.refreshOptions(!!t.settings.openOnFocus)),t.refreshState())}onBlur(e){if(!1!==document.hasFocus()){var t=this
if(t.isFocused){t.isFocused=!1,t.ignoreFocus=!1
var i=()=>{t.close(),t.setActiveItem(),t.setCaret(t.items.length),t.trigger("blur")}
t.settings.create&&t.settings.createOnBlur?t.createItem(null,!1,i):i()}}}onOptionSelect(e,t){var i,s=this
t&&(t.parentElement&&t.parentElement.matches("[data-disabled]")||(t.classList.contains("create")?s.createItem(null,!0,(()=>{s.settings.closeAfterSelect&&s.close()})):void 0!==(i=t.dataset.value)&&(s.lastQuery=null,s.addItem(i),s.settings.closeAfterSelect&&s.close(),!s.settings.hideSelected&&e.type&&/click/.test(e.type)&&s.setActiveOption(t))))}onItemSelect(e,t){var i=this
return!i.isLocked&&"multi"===i.settings.mode&&(H(e),i.setActiveItem(t,e),!0)}canLoad(e){return!!this.settings.load&&!this.loadedSearches.hasOwnProperty(e)}load(e){const t=this
if(!t.canLoad(e))return
C(t.wrapper,t.settings.loadingClass),t.loading++
const i=t.loadCallback.bind(t)
t.settings.load.call(t,e,i)}loadCallback(e,t){const i=this
i.loading=Math.max(i.loading-1,0),i.lastQuery=null,i.clearActiveOption(),i.setupOptions(e,t),i.refreshOptions(i.isFocused&&!i.isInputHidden),i.loading||S(i.wrapper,i.settings.loadingClass),i.trigger("load",e,t)}preload(){var e=this.wrapper.classList
e.contains("preloaded")||(e.add("preloaded"),this.load(""))}setTextboxValue(e=""){var t=this.control_input
t.value!==e&&(t.value=e,_(t,"update"),this.lastValue=e)}getValue(){return this.is_select_tag&&this.input.hasAttribute("multiple")?this.items:this.items.join(this.settings.delimiter)}setValue(e,t){R(this,t?[]:["change"],(()=>{this.clear(t),this.addItems(e,t)}))}setMaxItems(e){0===e&&(e=null),this.settings.maxItems=e,this.refreshState()}setActiveItem(e,t){var i,s,n,o,r,l,a=this
if("single"!==a.settings.mode){if(!e)return a.clearActiveItems(),void(a.isFocused&&a.showInput())
if("click"===(i=t&&t.type.toLowerCase())&&K("shiftKey",t)&&a.activeItems.length){for(l=a.getLastActive(),(n=Array.prototype.indexOf.call(a.control.children,l))>(o=Array.prototype.indexOf.call(a.control.children,e))&&(r=n,n=o,o=r),s=n;s<=o;s++)e=a.control.children[s],-1===a.activeItems.indexOf(e)&&a.setActiveItemClass(e)
H(t)}else"click"===i&&K(V,t)||"keydown"===i&&K("shiftKey",t)?e.classList.contains("active")?a.removeActiveItem(e):a.setActiveItemClass(e):(a.clearActiveItems(),a.setActiveItemClass(e))
a.hideInput(),a.isFocused||a.focus()}}setActiveItemClass(e){const t=this,i=t.control.querySelector(".last-active")
i&&S(i,"last-active"),C(e,"active last-active"),t.trigger("item_select",e),-1==t.activeItems.indexOf(e)&&t.activeItems.push(e)}removeActiveItem(e){var t=this.activeItems.indexOf(e)
this.activeItems.splice(t,1),S(e,"active")}clearActiveItems(){S(this.activeItems,"active"),this.activeItems=[]}setActiveOption(e){e!==this.activeOption&&(this.clearActiveOption(),e&&(this.activeOption=e,P(this.focus_node,{"aria-activedescendant":e.getAttribute("id")}),P(e,{"aria-selected":"true"}),C(e,"active"),this.scrollToOption(e)))}scrollToOption(e,t){if(!e)return
const i=this.dropdown_content,s=i.clientHeight,n=i.scrollTop||0,o=e.offsetHeight,r=e.getBoundingClientRect().top-i.getBoundingClientRect().top+n
r+o>s+n?this.scroll(r-s+o,t):r<n&&this.scroll(r,t)}scroll(e,t){const i=this.dropdown_content
t&&(i.style.scrollBehavior=t),i.scrollTop=e,i.style.scrollBehavior=""}clearActiveOption(){this.activeOption&&(S(this.activeOption,"active"),P(this.activeOption,{"aria-selected":null})),this.activeOption=null,P(this.focus_node,{"aria-activedescendant":null})}selectAll(){if("single"===this.settings.mode)return
const e=this.controlChildren()
e.length&&(this.hideInput(),this.close(),this.activeItems=e,C(e,"active"))}inputState(){var e=this
e.control.contains(e.control_input)&&(P(e.control_input,{placeholder:e.settings.placeholder}),e.activeItems.length>0||!e.isFocused&&e.settings.hidePlaceholder&&e.items.length>0?(e.setTextboxValue(),e.isInputHidden=!0):(e.settings.hidePlaceholder&&e.items.length>0&&P(e.control_input,{placeholder:""}),e.isInputHidden=!1),e.wrapper.classList.toggle("input-hidden",e.isInputHidden))}hideInput(){this.inputState()}showInput(){this.inputState()}inputValue(){return this.control_input.value.trim()}focus(){var e=this
e.isDisabled||(e.ignoreFocus=!0,e.control_input.offsetWidth?e.control_input.focus():e.focus_node.focus(),setTimeout((()=>{e.ignoreFocus=!1,e.onFocus()}),0))}blur(){this.focus_node.blur(),this.onBlur()}getScoreFunction(e){return this.sifter.getScoreFunction(e,this.getSearchOptions())}getSearchOptions(){var e=this.settings,t=e.sortField
return"string"==typeof e.sortField&&(t=[{field:e.sortField}]),{fields:e.searchField,conjunction:e.searchConjunction,sort:t,nesting:e.nesting}}search(e){var t,i,s,n=this,o=this.getSearchOptions()
if(n.settings.score&&"function"!=typeof(s=n.settings.score.call(n,e)))throw new Error('Tom Select "score" setting must be a function that returns a function')
if(e!==n.lastQuery?(n.lastQuery=e,i=n.sifter.search(e,Object.assign(o,{score:s})),n.currentResults=i):i=Object.assign({},n.currentResults),n.settings.hideSelected)for(t=i.items.length-1;t>=0;t--){let e=q(i.items[t].id)
e&&-1!==n.items.indexOf(e)&&i.items.splice(t,1)}return i}refreshOptions(e=!0){var t,i,s,n,o,r,l,a,c,d,p
const u={},h=[]
var g,f=this,v=f.inputValue(),m=f.search(v),O=f.activeOption,b=f.settings.shouldOpen||!1,w=f.dropdown_content
for(O&&(c=O.dataset.value,d=O.closest("[data-group]")),n=m.items.length,"number"==typeof f.settings.maxOptions&&(n=Math.min(n,f.settings.maxOptions)),n>0&&(b=!0),t=0;t<n;t++){let e=m.items[t].id,n=f.options[e],l=f.getOption(e,!0)
for(f.settings.hideSelected||l.classList.toggle("selected",f.items.includes(e)),o=n[f.settings.optgroupField]||"",i=0,s=(r=Array.isArray(o)?o:[o])&&r.length;i<s;i++)o=r[i],f.optgroups.hasOwnProperty(o)||(o=""),u.hasOwnProperty(o)||(u[o]=document.createDocumentFragment(),h.push(o)),i>0&&(l=l.cloneNode(!0),P(l,{id:n.$id+"-clone-"+i,"aria-selected":null}),l.classList.add("ts-cloned"),S(l,"active")),c==e&&d&&d.dataset.group===o&&(O=l),u[o].appendChild(l)}this.settings.lockOptgroupOrder&&h.sort(((e,t)=>(f.optgroups[e]&&f.optgroups[e].$order||0)-(f.optgroups[t]&&f.optgroups[t].$order||0))),l=document.createDocumentFragment(),y(h,(e=>{if(f.optgroups.hasOwnProperty(e)&&u[e].children.length){let t=document.createDocumentFragment(),i=f.render("optgroup_header",f.optgroups[e])
G(t,i),G(t,u[e])
let s=f.render("optgroup",{group:f.optgroups[e],options:t})
G(l,s)}else G(l,u[e])})),w.innerHTML="",G(w,l),f.settings.highlight&&(g=w.querySelectorAll("span.highlight"),Array.prototype.forEach.call(g,(function(e){var t=e.parentNode
t.replaceChild(e.firstChild,e),t.normalize()})),m.query.length&&m.tokens.length&&y(m.tokens,(e=>{T(w,e.regex)})))
var _=e=>{let t=f.render(e,{input:v})
return t&&(b=!0,w.insertBefore(t,w.firstChild)),t}
if(f.loading?_("loading"):f.settings.shouldLoad.call(f,v)?0===m.items.length&&_("no_results"):_("not_loading"),(a=f.canCreate(v))&&(p=_("option_create")),f.hasOptions=m.items.length>0||a,b){if(m.items.length>0){if(!w.contains(O)&&"single"===f.settings.mode&&f.items.length&&(O=f.getOption(f.items[0])),!w.contains(O)){let e=0
p&&!f.settings.addPrecedence&&(e=1),O=f.selectable()[e]}}else p&&(O=p)
e&&!f.isOpen&&(f.open(),f.scrollToOption(O,"auto")),f.setActiveOption(O)}else f.clearActiveOption(),e&&f.isOpen&&f.close(!1)}selectable(){return this.dropdown_content.querySelectorAll("[data-selectable]")}addOption(e,t=!1){const i=this
if(Array.isArray(e))return i.addOptions(e,t),!1
const s=q(e[i.settings.valueField])
return null!==s&&!i.options.hasOwnProperty(s)&&(e.$order=e.$order||++i.order,e.$id=i.inputId+"-opt-"+e.$order,i.options[s]=e,i.lastQuery=null,t&&(i.userOptions[s]=t,i.trigger("option_add",s,e)),s)}addOptions(e,t=!1){y(e,(e=>{this.addOption(e,t)}))}registerOption(e){return this.addOption(e)}registerOptionGroup(e){var t=q(e[this.settings.optgroupValueField])
return null!==t&&(e.$order=e.$order||++this.order,this.optgroups[t]=e,t)}addOptionGroup(e,t){var i
t[this.settings.optgroupValueField]=e,(i=this.registerOptionGroup(t))&&this.trigger("optgroup_add",i,t)}removeOptionGroup(e){this.optgroups.hasOwnProperty(e)&&(delete this.optgroups[e],this.clearCache(),this.trigger("optgroup_remove",e))}clearOptionGroups(){this.optgroups={},this.clearCache(),this.trigger("optgroup_clear")}updateOption(e,t){const i=this
var s,n
const o=q(e),r=q(t[i.settings.valueField])
if(null===o)return
if(!i.options.hasOwnProperty(o))return
if("string"!=typeof r)throw new Error("Value must be set in option data")
const l=i.getOption(o),a=i.getItem(o)
if(t.$order=t.$order||i.options[o].$order,delete i.options[o],i.uncacheValue(r),i.options[r]=t,l){if(i.dropdown_content.contains(l)){const e=i._render("option",t)
E(l,e),i.activeOption===l&&i.setActiveOption(e)}l.remove()}a&&(-1!==(n=i.items.indexOf(o))&&i.items.splice(n,1,r),s=i._render("item",t),a.classList.contains("active")&&C(s,"active"),E(a,s)),i.lastQuery=null}removeOption(e,t){const i=this
e=D(e),i.uncacheValue(e),delete i.userOptions[e],delete i.options[e],i.lastQuery=null,i.trigger("option_remove",e),i.removeItem(e,t)}clearOptions(){this.loadedSearches={},this.userOptions={},this.clearCache()
var e={}
y(this.options,((t,i)=>{this.items.indexOf(i)>=0&&(e[i]=this.options[i])})),this.options=this.sifter.items=e,this.lastQuery=null,this.trigger("option_clear")}getOption(e,t=!1){const i=q(e)
if(null!==i&&this.options.hasOwnProperty(i)){const e=this.options[i]
if(e.$div)return e.$div
if(t)return this._render("option",e)}return null}getAdjacent(e,t,i="option"){var s
if(!e)return null
s="item"==i?this.controlChildren():this.dropdown_content.querySelectorAll("[data-selectable]")
for(let i=0;i<s.length;i++)if(s[i]==e)return t>0?s[i+1]:s[i-1]
return null}getItem(e){if("object"==typeof e)return e
var t=q(e)
return null!==t?this.control.querySelector(`[data-value="${Q(t)}"]`):null}addItems(e,t){var i=this,s=Array.isArray(e)?e:[e]
for(let e=0,n=(s=s.filter((e=>-1===i.items.indexOf(e)))).length;e<n;e++)i.isPending=e<n-1,i.addItem(s[e],t)}addItem(e,t){R(this,t?[]:["change"],(()=>{var i,s
const n=this,o=n.settings.mode,r=q(e)
if((!r||-1===n.items.indexOf(r)||("single"===o&&n.close(),"single"!==o&&n.settings.duplicates))&&null!==r&&n.options.hasOwnProperty(r)&&("single"===o&&n.clear(t),"multi"!==o||!n.isFull())){if(i=n._render("item",n.options[r]),n.control.contains(i)&&(i=i.cloneNode(!0)),s=n.isFull(),n.items.splice(n.caretPos,0,r),n.insertAtCaret(i),n.isSetup){if(!n.isPending&&n.settings.hideSelected){let e=n.getOption(r),t=n.getAdjacent(e,1)
t&&n.setActiveOption(t)}n.isPending||n.refreshOptions(n.isFocused&&"single"!==o),0!=n.settings.closeAfterSelect&&n.isFull()?n.close():n.isPending||n.positionDropdown(),n.trigger("item_add",r,i),n.isPending||n.updateOriginalInput({silent:t})}(!n.isPending||!s&&n.isFull())&&(n.inputState(),n.refreshState())}}))}removeItem(e=null,t){const i=this
if(!(e=i.getItem(e)))return
var s,n
const o=e.dataset.value
s=L(e),e.remove(),e.classList.contains("active")&&(n=i.activeItems.indexOf(e),i.activeItems.splice(n,1),S(e,"active")),i.items.splice(s,1),i.lastQuery=null,!i.settings.persist&&i.userOptions.hasOwnProperty(o)&&i.removeOption(o,t),s<i.caretPos&&i.setCaret(i.caretPos-1),i.updateOriginalInput({silent:t}),i.refreshState(),i.positionDropdown(),i.trigger("item_remove",o,e)}createItem(e=null,t=!0,i=(()=>{})){var s,n=this,o=n.caretPos
if(e=e||n.inputValue(),!n.canCreate(e))return i(),!1
n.lock()
var r=!1,l=e=>{if(n.unlock(),!e||"object"!=typeof e)return i()
var s=q(e[n.settings.valueField])
if("string"!=typeof s)return i()
n.setTextboxValue(),n.addOption(e,!0),n.setCaret(o),n.addItem(s),n.refreshOptions(t&&"single"!==n.settings.mode),i(e),r=!0}
return s="function"==typeof n.settings.create?n.settings.create.call(this,e,l):{[n.settings.labelField]:e,[n.settings.valueField]:e},r||l(s),!0}refreshItems(){var e=this
e.lastQuery=null,e.isSetup&&e.addItems(e.items),e.updateOriginalInput(),e.refreshState()}refreshState(){const e=this
e.refreshValidityState()
const t=e.isFull(),i=e.isLocked
e.wrapper.classList.toggle("rtl",e.rtl)
const s=e.wrapper.classList
var n
s.toggle("focus",e.isFocused),s.toggle("disabled",e.isDisabled),s.toggle("required",e.isRequired),s.toggle("invalid",!e.isValid),s.toggle("locked",i),s.toggle("full",t),s.toggle("input-active",e.isFocused&&!e.isInputHidden),s.toggle("dropdown-active",e.isOpen),s.toggle("has-options",(n=e.options,0===Object.keys(n).length)),s.toggle("has-items",e.items.length>0)}refreshValidityState(){var e=this
e.input.checkValidity&&(e.isValid=e.input.checkValidity(),e.isInvalid=!e.isValid)}isFull(){return null!==this.settings.maxItems&&this.items.length>=this.settings.maxItems}updateOriginalInput(e={}){const t=this
var i,s
const n=t.input.querySelector('option[value=""]')
if(t.is_select_tag){const e=[]
function o(i,s,o){return i||(i=w('<option value="'+N(s)+'">'+N(o)+"</option>")),i!=n&&t.input.append(i),e.push(i),i.selected=!0,i}t.input.querySelectorAll("option:checked").forEach((e=>{e.selected=!1})),0==t.items.length&&"single"==t.settings.mode?o(n,"",""):t.items.forEach((n=>{if(i=t.options[n],s=i[t.settings.labelField]||"",e.includes(i.$option)){o(t.input.querySelector(`option[value="${Q(n)}"]:not(:checked)`),n,s)}else i.$option=o(i.$option,n,s)}))}else t.input.value=t.getValue()
t.isSetup&&(e.silent||t.trigger("change",t.getValue()))}open(){var e=this
e.isLocked||e.isOpen||"multi"===e.settings.mode&&e.isFull()||(e.isOpen=!0,P(e.focus_node,{"aria-expanded":"true"}),e.refreshState(),I(e.dropdown,{visibility:"hidden",display:"block"}),e.positionDropdown(),I(e.dropdown,{visibility:"visible",display:"block"}),e.focus(),e.trigger("dropdown_open",e.dropdown))}close(e=!0){var t=this,i=t.isOpen
e&&(t.setTextboxValue(),"single"===t.settings.mode&&t.items.length&&t.hideInput()),t.isOpen=!1,P(t.focus_node,{"aria-expanded":"false"}),I(t.dropdown,{display:"none"}),t.settings.hideSelected&&t.clearActiveOption(),t.refreshState(),i&&t.trigger("dropdown_close",t.dropdown)}positionDropdown(){if("body"===this.settings.dropdownParent){var e=this.control,t=e.getBoundingClientRect(),i=e.offsetHeight+t.top+window.scrollY,s=t.left+window.scrollX
I(this.dropdown,{width:t.width+"px",top:i+"px",left:s+"px"})}}clear(e){var t=this
if(t.items.length){var i=t.controlChildren()
y(i,(e=>{t.removeItem(e,!0)})),t.showInput(),e||t.updateOriginalInput(),t.trigger("clear")}}insertAtCaret(e){const t=this,i=t.caretPos,s=t.control
s.insertBefore(e,s.children[i]),t.setCaret(i+1)}deleteSelection(e){var t,i,s,n,o,r=this
t=e&&8===e.keyCode?-1:1,i={start:(o=r.control_input).selectionStart||0,length:(o.selectionEnd||0)-(o.selectionStart||0)}
const l=[]
if(r.activeItems.length)n=F(r.activeItems,t),s=L(n),t>0&&s++,y(r.activeItems,(e=>l.push(e)))
else if((r.isFocused||"single"===r.settings.mode)&&r.items.length){const e=r.controlChildren()
t<0&&0===i.start&&0===i.length?l.push(e[r.caretPos-1]):t>0&&i.start===r.inputValue().length&&l.push(e[r.caretPos])}const a=l.map((e=>e.dataset.value))
if(!a.length||"function"==typeof r.settings.onDelete&&!1===r.settings.onDelete.call(r,a,e))return!1
for(H(e,!0),void 0!==s&&r.setCaret(s);l.length;)r.removeItem(l.pop())
return r.showInput(),r.positionDropdown(),r.refreshOptions(!1),!0}advanceSelection(e,t){var i,s,n=this
n.rtl&&(e*=-1),n.inputValue().length||(K(V,t)||K("shiftKey",t)?(s=(i=n.getLastActive(e))?i.classList.contains("active")?n.getAdjacent(i,e,"item"):i:e>0?n.control_input.nextElementSibling:n.control_input.previousElementSibling)&&(s.classList.contains("active")&&n.removeActiveItem(i),n.setActiveItemClass(s)):n.moveCaret(e))}moveCaret(e){}getLastActive(e){let t=this.control.querySelector(".last-active")
if(t)return t
var i=this.control.querySelectorAll(".active")
return i?F(i,e):void 0}setCaret(e){this.caretPos=this.items.length}controlChildren(){return Array.from(this.control.querySelectorAll("[data-ts-item]"))}lock(){this.close(),this.isLocked=!0,this.refreshState()}unlock(){this.isLocked=!1,this.refreshState()}disable(){var e=this
e.input.disabled=!0,e.control_input.disabled=!0,e.focus_node.tabIndex=-1,e.isDisabled=!0,e.lock()}enable(){var e=this
e.input.disabled=!1,e.control_input.disabled=!1,e.focus_node.tabIndex=e.tabIndex,e.isDisabled=!1,e.unlock()}destroy(){var e=this,t=e.revertSettings
e.trigger("destroy"),e.off(),e.wrapper.remove(),e.dropdown.remove(),e.input.innerHTML=t.innerHTML,e.input.tabIndex=t.tabIndex,S(e.input,"tomselected","ts-hidden-accessible"),e._destroy(),delete e.input.tomselect}render(e,t){return"function"!=typeof this.settings.render[e]?null:this._render(e,t)}_render(e,t){var i,s,n=""
const o=this
return"option"!==e&&"item"!=e||(n=D(t[o.settings.valueField])),null==(s=o.settings.render[e].call(this,t,N))||(s=w(s),"option"===e||"option_create"===e?t[o.settings.disabledField]?P(s,{"aria-disabled":"true"}):P(s,{"data-selectable":""}):"optgroup"===e&&(i=t.group[o.settings.optgroupValueField],P(s,{"data-group":i}),t.group[o.settings.disabledField]&&P(s,{"data-disabled":""})),"option"!==e&&"item"!==e||(P(s,{"data-value":n}),"item"===e?(C(s,o.settings.itemClass),P(s,{"data-ts-item":""})):(C(s,o.settings.optionClass),P(s,{role:"option",id:t.$id}),o.options[n].$div=s))),s}clearCache(){y(this.options,((e,t)=>{e.$div&&(e.$div.remove(),delete e.$div)}))}uncacheValue(e){const t=this.getOption(e)
t&&t.remove()}canCreate(e){return this.settings.create&&e.length>0&&this.settings.createFilter.call(this,e)}hook(e,t,i){var s=this,n=s[t]
s[t]=function(){var t,o
return"after"===e&&(t=n.apply(s,arguments)),o=i.apply(s,arguments),"instead"===e?o:("before"===e&&(t=n.apply(s,arguments)),t)}}}return J.define("change_listener",(function(){B(this.input,"change",(()=>{this.sync()}))})),J.define("checkbox_options",(function(){var e=this,t=e.onOptionSelect
e.settings.hideSelected=!1
var i=function(e){setTimeout((()=>{var t=e.querySelector("input")
e.classList.contains("selected")?t.checked=!0:t.checked=!1}),1)}
e.hook("after","setupTemplates",(()=>{var t=e.settings.render.option
e.settings.render.option=(i,s)=>{var n=w(t.call(e,i,s)),o=document.createElement("input")
o.addEventListener("click",(function(e){H(e)})),o.type="checkbox"
const r=q(i[e.settings.valueField])
return r&&e.items.indexOf(r)>-1&&(o.checked=!0),n.prepend(o),n}})),e.on("item_remove",(t=>{var s=e.getOption(t)
s&&(s.classList.remove("selected"),i(s))})),e.hook("instead","onOptionSelect",((s,n)=>{if(n.classList.contains("selected"))return n.classList.remove("selected"),e.removeItem(n.dataset.value),e.refreshOptions(),void H(s,!0)
t.call(e,s,n),i(n)}))})),J.define("clear_button",(function(e){const t=this,i=Object.assign({className:"clear-button",title:"Clear All",html:e=>`<div class="${e.className}" title="${e.title}">&times;</div>`},e)
t.on("initialize",(()=>{var e=w(i.html(i))
e.addEventListener("click",(e=>{t.clear(),"single"===t.settings.mode&&t.settings.allowEmptyOption&&t.addItem(""),e.preventDefault(),e.stopPropagation()})),t.control.appendChild(e)}))})),J.define("drag_drop",(function(){var e=this
if(!$.fn.sortable)throw new Error('The "drag_drop" plugin requires jQuery UI "sortable".')
if("multi"===e.settings.mode){var t=e.lock,i=e.unlock
e.hook("instead","lock",(()=>{var i=$(e.control).data("sortable")
return i&&i.disable(),t.call(e)})),e.hook("instead","unlock",(()=>{var t=$(e.control).data("sortable")
return t&&t.enable(),i.call(e)})),e.on("initialize",(()=>{var t=$(e.control).sortable({items:"[data-value]",forcePlaceholderSize:!0,disabled:e.isLocked,start:(e,i)=>{i.placeholder.css("width",i.helper.css("width")),t.css({overflow:"visible"})},stop:()=>{t.css({overflow:"hidden"})
var i=[]
t.children("[data-value]").each((function(){this.dataset.value&&i.push(this.dataset.value)})),e.setValue(i)}})}))}})),J.define("dropdown_header",(function(e){const t=this,i=Object.assign({title:"Untitled",headerClass:"dropdown-header",titleRowClass:"dropdown-header-title",labelClass:"dropdown-header-label",closeClass:"dropdown-header-close",html:e=>'<div class="'+e.headerClass+'"><div class="'+e.titleRowClass+'"><span class="'+e.labelClass+'">'+e.title+'</span><a class="'+e.closeClass+'">&times;</a></div></div>'},e)
t.on("initialize",(()=>{var e=w(i.html(i)),s=e.querySelector("."+i.closeClass)
s&&s.addEventListener("click",(e=>{H(e,!0),t.close()})),t.dropdown.insertBefore(e,t.dropdown.firstChild)}))})),J.define("caret_position",(function(){var e=this
e.hook("instead","setCaret",(t=>{"single"!==e.settings.mode&&e.control.contains(e.control_input)?(t=Math.max(0,Math.min(e.items.length,t)))==e.caretPos||e.isPending||e.controlChildren().forEach(((i,s)=>{s<t?e.control_input.insertAdjacentElement("beforebegin",i):e.control.appendChild(i)})):t=e.items.length,e.caretPos=t})),e.hook("instead","moveCaret",(t=>{if(!e.isFocused)return
const i=e.getLastActive(t)
if(i){const s=L(i)
e.setCaret(t>0?s+1:s),e.setActiveItem()}else e.setCaret(e.caretPos+t)}))})),J.define("dropdown_input",(function(){var e=this
e.settings.shouldOpen=!0,e.hook("before","setup",(()=>{e.focus_node=e.control,C(e.control_input,"dropdown-input")
const t=w('<div class="dropdown-input-wrap">')
t.append(e.control_input),e.dropdown.insertBefore(t,e.dropdown.firstChild)})),e.on("initialize",(()=>{e.control_input.addEventListener("keydown",(t=>{switch(t.keyCode){case 27:return e.isOpen&&(H(t,!0),e.close()),void e.clearActiveItems()
case 9:e.focus_node.tabIndex=-1}return e.onKeyDown.call(e,t)})),e.on("blur",(()=>{e.focus_node.tabIndex=e.isDisabled?-1:e.tabIndex})),e.on("dropdown_open",(()=>{e.control_input.focus()}))
const t=e.onBlur
e.hook("instead","onBlur",(i=>{if(!i||i.relatedTarget!=e.control_input)return t.call(e)})),B(e.control_input,"blur",(()=>e.onBlur())),e.hook("before","close",(()=>{e.isOpen&&e.focus_node.focus()}))}))})),J.define("input_autogrow",(function(){var e=this
e.on("initialize",(()=>{var t=document.createElement("span"),i=e.control_input
t.style.cssText="position:absolute; top:-99999px; left:-99999px; width:auto; padding:0; white-space:pre; ",e.wrapper.appendChild(t)
for(const e of["letterSpacing","fontSize","fontFamily","fontWeight","textTransform"])t.style[e]=i.style[e]
var s=()=>{e.items.length>0?(t.textContent=i.value,i.style.width=t.clientWidth+"px"):i.style.width=""}
s(),e.on("update item_add item_remove",s),B(i,"input",s),B(i,"keyup",s),B(i,"blur",s),B(i,"update",s)}))})),J.define("no_backspace_delete",(function(){var e=this,t=e.deleteSelection
this.hook("instead","deleteSelection",(i=>!!e.activeItems.length&&t.call(e,i)))})),J.define("no_active_items",(function(){this.hook("instead","setActiveItem",(()=>{})),this.hook("instead","selectAll",(()=>{}))})),J.define("optgroup_columns",(function(){var e=this,t=e.onKeyDown
e.hook("instead","onKeyDown",(i=>{var s,n,o,r
if(!e.isOpen||37!==i.keyCode&&39!==i.keyCode)return t.call(e,i)
r=k(e.activeOption,"[data-group]"),s=L(e.activeOption,"[data-selectable]"),r&&(r=37===i.keyCode?r.previousSibling:r.nextSibling)&&(n=(o=r.querySelectorAll("[data-selectable]"))[Math.min(o.length-1,s)])&&e.setActiveOption(n)}))})),J.define("remove_button",(function(e){const t=Object.assign({label:"&times;",title:"Remove",className:"remove",append:!0},e)
var i=this
if(t.append){var s='<a href="javascript:void(0)" class="'+t.className+'" tabindex="-1" title="'+N(t.title)+'">'+t.label+"</a>"
i.hook("after","setupTemplates",(()=>{var e=i.settings.render.item
i.settings.render.item=(t,n)=>{var o=w(e.call(i,t,n)),r=w(s)
return o.appendChild(r),B(r,"mousedown",(e=>{H(e,!0)})),B(r,"click",(e=>{if(H(e,!0),!i.isLocked){var t=o.dataset.value
i.removeItem(t),i.refreshOptions(!1)}})),o}}))}})),J.define("restore_on_backspace",(function(e){const t=this,i=Object.assign({text:e=>e[t.settings.labelField]},e)
t.on("item_remove",(function(e){if(""===t.control_input.value.trim()){var s=t.options[e]
s&&t.setTextboxValue(i.text.call(t,s))}}))})),J.define("virtual_scroll",(function(){const e=this,t=e.canLoad,i=e.clearActiveOption,s=e.loadCallback
var n,o={},r=!1
if(!e.settings.firstUrl)throw"virtual_scroll plugin requires a firstUrl() method"
function l(t){return!("number"==typeof e.settings.maxOptions&&n.children.length>=e.settings.maxOptions)&&!(!(t in o)||!o[t])}e.settings.sortField=[{field:"$order"},{field:"$score"}],e.setNextUrl=function(e,t){o[e]=t},e.getUrl=function(t){if(t in o){const e=o[t]
return o[t]=!1,e}return o={},e.settings.firstUrl(t)},e.hook("instead","clearActiveOption",(()=>{if(!r)return i.call(e)})),e.hook("instead","canLoad",(i=>i in o?l(i):t.call(e,i))),e.hook("instead","loadCallback",((t,i)=>{r||e.clearOptions(),s.call(e,t,i),r=!1})),e.hook("after","refreshOptions",(()=>{const t=e.lastValue
var i
l(t)?(i=e.render("loading_more",{query:t}))&&i.setAttribute("data-selectable",""):t in o&&!n.querySelector(".no-results")&&(i=e.render("no_more_results",{query:t})),i&&(C(i,e.settings.optionClass),n.append(i))})),e.on("initialize",(()=>{n=e.dropdown_content,e.settings.render=Object.assign({},{loading_more:function(){return'<div class="loading-more-results">Loading more results ... </div>'},no_more_results:function(){return'<div class="no-more-results">No more results</div>'}},e.settings.render),n.addEventListener("scroll",(function(){n.clientHeight/(n.scrollHeight-n.scrollTop)<.95||l(e.lastValue)&&(r||(r=!0,e.load.call(e,e.lastValue)))}))}))})),J}))
var tomSelect=function(e,t){return new TomSelect(e,t)}
//# sourceMappingURL=tom-select.complete.min.js.map

View file

@ -1,334 +0,0 @@
/**
* tom-select.css (v2.0.0-rc.4)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
.ts-wrapper.plugin-drag_drop.multi > .ts-control > div.ui-sortable-placeholder {
visibility: visible !important;
background: #f2f2f2 !important;
background: rgba(0, 0, 0, 0.06) !important;
border: 0 none !important;
box-shadow: inset 0 0 12px 4px #fff; }
.ts-wrapper.plugin-drag_drop .ui-sortable-placeholder::after {
content: '!';
visibility: hidden; }
.ts-wrapper.plugin-drag_drop .ui-sortable-helper {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); }
.plugin-checkbox_options .option input {
margin-right: 0.5rem; }
.plugin-clear_button .ts-control {
padding-right: calc( 1em + (3 * 6px)) !important; }
.plugin-clear_button .clear-button {
opacity: 0;
position: absolute;
top: 8px;
right: calc(8px - 6px);
margin-right: 0 !important;
background: transparent !important;
transition: opacity 0.5s;
cursor: pointer; }
.plugin-clear_button.single .clear-button {
right: calc(8px - 6px + 2rem); }
.plugin-clear_button.focus.has-items .clear-button,
.plugin-clear_button:hover.has-items .clear-button {
opacity: 1; }
.ts-wrapper .dropdown-header {
position: relative;
padding: 10px 8px;
border-bottom: 1px solid #d0d0d0;
background: #f8f8f8;
border-radius: 3px 3px 0 0; }
.ts-wrapper .dropdown-header-close {
position: absolute;
right: 8px;
top: 50%;
color: #303030;
opacity: 0.4;
margin-top: -12px;
line-height: 20px;
font-size: 20px !important; }
.ts-wrapper .dropdown-header-close:hover {
color: black; }
.plugin-dropdown_input.focus.dropdown-active .ts-control {
box-shadow: none;
border: 1px solid #d0d0d0; }
.plugin-dropdown_input .dropdown-input {
border: 1px solid #d0d0d0;
border-width: 0 0 1px 0;
display: block;
padding: 8px 8px;
box-shadow: none;
width: 100%;
background: transparent; }
.ts-wrapper.plugin-input_autogrow.has-items .ts-control > input {
min-width: 0; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input {
flex: none;
min-width: 4px; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::-webkit-input-placeholder {
color: transparent; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::-ms-input-placeholder {
color: transparent; }
.ts-wrapper.plugin-input_autogrow.has-items.focus .ts-control > input::placeholder {
color: transparent; }
.ts-dropdown.plugin-optgroup_columns .ts-dropdown-content {
display: flex; }
.ts-dropdown.plugin-optgroup_columns .optgroup {
border-right: 1px solid #f2f2f2;
border-top: 0 none;
flex-grow: 1;
flex-basis: 0;
min-width: 0; }
.ts-dropdown.plugin-optgroup_columns .optgroup:last-child {
border-right: 0 none; }
.ts-dropdown.plugin-optgroup_columns .optgroup:before {
display: none; }
.ts-dropdown.plugin-optgroup_columns .optgroup-header {
border-top: 0 none; }
.ts-wrapper.plugin-remove_button .item {
display: inline-flex;
align-items: center;
padding-right: 0 !important; }
.ts-wrapper.plugin-remove_button .item .remove {
color: inherit;
text-decoration: none;
vertical-align: middle;
display: inline-block;
padding: 2px 6px;
border-left: 1px solid #d0d0d0;
border-radius: 0 2px 2px 0;
box-sizing: border-box;
margin-left: 6px; }
.ts-wrapper.plugin-remove_button .item .remove:hover {
background: rgba(0, 0, 0, 0.05); }
.ts-wrapper.plugin-remove_button .item.active .remove {
border-left-color: #cacaca; }
.ts-wrapper.plugin-remove_button.disabled .item .remove:hover {
background: none; }
.ts-wrapper.plugin-remove_button.disabled .item .remove {
border-left-color: white; }
.ts-wrapper.plugin-remove_button .remove-single {
position: absolute;
right: 0;
top: 0;
font-size: 23px; }
.ts-wrapper {
position: relative; }
.ts-dropdown,
.ts-control,
.ts-control input {
color: #303030;
font-family: inherit;
font-size: 13px;
line-height: 18px;
font-smoothing: inherit; }
.ts-control,
.ts-wrapper.single.input-active .ts-control {
background: #fff;
cursor: text; }
.ts-control {
border: 1px solid #d0d0d0;
padding: 8px 8px;
width: 100%;
overflow: hidden;
position: relative;
z-index: 1;
box-sizing: border-box;
box-shadow: none;
border-radius: 3px;
display: flex;
flex-wrap: wrap; }
.ts-wrapper.multi.has-items .ts-control {
padding: calc( 8px - 2px - 0) 8px calc( 8px - 2px - 3px - 0); }
.full .ts-control {
background-color: #fff; }
.disabled .ts-control,
.disabled .ts-control * {
cursor: default !important; }
.focus .ts-control {
box-shadow: none; }
.ts-control > * {
vertical-align: baseline;
display: inline-block; }
.ts-wrapper.multi .ts-control > div {
cursor: pointer;
margin: 0 3px 3px 0;
padding: 2px 6px;
background: #f2f2f2;
color: #303030;
border: 0 solid #d0d0d0; }
.ts-wrapper.multi .ts-control > div.active {
background: #e8e8e8;
color: #303030;
border: 0 solid #cacaca; }
.ts-wrapper.multi.disabled .ts-control > div, .ts-wrapper.multi.disabled .ts-control > div.active {
color: #7d7c7c;
background: white;
border: 0 solid white; }
.ts-control > input {
flex: 1 1 auto;
min-width: 7rem;
display: inline-block !important;
padding: 0 !important;
min-height: 0 !important;
max-height: none !important;
max-width: 100% !important;
margin: 0 !important;
text-indent: 0 !important;
border: 0 none !important;
background: none !important;
line-height: inherit !important;
-webkit-user-select: auto !important;
-moz-user-select: auto !important;
-ms-user-select: auto !important;
user-select: auto !important;
box-shadow: none !important; }
.ts-control > input::-ms-clear {
display: none; }
.ts-control > input:focus {
outline: none !important; }
.has-items .ts-control > input {
margin: 0 4px !important; }
.ts-control.rtl {
text-align: right; }
.ts-control.rtl.single .ts-control:after {
left: 15px;
right: auto; }
.ts-control.rtl .ts-control > input {
margin: 0 4px 0 -2px !important; }
.disabled .ts-control {
opacity: 0.5;
background-color: #fafafa; }
.input-hidden .ts-control > input {
opacity: 0;
position: absolute;
left: -10000px; }
.ts-dropdown {
position: absolute;
top: 100%;
left: 0;
width: 100%;
z-index: 10;
border: 1px solid #d0d0d0;
background: #fff;
margin: 0.25rem 0 0 0;
border-top: 0 none;
box-sizing: border-box;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-radius: 0 0 3px 3px; }
.ts-dropdown [data-selectable] {
cursor: pointer;
overflow: hidden; }
.ts-dropdown [data-selectable] .highlight {
background: rgba(125, 168, 208, 0.2);
border-radius: 1px; }
.ts-dropdown .option,
.ts-dropdown .optgroup-header,
.ts-dropdown .no-results,
.ts-dropdown .create {
padding: 5px 8px; }
.ts-dropdown .option, .ts-dropdown [data-disabled], .ts-dropdown [data-disabled] [data-selectable].option {
cursor: inherit;
opacity: 0.5; }
.ts-dropdown [data-selectable].option {
opacity: 1;
cursor: pointer; }
.ts-dropdown .optgroup:first-child .optgroup-header {
border-top: 0 none; }
.ts-dropdown .optgroup-header {
color: #303030;
background: #fff;
cursor: default; }
.ts-dropdown .create:hover,
.ts-dropdown .option:hover,
.ts-dropdown .active {
background-color: #f5fafd;
color: #495c68; }
.ts-dropdown .create:hover.create,
.ts-dropdown .option:hover.create,
.ts-dropdown .active.create {
color: #495c68; }
.ts-dropdown .create {
color: rgba(48, 48, 48, 0.5); }
.ts-dropdown .spinner {
display: inline-block;
width: 30px;
height: 30px;
margin: 5px 8px; }
.ts-dropdown .spinner:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 3px;
border-radius: 50%;
border: 5px solid #d0d0d0;
border-color: #d0d0d0 transparent #d0d0d0 transparent;
animation: lds-dual-ring 1.2s linear infinite; }
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg); }
100% {
transform: rotate(360deg); } }
.ts-dropdown-content {
overflow-y: auto;
overflow-x: hidden;
max-height: 200px;
overflow-scrolling: touch;
scroll-behavior: smooth; }
.ts-hidden-accessible {
border: 0 !important;
clip: rect(0 0 0 0) !important;
-webkit-clip-path: inset(50%) !important;
clip-path: inset(50%) !important;
height: 1px !important;
overflow: hidden !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important; }
/*# sourceMappingURL=tom-select.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,163 +0,0 @@
"""
自定义实体类型定义
只提取现实生活中真实存在的可以有行动的实体
"""
from pydantic import Field
from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel
# ============== 实体类型定义 ==============
class Person(EntityModel):
"""A real, named individual. Must have a specific name like "马化腾" or "Elon Musk". NOT generic terms like "某人", "用户", pronouns, or abstract roles."""
role: EntityText = Field(
description="职业或职位",
default=None
)
affiliation: EntityText = Field(
description="所属组织",
default=None
)
class Organization(EntityModel):
"""A real organization with an official name like "武汉大学", "联合国". NOT generic terms like "大学", "政府", "组织"."""
org_type: EntityText = Field(
description="组织类型",
default=None
)
location: EntityText = Field(
description="所在地",
default=None
)
class Company(EntityModel):
"""A real company with registered name like "腾讯", "Apple Inc.". NOT generic terms like "科技公司", "某企业"."""
industry: EntityText = Field(
description="所属行业",
default=None
)
headquarters: EntityText = Field(
description="总部位置",
default=None
)
class Location(EntityModel):
"""A real geographic location like "北京", "硅谷", "故宫". NOT generic terms like "某地", "一个城市"."""
location_type: EntityText = Field(
description="地点类型",
default=None
)
country: EntityText = Field(
description="所属国家",
default=None
)
class Product(EntityModel):
"""A real product/service with brand name like "iPhone", "微信". NOT generic categories like "手机", "软件"."""
category: EntityText = Field(
description="产品类别",
default=None
)
manufacturer: EntityText = Field(
description="制造商",
default=None
)
class Event(EntityModel):
"""A real, documented event like "2024巴黎奥运会", "新冠疫情". NOT generic terms like "会议", "活动"."""
event_type: EntityText = Field(
description="事件类型",
default=None
)
date: EntityText = Field(
description="发生日期",
default=None
)
class Media(EntityModel):
"""A real media outlet like "人民日报", "CNN", "微博". NOT generic terms like "媒体", "新闻"."""
media_type: EntityText = Field(
description="媒体类型",
default=None
)
# ============== 边类型定义 ==============
class WorksFor(EdgeModel):
"""Employment or affiliation relationship."""
position: EntityText = Field(
description="职位",
default=None
)
class LocatedIn(EdgeModel):
"""Geographic location relationship."""
pass
class PartOf(EdgeModel):
"""Part-of or subsidiary relationship."""
pass
class Produces(EdgeModel):
"""Production or creation relationship."""
pass
class ParticipatesIn(EdgeModel):
"""Participation in an event."""
role: EntityText = Field(
description="参与角色",
default=None
)
class Collaborates(EdgeModel):
"""Collaboration or partnership relationship."""
pass
class Competes(EdgeModel):
"""Competitive relationship."""
pass
class Reports(EdgeModel):
"""Media reporting or coverage relationship."""
pass
# ============== 本体配置 ==============
# 实体类型字典
ENTITY_TYPES = {
"Person": Person,
"Organization": Organization,
"Company": Company,
"Location": Location,
"Product": Product,
"Event": Event,
"Media": Media,
}
# 边类型字典
EDGE_TYPES = {
"WORKS_FOR": WorksFor,
"LOCATED_IN": LocatedIn,
"PART_OF": PartOf,
"PRODUCES": Produces,
"PARTICIPATES_IN": ParticipatesIn,
"COLLABORATES": Collaborates,
"COMPETES": Competes,
"REPORTS": Reports,
}

View file

@ -1,405 +0,0 @@
"""
Zep Graph HTML渲染器
从Zep云服务获取图谱数据并生成交互式HTML可视化
"""
import os
import json
import argparse
import tempfile
import time
from datetime import datetime
from dotenv import load_dotenv
from zep_cloud.client import Zep
from pyvis.network import Network
# 加载环境变量
load_dotenv()
# 实体类型对应的颜色
ENTITY_COLORS = {
"Person": "#ff6b6b",
"Company": "#4ecdc4",
"Organization": "#45b7d1",
"Location": "#96ceb4",
"Product": "#ffeead",
"Event": "#dcc6e0",
"Media": "#ffb74d",
"Preference": "#a29bfe",
"Topic": "#fd79a8",
"Object": "#636e72",
"Entity": "#b2bec3",
}
# 默认颜色
DEFAULT_COLOR = "#74b9ff"
def get_graph_data(client: Zep, graph_id: str, max_retries: int = 3) -> tuple[list, list]:
"""
从Zep获取图谱的所有节点和边
"""
print(f"正在获取图谱 {graph_id} 的数据...")
nodes = None
edges = None
# 获取所有节点(带重试)
for attempt in range(max_retries):
try:
nodes = client.graph.node.get_by_graph_id(graph_id=graph_id)
print(f" 获取到 {len(nodes)} 个节点")
break
except Exception as e:
if attempt < max_retries - 1:
print(f" 获取节点失败,重试中... ({attempt + 1}/{max_retries})")
time.sleep(2)
else:
raise e
# 获取所有边(带重试)
for attempt in range(max_retries):
try:
edges = client.graph.edge.get_by_graph_id(graph_id=graph_id)
print(f" 获取到 {len(edges)} 个边")
break
except Exception as e:
if attempt < max_retries - 1:
print(f" 获取边失败,重试中... ({attempt + 1}/{max_retries})")
time.sleep(2)
else:
raise e
return nodes or [], edges or []
def create_html_graph(nodes: list, edges: list, graph_id: str, output_file: str):
"""
创建交互式HTML图谱可视化
"""
print("正在生成HTML可视化...")
# 创建网络图
net = Network(
height="100vh",
width="100%",
bgcolor="#1a1a2e",
font_color="white",
directed=True,
select_menu=True,
filter_menu=True,
notebook=False,
)
# 配置物理引擎和交互
net.set_options("""
{
"nodes": {
"font": {
"size": 14,
"face": "Arial, sans-serif",
"color": "white"
},
"borderWidth": 2,
"borderWidthSelected": 4,
"shadow": {
"enabled": true,
"color": "rgba(0,0,0,0.5)",
"size": 10
}
},
"edges": {
"color": {
"color": "#555555",
"highlight": "#667eea",
"hover": "#667eea"
},
"arrows": {
"to": {
"enabled": true,
"scaleFactor": 0.5
}
},
"smooth": {
"type": "continuous",
"roundness": 0.2
},
"font": {
"size": 10,
"color": "#aaaaaa",
"face": "Arial, sans-serif",
"strokeWidth": 0,
"background": "rgba(26,26,46,0.8)"
},
"width": 1.5,
"hoverWidth": 2.5
},
"physics": {
"enabled": true,
"barnesHut": {
"gravitationalConstant": -8000,
"centralGravity": 0.3,
"springLength": 150,
"springConstant": 0.04,
"damping": 0.09,
"avoidOverlap": 0.5
},
"stabilization": {
"enabled": true,
"iterations": 300,
"updateInterval": 25
}
},
"interaction": {
"hover": true,
"tooltipDelay": 100,
"navigationButtons": true,
"keyboard": {
"enabled": true
},
"multiselect": true,
"zoomView": true
}
}
""")
# 构建节点UUID到节点的映射
node_map = {}
for node in nodes:
node_map[node.uuid_] = node
# 统计节点类型
type_counts = {}
# 添加节点
for node in nodes:
node_labels = node.labels or []
specific_labels = [l for l in node_labels if l not in ["Entity", "Node"]]
node_type = specific_labels[0] if specific_labels else (node_labels[0] if node_labels else "Unknown")
color = ENTITY_COLORS.get(node_type, DEFAULT_COLOR)
type_counts[node_type] = type_counts.get(node_type, 0) + 1
# 简化的工具提示避免复杂HTML
summary_text = (node.summary or '无摘要')[:200]
title = f"{node.name}\n类型: {', '.join(node_labels)}\n摘要: {summary_text}"
# 根据节点类型调整大小
if node_type == "Person":
size = 25
elif node_type in ["Company", "Organization"]:
size = 30
elif node_type == "Location":
size = 22
elif node_type == "Event":
size = 20
else:
size = 18
net.add_node(
node.uuid_,
label=node.name[:20] + "..." if len(node.name) > 20 else node.name,
title=title,
color=color,
size=size,
)
# 添加边
edge_count = 0
for edge in edges:
source_uuid = edge.source_node_uuid
target_uuid = edge.target_node_uuid
if source_uuid in node_map and target_uuid in node_map:
source_node = node_map[source_uuid]
target_node = node_map[target_uuid]
title = f"{source_node.name} -> {target_node.name}\n关系: {edge.name or '未命名'}\n事实: {edge.fact or '无描述'}"
edge_label = edge.name or ""
if len(edge_label) > 15:
edge_label = edge_label[:15] + "..."
net.add_edge(
source_uuid,
target_uuid,
title=title,
label=edge_label,
)
edge_count += 1
print(f" 添加了 {len(nodes)} 个节点和 {edge_count} 条边到可视化")
# 打印类型统计
print("\n节点类型统计:")
for t, count in sorted(type_counts.items(), key=lambda x: -x[1]):
print(f" {t}: {count}")
# 保存到临时文件获取原始HTML
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as tmp:
net.save_graph(tmp.name)
tmp_path = tmp.name
# 读取生成的HTML
with open(tmp_path, 'r', encoding='utf-8') as f:
html_content = f.read()
os.unlink(tmp_path)
# 构建自定义CSS和Header HTML
custom_css = """
<style>
#graph-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 15px 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
color: white;
font-family: Arial, sans-serif;
}
#graph-header h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
#graph-header .stats {
font-size: 0.9rem;
opacity: 0.9;
margin-top: 5px;
}
#graph-legend {
position: fixed;
top: 80px;
right: 20px;
z-index: 1000;
background: rgba(26, 26, 46, 0.95);
border: 1px solid #333;
border-radius: 8px;
padding: 15px;
max-width: 200px;
color: white;
font-family: Arial, sans-serif;
}
#graph-legend h3 {
margin: 0 0 10px 0;
font-size: 0.9rem;
border-bottom: 1px solid #444;
padding-bottom: 5px;
}
#graph-legend .item {
display: flex;
align-items: center;
margin: 5px 0;
font-size: 0.8rem;
}
#graph-legend .color-box {
width: 12px;
height: 12px;
border-radius: 3px;
margin-right: 8px;
flex-shrink: 0;
}
#mynetwork {
margin-top: 70px !important;
}
body {
background: #1a1a2e !important;
}
</style>
"""
# 构建图例HTML
legend_items = ""
for entity_type, color in ENTITY_COLORS.items():
if entity_type in type_counts:
legend_items += f'''
<div class="item">
<div class="color-box" style="background: {color};"></div>
<span>{entity_type} ({type_counts.get(entity_type, 0)})</span>
</div>'''
custom_html = f'''
<div id="graph-header">
<h1>Knowledge Graph Visualization</h1>
<div class="stats">
Graph ID: {graph_id} | 节点: {len(nodes)} | : {edge_count} | 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
</div>
</div>
<div id="graph-legend">
<h3>图例</h3>
{legend_items}
</div>
'''
# 在</head>前插入自定义CSS
html_content = html_content.replace('</head>', custom_css + '</head>')
# 在<body>后插入自定义HTML
html_content = html_content.replace('<body>', '<body>' + custom_html)
# 修改标题
html_content = html_content.replace('<title>Network</title>', f'<title>Zep Graph: {graph_id}</title>')
# 写入文件
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"\nHTML文件已生成: {output_file}")
def main():
parser = argparse.ArgumentParser(description='Zep Graph HTML渲染器')
parser.add_argument(
'--graph-id', '-g',
default='graph_6e3697873495400d',
help='Zep图谱ID'
)
parser.add_argument(
'--output', '-o',
default='graph_visualization.html',
help='输出HTML文件路径'
)
parser.add_argument(
'--api-key', '-k',
default=None,
help='Zep API Key (默认从环境变量ZEP_API_KEY读取)'
)
args = parser.parse_args()
# 获取API Key
api_key = args.api_key or os.environ.get('ZEP_API_KEY')
if not api_key:
print("错误: 请设置ZEP_API_KEY环境变量或通过--api-key参数提供")
return 1
# 创建客户端
client = Zep(api_key=api_key)
try:
# 获取图数据
nodes, edges = get_graph_data(client, args.graph_id)
if not nodes:
print("警告: 图谱中没有节点")
return 1
# 生成HTML
create_html_graph(nodes, edges, args.graph_id, args.output)
print(f"\n完成! 请在浏览器中打开 {args.output} 查看图谱")
return 0
except Exception as e:
print(f"错误: {str(e)}")
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
exit(main())

View file

@ -1,18 +0,0 @@
# Zep Cloud SDK
zep-cloud>=2.0.0
# PDF处理
PyMuPDF>=1.24.0
# Markdown处理
markdown>=3.5.0
# Web可视化界面
streamlit>=1.38.0
# 图可视化
pyvis>=0.3.2
# 环境变量
python-dotenv>=1.0.0

View file

@ -1,126 +0,0 @@
"""
文本提取模块
支持从 .md, .txt, .pdf 文件中提取纯文本
"""
import os
from pathlib import Path
from typing import Optional
def extract_from_txt(file_path: str) -> str:
"""从TXT文件提取文本"""
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
def extract_from_md(file_path: str) -> str:
"""从Markdown文件提取文本保留原始格式不转换HTML"""
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
def extract_from_pdf(file_path: str) -> str:
"""从PDF文件提取文本"""
try:
import fitz # PyMuPDF
except ImportError:
raise ImportError("请安装 PyMuPDF: pip install PyMuPDF")
text_parts = []
with fitz.open(file_path) as doc:
for page_num, page in enumerate(doc):
text = page.get_text()
if text.strip():
text_parts.append(f"--- 第 {page_num + 1} 页 ---\n{text}")
return "\n\n".join(text_parts)
def extract_text(file_path: str) -> str:
"""
根据文件扩展名自动选择提取方法
Args:
file_path: 文件路径
Returns:
提取的纯文本内容
Raises:
ValueError: 不支持的文件格式
FileNotFoundError: 文件不存在
"""
path = Path(file_path)
if not path.exists():
raise FileNotFoundError(f"文件不存在: {file_path}")
suffix = path.suffix.lower()
extractors = {
'.txt': extract_from_txt,
'.md': extract_from_md,
'.markdown': extract_from_md,
'.pdf': extract_from_pdf,
}
extractor = extractors.get(suffix)
if extractor is None:
supported = ', '.join(extractors.keys())
raise ValueError(f"不支持的文件格式: {suffix}。支持的格式: {supported}")
return extractor(file_path)
def split_text_into_chunks(text: str, max_chunk_size: int = 8000, overlap: int = 200) -> list[str]:
"""
将长文本分割成多个小块适合Zep处理
Args:
text: 原始文本
max_chunk_size: 每个块的最大字符数
overlap: 块之间的重叠字符数
Returns:
文本块列表
"""
if len(text) <= max_chunk_size:
return [text]
chunks = []
start = 0
while start < len(text):
end = start + max_chunk_size
# 尝试在句子边界处分割
if end < len(text):
# 查找最近的句子结束符
for sep in ['', '', '', '\n\n', '. ', '! ', '? ']:
last_sep = text[start:end].rfind(sep)
if last_sep != -1 and last_sep > max_chunk_size * 0.5:
end = start + last_sep + len(sep)
break
chunk = text[start:end].strip()
if chunk:
chunks.append(chunk)
# 下一个块从重叠位置开始
start = end - overlap if end < len(text) else len(text)
return chunks
if __name__ == "__main__":
# 测试
import sys
if len(sys.argv) > 1:
file_path = sys.argv[1]
text = extract_text(file_path)
print(f"提取了 {len(text)} 个字符")
print(f"前500字符:\n{text[:500]}")