article-introduction-generator 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,993 @@
1
+ import json
2
+ import sys
3
+ import os
4
+ import subprocess
5
+ import signal
6
+ import traceback
7
+
8
+ from PyQt5.QtWidgets import (
9
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
10
+ QLabel, QTextEdit, QLineEdit, QPushButton, QFileDialog,
11
+ QTabWidget, QListWidget, QMessageBox, QStatusBar, QToolBar,
12
+ QComboBox, QScrollArea, QListWidgetItem, QSizePolicy, QAction, QDialog
13
+ )
14
+
15
+ from PyQt5.QtGui import QIcon, QDesktopServices
16
+ from PyQt5.QtCore import Qt, QUrl, QSize
17
+ from PyQt5.QtCore import QObject, QThread, pyqtSignal
18
+
19
+ import article_introduction_generator.about as about
20
+ import article_introduction_generator.modules.configure as configure
21
+ from article_introduction_generator.modules.wabout import show_about_window
22
+ from article_introduction_generator.desktop import create_desktop_file, create_desktop_directory, create_desktop_menu
23
+
24
+
25
+ from article_introduction_generator.modules.consult import consultation_in_depth
26
+
27
+ # ---------- Path to config file ----------
28
+ CONFIG_PATH = os.path.join( os.path.expanduser("~"),
29
+ ".config",
30
+ about.__package__,
31
+ "config.json" )
32
+
33
+ DEFAULT_CONTENT={ "toolbar_llm_conf": "LLM Conf.",
34
+ "toolbar_llm_conf_tooltip": "Open the configure Json file of LLM",
35
+ "toolbar_url_usage": "LLM Usage",
36
+ "toolbar_url_usage_tooltip": "Open the web page that shows the data usage and cost.",
37
+ "toolbar_configure": "Configure",
38
+ "toolbar_configure_tooltip": "Open the configure Json file of program GUI",
39
+ "toolbar_about": "About",
40
+ "toolbar_about_tooltip": "About the program",
41
+ "toolbar_coffee": "Coffee",
42
+ "toolbar_coffee_tooltip": "Buy me a coffee (TrucomanX)",
43
+ "window_width": 1024,
44
+ "window_height": 800
45
+ }
46
+
47
+ configure.verify_default_config(CONFIG_PATH,default_content=DEFAULT_CONTENT)
48
+
49
+ CONFIG=configure.load_config(CONFIG_PATH)
50
+
51
+ # ---------- Path to config LLM file ----------
52
+ CONFIG_LLM_PATH = os.path.join( os.path.expanduser("~"),
53
+ ".config",
54
+ about.__package__,
55
+ "config.llm.json" )
56
+
57
+ DEFAULT_LLM_CONTENT={
58
+ "api_key": "",
59
+ "usage": "https://deepinfra.com/dash/usage",
60
+ "base_url": "https://api.deepinfra.com/v1/openai",
61
+ "model": "meta-llama/Meta-Llama-3.1-70B-Instruct"
62
+ }
63
+
64
+ configure.verify_default_config(CONFIG_LLM_PATH,default_content=DEFAULT_LLM_CONTENT)
65
+
66
+ CONFIG_LLM = configure.load_config(CONFIG_LLM_PATH)
67
+
68
+
69
+ # -------- Worker --------
70
+ class ConsultationWorker(QObject):
71
+ finished = pyqtSignal(str)
72
+ error = pyqtSignal(str)
73
+
74
+ def __init__(self, config, data):
75
+ super().__init__()
76
+ self.config = config
77
+ self.data = data
78
+
79
+ def run(self):
80
+ try:
81
+ result = consultation_in_depth(self.config, self.data)
82
+ self.finished.emit(result)
83
+ except Exception as e:
84
+ self.error.emit(str(e))
85
+
86
+ # -------- Error dialog --------
87
+ class MessageDialog(QDialog):
88
+ """Error dialog with scrollable text area"""
89
+ def __init__( self,
90
+ message,
91
+ parent=None,
92
+ window_title = "Title message",
93
+ title_message = "Some text",
94
+ button_ok_text = "OK",
95
+ button_copy_text = "Copy to clipboard",
96
+ width = 400,
97
+ height = 300
98
+ ):
99
+
100
+ super().__init__(parent)
101
+
102
+ self.setWindowTitle(window_title)
103
+ self.resize(width, height)
104
+
105
+ # Layout principal
106
+ layout = QVBoxLayout(self)
107
+
108
+ # Label
109
+ label = QLabel(title_message)
110
+ layout.addWidget(label)
111
+
112
+ # Text area
113
+ self.text_edit = QTextEdit()
114
+ self.text_edit.setPlainText(message)
115
+ self.text_edit.setReadOnly(True)
116
+ self.text_edit.setLineWrapMode(QTextEdit.WidgetWidth)
117
+ layout.addWidget(self.text_edit)
118
+
119
+ # Layout dos botões
120
+ button_layout = QHBoxLayout()
121
+
122
+ # Botão copiar
123
+ copy_button = QPushButton(button_copy_text)
124
+ copy_button.clicked.connect(self.copy_to_clipboard)
125
+ button_layout.addWidget(copy_button)
126
+
127
+ # Botão OK
128
+ ok_button = QPushButton(button_ok_text)
129
+ ok_button.clicked.connect(self.accept)
130
+ button_layout.addWidget(ok_button)
131
+
132
+ layout.addLayout(button_layout)
133
+
134
+ def copy_to_clipboard(self):
135
+ clipboard = QApplication.clipboard()
136
+ clipboard.setText(self.text_edit.toPlainText())
137
+
138
+
139
+ def show_error_dialog( message,
140
+ title_message = "An error occurred:",
141
+ width = 800,
142
+ height = 600 ):
143
+ dialog = MessageDialog( message,
144
+ window_title = "Error message",
145
+ title_message = title_message,
146
+ button_ok_text = "OK",
147
+ button_copy_text = "Copy to clipboard",
148
+ width = width,
149
+ height = height
150
+ )
151
+
152
+ dialog.exec_()
153
+
154
+ def show_info_dialog( message,
155
+ title_message = "",
156
+ width = 800,
157
+ height = 600 ):
158
+ dialog = MessageDialog( message,
159
+ window_title = "Information message",
160
+ title_message = title_message,
161
+ button_ok_text = "OK",
162
+ button_copy_text = "Copy to clipboard",
163
+ width = width,
164
+ height = height
165
+ )
166
+
167
+ dialog.exec_()
168
+
169
+ # ---------- Reusable Widgets ----------
170
+
171
+ class LabeledTextEdit(QWidget):
172
+ def __init__(self, label, tooltip=""):
173
+ super().__init__()
174
+ layout = QHBoxLayout()
175
+ self.label = QLabel(label)
176
+ self.text = QTextEdit()
177
+ self.text.setToolTip(tooltip)
178
+ self.text.setMinimumHeight(80)
179
+ layout.addWidget(self.label, 1)
180
+ layout.addWidget(self.text, 4)
181
+ self.setLayout(layout)
182
+
183
+ def get(self):
184
+ return self.text.toPlainText().strip()
185
+
186
+ def set(self, value):
187
+ self.text.setPlainText(value or "")
188
+
189
+
190
+ class LabeledLineEdit(QWidget):
191
+ def __init__(self, label, tooltip=""):
192
+ super().__init__()
193
+ layout = QHBoxLayout()
194
+ self.label = QLabel(label)
195
+ self.line = QLineEdit()
196
+ self.line.setToolTip(tooltip)
197
+ layout.addWidget(self.label, 1)
198
+ layout.addWidget(self.line, 4)
199
+ self.setLayout(layout)
200
+
201
+ def get(self):
202
+ return self.line.text().strip()
203
+
204
+ def set(self, value):
205
+ self.line.setText(value or "")
206
+
207
+
208
+ class StringListEditor(QWidget):
209
+ def __init__(self, label, tooltip=""):
210
+ super().__init__()
211
+ layout = QVBoxLayout()
212
+ title = QLabel(label)
213
+ title.setToolTip(tooltip)
214
+ self.list = QListWidget()
215
+
216
+ placeholder = QListWidgetItem("Click 'Add' to insert a new entry")
217
+ placeholder.setFlags(Qt.NoItemFlags) # não selecionável / não editável
218
+ placeholder.setForeground(Qt.gray)
219
+ self.list.addItem(placeholder)
220
+
221
+ self.list.setToolTip(tooltip)
222
+
223
+ btn_layout = QHBoxLayout()
224
+ add_btn = QPushButton("Add")
225
+ add_btn.setIcon(QIcon.fromTheme("list-add"))
226
+ remove_btn = QPushButton("Remove")
227
+ remove_btn.setIcon(QIcon.fromTheme("list-remove"))
228
+ btn_layout.addWidget(add_btn)
229
+ btn_layout.addWidget(remove_btn)
230
+
231
+ add_btn.clicked.connect(self.add_item)
232
+ remove_btn.clicked.connect(self.remove_item)
233
+
234
+ layout.addWidget(title)
235
+ layout.addWidget(self.list)
236
+ layout.addLayout(btn_layout)
237
+ self.setLayout(layout)
238
+
239
+ self.list.setEditTriggers(
240
+ self.list.DoubleClicked | self.list.SelectedClicked
241
+ )
242
+
243
+ def add_item(self, text=None):
244
+ from PyQt5.QtWidgets import QListWidgetItem
245
+
246
+ # Se o primeiro item for placeholder, remove
247
+ if self.list.count() == 1:
248
+ item0 = self.list.item(0)
249
+ if item0.flags() == Qt.NoItemFlags:
250
+ self.list.clear()
251
+
252
+
253
+ if not isinstance(text, str):
254
+ text = ""
255
+
256
+ item = QListWidgetItem(text)
257
+ item.setFlags(item.flags() | Qt.ItemIsEditable)
258
+ self.list.addItem(item)
259
+ self.list.editItem(item)
260
+
261
+
262
+ def remove_item(self):
263
+ for item in self.list.selectedItems():
264
+ self.list.takeItem(self.list.row(item))
265
+
266
+ if self.list.count() == 0:
267
+ self._add_placeholder()
268
+
269
+
270
+ def get(self):
271
+ values = []
272
+ for i in range(self.list.count()):
273
+ item = self.list.item(i)
274
+ if not (item.flags() & Qt.ItemIsEditable):
275
+ continue
276
+ text = item.text().strip()
277
+ if text:
278
+ values.append(text)
279
+ return values
280
+
281
+
282
+ def set(self, values):
283
+ self.list.clear()
284
+
285
+ if not values:
286
+ self._add_placeholder()
287
+ return
288
+
289
+ for v in values:
290
+ item = QListWidgetItem(v)
291
+ item.setFlags(item.flags() | Qt.ItemIsEditable)
292
+ self.list.addItem(item)
293
+
294
+
295
+ def _add_placeholder(self):
296
+ placeholder = QListWidgetItem("Click 'Add' to insert a new entry")
297
+ placeholder.setFlags(Qt.NoItemFlags)
298
+ placeholder.setForeground(Qt.gray)
299
+ self.list.addItem(placeholder)
300
+
301
+
302
+
303
+ # ---------- Main Window ----------
304
+
305
+ class JsonIntroductionEditor(QMainWindow):
306
+ def __init__(self):
307
+ super().__init__()
308
+
309
+ self.setWindowTitle(about.__program_name__)
310
+ #self.setGeometry(100, 100, 800, 240)
311
+ self.resize(CONFIG["window_width"], CONFIG["window_height"])
312
+
313
+ ## Icon
314
+ # Get base directory for icons
315
+ base_dir_path = os.path.dirname(os.path.abspath(__file__))
316
+ self.icon_path = os.path.join(base_dir_path, 'icons', 'logo.png')
317
+ self.setWindowIcon(QIcon(self.icon_path))
318
+
319
+ self.current_path = None
320
+
321
+ self.references_data = {}
322
+
323
+ self.current_reference_key = None
324
+
325
+ self.tabs = QTabWidget()
326
+ self.setCentralWidget(self.tabs)
327
+
328
+ self._create_toolbar()
329
+ self._create_status_bar()
330
+
331
+ self._create_tabs()
332
+ self._apply_styles()
333
+
334
+
335
+ # ---------- UI ----------
336
+ def _apply_styles(self):
337
+ self.setStyleSheet("""
338
+ QListWidget {
339
+ border: 1px solid #999;
340
+ border-radius: 4px;
341
+ background-color: #f9f9f9;
342
+ }
343
+
344
+ QListWidget::item {
345
+ padding: 4px;
346
+ }
347
+
348
+ QListWidget::item:selected {
349
+ background-color: #cce5ff;
350
+ color: black;
351
+ }
352
+
353
+ QTextEdit {
354
+ border: 1px solid #bbb;
355
+ border-radius: 4px;
356
+ background-color: #ffffff;
357
+ }
358
+
359
+ QLineEdit {
360
+ border: 1px solid #bbb;
361
+ border-radius: 4px;
362
+ background-color: #ffffff;
363
+ padding: 2px;
364
+ }
365
+ QListWidget:empty {
366
+ background-color: #f9f9f9;
367
+ }
368
+
369
+ QListWidget:empty::item {
370
+ color: #999;
371
+ }
372
+ """)
373
+
374
+ def _create_toolbar(self):
375
+ self.toolbar = self.addToolBar("Main")
376
+ self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
377
+
378
+ #
379
+ self.load_action = QAction(QIcon.fromTheme("document-open"), "Load JSON", self)
380
+ self.load_action.setToolTip("load JSON")
381
+ self.load_action.triggered.connect(self.load_json)
382
+ self.toolbar.addAction(self.load_action)
383
+
384
+ #
385
+ self.save_as_action = QAction(QIcon.fromTheme("document-save-as"), "Save as JSON", self)
386
+ self.save_as_action.setToolTip("save as")
387
+ self.save_as_action.triggered.connect(self.save_as_json)
388
+ self.toolbar.addAction(self.save_as_action)
389
+
390
+ #
391
+ self.generate_intro_action = QAction(QIcon.fromTheme("emblem-generic"), "Generate intro.", self)
392
+ self.generate_intro_action.setToolTip("Generate introduction")
393
+ self.generate_intro_action.triggered.connect(self.generate_intro)
394
+ self.toolbar.addAction(self.generate_intro_action)
395
+
396
+ # Adicionar o espaçador
397
+ spacer = QWidget()
398
+ spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
399
+ self.toolbar.addWidget(spacer)
400
+
401
+ #
402
+ self.llm_conf_action = QAction(QIcon.fromTheme("document-properties"), CONFIG["toolbar_llm_conf"], self)
403
+ self.llm_conf_action.setToolTip(CONFIG["toolbar_llm_conf_tooltip"])
404
+ self.llm_conf_action.triggered.connect(self.open_llm_conf_editor)
405
+ self.toolbar.addAction(self.llm_conf_action)
406
+
407
+ #
408
+ self.url_usage_action = QAction(QIcon.fromTheme("emblem-web"), CONFIG["toolbar_url_usage"], self)
409
+ self.url_usage_action.setToolTip(CONFIG["toolbar_url_usage_tooltip"])
410
+ self.url_usage_action.triggered.connect(self.open_url_usage_editor)
411
+ self.toolbar.addAction(self.url_usage_action)
412
+
413
+ #
414
+ self.configure_action = QAction(QIcon.fromTheme("document-properties"), CONFIG["toolbar_configure"], self)
415
+ self.configure_action.setToolTip(CONFIG["toolbar_configure_tooltip"])
416
+ self.configure_action.triggered.connect(self.open_configure_editor)
417
+ self.toolbar.addAction(self.configure_action)
418
+
419
+ #
420
+ self.about_action = QAction(QIcon.fromTheme("help-about"), CONFIG["toolbar_about"], self)
421
+ self.about_action.setToolTip(CONFIG["toolbar_about_tooltip"])
422
+ self.about_action.triggered.connect(self.open_about)
423
+ self.toolbar.addAction(self.about_action)
424
+
425
+ # Coffee
426
+ self.coffee_action = QAction(QIcon.fromTheme("emblem-favorite"), CONFIG["toolbar_coffee"], self)
427
+ self.coffee_action.setToolTip(CONFIG["toolbar_coffee_tooltip"])
428
+ self.coffee_action.triggered.connect(self.on_coffee_action_click)
429
+ self.toolbar.addAction(self.coffee_action)
430
+
431
+ def _open_file_in_text_editor(self, filepath):
432
+ if os.name == 'nt': # Windows
433
+ os.startfile(filepath)
434
+ elif os.name == 'posix': # Linux/macOS
435
+ subprocess.run(['xdg-open', filepath])
436
+
437
+ def open_url_usage_editor(self):
438
+ QDesktopServices.openUrl(QUrl(CONFIG_LLM["usage"]))
439
+
440
+ def open_configure_editor(self):
441
+ self._open_file_in_text_editor(CONFIG_PATH)
442
+
443
+ def open_llm_conf_editor(self):
444
+ self._open_file_in_text_editor(CONFIG_LLM_PATH)
445
+
446
+ def open_about(self):
447
+ data={
448
+ "version": about.__version__,
449
+ "package": about.__package__,
450
+ "program_name": about.__program_name__,
451
+ "author": about.__author__,
452
+ "email": about.__email__,
453
+ "description": about.__description__,
454
+ "url_source": about.__url_source__,
455
+ "url_doc": about.__url_doc__,
456
+ "url_funding": about.__url_funding__,
457
+ "url_bugs": about.__url_bugs__
458
+ }
459
+ show_about_window(data,self.icon_path)
460
+
461
+ def on_coffee_action_click(self):
462
+ QDesktopServices.openUrl(QUrl("https://ko-fi.com/trucomanx"))
463
+
464
+
465
+ def _create_status_bar(self):
466
+ self.status = QStatusBar()
467
+ self.setStatusBar(self.status)
468
+
469
+ def _wrap_scroll(self, widget):
470
+ scroll = QScrollArea()
471
+ scroll.setWidgetResizable(True)
472
+ scroll.setWidget(widget)
473
+ return scroll
474
+
475
+ def _create_tabs(self):
476
+ self.tabs.addTab(self._wrap_scroll(self._paper_profile_tab()), "Paper Profile")
477
+ self.tabs.addTab(self._wrap_scroll(self._research_problem_tab()), "Research Problem")
478
+ self.tabs.addTab(self._wrap_scroll(self._contributions_tab()), "Contributions")
479
+ self.tabs.addTab(self._related_work_tab(), "Related Work")
480
+ self.tabs.addTab(self._wrap_scroll(self._writing_guidelines_tab()), "Writing Guidelines")
481
+
482
+ self.tabs.currentChanged.connect(self._on_tab_changed)
483
+
484
+
485
+ # ---------- Tabs ----------
486
+
487
+ def _on_tab_changed(self, index):
488
+ self._save_current_reference()
489
+
490
+ def _paper_profile_tab(self):
491
+ w = QWidget()
492
+ layout = QVBoxLayout()
493
+
494
+ self.pp_title = LabeledLineEdit(
495
+ "Title",
496
+ "Full paper title."
497
+ )
498
+ self.pp_domain = LabeledLineEdit(
499
+ "Domain",
500
+ "Research domain, e.g., Computer Vision, NLP, Systems."
501
+ )
502
+ self.pp_journal = LabeledLineEdit(
503
+ "Target Journal",
504
+ "Intended journal (IEEE, Elsevier, ACM, etc.)."
505
+ )
506
+ self.pp_keywords = StringListEditor(
507
+ "Keywords",
508
+ "High-level keywords describing the paper."
509
+ )
510
+ self.pp_summary = LabeledTextEdit(
511
+ "Author Intended Summary",
512
+ "Human-written summary describing what the paper does and why."
513
+ )
514
+
515
+ for wdg in [self.pp_title, self.pp_domain, self.pp_journal, self.pp_keywords, self.pp_summary]:
516
+ layout.addWidget(wdg)
517
+
518
+ w.setLayout(layout)
519
+ return w
520
+
521
+ def _research_problem_tab(self):
522
+ w = QWidget()
523
+ layout = QVBoxLayout()
524
+
525
+ self.rp_overview = LabeledTextEdit(
526
+ "Research Domain Overview",
527
+ "General overview of the research domain and its importance."
528
+ )
529
+ self.rp_specific = LabeledTextEdit(
530
+ "Specific Problem",
531
+ "Precise formulation of the problem addressed."
532
+ )
533
+ self.rp_challenges = StringListEditor(
534
+ "Practical Challenges",
535
+ "Key practical or theoretical challenges."
536
+ )
537
+ self.rp_insufficient = LabeledTextEdit(
538
+ "Why Existing Solutions Are Insufficient",
539
+ "High-level human assessment without citing specific papers."
540
+ )
541
+
542
+ for wdg in [self.rp_overview, self.rp_specific, self.rp_challenges, self.rp_insufficient]:
543
+ layout.addWidget(wdg)
544
+
545
+ w.setLayout(layout)
546
+ return w
547
+
548
+ def _contributions_tab(self):
549
+ w = QWidget()
550
+ layout = QVBoxLayout()
551
+
552
+ self.contributions = StringListEditor(
553
+ "Contributions",
554
+ "Main contributions of the paper, one per entry."
555
+ )
556
+
557
+ layout.addWidget(self.contributions)
558
+ w.setLayout(layout)
559
+ return w
560
+
561
+ def _related_work_tab(self):
562
+ tabs = QTabWidget()
563
+ tabs.addTab(self._references_tab(), "References")
564
+ tabs.addTab(self._wrap_scroll(self._synthesis_tab()), "Human Curated Synthesis")
565
+ return tabs
566
+
567
+ def _references_tab(self):
568
+ w = QWidget()
569
+ main_layout = QVBoxLayout(w)
570
+
571
+ # ---- Lista de referências + painel de edição ----
572
+ content_layout = QHBoxLayout()
573
+
574
+ # Lista de referências
575
+ self.ref_list = QListWidget()
576
+ self.ref_list.setEditTriggers(
577
+ QListWidget.DoubleClicked | QListWidget.EditKeyPressed
578
+ )
579
+ self.ref_list.itemChanged.connect(self._on_reference_renamed)
580
+ self.ref_list.currentItemChanged.connect(self._load_reference)
581
+ content_layout.addWidget(self.ref_list, 1)
582
+
583
+ # Painel de edição (scrollable)
584
+ right_widget = QWidget()
585
+ right_layout = QVBoxLayout(right_widget)
586
+
587
+ self.ref_bibtex = LabeledTextEdit("BibTeX", "BibTeX entry. This is the only source for citations.")
588
+ self.ref_abstract = LabeledTextEdit("Abstract", "Original abstract of the cited paper.")
589
+ self.ref_category = LabeledLineEdit("Methodological Category", "e.g., deep_learning, transformer_based, graph_based.")
590
+ self.ref_contribution = LabeledTextEdit("Central Technical Idea", "Main technical idea introduced by this work.")
591
+ self.ref_strengths = StringListEditor("Author Reported Strengths", "Strengths explicitly claimed by the original authors.")
592
+ self.ref_limitations = StringListEditor("Reported Limitations", "Limitations discussed or implied by the paper.")
593
+ self.ref_relevance = LabeledTextEdit("Relevance to Our Work", "How this work relates to and differs from our paper.")
594
+ self.ref_role = LabeledLineEdit("Introduction Paragraph Role", "foundational, early_state_of_art, recent_advances, etc.")
595
+
596
+ for wdg in [
597
+ self.ref_bibtex, self.ref_abstract, self.ref_category,
598
+ self.ref_contribution, self.ref_strengths,
599
+ self.ref_limitations, self.ref_relevance, self.ref_role
600
+ ]:
601
+ right_layout.addWidget(wdg)
602
+
603
+ scroll = QScrollArea()
604
+ scroll.setWidgetResizable(True)
605
+ scroll.setWidget(right_widget)
606
+
607
+ content_layout.addWidget(scroll, 3)
608
+
609
+ main_layout.addLayout(content_layout)
610
+
611
+ # ---- Botões fora do scroll ----
612
+ btns = QHBoxLayout()
613
+
614
+ add = QPushButton("Add Reference")
615
+ add.setIcon(QIcon.fromTheme("list-add"))
616
+ add.setIconSize(QSize(32, 32))
617
+
618
+ remove = QPushButton("Remove Reference")
619
+ remove.setIcon(QIcon.fromTheme("list-remove"))
620
+ remove.setIconSize(QSize(32, 32))
621
+
622
+ btns.addWidget(add)
623
+ btns.addWidget(remove)
624
+
625
+ add.clicked.connect(self._add_reference)
626
+ remove.clicked.connect(self._remove_reference)
627
+
628
+ main_layout.addLayout(btns)
629
+
630
+ w.setLayout(main_layout)
631
+ return w
632
+
633
+
634
+
635
+ def _on_reference_renamed(self, item: QListWidgetItem):
636
+ old_key = self.current_reference_key
637
+ new_key = item.text().strip()
638
+
639
+ if not old_key:
640
+ return
641
+
642
+ if not new_key:
643
+ QMessageBox.warning(self, "Invalid name", "Reference key cannot be empty.")
644
+ self.ref_list.blockSignals(True)
645
+ item.setText(old_key)
646
+ self.ref_list.blockSignals(False)
647
+ return
648
+
649
+ if new_key in self.references_data and new_key != old_key:
650
+ QMessageBox.warning(self, "Duplicate key", "This reference key already exists.")
651
+ self.ref_list.blockSignals(True)
652
+ item.setText(old_key)
653
+ self.ref_list.blockSignals(False)
654
+ return
655
+
656
+ self._save_current_reference()
657
+
658
+ self.references_data[new_key] = self.references_data.pop(old_key)
659
+ self.current_reference_key = new_key
660
+
661
+
662
+ def _synthesis_tab(self):
663
+ w = QWidget()
664
+ layout = QVBoxLayout()
665
+
666
+ self.syn_trends = StringListEditor(
667
+ "Common Trends",
668
+ "Observed trends across the literature."
669
+ )
670
+ self.syn_open = StringListEditor(
671
+ "Open Problems",
672
+ "Unresolved problems identified by the author."
673
+ )
674
+ self.syn_gap = LabeledTextEdit(
675
+ "Explicit Research Gap",
676
+ "Clear formulation of the research gap addressed by the paper."
677
+ )
678
+
679
+ for wdg in [self.syn_trends, self.syn_open, self.syn_gap]:
680
+ layout.addWidget(wdg)
681
+
682
+ w.setLayout(layout)
683
+ return w
684
+
685
+ def _writing_guidelines_tab(self):
686
+ w = QWidget()
687
+ layout = QVBoxLayout()
688
+
689
+ self.wg = LabeledTextEdit(
690
+ "Writing Guidelines",
691
+ "Explicit instructions to be followed by the LLM when generating text."
692
+ )
693
+
694
+ layout.addWidget(self.wg)
695
+ w.setLayout(layout)
696
+ return w
697
+
698
+ # ---------- Reference Helpers ----------
699
+
700
+ def _add_reference(self):
701
+ if self.references_data:
702
+ indices = []
703
+ for k in self.references_data.keys():
704
+ try:
705
+ indices.append(int(k.split("_")[1]))
706
+ except (IndexError, ValueError):
707
+ pass
708
+ next_idx = max(indices) + 1 if indices else 1
709
+ else:
710
+ next_idx = 1
711
+
712
+ key = f"ref_{next_idx}"
713
+ self.references_data[key] = {}
714
+
715
+ self.ref_list.blockSignals(True)
716
+
717
+ item = QListWidgetItem(key)
718
+ item.setFlags(item.flags() | Qt.ItemIsEditable)
719
+ self.ref_list.addItem(item)
720
+ self.ref_list.setCurrentItem(item)
721
+
722
+ self.current_reference_key = key
723
+
724
+ self.ref_list.blockSignals(False)
725
+
726
+ self._clear_reference_editor()
727
+
728
+ def _clear_reference_editor(self):
729
+ self.ref_bibtex.set("")
730
+ self.ref_abstract.set("")
731
+ self.ref_category.set("")
732
+ self.ref_contribution.set("")
733
+ self.ref_strengths.set([])
734
+ self.ref_limitations.set([])
735
+ self.ref_relevance.set("")
736
+ self.ref_role.set("")
737
+
738
+ def _remove_reference(self):
739
+ for item in self.ref_list.selectedItems():
740
+ key = item.text()
741
+ self.references_data.pop(key, None)
742
+ self.ref_list.takeItem(self.ref_list.row(item))
743
+
744
+ if key == self.current_reference_key:
745
+ self.current_reference_key = None
746
+
747
+ self._clear_reference_editor()
748
+
749
+ def _save_current_reference(self):
750
+ if not self.current_reference_key:
751
+ return
752
+
753
+ self.references_data[self.current_reference_key] = {
754
+ "bibtex": self.ref_bibtex.get(),
755
+ "abstract": self.ref_abstract.get(),
756
+ "methodological_category": self.ref_category.get(),
757
+ "central_technical_idea": self.ref_contribution.get(),
758
+ "author_reported_strengths": self.ref_strengths.get(),
759
+ "reported_limitations": self.ref_limitations.get(),
760
+ "relevance_to_our_work": self.ref_relevance.get(),
761
+ "introduction_paragraph_role": self.ref_role.get(),
762
+ }
763
+
764
+
765
+ def _load_reference(self, current, previous):
766
+ if previous and self.current_reference_key:
767
+ self._save_current_reference()
768
+
769
+ if not current:
770
+ self.current_reference_key = None
771
+ return
772
+
773
+ self.ref_list.blockSignals(True)
774
+
775
+ self.current_reference_key = current.text()
776
+ ref = self.references_data.get(self.current_reference_key, {})
777
+
778
+ self.ref_bibtex.set(ref.get("bibtex"))
779
+ self.ref_abstract.set(ref.get("abstract"))
780
+ self.ref_category.set(ref.get("methodological_category"))
781
+ self.ref_contribution.set(ref.get("central_technical_idea"))
782
+ self.ref_strengths.set(ref.get("author_reported_strengths", []))
783
+ self.ref_limitations.set(ref.get("reported_limitations", []))
784
+ self.ref_relevance.set(ref.get("relevance_to_our_work"))
785
+ self.ref_role.set(ref.get("introduction_paragraph_role"))
786
+
787
+ self.ref_list.blockSignals(False)
788
+
789
+
790
+ # ---------- Load / Save ----------
791
+
792
+ def load_json(self):
793
+ self._save_current_reference()
794
+
795
+ path, _ = QFileDialog.getOpenFileName(self, "Load JSON", "", "JSON Files (*.json)")
796
+ if not path:
797
+ return
798
+
799
+ with open(path, "r", encoding="utf-8") as f:
800
+ data = json.load(f)
801
+
802
+ self.current_path = path
803
+ self.status.showMessage(f"Loaded from {path}")
804
+
805
+ # ---- Paper Profile ----
806
+ pp = data.get("paper_profile", {})
807
+ self.pp_title.set(pp.get("title"))
808
+ self.pp_domain.set(pp.get("domain"))
809
+ self.pp_journal.set(pp.get("target_journal"))
810
+ self.pp_keywords.set(pp.get("keywords", []))
811
+ self.pp_summary.set(pp.get("author_intended_summary"))
812
+
813
+ # ---- Research Problem ----
814
+ rp = data.get("research_problem", {})
815
+ self.rp_overview.set(rp.get("research_domain_overview"))
816
+ self.rp_specific.set(rp.get("specific_problem"))
817
+ self.rp_challenges.set(rp.get("practical_challenges", []))
818
+ self.rp_insufficient.set(rp.get("why_existing_solutions_are_insufficient"))
819
+
820
+ # ---- Contributions ----
821
+ self.contributions.set(data.get("contributions", []))
822
+
823
+ # ---- Writing Guidelines ----
824
+ self.wg.set(data.get("writing_guidelines", ""))
825
+
826
+ # ---- Related Work: References ----
827
+ self.references_data = data.get("related_work", {}).get("references", {})
828
+ self.ref_list.clear()
829
+
830
+ for key in self.references_data.keys():
831
+ item = QListWidgetItem(key)
832
+ item.setFlags(item.flags() | Qt.ItemIsEditable)
833
+ self.ref_list.addItem(item)
834
+
835
+ if self.ref_list.count() > 0:
836
+ self.ref_list.setCurrentRow(0)
837
+
838
+ # ---- Related Work: Human Curated Synthesis ----
839
+ synth = data.get("related_work", {}).get("human_curated_synthesis", {})
840
+ self.syn_trends.set(synth.get("common_trends", []))
841
+ self.syn_open.set(synth.get("open_problems", []))
842
+ self.syn_gap.set(synth.get("explicit_research_gap"))
843
+
844
+ def _obtaining_data(self):
845
+ self._save_current_reference()
846
+
847
+ data = {
848
+ "paper_profile": {
849
+ "title": self.pp_title.get(),
850
+ "domain": self.pp_domain.get(),
851
+ "target_journal": self.pp_journal.get(),
852
+ "keywords": self.pp_keywords.get(),
853
+ "author_intended_summary": self.pp_summary.get()
854
+ },
855
+ "research_problem": {
856
+ "research_domain_overview": self.rp_overview.get(),
857
+ "specific_problem": self.rp_specific.get(),
858
+ "practical_challenges": self.rp_challenges.get(),
859
+ "why_existing_solutions_are_insufficient": self.rp_insufficient.get()
860
+ },
861
+ "contributions": self.contributions.get(),
862
+ "related_work": {
863
+ "references": self.references_data,
864
+ "human_curated_synthesis": {
865
+ "common_trends": self.syn_trends.get(),
866
+ "open_problems": self.syn_open.get(),
867
+ "explicit_research_gap": self.syn_gap.get()
868
+ }
869
+ },
870
+ "writing_guidelines": self.wg.get()
871
+ }
872
+
873
+ return data
874
+
875
+ def save_as_json(self):
876
+
877
+ path, _ = QFileDialog.getSaveFileName(self, "Save JSON", "", "JSON Files (*.json)")
878
+ if not path:
879
+ return
880
+
881
+ data = self._obtaining_data()
882
+
883
+ with open(path, "w", encoding="utf-8") as f:
884
+ json.dump(data, f, indent=2)
885
+
886
+ self.current_path = path
887
+ self.status.showMessage(f"Saved to {path}")
888
+
889
+ def is_data_empty(self, data: dict) -> bool:
890
+ def has_content(value):
891
+ if isinstance(value, str):
892
+ return bool(value.strip())
893
+ if isinstance(value, list):
894
+ return any(has_content(v) for v in value)
895
+ if isinstance(value, dict):
896
+ return any(has_content(v) for v in value.values())
897
+ return False
898
+
899
+ return not has_content(data)
900
+
901
+ def generate_intro(self):
902
+ global CONFIG_LLM
903
+
904
+ data = self._obtaining_data()
905
+
906
+ if self.is_data_empty(data):
907
+ QMessageBox.warning(
908
+ self,
909
+ "Missing data",
910
+ "Please fill at least one relevant field before generating the introduction."
911
+ )
912
+ return
913
+
914
+ if CONFIG_LLM["api_key"]=="":
915
+ CONFIG_LLM = configure.load_config(CONFIG_LLM_PATH)
916
+
917
+ if CONFIG_LLM["api_key"]=="":
918
+ self.status.showMessage("Open: " + CONFIG_LLM_PATH)
919
+ self._open_file_in_text_editor(CONFIG_LLM_PATH)
920
+ QDesktopServices.openUrl(QUrl(CONFIG_LLM["usage"]))
921
+
922
+ return
923
+
924
+ # Feedback visual
925
+ self.status.showMessage("Consulting LLM… please wait")
926
+ self.generate_intro_action.setEnabled(False)
927
+
928
+ # Thread
929
+ self.thread = QThread()
930
+ self.worker = ConsultationWorker(CONFIG_LLM, data)
931
+
932
+ self.worker.moveToThread(self.thread)
933
+
934
+ # Conexões
935
+ self.thread.started.connect(self.worker.run)
936
+ self.worker.finished.connect(self.on_intro_ready)
937
+ self.worker.error.connect(self.on_intro_error)
938
+
939
+ self.worker.finished.connect(self.thread.quit)
940
+ self.worker.finished.connect(self.worker.deleteLater)
941
+ self.thread.finished.connect(self.thread.deleteLater)
942
+
943
+ self.thread.start()
944
+
945
+ def on_intro_ready(self, out):
946
+ self.generate_intro_action.setEnabled(True)
947
+ self.status.showMessage("Done")
948
+ show_info_dialog(out, title_message="LLM response:")
949
+
950
+ def on_intro_error(self, error_msg):
951
+ self.generate_intro_action.setEnabled(True)
952
+ self.status.showMessage("Error")
953
+ show_error_dialog(error_msg)
954
+
955
+ # ---------- Main ----------
956
+
957
+ def main():
958
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
959
+
960
+ '''
961
+ create_desktop_directory()
962
+ create_desktop_menu()
963
+ create_desktop_file(os.path.join("~",".local","share","applications"),
964
+ program_name=about.__program_name__)
965
+
966
+ for n in range(len(sys.argv)):
967
+ if sys.argv[n] == "--autostart":
968
+ create_desktop_directory(overwrite = True)
969
+ create_desktop_menu(overwrite = True)
970
+ create_desktop_file(os.path.join("~",".config","autostart"),
971
+ overwrite=True,
972
+ program_name=about.__program_name__)
973
+ return
974
+ if sys.argv[n] == "--applications":
975
+ create_desktop_directory(overwrite = True)
976
+ create_desktop_menu(overwrite = True)
977
+ create_desktop_file(os.path.join("~",".local","share","applications"),
978
+ overwrite=True,
979
+ program_name=about.__program_name__)
980
+ return
981
+ '''
982
+
983
+ app = QApplication(sys.argv)
984
+ app.setApplicationName(about.__package__)
985
+
986
+ win = JsonIntroductionEditor()
987
+ win.show()
988
+ sys.exit(app.exec_())
989
+
990
+
991
+ if __name__ == "__main__":
992
+ main()
993
+