shinychat 0.1.0__py3-none-any.whl → 0.2.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,449 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import warnings
6
+ from typing import TYPE_CHECKING, Any, Literal, Optional, Sequence, Union
7
+
8
+ from htmltools import (
9
+ HTML,
10
+ MetadataNode,
11
+ RenderedHTML,
12
+ ReprHtml,
13
+ Tag,
14
+ Tagifiable,
15
+ TagList,
16
+ )
17
+ from packaging import version
18
+ from pydantic import BaseModel, field_serializer, field_validator
19
+ from typing_extensions import TypeAliasType
20
+
21
+ from ._typing_extensions import TypeGuard
22
+
23
+ if TYPE_CHECKING:
24
+ from chatlas.types import ContentToolRequest, ContentToolResult
25
+
26
+ __all__ = [
27
+ "ToolResultDisplay",
28
+ ]
29
+
30
+ # A version of the (recursive) TagChild type that actually works with Pydantic
31
+ # https://docs.pydantic.dev/2.11/concepts/types/#named-type-aliases
32
+ TagNode = Union[Tagifiable, MetadataNode, ReprHtml, str, HTML]
33
+ TagChild = TypeAliasType(
34
+ "TagChild",
35
+ "Union[TagNode, TagList, float, None, Sequence[TagChild]]",
36
+ )
37
+
38
+
39
+ class ToolCardComponent(BaseModel):
40
+ "A class that mirrors the ShinyToolCard component class in chat-tools.ts"
41
+
42
+ request_id: str
43
+ """
44
+ Unique identifier for the tool request or result.
45
+ This value links a request to a result and is therefore not unique on the page.
46
+ """
47
+
48
+ tool_name: str
49
+ "Name of the tool being executed, e.g. `get_weather`."
50
+
51
+ tool_title: Optional[str] = None
52
+ "Display title for the card. If not provided, falls back to `tool_name`."
53
+
54
+ icon: TagChild = None
55
+ "HTML content for the icon displayed in the card header."
56
+
57
+ intent: Optional[str] = None
58
+ "Optional intent description explaining the purpose of the tool execution."
59
+
60
+ expanded: bool = False
61
+ "Controls whether the card content is expanded/visible."
62
+
63
+ model_config = {"arbitrary_types_allowed": True}
64
+
65
+ @field_serializer("icon")
66
+ def _serialize_icon(self, value: TagChild):
67
+ return TagList(value).render()
68
+
69
+ @field_validator("icon", mode="before")
70
+ @classmethod
71
+ def _validate_icon(cls, value: TagChild) -> TagChild:
72
+ if isinstance(value, dict):
73
+ return restore_rendered_html(value)
74
+ else:
75
+ return value
76
+
77
+
78
+ class ToolRequestComponent(ToolCardComponent):
79
+ "A class that mirrors the ShinyToolRequest component class from chat-tools.ts"
80
+
81
+ arguments: str = ""
82
+ "The function arguments as requested by the LLM, typically in JSON format."
83
+
84
+ def tagify(self):
85
+ icon_ui = TagList(self.icon).render()
86
+
87
+ return Tag(
88
+ "shiny-tool-request",
89
+ request_id=self.request_id,
90
+ tool_name=self.tool_name,
91
+ tool_title=self.tool_title,
92
+ icon=icon_ui["html"] if self.icon else None,
93
+ intent=self.intent,
94
+ expanded="" if self.expanded else None,
95
+ arguments=self.arguments,
96
+ *icon_ui["dependencies"],
97
+ )
98
+
99
+
100
+ ValueType = Literal["html", "markdown", "text", "code"]
101
+
102
+
103
+ class ToolResultComponent(ToolCardComponent):
104
+ "A class that mirrors the ShinyToolResult component class from chat-tools.ts"
105
+
106
+ request_call: str = ""
107
+ "The original tool call that generated this result. Used to display the tool invocation."
108
+
109
+ status: Literal["success", "error"] = "success"
110
+ """
111
+ The status of the tool execution. When set to "error", displays in an error state with
112
+ red text and an exclamation icon.
113
+ """
114
+
115
+ show_request: bool = True
116
+ "Should the tool request should be displayed alongside the result?"
117
+
118
+ value: TagChild = None
119
+ "The actual result content returned by the tool execution."
120
+
121
+ value_type: ValueType = "code"
122
+ """
123
+ Specifies how the value should be rendered. Supported types:
124
+ - "html": Renders the value as raw HTML
125
+ - "text": Renders the value as plain text in a paragraph
126
+ - "markdown": Renders the value as Markdown (default)
127
+ - "code": Renders the value as a code block
128
+ Any other value defaults to markdown rendering.
129
+ """
130
+
131
+ def tagify(self):
132
+ icon_ui = TagList(self.icon).render()
133
+
134
+ if self.value_type == "html":
135
+ value_ui = TagList(self.value).render()
136
+ else:
137
+ value_ui: "RenderedHTML" = {
138
+ "html": str(self.value),
139
+ "dependencies": [],
140
+ }
141
+
142
+ return Tag(
143
+ "shiny-tool-result",
144
+ request_id=self.request_id,
145
+ tool_name=self.tool_name,
146
+ tool_title=self.tool_title,
147
+ icon=icon_ui["html"] if self.icon else None,
148
+ intent=self.intent,
149
+ request_call=self.request_call,
150
+ status=self.status,
151
+ value=value_ui["html"],
152
+ value_type=self.value_type,
153
+ show_request="" if self.show_request else None,
154
+ expanded="" if self.expanded else None,
155
+ *icon_ui["dependencies"],
156
+ *value_ui["dependencies"],
157
+ )
158
+
159
+
160
+ class ToolResultDisplay(BaseModel):
161
+ """
162
+ Customize how tool results are displayed.
163
+
164
+ Assign a `ToolResultDisplay` instance to a
165
+ [`chatlas.ContentToolResult`](https://posit-dev.github.io/chatlas/reference/types.ContentToolResult.html)
166
+ to customize the UI shown to the user when tool calls occur.
167
+
168
+ Examples
169
+ --------
170
+
171
+ ```python
172
+ import chatlas as ctl
173
+ from shinychat.types import ToolResultDisplay
174
+
175
+
176
+ def my_tool():
177
+ display = ToolResultDisplay(
178
+ title="Tool result title",
179
+ markdown="A _markdown_ message shown to user.",
180
+ )
181
+ return ctl.ContentToolResult(
182
+ value="Value the model sees",
183
+ extra={"display": display},
184
+ )
185
+
186
+
187
+ chat_client = ctl.ChatAuto()
188
+ chat_client.register_tool(my_tool)
189
+ ```
190
+
191
+ Parameters
192
+ ---------
193
+ title
194
+ The title to display in the header of the tool result.
195
+ icon
196
+ An icon to display in the header (alongside the title).
197
+ show_request
198
+ Whether to show the tool request inside the tool result container.
199
+ open
200
+ Whether or not the tool result details are expanded by default.
201
+ html
202
+ Custom HTML content (to use in place of the default result display).
203
+ markdown
204
+ Custom Markdown string (to use in place of the default result display).
205
+ text
206
+ Custom plain text string (to use in place of the default result display).
207
+ """
208
+
209
+ title: Optional[str] = None
210
+ icon: TagChild = None
211
+ html: TagChild = None
212
+ show_request: bool = True
213
+ open: bool = False
214
+ markdown: Optional[str] = None
215
+ text: Optional[str] = None
216
+
217
+ model_config = {"arbitrary_types_allowed": True}
218
+
219
+ @field_serializer("html", "icon")
220
+ def _serialize_html_icon(self, value: TagChild):
221
+ return TagList(value).render()
222
+
223
+ @field_validator("html", "icon", mode="before")
224
+ @classmethod
225
+ def _validate_html_icon(cls, value: TagChild) -> TagChild:
226
+ if isinstance(value, dict):
227
+ return restore_rendered_html(value)
228
+ else:
229
+ return value
230
+
231
+
232
+ def tool_request_contents(x: "ContentToolRequest") -> Tagifiable:
233
+ if tool_display_override() == "none":
234
+ return TagList()
235
+
236
+ # These content objects do have tagify() methods,
237
+ # but that's for legacy behavior
238
+ if is_legacy():
239
+ return x
240
+
241
+ intent = None
242
+ if isinstance(x.arguments, dict):
243
+ intent = x.arguments.get("_intent")
244
+
245
+ tool_title = None
246
+ if x.tool and x.tool.annotations:
247
+ tool_title = x.tool.annotations.get("title")
248
+
249
+ return ToolRequestComponent(
250
+ request_id=x.id,
251
+ tool_name=x.name,
252
+ arguments=json.dumps(x.arguments),
253
+ intent=intent,
254
+ tool_title=tool_title,
255
+ )
256
+
257
+
258
+ def tool_result_contents(x: "ContentToolResult") -> Tagifiable:
259
+ if tool_display_override() == "none":
260
+ return TagList()
261
+
262
+ # These content objects do have tagify() methods,
263
+ # but that's the legacy behavior
264
+ if is_legacy():
265
+ return x
266
+
267
+ if x.request is None:
268
+ raise ValueError(
269
+ "`ContentToolResult` objects must have an associated `.request` attribute."
270
+ )
271
+
272
+ # TODO: look into better formating of the call?
273
+ request_call = json.dumps(
274
+ {
275
+ "id": x.id,
276
+ "name": x.request.name,
277
+ "arguments": x.request.arguments,
278
+ },
279
+ indent=2,
280
+ )
281
+
282
+ display = get_tool_result_display(x, x.request)
283
+ value, value_type = tool_result_display(x, display)
284
+
285
+ intent = None
286
+ if isinstance(x.arguments, dict):
287
+ intent = x.arguments.get("_intent")
288
+
289
+ tool = x.request.tool
290
+ tool_title = None
291
+ icon = None
292
+ if tool and tool.annotations:
293
+ tool_title = tool.annotations.get("title")
294
+ icon = tool.annotations.get("extra", {}).get("icon")
295
+ icon = icon or tool.annotations.get("icon")
296
+
297
+ # Icon strings and HTML display never get escaped
298
+ icon = display.icon or icon
299
+ if icon and isinstance(icon, str):
300
+ icon = HTML(icon)
301
+ if value_type == "html" and isinstance(value, str):
302
+ value = HTML(value)
303
+
304
+ # display (tool *result* level) takes precedence over
305
+ # annotations (tool *definition* level)
306
+ return ToolResultComponent(
307
+ request_id=x.id,
308
+ request_call=request_call,
309
+ tool_name=x.request.name,
310
+ tool_title=display.title or tool_title,
311
+ status="success" if x.error is None else "error",
312
+ value=value,
313
+ value_type=value_type,
314
+ icon=icon,
315
+ intent=intent,
316
+ show_request=display.show_request,
317
+ expanded=display.open,
318
+ )
319
+
320
+
321
+ def get_tool_result_display(
322
+ x: "ContentToolResult",
323
+ request: "ContentToolRequest",
324
+ ) -> ToolResultDisplay:
325
+ if not isinstance(x.extra, dict) or tool_display_override() == "basic":
326
+ return ToolResultDisplay()
327
+
328
+ display = x.extra.get("display", ToolResultDisplay())
329
+
330
+ if isinstance(display, ToolResultDisplay):
331
+ return display
332
+
333
+ if isinstance(display, dict):
334
+ return ToolResultDisplay(**display)
335
+
336
+ warnings.warn(
337
+ "Invalid `display` value inside `ContentToolResult(extra={'display': display})` "
338
+ f"from {request.name} (call id: {request.id}). "
339
+ "Expected either a `shinychat.ToolResultDisplay()` instance or a dictionary, "
340
+ f"but got {type(display)}."
341
+ )
342
+
343
+ return ToolResultDisplay()
344
+
345
+
346
+ def tool_result_display(
347
+ x: "ContentToolResult",
348
+ display: ToolResultDisplay,
349
+ ) -> tuple[TagChild, ValueType]:
350
+ if x.error is not None:
351
+ return str(x.error), "code"
352
+
353
+ if tool_display_override() == "basic":
354
+ return str(x.get_model_value()), "code"
355
+
356
+ if display.html is not None:
357
+ return display.html, "html"
358
+
359
+ if display.markdown is not None:
360
+ return display.markdown, "markdown"
361
+
362
+ if display.text is not None:
363
+ return display.text, "text"
364
+
365
+ return str(x.get_model_value()), "code"
366
+
367
+
368
+ async def hide_corresponding_request(x: "ContentToolResult"):
369
+ if x.request is None:
370
+ return
371
+
372
+ session = None
373
+ try:
374
+ from shiny.session import get_current_session
375
+
376
+ session = get_current_session()
377
+ except Exception:
378
+ return
379
+
380
+ if session is None:
381
+ return
382
+
383
+ await session.send_custom_message(
384
+ "shiny-tool-request-hide",
385
+ x.request.id, # type: ignore
386
+ )
387
+
388
+
389
+ def is_tool_result(val: object) -> "TypeGuard[ContentToolResult]":
390
+ try:
391
+ from chatlas.types import ContentToolResult
392
+
393
+ return isinstance(val, ContentToolResult)
394
+ except ImportError:
395
+ return False
396
+
397
+
398
+ # Tools started getting added to ContentToolRequest staring with 0.11.1
399
+ def is_legacy():
400
+ import chatlas
401
+
402
+ v = chatlas._version.version_tuple
403
+ ver = f"{v[0]}.{v[1]}.{v[2]}"
404
+ return version.parse(ver) < version.parse("0.11.1")
405
+
406
+
407
+ def tool_display_override() -> Literal["none", "basic", "rich"]:
408
+ val = os.getenv("SHINYCHAT_TOOL_DISPLAY", "rich")
409
+ if val == "rich" or val == "basic" or val == "none":
410
+ return val
411
+ else:
412
+ raise ValueError(
413
+ 'The `SHINYCHAT_TOOL_DISPLAY` env var must be one of: "none", "basic", or "rich"'
414
+ )
415
+
416
+
417
+ def restore_rendered_html(x: dict[str, Any]):
418
+ from htmltools import HTMLDependency
419
+
420
+ if "html" not in x or "dependencies" not in x:
421
+ raise ValueError(f"Don't know how to restore HTML from {x}")
422
+
423
+ deps: list[HTMLDependency] = []
424
+ for d in x["dependencies"]:
425
+ if not isinstance(d, dict):
426
+ continue
427
+ name = d["name"]
428
+ version = d["version"]
429
+ other = {k: v for k, v in d.items() if k not in ("name", "version")}
430
+ # TODO: warn if the source is a tempdir?
431
+ deps.append(HTMLDependency(name=name, version=version, **other))
432
+
433
+ res = TagList(HTML(x["html"]), *deps)
434
+ if not deps:
435
+ return res
436
+
437
+ session = None
438
+ try:
439
+ from shiny.session import get_current_session
440
+
441
+ session = get_current_session()
442
+ except Exception:
443
+ pass
444
+
445
+ # De-dupe dependencies for the current Shiny session
446
+ if session:
447
+ session._process_ui(res)
448
+
449
+ return res
@@ -4,7 +4,9 @@ from typing import TYPE_CHECKING, Literal, Union
4
4
  from ._chat_types import ChatMessageDict
