Skip to content

Commit

Permalink
Merge branch 'main' into add_custom_callback_exception
Browse files Browse the repository at this point in the history
  • Loading branch information
ahuang11 authored Jan 10, 2025
2 parents 3b160b8 + 6544d78 commit 1d1b206
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 12 deletions.
6 changes: 5 additions & 1 deletion panel/chat/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ def stream(

if message_params:
message.param.update(**message_params)
self._chat_log.scroll_to_latest(scroll_limit=self.auto_scroll_limit)
return message

if isinstance(value, ChatMessage):
Expand All @@ -721,6 +722,7 @@ def stream(
self._replace_placeholder(message)

self.param.trigger("_post_hook_trigger")
self._chat_log.scroll_to_latest(scroll_limit=self.auto_scroll_limit)
return message

def add_step(
Expand Down Expand Up @@ -784,6 +786,8 @@ def add_step(
step_params["context_exception"] = self.callback_exception
step = ChatStep(**step_params)

step._instance = self

if append:
for i in range(1, last_messages + 1):
if not self._chat_log:
Expand Down Expand Up @@ -828,7 +832,7 @@ def add_step(
self.stream(steps_layout, user=user or self.callback_user, avatar=avatar)
else:
steps_layout.append(step)
self._chat_log.scroll_to_latest()
self._chat_log.scroll_to_latest(scroll_limit=self.auto_scroll_limit)
return step

def prompt_user(
Expand Down
3 changes: 3 additions & 0 deletions panel/chat/step.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ def stream(self, token: str | None, replace: bool = False):
else:
stream_to(self.objects[-1], token, replace=replace)

if self._instance is not None:
self._instance._chat_log.scroll_to_latest(self._instance.auto_scroll_limit)

def serialize(
self,
prefix_with_viewable_label: bool = True,
Expand Down
11 changes: 9 additions & 2 deletions panel/layout/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,18 @@ def _process_event(self, event: ScrollButtonClick | None = None) -> None:
# reset the buffers and loaded objects
self.load_buffer = load_buffer

def scroll_to_latest(self):
def scroll_to_latest(self, scroll_limit: float | None = None) -> None:
"""
Scrolls the Feed to the latest entry.
Parameters
----------
scroll_limit : float, optional
Maximum pixel distance from the latest object in the Feed to
trigger scrolling. If the distance exceeds this limit, scrolling will not occur.
If this is not set, it will always scroll to the latest while setting this to 0 disables scrolling.
"""
rerender = self._last_synced and self._last_synced[-1] < len(self.objects)
if rerender:
self._process_event()
self._send_event(ScrollLatestEvent, rerender=rerender)
self._send_event(ScrollLatestEvent, rerender=rerender, scroll_limit=scroll_limit)
10 changes: 8 additions & 2 deletions panel/models/column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,14 @@ export class ColumnView extends BkColumnView {
})
}

scroll_to_latest(): void {
// Waits for the child to be rendered before scrolling
scroll_to_latest(scroll_limit: number | null = null): void {
if (scroll_limit !== null) {
const within_limit = this.distance_from_latest <= scroll_limit
if (!within_limit) {
return
}
}

requestAnimationFrame(() => {
this.model.scroll_position = Math.round(this.el.scrollHeight)
})
Expand Down
5 changes: 3 additions & 2 deletions panel/models/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ class ScrollLatestEvent(ModelEvent):

event_name = 'scroll_latest_event'

def __init__(self, model, rerender=False):
def __init__(self, model, rerender=False, scroll_limit=None):
super().__init__(model=model)
self.rerender = rerender
self.scroll_limit = scroll_limit

def event_values(self) -> dict[str, Any]:
return dict(super().event_values(), rerender=self.rerender)
return dict(super().event_values(), rerender=self.rerender, scroll_limit=self.scroll_limit)


class ScrollButtonClick(ModelEvent):
Expand Down
11 changes: 6 additions & 5 deletions panel/models/feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ import {ColumnView as BkColumnView} from "@bokehjs/models/layouts/column"

@server_event("scroll_latest_event")
export class ScrollLatestEvent extends ModelEvent {
constructor(readonly model: Feed, readonly rerender: boolean) {
constructor(readonly model: Feed, readonly rerender: boolean, readonly scroll_limit?: number | null) {
super()
this.origin = model
this.rerender = rerender
this.scroll_limit = scroll_limit
}

protected override get event_values(): Attrs {
return {model: this.origin, rerender: this.rerender}
return {model: this.origin, rerender: this.rerender, scroll_limit: this.scroll_limit}
}

static override from_values(values: object) {
const {model, rerender} = values as {model: Feed, rerender: boolean}
return new ScrollLatestEvent(model, rerender)
const {model, rerender, scroll_limit} = values as {model: Feed, rerender: boolean, scroll_limit?: number}
return new ScrollLatestEvent(model, rerender, scroll_limit)
}
}

Expand Down Expand Up @@ -71,7 +72,7 @@ export class FeedView extends ColumnView {
override connect_signals(): void {
super.connect_signals()
this.model.on_event(ScrollLatestEvent, (event: ScrollLatestEvent) => {
this.scroll_to_latest()
this.scroll_to_latest(event.scroll_limit)
if (event.rerender) {
this._rendered = false
}
Expand Down
7 changes: 7 additions & 0 deletions panel/models/jsoneditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ export class JSONEditorView extends HTMLBoxView {
onChangeJSON: (json: any) => {
this.model.data = json
},
onChangeText: (text: any) => {
try {
this.model.data = JSON.parse(text)
} catch (e) {
console.warn(e)
}
},
onSelectionChange: (start: any, end: any) => {
this.model.selection = [start, end]
},
Expand Down
66 changes: 66 additions & 0 deletions panel/tests/ui/layout/test_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from playwright.sync_api import expect

from panel import Feed
from panel.layout.spacer import Spacer
from panel.tests.util import serve_component, wait_until

pytestmark = pytest.mark.ui
Expand Down Expand Up @@ -50,6 +51,7 @@ def test_feed_view_latest(page):

wait_until(lambda: int(page.locator('pre').last.inner_text()) > 0.9 * ITEMS, page)


def test_feed_view_scroll_to_latest(page):
feed = Feed(*list(range(ITEMS)), height=250)
serve_component(page, feed)
Expand All @@ -68,6 +70,70 @@ def test_feed_view_scroll_to_latest(page):

wait_until(lambda: int(page.locator('pre').last.inner_text() or 0) > 0.9 * ITEMS, page)


def test_feed_scroll_to_latest_disabled_when_limit_zero(page):
"""Test that scroll_to_latest is disabled when scroll_limit = 0"""
feed = Feed(*list(range(ITEMS)), height=250)
serve_component(page, feed)

feed_el = page.locator(".bk-panel-models-feed-Feed")
initial_scroll = feed_el.evaluate('(el) => el.scrollTop')

# Try to scroll to latest
feed.scroll_to_latest(scroll_limit=0)

# Verify scroll position hasn't changed
final_scroll = feed_el.evaluate('(el) => el.scrollTop')
assert initial_scroll == final_scroll, "Scroll position should not change when limit is 0"


def test_feed_scroll_to_latest_always_when_limit_null(page):
"""Test that scroll_to_latest always triggers when scroll_limit is null"""
feed = Feed(*list(range(ITEMS)), height=250)
serve_component(page, feed)

wait_until(lambda: int(page.locator('pre').last.inner_text() or 0) < 0.9 * ITEMS, page)
feed.scroll_to_latest(scroll_limit=None)
wait_until(lambda: int(page.locator('pre').last.inner_text() or 0) > 0.9 * ITEMS, page)


def test_feed_scroll_to_latest_within_limit(page):
"""Test that scroll_to_latest only triggers within the specified limit"""
feed = Feed(
Spacer(styles=dict(background='red'), width=200, height=200),
Spacer(styles=dict(background='green'), width=200, height=200),
Spacer(styles=dict(background='blue'), width=200, height=200),
auto_scroll_limit=0, height=420
)
serve_component(page, feed)

feed_el = page.locator(".bk-panel-models-feed-Feed")

expect(feed_el).to_have_js_property('scrollTop', 0)

feed.scroll_to_latest(scroll_limit=100)

# assert scroll location is still at top
feed.append(Spacer(styles=dict(background='yellow'), width=200, height=200))

page.wait_for_timeout(500)

expect(feed_el.locator('div')).to_have_count(5)

Check failure on line 121 in panel/tests/ui/layout/test_feed.py

View workflow job for this annotation

GitHub Actions / ui:test-ui:ubuntu-latest

test_feed_scroll_to_latest_within_limit AssertionError: Locator expected to have count '5' Actual value: 4 Call log: LocatorAssertions.to_have_count with timeout 5000ms - waiting for locator(".bk-panel-models-feed-Feed").locator("div") - locator resolved to 4 elements - unexpected value "4" - locator resolved to 4 elements - unexpected value "4" - locator resolved to 4 elements - unexpected value "4" - locator resolved to 4 elements - unexpected value "4" - locator resolved to 4 elements - unexpected value "4" - locator resolved to 4 elements - unexpected value "4" - locator resolved to 4 elements - unexpected value "4" - locator resolved to 4 elements - unexpected value "4" - locator resolved to 4 elements - unexpected value "4"
expect(feed_el).to_have_js_property('scrollTop', 0)

# scroll to close to bottom
feed_el.evaluate('(el) => el.scrollTo({top: el.scrollHeight})')

# assert auto scroll works; i.e. distance from bottom is 0
feed.append(Spacer(styles=dict(background='yellow'), width=200, height=200))

feed.scroll_to_latest(scroll_limit=100)

wait_until(lambda: feed_el.evaluate(

Check failure on line 132 in panel/tests/ui/layout/test_feed.py

View workflow job for this annotation

GitHub Actions / ui:test-ui:ubuntu-latest

test_feed_scroll_to_latest_within_limit TimeoutError: wait_until timed out in 5000 milliseconds

Check failure on line 132 in panel/tests/ui/layout/test_feed.py

View workflow job for this annotation

GitHub Actions / ui:test-ui:ubuntu-latest

test_feed_scroll_to_latest_within_limit TimeoutError: wait_until timed out in 5000 milliseconds
'(el) => el.scrollHeight - el.scrollTop - el.clientHeight'
) == 0, page)


def test_feed_view_scroll_button(page):
feed = Feed(*list(range(ITEMS)), height=250, scroll_button_threshold=50)
serve_component(page, feed)
Expand Down
16 changes: 16 additions & 0 deletions panel/tests/ui/widgets/test_jsoneditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,19 @@ def test_json_editor_edit(page):
page.locator('.jsoneditor').click()

wait_until(lambda: editor.value['str'] == 'new', page)

def test_json_editor_edit_in_text_mode(page):
editor = JSONEditor(value={'str': 'string', 'int': 1}, mode='text')

msgs, _ = serve_component(page, editor)

expect(page.locator('.jsoneditor')).to_have_count(1)

page.locator('.jsoneditor-text').click()
ctrl_key = 'Meta' if sys.platform == 'darwin' else 'Control'
page.keyboard.press(f'{ctrl_key}+A')
page.keyboard.press('Backspace')
page.keyboard.type('{"str": "new"}')
page.locator('.jsoneditor').click()

wait_until(lambda: editor.value['str'] == 'new', page)

0 comments on commit 1d1b206

Please sign in to comment.