Spaces:
Running on Zero
Running on Zero
Require method selection and show ChatGPT sign-in in a modal
Browse files- ARCHITECTURE.md +1 -1
- README.md +2 -2
- agent_app.py +5 -39
- styles.css +63 -17
- tests/test_ui_workflow.py +99 -0
- ui_dialog.js +61 -0
- ui_workflow.py +156 -0
ARCHITECTURE.md
CHANGED
|
@@ -62,7 +62,7 @@ The host clears this run's patent collection on success, failure or cancellation
|
|
| 62 |
|
| 63 |
SysML export is deterministic: nested actions for the function hierarchy and directed item flows with generic `FlowItem` payloads. Claims/interfaces remain documentation, not formal requirements or complete physical simulation semantics. The pinned SysIDE Legacy 0.9.1 language server checks the generated file against its 2024-12 grammar/library. This is a useful parser and semantic check, not certification against the final SysML 2.0 standard. Parser errors or unavailable validation prevent a successful generated export.
|
| 64 |
|
| 65 |
-
The
|
| 66 |
|
| 67 |
## Research record and persistence
|
| 68 |
|
|
|
|
| 62 |
|
| 63 |
SysML export is deterministic: nested actions for the function hierarchy and directed item flows with generic `FlowItem` payloads. Claims/interfaces remain documentation, not formal requirements or complete physical simulation semantics. The pinned SysIDE Legacy 0.9.1 language server checks the generated file against its 2024-12 grammar/library. This is a useful parser and semantic check, not certification against the final SysML 2.0 standard. Parser errors or unavailable validation prevent a successful generated export.
|
| 64 |
|
| 65 |
+
The Gradio interface reveals methods after upload and requires an explicit selection before exposing Process. AI agents opens a session-specific sign-in popup, which closes after connection. Results are revealed after processing; the three tabs remain Diagram, JSON text, and SysML download. A research ZIP download is inside the JSON tab. The uploader accepts patent HTML (.html or .htm) only. JSON is available as an output download and cannot be uploaded as input.
|
| 66 |
|
| 67 |
## Research record and persistence
|
| 68 |
|
README.md
CHANGED
|
@@ -16,7 +16,7 @@ short_description: Turn patent HTML into diagrams, JSON, and SysML
|
|
| 16 |
|
| 17 |
[Open the app](https://huggingface.co/spaces/cmuchancel/patent2sysml) · [Workflow](ARCHITECTURE.md) · [Research record](RESEARCH_RECORD.md)
|
| 18 |
|
| 19 |
-
A simple Gradio interface for your teammate's [Info-extraction](https://github.com/eandujar09/Info-extraction) agents, pinned at `848d0337cce01bebc620da4a2b3d873c16ffb326`. Upload patent HTML, connect ChatGPT, then click **Process**. The three output tabs show a diagram, JSON, and a downloadable SysML file. Fine Tuned NLP remains disabled.
|
| 20 |
|
| 21 |
The pipeline keeps the five fixed textbook queries, adds patent-specific questions, drafts a model, independently reviews it, and allows one repair pass followed by re-review. Unsupported unresolved items and dependent connections are omitted from the final model. Full findings, drafts and removal explanations stay in the private research archive. The JSON tab also downloads that complete ZIP.
|
| 22 |
|
|
@@ -46,7 +46,7 @@ The parser is pinned to open-source SysIDE Legacy 0.9.1 and its 2024-12 library.
|
|
| 46 |
|
| 47 |
| File | Purpose |
|
| 48 |
| --- | --- |
|
| 49 |
-
| `agent_app.py`, `styles.css` |
|
| 50 |
| `workflow.py` | Fixed questions, new agent instructions, retrieval budgets. |
|
| 51 |
| `agent_runner.py` | Configure/launch agents, enforce completion, cleanup and exports. |
|
| 52 |
| `workspace_mcp.py`, `quality.py` | Draft/review/repair gates and final omission rules. |
|
|
|
|
| 16 |
|
| 17 |
[Open the app](https://huggingface.co/spaces/cmuchancel/patent2sysml) · [Workflow](ARCHITECTURE.md) · [Research record](RESEARCH_RECORD.md)
|
| 18 |
|
| 19 |
+
A simple Gradio interface for your teammate's [Info-extraction](https://github.com/eandujar09/Info-extraction) agents, pinned at `848d0337cce01bebc620da4a2b3d873c16ffb326`. Upload patent HTML, choose **AI agents**, connect ChatGPT in the sign-in popup, then click **Process**. No method is selected automatically. The popup closes on successful sign-in, and output tabs appear after processing. The three output tabs show a diagram, JSON, and a downloadable SysML file. Fine Tuned NLP remains disabled.
|
| 20 |
|
| 21 |
The pipeline keeps the five fixed textbook queries, adds patent-specific questions, drafts a model, independently reviews it, and allows one repair pass followed by re-review. Unsupported unresolved items and dependent connections are omitted from the final model. Full findings, drafts and removal explanations stay in the private research archive. The JSON tab also downloads that complete ZIP.
|
| 22 |
|
|
|
|
| 46 |
|
| 47 |
| File | Purpose |
|
| 48 |
| --- | --- |
|
| 49 |
+
| `agent_app.py`, `ui_workflow.py`, `ui_dialog.js`, `styles.css` | Gradio processing, progressive controls, accessible sign-in popup, and styling. |
|
| 50 |
| `workflow.py` | Fixed questions, new agent instructions, retrieval budgets. |
|
| 51 |
| `agent_runner.py` | Configure/launch agents, enforce completion, cleanup and exports. |
|
| 52 |
| `workspace_mcp.py`, `quality.py` | Draft/review/repair gates and final omission rules. |
|
agent_app.py
CHANGED
|
@@ -4,14 +4,13 @@ import os
|
|
| 4 |
import tempfile
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
-
import gradio as gr
|
| 8 |
from rendering import diagram
|
| 9 |
|
| 10 |
from agent_runner import run_agents
|
| 11 |
-
from chatgpt_login import connect_chatgpt
|
| 12 |
from sysml_export import to_sysml
|
| 13 |
from textbook_index import load_index
|
| 14 |
-
from user_session import SESSIONS
|
|
|
|
| 15 |
|
| 16 |
load_index()
|
| 17 |
|
|
@@ -50,43 +49,10 @@ def process(file: str | None, session=None):
|
|
| 50 |
yield str(error), "", None, "", "", None, None
|
| 51 |
|
| 52 |
|
| 53 |
-
|
| 54 |
-
session = gr.State(new_session if os.getenv("SPACE_ID") else None,
|
| 55 |
-
time_to_live=3600, delete_callback=delete_session)
|
| 56 |
-
if os.getenv("PATENT_SPACE_WRAPPER"):
|
| 57 |
-
gr.Button(visible=False).click(unused_gpu_slot, api_name=False)
|
| 58 |
-
gr.Markdown("# Translate patents to SysML v2\nUpload a patent. Generate a system model.")
|
| 59 |
-
if os.getenv("SPACE_ID"):
|
| 60 |
-
with gr.Accordion("Connect ChatGPT", open=True):
|
| 61 |
-
gr.Markdown("Sign in with your own ChatGPT account for this session. "
|
| 62 |
-
"Your login is separate from other visitors and expires when the session is cleaned up.")
|
| 63 |
-
connect = gr.Button("Connect ChatGPT")
|
| 64 |
-
login_status = gr.Markdown()
|
| 65 |
-
connect.click(connect_chatgpt, inputs=session, outputs=login_status,
|
| 66 |
-
api_name=False, concurrency_limit=4)
|
| 67 |
-
patent = gr.File(label="Patent HTML · .html or .htm · up to 10 MB", file_types=[".html", ".htm"], type="filepath")
|
| 68 |
-
with gr.Row():
|
| 69 |
-
agents = gr.Button("AI agents", variant="primary")
|
| 70 |
-
gr.Button("Fine Tuned NLP", interactive=False)
|
| 71 |
-
gr.Markdown("Research mode: uploaded patents, agent conversations, retrieved evidence, and outputs "
|
| 72 |
-
"are retained in the project owner’s private research archive. Login credentials are excluded.")
|
| 73 |
-
start = gr.Button("Process", variant="primary")
|
| 74 |
-
status = gr.Markdown("Ready. Each run uses your connected OpenCode model.")
|
| 75 |
-
agents.click(lambda: "AI agents selected.", outputs=status, api_name=False)
|
| 76 |
-
with gr.Tab("Diagram"):
|
| 77 |
-
preview = gr.HTML(elem_id="diagram")
|
| 78 |
-
with gr.Tab("JSON text"):
|
| 79 |
-
result = gr.Code(label="Functional-decomposition JSON", language="json", interactive=False)
|
| 80 |
-
download = gr.DownloadButton("Download JSON")
|
| 81 |
-
research_download = gr.DownloadButton("Download complete research record (.zip)")
|
| 82 |
-
with gr.Tab("SysML download"):
|
| 83 |
-
sysml_download = gr.DownloadButton("Download .sysml")
|
| 84 |
-
sysml_text = gr.Code(label="SysML v2", language=None, interactive=False)
|
| 85 |
-
gr.Markdown("Uses the Info-extraction agents. SJS conversion is a separate teammate integration.")
|
| 86 |
-
start.click(process, [patent, session], [status, result, download, preview, sysml_text, sysml_download, research_download],
|
| 87 |
-
api_name="process_patent", concurrency_limit=1)
|
| 88 |
|
| 89 |
if __name__ == "__main__":
|
| 90 |
app.queue(max_size=5).launch(server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0" if os.getenv("SPACE_ID") else "127.0.0.1"),
|
| 91 |
-
css_paths=Path(__file__).with_name("styles.css"),
|
|
|
|
| 92 |
blocked_paths=[str(SESSIONS), str(Path(__file__).parent / "assets")])
|
|
|
|
| 4 |
import tempfile
|
| 5 |
from pathlib import Path
|
| 6 |
|
|
|
|
| 7 |
from rendering import diagram
|
| 8 |
|
| 9 |
from agent_runner import run_agents
|
|
|
|
| 10 |
from sysml_export import to_sysml
|
| 11 |
from textbook_index import load_index
|
| 12 |
+
from user_session import SESSIONS
|
| 13 |
+
from ui_workflow import build_app
|
| 14 |
|
| 15 |
load_index()
|
| 16 |
|
|
|
|
| 49 |
yield str(error), "", None, "", "", None, None
|
| 50 |
|
| 51 |
|
| 52 |
+
app = build_app(process, globals().get("unused_gpu_slot"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
if __name__ == "__main__":
|
| 55 |
app.queue(max_size=5).launch(server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0" if os.getenv("SPACE_ID") else "127.0.0.1"),
|
| 56 |
+
css_paths=Path(__file__).with_name("styles.css"),
|
| 57 |
+
js=Path(__file__).with_name("ui_dialog.js").read_text(), max_file_size="10mb",
|
| 58 |
blocked_paths=[str(SESSIONS), str(Path(__file__).parent / "assets")])
|
styles.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
/*
|
| 2 |
:root {
|
| 3 |
color-scheme: light;
|
| 4 |
--patent-bg: #f8fafc;
|
|
@@ -127,10 +127,9 @@ body, gradio-app { background: var(--patent-bg) !important; }
|
|
| 127 |
border-style: solid !important;
|
| 128 |
border-color: #ced9f3 !important;
|
| 129 |
}
|
| 130 |
-
/*
|
| 131 |
-
.gradio-container
|
| 132 |
-
.gradio-container
|
| 133 |
-
.gradio-container main > .column > .row > button {
|
| 134 |
display: flex;
|
| 135 |
flex-direction: column;
|
| 136 |
align-items: flex-start;
|
|
@@ -147,34 +146,34 @@ body, gradio-app { background: var(--patent-bg) !important; }
|
|
| 147 |
box-shadow: none;
|
| 148 |
transition: background .2s, border-color .2s;
|
| 149 |
}
|
| 150 |
-
.gradio-container
|
| 151 |
background: #f2f5ff !important;
|
| 152 |
border: 1px solid #a4b8f2 !important;
|
| 153 |
color: #233b78 !important;
|
| 154 |
}
|
| 155 |
-
.gradio-container
|
| 156 |
content: 'Use your connected ChatGPT account';
|
| 157 |
font-size: 12px;
|
| 158 |
font-weight: 400;
|
| 159 |
letter-spacing: 0;
|
| 160 |
color: #5c6f99;
|
| 161 |
}
|
| 162 |
-
.gradio-container
|
| 163 |
-
.gradio-container
|
| 164 |
opacity: 1 !important;
|
| 165 |
background: #f5f7fa !important;
|
| 166 |
border: 1px solid #e1e6ed !important;
|
| 167 |
color: #6d788b !important;
|
| 168 |
cursor: not-allowed;
|
| 169 |
}
|
| 170 |
-
.gradio-container
|
| 171 |
content: 'Coming soon';
|
| 172 |
color: #737f90;
|
| 173 |
font-size: 12px;
|
| 174 |
font-weight: 400;
|
| 175 |
letter-spacing: 0;
|
| 176 |
}
|
| 177 |
-
.gradio-container
|
| 178 |
min-height: 52px;
|
| 179 |
border-radius: 11px;
|
| 180 |
border: 1px solid #315de8 !important;
|
|
@@ -185,11 +184,11 @@ body, gradio-app { background: var(--patent-bg) !important; }
|
|
| 185 |
box-shadow: 0 4px 10px #315de81a;
|
| 186 |
transition: background .2s;
|
| 187 |
}
|
| 188 |
-
.gradio-container
|
| 189 |
-
.gradio-container
|
| 190 |
-
.gradio-container
|
| 191 |
-
.gradio-container
|
| 192 |
-
.gradio-container
|
| 193 |
.gradio-container .block:has(> .label) { border-radius: 14px !important; border-color: #dce3ed !important; background: #fff !important; }
|
| 194 |
.gradio-container .block:has(> .label) .prose p { color: #66748b; font-size: 13px; line-height: 1.7; }
|
| 195 |
.gradio-container .prose a { color: #315de8 !important; text-underline-offset: 4px; }
|
|
@@ -229,7 +228,7 @@ body, gradio-app { background: var(--patent-bg) !important; }
|
|
| 229 |
.prose h1 + p { font-size: 13px !important; }
|
| 230 |
.upload-container { height: 265px !important; min-height: 265px; }
|
| 231 |
.block:has(> [data-testid="block-label"]) { min-height: 265px; }
|
| 232 |
-
|
| 233 |
.row > button { width: 100%; min-height: 115px; padding: 22px; }
|
| 234 |
.tabs { padding: 6px 12px 12px; }
|
| 235 |
[role="tablist"] { gap: 9px; }
|
|
@@ -240,3 +239,50 @@ body, gradio-app { background: var(--patent-bg) !important; }
|
|
| 240 |
*, *::before, *::after { animation: none !important; transition: none !important; }
|
| 241 |
.patent-progress[aria-busy="true"] .patent-progress__fill { width: 100%; opacity: .6; }
|
| 242 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Light styling for the native Gradio upload, method, sign-in and result controls. */
|
| 2 |
:root {
|
| 3 |
color-scheme: light;
|
| 4 |
--patent-bg: #f8fafc;
|
|
|
|
| 127 |
border-style: solid !important;
|
| 128 |
border-color: #ced9f3 !important;
|
| 129 |
}
|
| 130 |
+
/* Visibility is controlled by Gradio events, not CSS guesses about upload state. */
|
| 131 |
+
.gradio-container #method-options { gap: 15px; margin-top: 5px; }
|
| 132 |
+
.gradio-container #method-options > button {
|
|
|
|
| 133 |
display: flex;
|
| 134 |
flex-direction: column;
|
| 135 |
align-items: flex-start;
|
|
|
|
| 146 |
box-shadow: none;
|
| 147 |
transition: background .2s, border-color .2s;
|
| 148 |
}
|
| 149 |
+
.gradio-container #method-agents.primary {
|
| 150 |
background: #f2f5ff !important;
|
| 151 |
border: 1px solid #a4b8f2 !important;
|
| 152 |
color: #233b78 !important;
|
| 153 |
}
|
| 154 |
+
.gradio-container #method-agents::after {
|
| 155 |
content: 'Use your connected ChatGPT account';
|
| 156 |
font-size: 12px;
|
| 157 |
font-weight: 400;
|
| 158 |
letter-spacing: 0;
|
| 159 |
color: #5c6f99;
|
| 160 |
}
|
| 161 |
+
.gradio-container #method-agents.primary:hover { background: #eaf0ff !important; border-color: #6483e6 !important; }
|
| 162 |
+
.gradio-container #method-nlp {
|
| 163 |
opacity: 1 !important;
|
| 164 |
background: #f5f7fa !important;
|
| 165 |
border: 1px solid #e1e6ed !important;
|
| 166 |
color: #6d788b !important;
|
| 167 |
cursor: not-allowed;
|
| 168 |
}
|
| 169 |
+
.gradio-container #method-nlp::after {
|
| 170 |
content: 'Coming soon';
|
| 171 |
color: #737f90;
|
| 172 |
font-size: 12px;
|
| 173 |
font-weight: 400;
|
| 174 |
letter-spacing: 0;
|
| 175 |
}
|
| 176 |
+
.gradio-container #process-button {
|
| 177 |
min-height: 52px;
|
| 178 |
border-radius: 11px;
|
| 179 |
border: 1px solid #315de8 !important;
|
|
|
|
| 184 |
box-shadow: 0 4px 10px #315de81a;
|
| 185 |
transition: background .2s;
|
| 186 |
}
|
| 187 |
+
.gradio-container #process-button::after { content: ' →'; padding-left: 7px; }
|
| 188 |
+
.gradio-container #process-button:hover { background: #244bcc !important; }
|
| 189 |
+
.gradio-container #research-disclosure .prose p { color: #718096; font-size: 11px; line-height: 1.75; }
|
| 190 |
+
.gradio-container #process-status { padding: 6px 0 6px 12px !important; border-left: 2px solid #c9d6f8 !important; min-height: 34px; }
|
| 191 |
+
.gradio-container #process-status .prose p { color: #5b6980; font-size: 12px; line-height: 1.7; }
|
| 192 |
.gradio-container .block:has(> .label) { border-radius: 14px !important; border-color: #dce3ed !important; background: #fff !important; }
|
| 193 |
.gradio-container .block:has(> .label) .prose p { color: #66748b; font-size: 13px; line-height: 1.7; }
|
| 194 |
.gradio-container .prose a { color: #315de8 !important; text-underline-offset: 4px; }
|
|
|
|
| 228 |
.prose h1 + p { font-size: 13px !important; }
|
| 229 |
.upload-container { height: 265px !important; min-height: 265px; }
|
| 230 |
.block:has(> [data-testid="block-label"]) { min-height: 265px; }
|
| 231 |
+
#method-options { flex-direction: column; }
|
| 232 |
.row > button { width: 100%; min-height: 115px; padding: 22px; }
|
| 233 |
.tabs { padding: 6px 12px 12px; }
|
| 234 |
[role="tablist"] { gap: 9px; }
|
|
|
|
| 239 |
*, *::before, *::after { animation: none !important; transition: none !important; }
|
| 240 |
.patent-progress[aria-busy="true"] .patent-progress__fill { width: 100%; opacity: .6; }
|
| 241 |
}
|
| 242 |
+
|
| 243 |
+
/* Explicit neutral method state; only the selected card uses the blue treatment. */
|
| 244 |
+
.gradio-container #method-agents.secondary {
|
| 245 |
+
background: #fff !important; border: 1px solid #d9e1ec !important; color: #263346 !important;
|
| 246 |
+
}
|
| 247 |
+
.gradio-container #method-agents.secondary:hover { border-color: #7896e9 !important; background: #f8faff !important; }
|
| 248 |
+
.gradio-container #process-button:disabled { opacity: 1; background: #e7ecf5 !important; border-color: #e0e6ef !important; color: #6e7c92 !important; box-shadow: none; }
|
| 249 |
+
.gradio-container #reconnect-chatgpt { width: auto; align-self: flex-start; min-height: 40px; padding: 9px 16px; font-size: 13px; color: #315de8 !important; background: #f0f4ff !important; border: 1px solid #d5dff8 !important; }
|
| 250 |
+
/* Override table colors explicitly, including when the visitor prefers dark mode. */
|
| 251 |
+
.gradio-container #patent-upload:has(.file-preview-holder) { padding: 19px 20px 17px !important; }
|
| 252 |
+
.gradio-container #patent-upload:has(.file-preview-holder) > [data-testid="block-label"] { position: static !important; margin: 0 30px 13px 0 !important; }
|
| 253 |
+
.gradio-container #patent-upload .file-preview-holder { background: transparent !important; padding: 0 !important; border: 0 !important; }
|
| 254 |
+
.gradio-container #patent-upload :is(table, tbody, tr, td) { background: #f2f5fb !important; color: #263346 !important; border: 0 !important; }
|
| 255 |
+
.gradio-container #patent-upload table { border-collapse: separate; border-spacing: 0; border-radius: 11px; overflow: hidden; }
|
| 256 |
+
.gradio-container #patent-upload td { padding: 17px 15px !important; font-size: 13px; }
|
| 257 |
+
.gradio-container #patent-upload :is(.filename, .stem, .ext) { color: #263346 !important; }
|
| 258 |
+
.gradio-container #patent-upload .download-link { color: #315de8 !important; font-size: 12px; white-space: nowrap; }
|
| 259 |
+
.gradio-container #patent-upload button[aria-label="Clear"] { background: #fff !important; color: #64748b !important; border-radius: 9px; }
|
| 260 |
+
/* Gradio Column overlay; it is absent until the user chooses AI agents. */
|
| 261 |
+
.gradio-container #login-modal {
|
| 262 |
+
position: fixed !important; inset: 0 !important; z-index: 10000 !important;
|
| 263 |
+
width: 100vw !important; height: 100dvh !important; max-width: none !important;
|
| 264 |
+
min-width: 0 !important; margin: 0 !important; padding: 24px !important;
|
| 265 |
+
align-items: center; justify-content: center; overflow-y: auto;
|
| 266 |
+
background: #17203366 !important; backdrop-filter: blur(7px);
|
| 267 |
+
}
|
| 268 |
+
.gradio-container #login-card {
|
| 269 |
+
position: relative; flex: 0 0 auto; width: min(440px, 100%); min-width: 0 !important;
|
| 270 |
+
gap: 19px; padding: 38px 30px 25px !important; border: 1px solid #dce4ef;
|
| 271 |
+
border-radius: 20px; background: #fff !important; color: #17202d !important;
|
| 272 |
+
box-shadow: 0 24px 100px #16233c30;
|
| 273 |
+
}
|
| 274 |
+
.gradio-container #login-card h2 { color: #17202d !important; font-size: 27px !important; font-weight: 600 !important; line-height: 1.2; letter-spacing: -.8px; margin: 9px 0 15px !important; }
|
| 275 |
+
.gradio-container #login-description { color: #5c6a80 !important; font-size: 14px; line-height: 1.7; }
|
| 276 |
+
.gradio-container #close-login { position: absolute; top: 12px; right: 12px; z-index: 1; width: 34px; min-width: 34px; min-height: 34px; padding: 0; border: 0; background: #f3f6fa !important; color: #526178 !important; }
|
| 277 |
+
.gradio-container #login-status { padding: 16px !important; border: 1px solid #dce5f6 !important; border-radius: 11px; background: #f4f7fd !important; }
|
| 278 |
+
.gradio-container #login-status .prose { color: #34445e !important; font-size: 13px; line-height: 1.75; }
|
| 279 |
+
.gradio-container #login-status strong { color: #17202d !important; }
|
| 280 |
+
.gradio-container #connect-chatgpt { min-height: 48px; background: #315de8 !important; color: #fff !important; border: 1px solid #315de8 !important; border-radius: 10px; font-size: 14px; }
|
| 281 |
+
.gradio-container #connect-chatgpt:disabled { background: #e7ecf5 !important; color: #526178 !important; border-color: #dbe3ef !important; opacity: 1; }
|
| 282 |
+
.gradio-container #login-note p { color: #6c7a8e !important; font-size: 11px; line-height: 1.6; }
|
| 283 |
+
@media (max-width: 640px) {
|
| 284 |
+
#login-modal { padding: 16px !important; }
|
| 285 |
+
#login-card { padding: 36px 22px 23px !important; }
|
| 286 |
+
#patent-upload:has(.file-preview-holder) { min-height: 0 !important; padding: 17px 13px !important; }
|
| 287 |
+
#patent-upload td { padding: 15px 10px !important; }
|
| 288 |
+
}
|
tests/test_ui_workflow.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Regression checks for progressive controls and login lifecycle, without model calls."""
|
| 2 |
+
import sys
|
| 3 |
+
import unittest
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from unittest.mock import patch
|
| 6 |
+
|
| 7 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 8 |
+
from ui_workflow import build_app
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def example_process(file, session):
|
| 12 |
+
yield 'Working', '', None, '', '', None, None
|
| 13 |
+
yield 'Complete', '{"views": {}}', '/tmp/result.json', '<svg/>', 'package Example {}', '/tmp/result.sysml', '/tmp/research.zip'
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class InterfaceTests(unittest.TestCase):
|
| 17 |
+
@classmethod
|
| 18 |
+
def setUpClass(cls):
|
| 19 |
+
cls.app = build_app(example_process)
|
| 20 |
+
cls.handlers = {f.fn.__name__: f.fn for f in cls.app.fns.values() if f.fn}
|
| 21 |
+
cls.components = {c.elem_id: c for c in cls.app.blocks.values() if getattr(c, 'elem_id', None)}
|
| 22 |
+
|
| 23 |
+
def component(self, name):
|
| 24 |
+
return self.components[name]
|
| 25 |
+
|
| 26 |
+
def test_initial_state_has_no_method_login_process_or_results(self):
|
| 27 |
+
for name in ['method-options', 'login-modal', 'process-button', 'results-section']:
|
| 28 |
+
self.assertFalse(self.component(name).visible)
|
| 29 |
+
self.assertEqual(self.component('method-agents').variant, 'secondary')
|
| 30 |
+
|
| 31 |
+
def test_upload_requires_fresh_selection_and_clears_results(self):
|
| 32 |
+
updates = self.handlers['on_upload']('/tmp/patent.html')
|
| 33 |
+
self.assertTrue(updates[self.component('method-options')]['visible'])
|
| 34 |
+
for name in ['process-button', 'login-modal', 'results-section']:
|
| 35 |
+
self.assertFalse(updates[self.component(name)]['visible'])
|
| 36 |
+
self.assertEqual(updates[self.component('method-agents')]['variant'], 'secondary')
|
| 37 |
+
self.assertFalse(self.handlers['on_upload'](None)[self.component('method-options')]['visible'])
|
| 38 |
+
|
| 39 |
+
@patch('ui_workflow.login_needed', return_value=True)
|
| 40 |
+
def test_explicit_selection_opens_login_and_disables_process(self, _):
|
| 41 |
+
updates = self.handlers['choose_agents']('/tmp/patent.html', 'session')
|
| 42 |
+
self.assertTrue(updates[self.component('login-modal')]['visible'])
|
| 43 |
+
self.assertTrue(updates[self.component('process-button')]['visible'])
|
| 44 |
+
self.assertFalse(updates[self.component('process-button')]['interactive'])
|
| 45 |
+
|
| 46 |
+
@patch('ui_workflow.login_needed', return_value=False)
|
| 47 |
+
def test_connected_user_can_choose_method_without_another_login(self, _):
|
| 48 |
+
updates = self.handlers['choose_agents']('/tmp/patent.html', 'session')
|
| 49 |
+
self.assertFalse(updates[self.component('login-modal')]['visible'])
|
| 50 |
+
self.assertTrue(updates[self.component('process-button')]['interactive'])
|
| 51 |
+
|
| 52 |
+
def test_successful_login_closes_dialog_and_enables_process(self):
|
| 53 |
+
state = {'needs_login': True}
|
| 54 |
+
def login(visitor):
|
| 55 |
+
yield 'Enter your code'
|
| 56 |
+
state['needs_login'] = False
|
| 57 |
+
yield 'Connected'
|
| 58 |
+
with patch('ui_workflow.login_needed', side_effect=lambda _: state['needs_login']), patch('ui_workflow.connect_chatgpt', login):
|
| 59 |
+
updates = list(self.handlers['sign_in']('session'))
|
| 60 |
+
completed = next(x for x in updates if self.component('login-modal') in x)
|
| 61 |
+
self.assertFalse(completed[self.component('login-modal')]['visible'])
|
| 62 |
+
self.assertFalse(completed[self.component('reconnect-chatgpt')]['visible'])
|
| 63 |
+
self.assertTrue(completed[self.component('process-button')]['interactive'])
|
| 64 |
+
|
| 65 |
+
def test_cancel_closes_underlying_login_generator(self):
|
| 66 |
+
state = {'closed': False}
|
| 67 |
+
def login(visitor):
|
| 68 |
+
try:
|
| 69 |
+
yield 'Waiting for sign-in'
|
| 70 |
+
yield 'Still waiting'
|
| 71 |
+
finally:
|
| 72 |
+
state['closed'] = True
|
| 73 |
+
with patch('ui_workflow.login_needed', return_value=True), patch('ui_workflow.connect_chatgpt', login):
|
| 74 |
+
stream = self.handlers['sign_in']('session')
|
| 75 |
+
next(stream)
|
| 76 |
+
next(stream)
|
| 77 |
+
stream.close()
|
| 78 |
+
self.assertTrue(state['closed'])
|
| 79 |
+
|
| 80 |
+
@patch('ui_workflow.login_needed', return_value=True)
|
| 81 |
+
def test_failed_login_does_not_unlock_process(self, _):
|
| 82 |
+
def failed_login(visitor):
|
| 83 |
+
yield 'Sign-in expired'
|
| 84 |
+
with patch('ui_workflow.connect_chatgpt', failed_login):
|
| 85 |
+
updates = list(self.handlers['sign_in']('session'))
|
| 86 |
+
self.assertFalse(any(x.get(self.component('process-button'), {}).get('interactive') for x in updates))
|
| 87 |
+
self.assertTrue(updates[-1][self.component('connect-chatgpt')]['interactive'])
|
| 88 |
+
|
| 89 |
+
@patch('ui_workflow.login_needed', return_value=False)
|
| 90 |
+
def test_results_are_revealed_only_after_processing_finishes(self, _):
|
| 91 |
+
updates = list(self.handlers['run_ui']('/tmp/patent.html', 'agents', 'session'))
|
| 92 |
+
result_states = [u[self.component('results-section')]['visible'] for u in updates if self.component('results-section') in u]
|
| 93 |
+
self.assertEqual(result_states, [False, True])
|
| 94 |
+
self.assertTrue(updates[-1][self.component('patent-upload')]['interactive'])
|
| 95 |
+
self.assertTrue(updates[-1][self.component('process-button')]['interactive'])
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
if __name__ == '__main__':
|
| 99 |
+
unittest.main()
|
ui_dialog.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
() => {
|
| 2 |
+
window.__patentDialogCleanup?.();
|
| 3 |
+
let open = false;
|
| 4 |
+
let previousFocus;
|
| 5 |
+
let previousOverflow = '';
|
| 6 |
+
let inerted = [];
|
| 7 |
+
const visible = element => !!element?.getClientRects().length;
|
| 8 |
+
const sync = () => {
|
| 9 |
+
const modal = document.getElementById('login-modal');
|
| 10 |
+
const card = document.getElementById('login-card');
|
| 11 |
+
const close = document.getElementById('close-login');
|
| 12 |
+
if (card) {
|
| 13 |
+
card.setAttribute('role', 'dialog');
|
| 14 |
+
card.setAttribute('aria-modal', 'true');
|
| 15 |
+
card.setAttribute('aria-labelledby', 'login-title');
|
| 16 |
+
card.setAttribute('aria-describedby', 'login-description');
|
| 17 |
+
}
|
| 18 |
+
close?.setAttribute('aria-label', 'Close ChatGPT sign-in');
|
| 19 |
+
const agents = document.getElementById('method-agents');
|
| 20 |
+
agents?.setAttribute('aria-pressed', String(agents.classList.contains('primary')));
|
| 21 |
+
const next = visible(modal);
|
| 22 |
+
if (next === open) return;
|
| 23 |
+
open = next;
|
| 24 |
+
if (open) {
|
| 25 |
+
previousFocus = document.activeElement;
|
| 26 |
+
previousOverflow = document.body.style.overflow;
|
| 27 |
+
document.body.style.overflow = 'hidden';
|
| 28 |
+
inerted = [...(modal.parentElement?.children || [])]
|
| 29 |
+
.filter(el => el !== modal && !el.inert);
|
| 30 |
+
inerted.forEach(el => { el.inert = true; });
|
| 31 |
+
close?.focus();
|
| 32 |
+
} else {
|
| 33 |
+
document.body.style.overflow = previousOverflow;
|
| 34 |
+
inerted.forEach(el => { el.inert = false; });
|
| 35 |
+
inerted = [];
|
| 36 |
+
if (previousFocus?.isConnected) previousFocus.focus();
|
| 37 |
+
}
|
| 38 |
+
};
|
| 39 |
+
const onKey = event => {
|
| 40 |
+
if (!open) return;
|
| 41 |
+
if (event.key === 'Escape') {
|
| 42 |
+
event.preventDefault();
|
| 43 |
+
document.getElementById('close-login')?.click();
|
| 44 |
+
}
|
| 45 |
+
if (event.key !== 'Tab') return;
|
| 46 |
+
const controls = [...document.querySelectorAll('#login-card button:not(:disabled), #login-card a[href]')].filter(visible);
|
| 47 |
+
const first = controls[0], last = controls[controls.length - 1];
|
| 48 |
+
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
|
| 49 |
+
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
|
| 50 |
+
};
|
| 51 |
+
const observer = new MutationObserver(sync);
|
| 52 |
+
observer.observe(document.body, {childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style']});
|
| 53 |
+
document.addEventListener('keydown', onKey);
|
| 54 |
+
window.__patentDialogCleanup = () => {
|
| 55 |
+
observer.disconnect();
|
| 56 |
+
document.removeEventListener('keydown', onKey);
|
| 57 |
+
inerted.forEach(el => { el.inert = false; });
|
| 58 |
+
if (open) document.body.style.overflow = previousOverflow;
|
| 59 |
+
};
|
| 60 |
+
sync();
|
| 61 |
+
}
|
ui_workflow.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Gradio presentation states for upload, explicit method choice, sign-in and results."""
|
| 2 |
+
from contextlib import closing
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
|
| 8 |
+
from chatgpt_login import connect_chatgpt
|
| 9 |
+
from user_session import connected, delete_session, new_session
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def login_needed(visitor):
|
| 13 |
+
if not os.getenv('SPACE_ID'):
|
| 14 |
+
return False
|
| 15 |
+
try:
|
| 16 |
+
return not connected(visitor)
|
| 17 |
+
except ValueError:
|
| 18 |
+
return True
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def build_app(process, unused_gpu_slot=None):
|
| 22 |
+
with gr.Blocks(title='Translate patents to SysML v2') as app:
|
| 23 |
+
session = gr.State(new_session if os.getenv('SPACE_ID') else None,
|
| 24 |
+
time_to_live=3600, delete_callback=delete_session)
|
| 25 |
+
method = gr.State(None)
|
| 26 |
+
if unused_gpu_slot:
|
| 27 |
+
gr.Button(visible=False).click(unused_gpu_slot, api_name=False)
|
| 28 |
+
gr.Markdown('# Translate patents to SysML v2\nUpload a patent. Generate a system model.')
|
| 29 |
+
patent = gr.File(label='Patent HTML · .html or .htm · up to 10 MB',
|
| 30 |
+
file_types=['.html', '.htm'], type='filepath', elem_id='patent-upload')
|
| 31 |
+
with gr.Row(visible=False, elem_id='method-options') as methods:
|
| 32 |
+
agents = gr.Button('AI agents', variant='secondary', elem_id='method-agents')
|
| 33 |
+
gr.Button('Fine Tuned NLP', interactive=False, elem_id='method-nlp')
|
| 34 |
+
disclosure = gr.Markdown('Research mode: uploaded patents, agent conversations, retrieved evidence, and outputs '
|
| 35 |
+
'are retained in the project owner’s private research archive. Login credentials are excluded.',
|
| 36 |
+
visible=False, elem_id='research-disclosure')
|
| 37 |
+
reconnect = gr.Button('Connect ChatGPT', visible=False, elem_id='reconnect-chatgpt')
|
| 38 |
+
start = gr.Button('Process', variant='primary', visible=False, interactive=False, elem_id='process-button')
|
| 39 |
+
status = gr.Markdown(visible=False, elem_id='process-status')
|
| 40 |
+
with gr.Column(visible=False, elem_id='results-section') as results:
|
| 41 |
+
with gr.Tab('Diagram'):
|
| 42 |
+
preview = gr.HTML(elem_id='diagram')
|
| 43 |
+
with gr.Tab('JSON text'):
|
| 44 |
+
result = gr.Code(label='Functional-decomposition JSON', language='json', interactive=False)
|
| 45 |
+
download = gr.DownloadButton('Download JSON')
|
| 46 |
+
research_download = gr.DownloadButton('Download complete research record (.zip)')
|
| 47 |
+
with gr.Tab('SysML download'):
|
| 48 |
+
sysml_download = gr.DownloadButton('Download .sysml')
|
| 49 |
+
sysml_text = gr.Code(label='SysML v2', language=None, interactive=False)
|
| 50 |
+
|
| 51 |
+
# Native Gradio components styled as a dialog; JS supplies focus trapping and Escape.
|
| 52 |
+
with gr.Column(visible=False, elem_id='login-modal') as modal:
|
| 53 |
+
with gr.Column(elem_id='login-card'):
|
| 54 |
+
close = gr.Button('✕', elem_id='close-login', min_width=36, scale=0, size='sm')
|
| 55 |
+
gr.HTML('<h2 id="login-title">Connect ChatGPT</h2>'
|
| 56 |
+
'<p id="login-description">Sign in with your ChatGPT account to use AI agents. '
|
| 57 |
+
'Your connection is private to this session.</p>')
|
| 58 |
+
login_status = gr.Markdown('Continue to get a secure OpenAI sign-in link and code.', elem_id='login-status')
|
| 59 |
+
connect = gr.Button('Connect ChatGPT', variant='primary', elem_id='connect-chatgpt')
|
| 60 |
+
gr.Markdown('This window closes automatically once you’re connected.', elem_id='login-note')
|
| 61 |
+
|
| 62 |
+
output_components = [status, result, download, preview, sysml_text, sysml_download, research_download]
|
| 63 |
+
|
| 64 |
+
def on_upload(file):
|
| 65 |
+
return {methods: gr.update(visible=bool(file)), method: None,
|
| 66 |
+
agents: gr.update(variant='secondary'), disclosure: gr.update(visible=bool(file)),
|
| 67 |
+
start: gr.update(visible=False, interactive=False, value='Process'),
|
| 68 |
+
status: gr.update(visible=False, value=''), modal: gr.update(visible=False),
|
| 69 |
+
reconnect: gr.update(visible=False), connect: gr.update(interactive=True, value='Connect ChatGPT'),
|
| 70 |
+
results: gr.update(visible=False), result: '', download: None, preview: '',
|
| 71 |
+
sysml_text: '', sysml_download: None, research_download: None}
|
| 72 |
+
|
| 73 |
+
def choose_agents(file, visitor):
|
| 74 |
+
if not file:
|
| 75 |
+
raise gr.Error('Upload a patent HTML file first.')
|
| 76 |
+
needs_login = login_needed(visitor)
|
| 77 |
+
return {method: 'agents', agents: gr.update(variant='primary'),
|
| 78 |
+
modal: gr.update(visible=needs_login),
|
| 79 |
+
start: gr.update(visible=True, interactive=not needs_login),
|
| 80 |
+
reconnect: gr.update(visible=needs_login),
|
| 81 |
+
login_status: 'Continue to get a secure OpenAI sign-in link and code.',
|
| 82 |
+
connect: gr.update(interactive=True, value='Connect ChatGPT'),
|
| 83 |
+
status: gr.update(visible=True, value='AI agents selected. Connect ChatGPT to continue.' if needs_login
|
| 84 |
+
else 'AI agents selected. Ready to process.')}
|
| 85 |
+
|
| 86 |
+
def sign_in(visitor):
|
| 87 |
+
yield {connect: gr.update(interactive=False, value='Waiting for sign-in…')}
|
| 88 |
+
try:
|
| 89 |
+
if login_needed(visitor):
|
| 90 |
+
# Closing the dialog cancels this generator and the login subprocess.
|
| 91 |
+
with closing(connect_chatgpt(visitor)) as messages:
|
| 92 |
+
for message in messages:
|
| 93 |
+
yield {login_status: message}
|
| 94 |
+
if not login_needed(visitor):
|
| 95 |
+
yield {modal: gr.update(visible=False), reconnect: gr.update(visible=False),
|
| 96 |
+
start: gr.update(interactive=True),
|
| 97 |
+
status: gr.update(visible=True, value='ChatGPT connected. Ready to process.')}
|
| 98 |
+
else:
|
| 99 |
+
yield {start: gr.update(interactive=False)}
|
| 100 |
+
except (ValueError, OSError) as error:
|
| 101 |
+
yield {login_status: str(error), start: gr.update(interactive=False)}
|
| 102 |
+
yield {connect: gr.update(interactive=True, value='Connect ChatGPT')}
|
| 103 |
+
|
| 104 |
+
def dismiss_login():
|
| 105 |
+
return {modal: gr.update(visible=False), connect: gr.update(interactive=True, value='Connect ChatGPT')}
|
| 106 |
+
|
| 107 |
+
def run_ui(file, selected, visitor):
|
| 108 |
+
if not file or selected != 'agents':
|
| 109 |
+
yield {status: gr.update(visible=True, value='Upload a patent and select a method first.')}
|
| 110 |
+
return
|
| 111 |
+
if login_needed(visitor):
|
| 112 |
+
yield {modal: gr.update(visible=True), reconnect: gr.update(visible=True),
|
| 113 |
+
start: gr.update(interactive=False),
|
| 114 |
+
status: gr.update(visible=True, value='Connect ChatGPT to continue.')}
|
| 115 |
+
return
|
| 116 |
+
yield {start: gr.update(interactive=False, value='Processing…'), patent: gr.update(interactive=False),
|
| 117 |
+
agents: gr.update(interactive=False), results: gr.update(visible=False),
|
| 118 |
+
reconnect: gr.update(visible=False), status: gr.update(visible=True, value='Processing your patent…'),
|
| 119 |
+
result: '', download: None, preview: '', sysml_text: '', sysml_download: None, research_download: None}
|
| 120 |
+
finished = None
|
| 121 |
+
try:
|
| 122 |
+
with closing(process(file, visitor)) as rows:
|
| 123 |
+
for row in rows:
|
| 124 |
+
finished = row
|
| 125 |
+
yield {status: gr.update(visible=True, value=row[0])}
|
| 126 |
+
if finished:
|
| 127 |
+
updates = dict(zip(output_components, finished))
|
| 128 |
+
updates[status] = gr.update(visible=True, value=finished[0])
|
| 129 |
+
# Failed runs can still expose their retained research archive.
|
| 130 |
+
updates[results] = gr.update(visible=bool(finished[1] or finished[6]))
|
| 131 |
+
yield updates
|
| 132 |
+
except Exception:
|
| 133 |
+
logging.exception('Patent processing failed')
|
| 134 |
+
yield {status: gr.update(visible=True, value='Processing could not finish. Please try again.')}
|
| 135 |
+
yield {start: gr.update(interactive=True, value='Process'), patent: gr.update(interactive=True),
|
| 136 |
+
agents: gr.update(interactive=True)}
|
| 137 |
+
|
| 138 |
+
selection_outputs = [method, agents, modal, start, reconnect, login_status, connect, status]
|
| 139 |
+
agents.click(choose_agents, [patent, session], selection_outputs,
|
| 140 |
+
api_name=False, queue=False, show_progress='hidden')
|
| 141 |
+
reconnect.click(choose_agents, [patent, session], selection_outputs,
|
| 142 |
+
api_name=False, queue=False, show_progress='hidden')
|
| 143 |
+
login_event = connect.click(sign_in, session, [connect, login_status, modal, reconnect, start, status],
|
| 144 |
+
api_name=False, concurrency_limit=4, show_progress='hidden')
|
| 145 |
+
close.click(dismiss_login, outputs=[modal, connect], cancels=[login_event],
|
| 146 |
+
api_name=False, queue=False, show_progress='hidden')
|
| 147 |
+
patent.change(on_upload, patent, [methods, method, agents, disclosure, start, status, modal, reconnect, connect,
|
| 148 |
+
results, result, download, preview, sysml_text, sysml_download, research_download],
|
| 149 |
+
cancels=[login_event], api_name=False, queue=False, show_progress='hidden')
|
| 150 |
+
start.click(run_ui, [patent, method, session],
|
| 151 |
+
[*output_components, start, patent, agents, results, modal, reconnect],
|
| 152 |
+
api_name=False, concurrency_limit=1, concurrency_id='patent_processing', show_progress='hidden')
|
| 153 |
+
# Preserve the existing seven-output API and backend authentication gate.
|
| 154 |
+
gr.Button(visible=False).click(process, [patent, session], output_components,
|
| 155 |
+
api_name='process_patent', concurrency_limit=1, concurrency_id='patent_processing')
|
| 156 |
+
return app
|