5
5
 
6
6
  if TYPE_CHECKING:
7
- from anthropic.types import MessageParam as AnthropicMessage
7
+ from anthropic.types import ( # pyright: ignore[reportMissingImports]
8
+ MessageParam as AnthropicMessage,
9
+ )
8
10
  from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
9
11
  from ollama import Message as OllamaMessage
10
12
  from openai.types.chat import (
@@ -28,7 +30,11 @@ if TYPE_CHECKING:
28
30
  ]
29
31
 
30
32
  ProviderMessage = Union[
31
- AnthropicMessage, GoogleMessage, LangChainMessage, OpenAIMessage, OllamaMessage
33
+ AnthropicMessage,
34
+ GoogleMessage,
35
+ LangChainMessage,
36
+ OpenAIMessage,
37
+ OllamaMessage,
32
38
  ]
33
39
  else:
34
40
  AnthropicMessage = GoogleMessage = LangChainMessage = OpenAIMessage = (
@@ -63,7 +69,9 @@ def as_provider_message(
63
69
 
64
70
 
65
71
  def as_anthropic_message(message: ChatMessageDict) -> "AnthropicMessage":
66
- from anthropic.types import MessageParam as AnthropicMessage
72
+ from anthropic.types import ( # pyright: ignore[reportMissingImports]
73
+ MessageParam as AnthropicMessage,
74
+ )
67
75
 
68
76
  if message["role"] == "system":
69
77
  raise ValueError(
shinychat/_chat_types.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import Literal, TypedDict
5
5
 
6
- from htmltools import HTML, TagChild
6
+ from htmltools import HTML, Tag, TagChild, Tagifiable, TagList
7
7
  from shiny.session import require_active_session
8
8
 
9
9
  from ._typing_extensions import NotRequired
@@ -22,10 +22,12 @@ class ChatMessage:
22
22
  def __init__(
23
23
  self,
24
24
  content: TagChild,
25
- role: Role,
25
+ role: Role = "assistant",
26
26
  ):
27
27
  self.role: Role = role
28
28
 
29
+ is_html = isinstance(content, (Tag, TagList, HTML, Tagifiable))
30
+
29
31
  # content _can_ be a TagChild, but it's most likely just a string (of
30
32
  # markdown), so only process it if it's not a string.
31
33
  deps = []
@@ -35,6 +37,11 @@ class ChatMessage:
35
37
  content = res["html"]
36
38
  deps = res["deps"]
37
39
 
40
+ if is_html:
41
+ # Code blocks with `{=html}` infostrings are rendered as-is by a
42
+ # custom rendering method in markdown-stream.ts
43
+ content = f"\n\n````````{{=html}}\n{content}\n````````\n\n"
44
+
38
45
  self.content = content
39
46
  self.html_deps = deps
40
47
 
@@ -92,9 +92,9 @@ class MarkdownStream:
92
92
  async def _mock_task() -> str:
93
93
  return ""
94
94
 
95
- self._latest_stream: reactive.Value[reactive.ExtendedTask[[], str]] = (
96
- reactive.Value(_mock_task)
97
- )
95
+ self._latest_stream: reactive.Value[
96
+ reactive.ExtendedTask[[], str]
97
+ ] = reactive.Value(_mock_task)
98
98
 
99
99
  async def stream(
100
100
  self,
@@ -149,7 +149,9 @@ class MarkdownStream:
149
149
  ui = self._session._process_ui(x)
150
150
 
151
151
  result += ui["html"]
152
- await self._send_content_message(ui["html"], "append", ui["deps"])
152
+ await self._send_content_message(
153
+ ui["html"], "append", ui["deps"]
154
+ )
153
155
 
154
156
  return result
155
157
 
@@ -249,7 +251,9 @@ class MarkdownStream:
249
251
  ):
250
252
  if self._session.is_stub_session():
251
253
  return
252
- await self._session.send_custom_message("shinyMarkdownStreamMessage", {**msg})
254
+ await self._session.send_custom_message(
255
+ "shinyMarkdownStreamMessage", {**msg}
256
+ )
253
257
 
254
258
  async def _raise_exception(self, e: BaseException):
255
259
  if self.on_error == "unhandled":
@@ -1,11 +1,50 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Literal
3
+ from typing import Literal, Pattern, Union
4
4
 
5
5
  from playwright.sync_api import Locator, Page
6
6
  from playwright.sync_api import expect as playwright_expect
7
- from shiny.playwright._types import PatternOrStr, Timeout
8
- from shiny.playwright.controller._base import UiBase
7
+
8
+ PatternStr = Pattern[str]
9
+ PatternOrStr = Union[str, PatternStr]
10
+ Timeout = Union[float, None]
11
+
12
+
13
+ # Avoid circular import with shiny.playwright by copy/pasting UiBase class
14
+ class UiBase:
15
+ """A base class representing shiny UI components."""
16
+
17
+ id: str
18
+ """
19
+ The browser DOM `id` of the UI element.
20
+ """
21
+ loc: Locator
22
+ """
23
+ Playwright `Locator` of the UI element.
24
+ """
25
+ page: Page
26
+ """
27
+ Playwright `Page` of the Shiny app.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ page: Page,
33
+ *,
34
+ id: str,
35
+ loc: Locator | str,
36
+ ) -> None:
37
+ self.page = page
38
+ # Needed?!? This is covered by `self.loc_root` and possibly `self.loc`
39
+ self.id = id
40
+ if isinstance(loc, str):
41
+ loc = page.locator(loc)
42
+ self.loc = loc
43
+
44
+ @property
45
+ def expect(self):
46
+ """Expectation method equivalent to `playwright.expect(self.loc)`."""
47
+ return playwright_expect(self.loc)
9
48
 
10
49
 
11
50
  class Chat(UiBase):
@@ -1,5 +1,10 @@
1
- from .._chat import ChatMessageDict
1
+ from .._chat import ChatMessage, ChatMessageDict
2
+ from .._chat_normalize_chatlas import ToolResultDisplay
3
+
4
+ ToolResultDisplay.model_rebuild()
2
5
 
3
6
  __all__ = [
7
+ "ChatMessage",
4
8
  "ChatMessageDict",
9
+ "ToolResultDisplay",
5
10
  ]
shinychat/www/GIT_VERSION CHANGED
@@ -1 +1 @@
1
- 084033e8198070adf6ea80ce826fa1b620900658
1
+ f77d543efedf2425f0859d204779218de3cafcf3
@@ -1,2 +1,2 @@
1
- @charset "UTF-8";shiny-chat-container{--shiny-chat-border: var(--bs-border-width, 1px) solid var(--bs-border-color, #e9ecef);--shiny-chat-user-message-bg: RGBA(var(--bs-primary-rgb, 0, 123, 194), .06);--_chat-container-padding: .25rem;display:grid;grid-template-columns:1fr;grid-template-rows:1fr auto;margin:0 auto;gap:0;padding:var(--_chat-container-padding);padding-bottom:0}shiny-chat-container p:last-child{margin-bottom:0}shiny-chat-container .suggestion,shiny-chat-container [data-suggestion]{cursor:pointer}shiny-chat-container .suggestion{color:var(--bs-link-color, #007bc2);text-decoration-color:var(--bs-link-color, #007bc2);text-decoration-line:underline;text-decoration-style:dotted;text-underline-offset:2px;text-underline-offset:4px;text-decoration-thickness:2px;padding-inline:2px}shiny-chat-container .suggestion:hover{text-decoration-style:solid}shiny-chat-container .suggestion:after{content:"\2726";display:inline-block;margin-inline-start:.15em}shiny-chat-container .suggestion.submit:after,shiny-chat-container .suggestion[data-suggestion-submit=""]:after,shiny-chat-container .suggestion[data-suggestion-submit=true]:after{content:"\21b5"}shiny-chat-container .card[data-suggestion]:hover{color:var(--bs-link-color, #007bc2);border-color:rgba(var(--bs-link-color-rgb),.5)}shiny-chat-messages{display:flex;flex-direction:column;gap:2rem;overflow:auto;margin-bottom:1rem;--_scroll-margin: 1rem;padding-right:var(--_scroll-margin);margin-right:calc(-1 * var(--_scroll-margin))}shiny-chat-message{display:grid;grid-template-columns:auto minmax(0,1fr);gap:1rem}shiny-chat-message>*{height:fit-content}shiny-chat-message .message-icon{border-radius:50%;border:var(--shiny-chat-border);height:2rem;width:2rem;display:grid;place-items:center;overflow:clip}shiny-chat-message .message-icon>*{height:100%;width:100%;max-width:100%;max-height:100%;margin:0!important;object-fit:contain}shiny-chat-message .message-icon>svg,shiny-chat-message .message-icon>.icon,shiny-chat-message .message-icon>.fa,shiny-chat-message .message-icon>.bi{max-height:66%;max-width:66%}shiny-chat-message .message-icon:has(>.border-0){border:none;border-radius:unset;overflow:unset}shiny-chat-message shiny-markdown-stream{align-self:center}shiny-user-message{align-self:flex-end;padding:.75rem 1rem;border-radius:10px;background-color:var(--shiny-chat-user-message-bg);max-width:100%}shiny-user-message[content_type=text],shiny-chat-message[content_type=text]{white-space:pre;overflow-x:auto}shiny-chat-input{--_input-padding-top: 0;--_input-padding-bottom: var(--_chat-container-padding, .25rem);margin-top:calc(-1 * var(--_input-padding-top));position:sticky;bottom:calc(-1 * var(--_input-padding-bottom) + 4px);padding-block:var(--_input-padding-top) var(--_input-padding-bottom)}shiny-chat-input textarea{--bs-border-radius: 26px;resize:none;padding-right:36px!important;max-height:175px}shiny-chat-input textarea::placeholder{color:var(--bs-gray-600, #707782)!important}shiny-chat-input button{position:absolute;bottom:calc(6px + var(--_input-padding-bottom));right:8px;background-color:transparent;color:var(--bs-primary, #007bc2);transition:color .25s ease-in-out;border:none;padding:0;cursor:pointer;line-height:16px;border-radius:50%}shiny-chat-input button:disabled{cursor:not-allowed;color:var(--bs-gray-500, #8d959e)}.shiny-busy:has(shiny-chat-input[disabled]):after{display:none}
1
+ @charset "UTF-8";.shiny-tool-card{max-height:var(--shiny-tool-card-max-height, 500px)}.shiny-tool-card .tool-title{min-width:25%;flex-shrink:2}.shiny-tool-card .tool-intent{opacity:.66;font-style:italic;font-weight:400;text-align:end;flex-shrink:3;max-width:60%;min-width:20%}.shiny-tool-card .tool-spacer{margin-inline-start:auto}.shiny-tool-card .tool-icon{--_icon-size: var(--shiny-tool-card-icon-size, 16px);width:var(--_icon-size);height:var(--_icon-size);display:flex;align-items:center;flex:none}.shiny-tool-card .tool-icon [class^=spinner]{--bs-spinner-width: var(--_icon-size);--bs-spinner-height: var(--_icon-size);--bs-spinner-border-width: .2em;color:var(--shiny-tool-card-spinner-color, var(--bs-primary, #007bc2))}.shiny-tool-card .tool-title-name{font-weight:600}.shiny-tool-card .card-header,.shiny-tool-card .card-footer{font-size:inherit;font-weight:400;word-break:break-word}.shiny-tool-card>.card-header{display:flex;flex-direction:row;align-items:center;align-self:stretch;gap:.5rem;width:100%;text-align:left;cursor:pointer;border-top-width:0;border-right-width:0;border-left-width:0}.shiny-tool-card>.card-header[aria-expanded=false]{border-bottom-width:0}.shiny-tool-card>.card-header>*{line-height:1}.shiny-tool-card>.card-header:hover,.shiny-tool-card>.card-header:focus-visible{background-color:rgba(var(--bs-emphasis-color-rgb),.05);outline:none}.shiny-tool-card>.card-header .function-name{font-weight:700}.shiny-tool-card>.card-header .collapse-indicator{display:grid;place-items:center;width:1em;height:1em;opacity:.66}.shiny-tool-card>.card-header .collapse-indicator,.shiny-tool-card>.card-header .collapse-indicator>.horizontal{transition:.3s ease-in-out all;transform-origin:center center}.shiny-tool-card>.card-header[aria-expanded=true]>.collapse-indicator{transform:rotate(-90deg)}.shiny-tool-card>.card-header[aria-expanded=true]>.collapse-indicator .horizontal{transform:scale(0)}.shiny-tool-card>.card-body{transition:max-height .3s ease-out,opacity .2s ease-out;opacity:1;overflow:auto}.shiny-tool-card>.card-body.collapsed{max-height:0;opacity:0;padding:0}.shiny-tool-card pre{margin:0;white-space:pre-wrap;padding:1em;border-radius:var(--bs-border-radius, 4px)}shiny-tool-result,shiny-tool-request{display:block;margin:1em 0;border-radius:var(--bs-border-radius, 4px);overflow:visible;padding:0;font-size:.8em}shiny-tool-result[hidden],shiny-tool-request[hidden]{display:none}shiny-tool-result+p,shiny-tool-request+p{margin-top:1rem}shiny-tool-result:first-child,shiny-tool-request:first-child{margin-top:0}shiny-tool-result:last-child,shiny-tool-request:last-child{margin-bottom:0}shiny-tool-request[hidden]+shiny-tool-result:first-of-type{margin-top:0}shiny-chat-container{--shiny-chat-border: var(--bs-border-width, 1px) solid var(--bs-border-color, #e9ecef);--shiny-chat-user-message-bg: RGBA(var(--bs-primary-rgb, 0, 123, 194), .06);--_chat-container-padding: .25rem;display:grid;grid-template-columns:1fr;grid-template-rows:1fr auto;margin:0 auto;gap:0;padding:var(--_chat-container-padding);padding-bottom:0}shiny-chat-container p:last-child{margin-bottom:0}shiny-chat-container .suggestion,shiny-chat-container [data-suggestion]{cursor:pointer}shiny-chat-container .suggestion{color:var(--bs-link-color, #007bc2);text-decoration-color:var(--bs-link-color, #007bc2);text-decoration-line:underline;text-decoration-style:dotted;text-underline-offset:2px;text-underline-offset:4px;text-decoration-thickness:2px;padding-inline:2px}shiny-chat-container .suggestion:hover{text-decoration-style:solid}shiny-chat-container .suggestion:after{content:"\2726";display:inline-block;margin-inline-start:.15em}shiny-chat-container .suggestion.submit:after,shiny-chat-container .suggestion[data-suggestion-submit=""]:after,shiny-chat-container .suggestion[data-suggestion-submit=true]:after{content:"\21b5"}shiny-chat-container .card[data-suggestion]:hover{color:var(--bs-link-color, #007bc2);border-color:rgba(var(--bs-link-color-rgb),.5)}shiny-chat-messages{display:flex;flex-direction:column;gap:2rem;overflow:auto;margin-bottom:1rem;--_scroll-margin: 1rem;padding-right:var(--_scroll-margin);margin-right:calc(-1 * var(--_scroll-margin))}shiny-chat-message{display:grid;grid-template-columns:auto minmax(0,1fr);gap:1rem}shiny-chat-message>*{height:fit-content}shiny-chat-message .message-icon{border-radius:50%;border:var(--shiny-chat-border);height:2rem;width:2rem;display:grid;place-items:center;overflow:clip}shiny-chat-message .message-icon>*{height:100%;width:100%;max-width:100%;max-height:100%;margin:0!important;object-fit:contain}shiny-chat-message .message-icon>svg,shiny-chat-message .message-icon>.icon,shiny-chat-message .message-icon>.fa,shiny-chat-message .message-icon>.bi{max-height:66%;max-width:66%}shiny-chat-message .message-icon:has(>.border-0){border:none;border-radius:unset;overflow:unset}shiny-chat-message shiny-markdown-stream{align-self:center}shiny-user-message,shiny-chat-message[data-role=user]{align-self:flex-end;padding:.75rem 1rem;border-radius:10px;background-color:var(--shiny-chat-user-message-bg);max-width:100%}shiny-chat-message[data-role=user]:not([icon]){grid-template-columns:auto}shiny-user-message[content_type=text],shiny-chat-message[content_type=text]{white-space:pre;overflow-x:auto}shiny-chat-input{--_input-padding-top: 0;--_input-padding-bottom: var(--_chat-container-padding, .25rem);margin-top:calc(-1 * var(--_input-padding-top));position:sticky;bottom:calc(-1 * var(--_input-padding-bottom) + 4px);padding-block:var(--_input-padding-top) var(--_input-padding-bottom)}shiny-chat-input textarea{--bs-border-radius: 26px;resize:none;padding-right:36px!important;max-height:175px}shiny-chat-input textarea::placeholder{color:var(--bs-gray-600, #707782)!important}shiny-chat-input button{position:absolute;bottom:calc(6px + var(--_input-padding-bottom));right:8px;background-color:transparent;color:var(--bs-primary, #007bc2);transition:color .25s ease-in-out;border:none;padding:0;cursor:pointer;line-height:16px;border-radius:50%}shiny-chat-input button:disabled{cursor:not-allowed;color:var(--bs-gray-500, #8d959e)}.shiny-busy:has(shiny-chat-input[disabled]):after{display:none}.shinychat-external-link-dialog{padding:0;border:none;background:none;max-width:min(680px,90%)}.shinychat-external-link-dialog::backdrop{background-color:#00000080}.shinychat-external-link-dialog .link-url{word-break:break-all;font-weight:500}
2
2
  /*# sourceMappingURL=chat.css.map */