Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion frontend/src/components/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 5 additions & 23 deletions src/perspicacite/rag/modes/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,6 @@ async def execute_stream(
]

# Stream the LLM response
full_response = ""
try:
async for chunk in llm.stream(
messages=messages,
Expand All @@ -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))
Expand All @@ -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:
Expand Down
Loading