From 5aa047c2029b95104f510ec13d5454d33b0e4534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis-F=C3=A9lix=20Nothias?= Date: Mon, 22 Jun 2026 14:05:31 -0300 Subject: [PATCH] Deactivate streaming copyright pass + smooth reference reveal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GUI streamed answer froze for ~0.6s at the body→references boundary, then dumped the references all at once. - basic.py execute_stream: skip the synchronous copyright-detection pass on the streaming (GUI) path. The body has already streamed and the client ignores the "revision" event, so the pass only added a visible mid-stream stall before the references with no user-facing effect. The non-streaming execute() path still runs it for compliance. Also drops the now-unused full_response accumulation. - ChatPanel: finish the post-stream buffered tail (the one-chunk references block) at a finite 48 chars/frame instead of snapping to instant on `done`, so it eases in smoothly. Historical loads stay instant (the typewriter starts caught up, so no animation runs). Verified: max inter-token gap 131ms (was ~605-668ms); no copyright pass on the stream; 91 targeted unit tests pass. Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/ChatPanel.tsx | 7 ++++++- src/perspicacite/rag/modes/basic.py | 28 +++++---------------------- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/ChatPanel.tsx b/frontend/src/components/ChatPanel.tsx index 9da040f..ac37a0c 100644 --- a/frontend/src/components/ChatPanel.tsx +++ b/frontend/src/components/ChatPanel.tsx @@ -907,7 +907,12 @@ function AssistantMessage({ // once the turn is final. const displayed = useTypewriter( turn.text, - turn.streaming && streaming ? 12 : 99999, + // While streaming, type at a deliberate pace. Once the stream ends, finish + // the buffered tail (e.g. the references block, which arrives in one chunk) + // at a fast but FINITE rate so it eases in smoothly instead of snapping in + // all at once. Historical loads stay instant — useTypewriter starts caught + // up, so no animation runs. + turn.streaming && streaming ? 12 : 48, ); // Progressively reveal steps + sources while streaming, so a burst diff --git a/src/perspicacite/rag/modes/basic.py b/src/perspicacite/rag/modes/basic.py index dd3319d..eccfefe 100644 --- a/src/perspicacite/rag/modes/basic.py +++ b/src/perspicacite/rag/modes/basic.py @@ -879,7 +879,6 @@ async def execute_stream( ] # Stream the LLM response - full_response = "" try: async for chunk in llm.stream( messages=messages, @@ -889,7 +888,6 @@ async def execute_stream( temperature=0.3, stage="basic.answer", ): - full_response += chunk yield StreamEvent.content(chunk) except Exception as e: logger.error("basic_streaming_error", error=str(e)) @@ -903,28 +901,12 @@ async def execute_stream( preamble=scope.scope_note, ) yield StreamEvent.content(answer) - full_response = answer - # Defense-in-depth copyright filter on the full streamed - # response. For action="log" we just emit a warning log; for - # quote/strip/rewrite we emit a "revision" event after the - # answer with the corrected text — clients may render it or - # ignore. Does not retract the already-streamed content. - try: - revised = await _apply_copyright_filter( - answer=full_response, paper_results=paper_results, llm=llm, - config=self.config, - ) - if revised != full_response: - yield StreamEvent( - event="revision", - data=json.dumps({ - "kind": "copyright_filter", - "revised_content": revised, - }), - ) - except Exception as exc: - logger.warning("copyright_filter_stream_failed", error=str(exc)) + # The synchronous copyright-detection pass is intentionally NOT run on + # the streaming (GUI) path. The body has already streamed to the client + # and the client ignores the "revision" event, so it only added a + # visible mid-stream stall before the references with no user-facing + # effect. The non-streaming execute() path still runs it for compliance. # Append references section after streaming completes if sources: