-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgui.py
More file actions
3869 lines (3260 loc) · 159 KB
/
gui.py
File metadata and controls
3869 lines (3260 loc) · 159 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import sys
import os
import yaml
import pandas as pd
import logging
from pathlib import Path
from datetime import datetime
from typing import Dict, Any
import getpass
import platform
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QLineEdit, QComboBox, QTextEdit,
QPushButton, QFileDialog, QMessageBox, QProgressBar,
QSplitter, QTableWidget, QTableWidgetItem, QHeaderView,
QGroupBox, QFormLayout, QFrame, QSizePolicy, QStyleFactory,
QGraphicsDropShadowEffect, QStackedWidget, QListWidget,
QListWidgetItem, QScrollArea, QCheckBox, QTabWidget,
QSpinBox, QSlider, QToolButton, QPlainTextEdit, QGridLayout)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSize, QPropertyAnimation, QEasingCurve
from PyQt6.QtGui import QFont, QIcon, QPalette, QColor, QAction
# Add project root to path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from modules.universal_enricher import UniversalEnricher
# Setup logging
logger = logging.getLogger(__name__)
# Constants
CONFIG_DIR = Path("config")
DOMAINS_FILE = CONFIG_DIR / "domains.yaml"
SETTINGS_FILE = CONFIG_DIR / "settings.yaml"
DATA_DIR = Path("data")
# --- Default Prompts (Kept from original) ---
DEFAULT_UNIVERSAL_PROMPT = """
# 🌐 通用知识图谱数据构建指令
## 🎯 核心目标
- **实体名称**: {entity_name}
- **任务**: 为该实体构建详细的结构化属性数据
## 📋 数据要求
1. **准确性**: 确保所有信息基于事实,优先参考权威来源。
2. **完整性**: 尽可能完善地填写所有定义的属性字段。
3. **语言**: 除非专有名词,否则请使用**简体中文**。
## 🏗️ 属性定义
请基于以下维度提取信息(请根据实际Schema调整):
- **基础信息**: 定义、分类、别名等
- **核心特征**: 关键参数、规格、性质
- **关联关系**: 上游来源、下游应用、相关实体
- **描述信息**: 功能描述、背景介绍
## 📤 输出规范
请返回严格的 JSON 格式数据:
```json
{{
"data_source": "模型知识",
"名称": "{entity_name}",
"别名": "别名1; 别名2",
"类型": "实体类型",
"描述": "详细描述...",
"关键属性": "属性值..."
}}
```
"""
DEFAULT_CHEMICAL_PROMPT = """
## 🎯 查询目标
- **化学品名称**: {entity_name}
## 🔍 CAS号与流水号智能识别指引
### 📋 "CAS号或流水号"字段说明:
- **CAS号**:国际通用化学物质唯一标识编号(格式:XXXX-XX-X,如64-17-5代表乙醇)
- **流水号**:名录编制单位自定义编号,用于无CAS号的新化学品、复合物、特殊材料
- **优先级**:优先使用国际标准CAS号,无CAS号时用本地流水号保证唯一性
### 🎯 核心任务
1. **CAS号验证与补充**:如当前编号为空、格式错误或为流水号,必须查询补充准确的CAS号
2. **编号唯一性检查**:确保每个化学品都有唯一标识符
3. **格式标准化**:CAS号格式必须为"数字-数字-数字"标准格式
4. **数据关联性验证**:确认编号与化学品名称的准确对应关系
5. **源数据兼容性**:兼容《中国化学品名录2013年版》的"CAS号或流水号"字段结构
## 📋 知识图谱属性要求 (用于构建化学品知识图谱)
请为上述化学品提供以下详细信息,用于构建完整的化学品知识图谱。所有数据必须以**简体中文**表述,并确保内容的详尽和准确。
### 🔬 基础标识信息(重点优化)
- **名称**: 化学品的标准中文名称
- **CAS号或流水号**:
- 如为标准CAS号,保持原格式并验证准确性
- 如为流水号,查询是否存在对应CAS号并优先使用CAS号
- 如为空值,从权威数据库查询补充标准CAS号
- 格式要求:CAS号严格为"XXXX-XX-X",流水号为纯数字
- **别名**: 包含所有常用别名,例如英文名、商品名、俗称、学名等。格式:"别名1; 别名2; 别名3; 别名4; 别名5",至少提供3-5个有价值的别名,用分号分隔
- **分子式**: 准确的化学分子式,例如 "C2H6O", "H2SO4"
- **分子量**: 准确的分子量数值,单位为 g/mol,保留至少两位小数,例如 "46.07"
### ⚠️ 危害与安全信息
- **是否为危化品**: 基于《危险化学品目录》,必须明确回答"是"或"否"
- **浓度阈值**: 参考原名录"浓度阈值"字段,详细说明毒理学数据,例如 "LC50(大鼠吸入): 20000 ppm/10h; LD50(大鼠经口): 7060 mg/kg; LD50(兔子皮肤): >5000 mg/kg"
- **危害**: 详细描述对人体和环境的具体危害,需分类说明:
- **健康危害**: 急性毒性、皮肤腐蚀/刺激、严重眼损伤/眼刺激、致癌性、生殖毒性等
- **环境危害**: 对水生生物的危害、持久性、生物累积性等
- **物理危害**: 易燃性、爆炸性、氧化性等
- **防范**: 具体的防护措施和注意事项,需分类说明:
- **工程控制**: 通风系统、密闭操作等
- **个体防护**: 呼吸系统防护、眼睛防护、身体防护、手部防护
- **操作处置与储存**: 操作注意事项、储存条件
- **危害处置**: 发生事故时的具体应急处置方法和急救措施,需分类说明:
- **泄漏应急处理**: 环境、人员、处理方法
- **火灾处置**: 灭火方法、有害燃烧产物
- **急救措施**: 皮肤接触、眼睛接触、吸入、食入后的急救方法
### 🏭 产业链信息 (知识图谱核心)
- **用途**: 详细说明主要用途和应用领域,至少列举5个具体用途,并描述其在应用中扮演的角色
- **自然来源**: 详细说明该化学品在自然界中的存在形式、分布情况、天然来源(如植物、矿物、微生物等),以及天然提取方法。如果是纯人工合成的化学品,则说明"无天然来源,纯人工合成"
- **生产来源 (上游)**: 详细列出其直接上游原料化学品,以及主要的生产商或供应商信息。这是构建产业链上游关系的关键
- **工业生产原料 (下游)**: 详细列出该化学品作为原料可以用于生产哪些下游产品或化学品。这是构建产业链下游关系的关键
### ⚗️ 物理化学性质
- **性质**: 提供一个综合性的、结构化的物理化学性质描述,至少包括:
- **外观与性状**: 详细描述常温常压下的颜色、状态、气味等感官特征
- **熔点**: 数值+单位(°C),例如 "-114.1°C"
- **沸点**: 数值+单位(°C),例如 "78.3°C"
- **密度**: 数值+单位,并注明温度,例如 "0.789 g/cm³(20°C)"
- **溶解性**: 在水、乙醇等常见溶剂中的溶解情况,可包含定量数据
- **闪点**: 数值+单位(°C),并注明开杯/闭杯,例如 "13°C (闭杯)"
- **稳定性**: 描述其化学稳定性、需要避免的条件(如光、热)和禁配物质
## 📤 输出格式
**必须严格以JSON对象格式返回,确保所有字段完整填写,不要包含任何额外的解释或Markdown标记。**
**重要:除了 `data_source` 字段,其他字段的数据后不需要标明数据来源。**
```json
{{
"data_source": "网络搜索/模型知识 {{数据来源}}",
"名称": "化学品标准中文名称",
"CAS号或流水号": "优先CAS号格式XXXX-XX-X,无CAS号时为流水号",
"编号类型说明": "标准CAS号/本地流水号/新分配CAS号",
"别名": "别名1; 别名2; 别名3",
"分子式": "化学分子式",
"分子量": "数值 g/mol",
"是否为危化品": "是/否",
"浓度阈值": "毒理学数据详情",
"危害": "危害描述",
"防范": "防护措施详情",
"危害处置": "应急处置详情",
"用途": "用途详情",
"自然来源": "天然来源详情",
"生产来源": "上游原料详情",
"工业生产原料": "下游产品详情",
"性质": "物理化学性质详情"
}}
```
## ⚡ 特别要求 (知识图谱专用)
- 🔍 **CAS号/流水号智能处理**:
- 如输入为流水号,必须查询是否存在对应的标准CAS号
- 如输入为空值或格式错误,必须从权威数据库查询补充标准CAS号
- 优先使用国际CAS号,确保全球通用性和数据关联性
- 保证每个化学品都有唯一且准确的标识符
- 📊 **数据来源权威性**: 优先使用PubChem、ECHA等权威源,确保数据质量
- 🎯 **内容详细具体**: 每个字段都要详细填写,避免使用模糊或笼统的描述,为知识图谱提供高质量的属性信息
- 🔗 **关联性描述**: 特别注意产业链(生产来源、工业生产原料)和危害信息的准确性和完整性,这是构建关系图谱的核心
- 📋 **格式严格统一**: 严格遵守JSON输出格式,便于知识图谱的自动化结构化处理
## 💡 字段填写指导
- **CAS号或流水号字段**:
- 标准CAS号示例:64-17-5(乙醇)、7732-18-5(水)
- 流水号示例:202401001、300015678(纯数字格式)
- 格式验证:确保CAS号符合"数字-数字-数字"标准
- **别名字段**: 包含学名、俗名、商品名、英文名等,用分号分隔,以提供丰富的检索入口
- **浓度阈值**: 尽量提供多种物种(大鼠、兔子等)和多种途径(经口、吸入、皮肤)的毒理学数据,**参考原名录"浓度阈值"字段进行详细说明**。
- **自然来源**: 详细描述天然存在情况,包括在哪些植物、动物、矿物或微生物中发现,以及天然提取工艺
- **产业链信息**: 要清晰地体现化学品在整个产业链中的上游原料和下游产品关系
- **性质描述**: 尽量提供定量数据,并注明测试条件(如温度、压力)
- **数据来源标注**: **仅在顶层 `data_source` 字段中说明本次查询的主要信息来源,例如 "网络搜索 {{PubChem; ECHA}}" 或 "模型知识 {{模型知识}}"。其他字段无需标注来源。**
现在开始查询并生成用于知识图谱的化学品详细数据:
"""
# --- Styles ---
class Theme:
LIGHT = {
"bg_main": "#f8f9fa", # 更现代的浅灰背景
"bg_card": "#ffffff", # 纯白卡片
"bg_sidebar": "#ffffff", # 侧边栏背景
"text_main": "#212529", # 深灰主文本
"text_secondary": "#495057", # 次要文本
"text_muted": "#adb5bd", # 弱化文本
"accent": "#0d6efd", # 现代蓝
"accent_hover": "#0b5ed7", # 悬停状态
"border": "#dee2e6", # 边框色
"input_bg": "#ffffff", # 输入框背景
"selection": "#e7f1ff", # 选中背景
"selection_text": "#0d6efd",# 选中文本
"danger": "#dc3545", # 危险色
"danger_hover": "#bb2d3b", # 危险色悬停
"scroll_bg": "#f8f9fa",
"scroll_handle": "#ced4da", # 滚动条
"success": "#198754", # 成功色
"warning": "#ffc107" # 警告色
}
DARK = {
"bg_main": "#212529", # 深色背景
"bg_card": "#2c3034", # 卡片背景
"bg_sidebar": "#2c3034", # 侧边栏背景
"text_main": "#f8f9fa", # 主文本
"text_secondary": "#dee2e6", # 次要文本
"text_muted": "#6c757d", # 弱化文本
"accent": "#0d6efd", # 现代蓝
"accent_hover": "#0b5ed7", # 悬停状态
"border": "#495057", # 边框颜色
"input_bg": "#343a40", # 输入框背景
"selection": "#0a58ca", # 选中背景
"selection_text": "#ffffff",# 选中文本
"danger": "#dc3545", # 危险色
"danger_hover": "#bb2d3b", # 危险色悬停
"scroll_bg": "#212529",
"scroll_handle": "#495057", # 滚动条
"success": "#198754", # 成功色
"warning": "#ffc107" # 警告色
}
class ModernStyle:
@staticmethod
def get_style(theme_name="Light"):
colors = Theme.DARK if theme_name == "Dark" else Theme.LIGHT
return f"""
/* Global */
QMainWindow {{
background-color: {colors['bg_main']};
}}
QWidget {{
font-family: 'Segoe UI', 'Microsoft YaHei UI', sans-serif;
font-size: 14px;
color: {colors['text_main']};
}}
/* Sidebar */
QListWidget {{
background-color: {colors['bg_sidebar']};
border: none;
outline: none;
padding: 10px;
border-right: 1px solid {colors['border']};
}}
QListWidget::item {{
height: 40px;
border-radius: 6px;
padding-left: 10px;
margin-bottom: 2px;
color: {colors['text_secondary']};
font-weight: 500;
}}
QListWidget::item:selected {{
background-color: {colors['selection']};
color: {colors['selection_text']};
font-weight: 600;
}}
QListWidget::item:hover {{
background-color: {colors['bg_main']};
color: {colors['text_main']};
}}
/* Cards/Containers */
QFrame#Card {{
background-color: {colors['bg_card']};
border-radius: 8px;
border: 1px solid {colors['border']};
}}
/* Buttons */
QPushButton {{
background-color: {colors['accent']};
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-weight: 600;
font-size: 13px;
}}
QPushButton:hover {{
background-color: {colors['accent_hover']};
}}
QPushButton:pressed {{
background-color: {colors['accent']};
padding-top: 7px;
padding-bottom: 5px;
}}
QPushButton:disabled {{
background-color: {colors['border']};
color: {colors['text_muted']};
}}
QPushButton#SecondaryButton {{
background-color: transparent;
border: 1px solid {colors['border']};
color: {colors['text_secondary']};
}}
QPushButton#SecondaryButton:hover {{
background-color: {colors['bg_main']};
border-color: {colors['accent']};
color: {colors['accent']};
}}
QPushButton#DangerButton {{
background-color: {colors['danger']};
}}
QPushButton#DangerButton:hover {{
background-color: {colors['danger_hover']};
}}
QPushButton#GhostButton {{
background-color: transparent;
color: {colors['text_secondary']};
border: none;
}}
QPushButton#GhostButton:hover {{
background-color: {colors['bg_main']};
color: {colors['text_main']};
}}
/* Inputs */
QLineEdit, QTextEdit, QPlainTextEdit {{
border: 1px solid {colors['border']};
border-radius: 6px;
padding: 8px;
background-color: {colors['input_bg']};
color: {colors['text_main']};
selection-background-color: {colors['selection']};
selection-color: {colors['selection_text']};
}}
QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus {{
border: 1px solid {colors['accent']};
background-color: {colors['bg_card']};
}}
/* ComboBox */
QComboBox {{
border: 1px solid {colors['border']};
border-radius: 6px;
padding: 6px 10px;
background-color: {colors['input_bg']};
color: {colors['text_main']};
min-width: 6em;
}}
QComboBox:hover {{
border-color: {colors['accent']};
}}
QComboBox::drop-down {{
subcontrol-origin: padding;
subcontrol-position: top right;
width: 20px;
border-left-width: 0px;
}}
QComboBox QAbstractItemView {{
border: 1px solid {colors['border']};
background-color: {colors['bg_card']};
selection-background-color: {colors['selection']};
selection-color: {colors['selection_text']};
outline: none;
}}
/* GroupBox */
QGroupBox {{
border: 1px solid {colors['border']};
border-radius: 8px;
margin-top: 1.2em;
padding-top: 10px;
font-weight: 600;
color: {colors['text_secondary']};
}}
QGroupBox::title {{
subcontrol-origin: margin;
subcontrol-position: top left;
padding: 0 5px;
left: 10px;
}}
/* TabWidget */
QTabWidget::pane {{
border: 1px solid {colors['border']};
border-radius: 6px;
background-color: {colors['bg_card']};
}}
QTabBar::tab {{
background: {colors['bg_main']};
color: {colors['text_secondary']};
padding: 8px 16px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
margin-right: 2px;
}}
QTabBar::tab:selected {{
background: {colors['bg_card']};
color: {colors['accent']};
border-bottom: 2px solid {colors['accent']};
font-weight: bold;
}}
QTabBar::tab:hover {{
color: {colors['text_main']};
}}
/* ProgressBar */
QProgressBar {{
border: none;
background-color: {colors['border']};
border-radius: 4px;
text-align: center;
color: white;
}}
QProgressBar::chunk {{
background-color: {colors['accent']};
border-radius: 4px;
}}
/* ToolTip */
QToolTip {{
border: 1px solid {colors['border']};
background-color: {colors['bg_card']};
color: {colors['text_main']};
padding: 4px;
border-radius: 4px;
opacity: 230;
}}
/* Tables */
QTableWidget {{
border: 1px solid {colors['border']};
border-radius: 8px;
background-color: {colors['bg_card']};
gridline-color: {colors['border']};
color: {colors['text_main']};
selection-background-color: {colors['selection']};
selection-color: {colors['selection_text']};
alternate-background-color: {colors['bg_main']};
}}
QHeaderView::section {{
background-color: {colors['bg_main']};
padding: 8px;
border: none;
border-bottom: 1px solid {colors['border']};
font-weight: 600;
color: {colors['text_secondary']};
}}
QTableWidget::item {{
padding: 6px;
}}
/* Scrollbar */
QScrollBar:vertical {{
border: none;
background: {colors['scroll_bg']};
width: 8px;
border-radius: 4px;
margin: 0px;
}}
QScrollBar::handle:vertical {{
background: {colors['scroll_handle']};
border-radius: 4px;
min-height: 20px;
}}
QScrollBar::handle:vertical:hover {{
background: {colors['accent']};
}}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
height: 0px;
}}
/* Status Bar */
QStatusBar {{
background-color: {colors['bg_main']};
color: {colors['text_secondary']};
border-top: 1px solid {colors['border']};
}}
"""
# --- Worker Thread ---
class WorkerThread(QThread):
finished = pyqtSignal(object)
error = pyqtSignal(str)
progress = pyqtSignal(int)
status = pyqtSignal(str)
def __init__(self, task_func, *args, **kwargs):
super().__init__()
self.task_func = task_func
self.args = args
self.kwargs = kwargs
def run(self):
try:
result = self.task_func(*self.args, **self.kwargs)
self.finished.emit(result)
except Exception as e:
self.error.emit(str(e))
# --- Components ---
class ToastNotification(QWidget):
def __init__(self, parent, message, type="info"):
super().__init__(parent)
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.SubWindow)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating)
layout = QHBoxLayout(self)
layout.setContentsMargins(24, 12, 24, 12)
# Add icon based on type (using emoji for simplicity)
icon_map = {"info": "ℹ️", "success": "✅", "error": "❌", "warning": "⚠️"}
icon_label = QLabel(icon_map.get(type, "ℹ️"))
icon_label.setStyleSheet("font-size: 16px; margin-right: 8px; color: white; background: transparent;")
layout.addWidget(icon_label)
self.label = QLabel(message)
self.label.setStyleSheet("color: white; font-weight: 600; font-size: 14px; background: transparent;")
layout.addWidget(self.label)
color = "#0d6efd" # Info
if type == "success": color = "#198754"
elif type == "error": color = "#dc3545"
elif type == "warning": color = "#ffc107"
self.setStyleSheet(f"""
QWidget {{
background-color: {color};
border-radius: 8px;
}}
""")
# Shadow effect
shadow = QGraphicsDropShadowEffect(self)
shadow.setBlurRadius(15)
shadow.setColor(QColor(0, 0, 0, 60))
shadow.setOffset(0, 4)
self.setGraphicsEffect(shadow)
# Animation
self.opacity_anim = QPropertyAnimation(self, b"windowOpacity")
self.opacity_anim.setDuration(300)
self.opacity_anim.setStartValue(0.0)
self.opacity_anim.setEndValue(1.0)
self.opacity_anim.setEasingCurve(QEasingCurve.Type.OutCubic)
self.opacity_anim.start()
# Auto close
from PyQt6.QtCore import QTimer
QTimer.singleShot(3000, self.fade_out)
def fade_out(self):
self.opacity_anim.setDirection(QPropertyAnimation.Direction.Backward)
self.opacity_anim.finished.connect(self.close)
self.opacity_anim.start()
class Sidebar(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedWidth(260)
self.setObjectName("Sidebar")
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Logo Area
logo_frame = QFrame()
logo_frame.setFixedHeight(80)
logo_layout = QHBoxLayout(logo_frame)
logo_layout.setContentsMargins(20, 20, 20, 20)
# Logo Icon
logo_icon = QLabel("🌐")
logo_icon.setStyleSheet("""
font-size: 24px;
background-color: #0d6efd;
color: white;
border-radius: 8px;
padding: 4px;
""")
logo_layout.addWidget(logo_icon)
logo_text_layout = QVBoxLayout()
logo_text_layout.setSpacing(0)
logo_title = QLabel("Universal KG")
logo_title.setStyleSheet("""
font-size: 18px;
font-weight: 800;
font-family: 'Segoe UI', sans-serif;
""")
logo_subtitle = QLabel("Builder v0.5.0")
logo_subtitle.setStyleSheet("font-size: 11px; color: #0d6efd; font-weight: 600; margin-top: 2px;")
logo_text_layout.addWidget(logo_title)
logo_text_layout.addWidget(logo_subtitle)
logo_layout.addLayout(logo_text_layout)
logo_layout.addStretch()
layout.addWidget(logo_frame)
# Navigation List
self.nav_list = QListWidget()
self.nav_list.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.nav_list.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
items = [
("🏠 仪表盘", "dashboard", "项目概览与快捷入口"),
("🚀 智能向导", "wizard", "AI辅助创建领域和生成初始数据集"),
("🏷️ 领域配置", "domain", "配置知识图谱的领域Schema和提示词"),
("📂 数据处理", "data", "导入CSV数据并进行知识补全"),
("📊 结果预览", "preview", "查看处理后的数据结果"),
("⚙️ 流水线", "pipeline", "运行完整的数据处理流水线"),
("🔧 设置", "settings", "配置API Key和外观")
]
for text, data, tooltip in items:
item = QListWidgetItem(text)
item.setData(Qt.ItemDataRole.UserRole, data)
item.setToolTip(tooltip)
self.nav_list.addItem(item)
self.nav_list.setCurrentRow(0)
layout.addWidget(self.nav_list)
# Theme Toggle & User Profile
bottom_frame = QFrame()
bottom_frame.setStyleSheet("border-top: 1px solid #dee2e6;")
bottom_layout = QVBoxLayout(bottom_frame)
bottom_layout.setContentsMargins(16, 16, 16, 16)
bottom_layout.setSpacing(12)
# Theme Toggle
theme_layout = QHBoxLayout()
theme_label = QLabel("深色模式")
theme_label.setStyleSheet("font-size: 12px; font-weight: 600;")
self.theme_toggle = QCheckBox()
self.theme_toggle.setCursor(Qt.CursorShape.PointingHandCursor)
self.theme_toggle.toggled.connect(self.toggle_theme)
theme_layout.addWidget(theme_label)
theme_layout.addStretch()
theme_layout.addWidget(self.theme_toggle)
bottom_layout.addLayout(theme_layout)
# User Info
user_layout = QHBoxLayout()
avatar = QLabel("👤")
avatar.setStyleSheet("""
font-size: 20px;
background-color: #e7f1ff;
color: #0d6efd;
border-radius: 18px;
padding: 6px;
""")
avatar.setAlignment(Qt.AlignmentFlag.AlignCenter)
avatar.setFixedSize(36, 36)
user_info = QVBoxLayout()
user_info.setSpacing(0)
current_user = getpass.getuser()
user_name = QLabel(current_user)
user_name.setStyleSheet("font-weight: bold; font-size: 13px;")
status_lbl = QLabel("Online")
status_lbl.setStyleSheet("color: #198754; font-size: 11px;")
user_info.addWidget(user_name)
user_info.addWidget(status_lbl)
user_layout.addWidget(avatar)
user_layout.addLayout(user_info)
user_layout.addStretch()
bottom_layout.addLayout(user_layout)
layout.addWidget(bottom_frame)
def toggle_theme(self, checked):
window = self.window()
if hasattr(window, 'apply_theme'):
window.apply_theme("Dark" if checked else "Light")
class SchemaEditor(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# Entity Type
type_layout = QHBoxLayout()
type_layout.addWidget(QLabel("实体类型:"))
self.entity_type_input = QLineEdit()
self.entity_type_input.setPlaceholderText("例如: Chemical, Protein... (建议使用英文)")
type_layout.addWidget(self.entity_type_input)
layout.addLayout(type_layout)
# Attributes Table
self.table = QTableWidget()
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels(["属性名称", "数据类型", "属性描述"])
self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.table.setAlternatingRowColors(True)
layout.addWidget(self.table)
# Buttons
btn_layout = QHBoxLayout()
self.btn_add = QPushButton("➕ 添加属性")
self.btn_add.clicked.connect(lambda: self.add_row())
self.btn_remove = QPushButton("➖ 删除选中")
self.btn_remove.setObjectName("DangerButton")
self.btn_remove.clicked.connect(self.remove_row)
btn_layout.addWidget(self.btn_add)
btn_layout.addWidget(self.btn_remove)
btn_layout.addStretch()
layout.addLayout(btn_layout)
def set_data(self, schema_data):
self.entity_type_input.setText(schema_data.get('entity_type', ''))
attributes = schema_data.get('attributes', [])
self.table.setRowCount(0)
for attr in attributes:
self.add_row(attr.get('name', ''), attr.get('type', 'String'), attr.get('description', ''))
def get_data(self):
attributes = []
for i in range(self.table.rowCount()):
name_item = self.table.item(i, 0)
type_widget = self.table.cellWidget(i, 1)
desc_item = self.table.item(i, 2)
if name_item and name_item.text().strip():
attributes.append({
"name": name_item.text().strip(),
"type": type_widget.currentText() if type_widget else "String",
"description": desc_item.text().strip() if desc_item else ""
})
return {
"entity_type": self.entity_type_input.text().strip(),
"attributes": attributes
}
def add_row(self, name="", type_val="String", desc=""):
row = self.table.rowCount()
self.table.insertRow(row)
self.table.setItem(row, 0, QTableWidgetItem(str(name)))
combo = QComboBox()
combo.addItems(["String", "Integer", "Float", "Boolean", "List", "Date"])
combo.setCurrentText(type_val if type_val else "String")
self.table.setCellWidget(row, 1, combo)
self.table.setItem(row, 2, QTableWidgetItem(str(desc)))
def remove_row(self):
rows = set(index.row() for index in self.table.selectedIndexes())
for row in sorted(rows, reverse=True):
self.table.removeRow(row)
class VariableButton(QPushButton):
"""可点击的变量标签按钮,点击后插入变量到文本框"""
def __init__(self, var_name, description, target_editor=None):
super().__init__()
self.var_name = var_name
self.target_editor = target_editor
self.setText(f"{{{var_name}}}")
self.setToolTip(description)
self.setFixedHeight(28)
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.setStyleSheet("""
QPushButton {
background-color: #fff3e0;
color: #e67e22;
border: 1px solid #ffcc80;
border-radius: 4px;
padding: 2px 8px;
font-size: 12px;
font-family: 'Consolas', 'Courier New', monospace;
}
QPushButton:hover {
background-color: #ffe0b2;
border-color: #ffb74d;
}
QPushButton:pressed {
background-color: #ffcc80;
}
""")
self.clicked.connect(self.insert_variable)
def insert_variable(self):
if self.target_editor and hasattr(self.target_editor, 'textCursor'):
try:
cursor = self.target_editor.textCursor()
# 检查文本编辑器是否有内容,避免位置越界
text_length = len(self.target_editor.toPlainText())
current_pos = cursor.position()
# 确保位置在有效范围内
if current_pos <= text_length:
cursor.insertText(f"{{{self.var_name}}}")
self.target_editor.setFocus()
else:
# 如果位置无效,移动到文本末尾再插入
cursor.movePosition(cursor.MoveOperation.End)
cursor.insertText(f"{{{self.var_name}}}")
self.target_editor.setFocus()
except Exception as e:
# 记录错误但不中断程序
print(f"插入变量时出错: {e}")
# 作为备用方案,直接在末尾添加文本
try:
current_text = self.target_editor.toPlainText()
self.target_editor.setPlainText(current_text + f"{{{self.var_name}}}")
except:
pass # 如果备用方案也失败,静默忽略
class PromptBuilderWidget(QWidget):
"""增强的 Prompt 构建器,支持变量插入、预览和模板选择"""
prompt_changed = pyqtSignal() # 当 prompt 内容改变时发出
def __init__(self, prompt_type="user", parent=None):
super().__init__(parent)
self.prompt_type = prompt_type # "system" or "user"
self.preview_entity = "示例实体"
self.preview_attributes = "属性1, 属性2, 属性3"
self.preview_source = "优先参考权威数据库"
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(8)
# 初始化变量按钮列表
self.var_buttons = []
# 标题和模板选择
header_layout = QHBoxLayout()
title = QLabel("📝 System Prompt" if self.prompt_type == "system" else "💬 User Prompt Template")
title.setStyleSheet("font-weight: bold; font-size: 14px;")
header_layout.addWidget(title)
header_layout.addStretch()
# 模板下拉菜单
if self.prompt_type == "user":
self.template_combo = QComboBox()
self.template_combo.setMinimumWidth(150)
self.template_combo.addItems([
"-- 选择模板 --",
"🌐 通用知识模板",
"🧪 化学品模板",
"🏥 医药模板",
"🏭 制造业模板",
"📚 学术模板",
"🔬 科研模板"
])
self.template_combo.currentTextChanged.connect(self._on_template_selected)
header_layout.addWidget(QLabel("快速模板:"))
header_layout.addWidget(self.template_combo)
layout.addLayout(header_layout)
# 变量插入区域 (仅用于 User Prompt)
if self.prompt_type == "user":
var_frame = QFrame()
var_frame.setStyleSheet("""
QFrame {
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 5px;
}
""")
var_layout = QVBoxLayout(var_frame)
var_layout.setContentsMargins(10, 8, 10, 8)
var_layout.setSpacing(6)
var_title = QLabel("📌 点击插入变量:")
var_title.setStyleSheet("color: #666; font-size: 12px; font-weight: bold;")
var_layout.addWidget(var_title)
# 变量按钮行
btn_layout = QHBoxLayout()
btn_layout.setSpacing(8)
variables = [
("entity_name", "实体名称 - 当前处理的实体名"),
("attributes", "属性列表 - Schema中定义的属性"),
("source_instruction", "数据来源 - 数据来源要求说明"),
]
for var_name, desc in variables:
btn = VariableButton(var_name, desc)
self.var_buttons.append(btn)
btn_layout.addWidget(btn)
btn_layout.addStretch()
var_layout.addLayout(btn_layout)
layout.addWidget(var_frame)
# 编辑器
self.editor = QTextEdit()
self.editor.setFont(QFont("Consolas", 11))
self.editor.setMinimumHeight(120 if self.prompt_type == "system" else 200)
self.editor.setPlaceholderText(
"输入 System Prompt,设定 AI 的角色和行为..." if self.prompt_type == "system"
else "输入 User Prompt 模板,使用 {变量名} 插入动态变量..."
)
self.editor.textChanged.connect(self._on_text_changed)
layout.addWidget(self.editor)
# 更新变量按钮的目标编辑器
for btn in self.var_buttons:
btn.target_editor = self.editor
# 预览区域 (仅用于 User Prompt)
if self.prompt_type == "user":
# 预览折叠面板
preview_header = QHBoxLayout()
self.preview_toggle = QPushButton("👁️ 预览效果")
self.preview_toggle.setObjectName("SecondaryButton")
self.preview_toggle.setCheckable(True)
self.preview_toggle.clicked.connect(self._toggle_preview)
preview_header.addWidget(self.preview_toggle)
preview_header.addStretch()
# 预览参数输入
preview_header.addWidget(QLabel("测试实体:"))
self.preview_entity_input = QLineEdit("示例化学品")
self.preview_entity_input.setMaximumWidth(120)
self.preview_entity_input.textChanged.connect(self._update_preview)
preview_header.addWidget(self.preview_entity_input)
layout.addLayout(preview_header)
# 预览内容区
self.preview_frame = QFrame()
self.preview_frame.setStyleSheet("""
QFrame {
background-color: #fff8e1;
border: 1px solid #ffcc80;
border-radius: 6px;
}
""")
self.preview_frame.setVisible(False)
preview_layout = QVBoxLayout(self.preview_frame)
preview_layout.setContentsMargins(12, 12, 12, 12)
preview_label = QLabel("📋 渲染后的 Prompt:")
preview_label.setStyleSheet("color: #e65100; font-weight: bold; font-size: 12px;")
preview_layout.addWidget(preview_label)
self.preview_text = QTextEdit()
self.preview_text.setReadOnly(True)
self.preview_text.setMaximumHeight(150)
self.preview_text.setStyleSheet("""
QTextEdit {
background-color: #fffde7;
border: none;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 11px;
}
""")
preview_layout.addWidget(self.preview_text)
layout.addWidget(self.preview_frame)
def _on_template_selected(self, template_name):
"""当选择模板时填充内容"""
templates = {
"🌐 通用知识模板": DEFAULT_UNIVERSAL_PROMPT,
"🧪 化学品模板": DEFAULT_CHEMICAL_PROMPT,
"🏥 医药模板": self._get_medical_template(),
"🏭 制造业模板": self._get_manufacturing_template(),
"📚 学术模板": self._get_academic_template(),
"🔬 科研模板": self._get_research_template(),
}
if template_name in templates:
self.editor.setText(templates[template_name])
def _get_medical_template(self):