Spaces:
Running
Running
Upload folder using huggingface_hub (part 10)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- tools/unknown.py.dox.md +3 -48
- tools/vision_load.py +3 -233
- tools/vision_load.py.dox.md +3 -62
- tools/wait.py +3 -89
- tools/wait.py.dox.md +3 -54
- update_reqs.py +3 -38
- usr/agents/.gitkeep +0 -0
- usr/knowledge/.gitkeep +0 -0
- usr/knowledge/main/.gitkeep +0 -0
- usr/knowledge/solutions/.gitkeep +0 -0
- usr/plugins/.gitkeep +0 -0
- usr/skills/.gitkeep +3 -0
- usr/workdir/.gitkeep +0 -0
- webui/AGENTS.md +3 -60
- webui/components/AGENTS.md +3 -77
- webui/components/_examples/AGENTS.md +3 -27
- webui/components/_examples/_example-component.html +3 -36
- webui/components/_examples/_example-store.js +3 -23
- webui/components/canvas/AGENTS.md +3 -33
- webui/components/canvas/right-canvas-store.js +3 -620
- webui/components/canvas/right-canvas.css +3 -384
- webui/components/canvas/right-canvas.html +3 -132
- webui/components/chat/AGENTS.md +3 -47
- webui/components/chat/attachments/attachmentsStore.js +3 -511
- webui/components/chat/attachments/dragDropOverlay.html +3 -28
- webui/components/chat/attachments/inputPreview.html +3 -30
- webui/components/chat/input/bottom-actions-bar.html +3 -98
- webui/components/chat/input/bottom-actions.html +3 -161
- webui/components/chat/input/chat-bar-input.html +3 -457
- webui/components/chat/input/chat-bar.html +3 -52
- webui/components/chat/input/composer-banner-store.js +3 -69
- webui/components/chat/input/input-store.js +3 -780
- webui/components/chat/input/progress.html +3 -57
- webui/components/chat/message-queue/message-queue-store.js +3 -245
- webui/components/chat/message-queue/message-queue.html +3 -245
- webui/components/chat/model-gate-store.js +3 -233
- webui/components/chat/model-setup-gate.html +3 -203
- webui/components/chat/navigation/chat-navigation-store.js +3 -127
- webui/components/chat/top-section/chat-top-store.js +3 -13
- webui/components/chat/top-section/chat-top.html +3 -42
- webui/components/dropdown/AGENTS.md +3 -26
- webui/components/dropdown/dropdown.html +3 -69
- webui/components/messages/AGENTS.md +3 -38
- webui/components/messages/action-buttons/simple-action-buttons.css +3 -153
- webui/components/messages/action-buttons/simple-action-buttons.js +3 -134
- webui/components/messages/process-group/process-group-dom.js +3 -76
- webui/components/messages/process-group/process-group.css +3 -914
- webui/components/messages/resize/message-resize-store.js +3 -137
- webui/components/modals/AGENTS.md +3 -34
- webui/components/modals/context/context-store.js +3 -132
tools/unknown.py.dox.md
CHANGED
|
@@ -1,48 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
- Own the `unknown.py` agent tool.
|
| 6 |
-
- This module handles unknown or malformed tool-call requests.
|
| 7 |
-
- Keep this file-level DOX profile synchronized with `unknown.py` because this directory is intentionally flat.
|
| 8 |
-
|
| 9 |
-
## Ownership
|
| 10 |
-
|
| 11 |
-
- `unknown.py` owns the runtime implementation.
|
| 12 |
-
- `unknown.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
|
| 13 |
-
- Classes:
|
| 14 |
-
- `Unknown` (`Tool`)
|
| 15 |
-
- `async execute(self, **kwargs)`
|
| 16 |
-
|
| 17 |
-
## Runtime Contracts
|
| 18 |
-
|
| 19 |
-
- Tool modules must define `helpers.tool.Tool` subclasses and return `helpers.tool.Response` from `execute(...)`.
|
| 20 |
-
- Update this file whenever tool arguments, output shape, `break_loop` behavior, intervention handling, prompt instructions, or side effects change.
|
| 21 |
-
- `Unknown` is a `Tool`.
|
| 22 |
-
- `Unknown` defines `execute(...)`.
|
| 23 |
-
- Imported dependency areas include: `extensions.python.system_prompt._11_tools_prompt`, `helpers.tool`.
|
| 24 |
-
|
| 25 |
-
## Key Concepts
|
| 26 |
-
|
| 27 |
-
- Important called helpers/classes observed in the source: `Response`, `build_tools_prompt`, `self.agent.read_prompt`.
|
| 28 |
-
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
|
| 29 |
-
|
| 30 |
-
## Work Guidance
|
| 31 |
-
|
| 32 |
-
- Keep tool output concise, model-readable, and safe for history persistence.
|
| 33 |
-
- Coordinate argument or behavior changes with prompt tool instructions and skill guidance.
|
| 34 |
-
- Respect intervention flow for long-running, external, or user-visible operations.
|
| 35 |
-
|
| 36 |
-
## Verification
|
| 37 |
-
|
| 38 |
-
- Run targeted tool and prompt-contract tests for changed behavior; smoke-test agent execution when no focused test exists.
|
| 39 |
-
- Related tests observed by source search:
|
| 40 |
-
- `tests/test_file_tree_visualize.py`
|
| 41 |
-
- `tests/test_oauth_providers.py`
|
| 42 |
-
- `tests/test_socketio_unknown_namespace.py`
|
| 43 |
-
- `tests/test_tunnel_remote_link.py`
|
| 44 |
-
- `tests/test_ws_manager.py`
|
| 45 |
-
|
| 46 |
-
## Child DOX Index
|
| 47 |
-
|
| 48 |
-
No child DOX files.
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:33edae4ca6280348d9de290a5f09cf28a9f5cfb2b3b843853693b9dca3404388
|
| 3 |
+
size 1887
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tools/vision_load.py
CHANGED
|
@@ -1,233 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
from helpers import (
|
| 6 |
-
chat_media,
|
| 7 |
-
ephemeral_images,
|
| 8 |
-
files,
|
| 9 |
-
history,
|
| 10 |
-
images,
|
| 11 |
-
parallel_tools,
|
| 12 |
-
runtime,
|
| 13 |
-
)
|
| 14 |
-
from helpers.tool import Response, Tool
|
| 15 |
-
from plugins._model_config.helpers.model_config import (
|
| 16 |
-
build_vision_model,
|
| 17 |
-
get_chat_model_config,
|
| 18 |
-
get_vision_model_config,
|
| 19 |
-
)
|
| 20 |
-
|
| 21 |
-
# image token estimation for context window
|
| 22 |
-
TOKENS_ESTIMATE = 1500
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
class VisionLoad(Tool):
|
| 26 |
-
async def execute(
|
| 27 |
-
self,
|
| 28 |
-
paths: list[str] | str = [],
|
| 29 |
-
query: str = "",
|
| 30 |
-
**kwargs,
|
| 31 |
-
) -> Response:
|
| 32 |
-
|
| 33 |
-
self.images_dict = {}
|
| 34 |
-
self.loaded_paths: list[str] = []
|
| 35 |
-
self.skipped_paths: list[str] = []
|
| 36 |
-
self.vision_config = get_vision_model_config(self.agent)
|
| 37 |
-
if isinstance(paths, str):
|
| 38 |
-
paths = [paths]
|
| 39 |
-
if not isinstance(paths, list):
|
| 40 |
-
return Response(
|
| 41 |
-
message="vision_load error: `paths` must be a string or an array.",
|
| 42 |
-
break_loop=False,
|
| 43 |
-
)
|
| 44 |
-
|
| 45 |
-
max_embeds = self._get_max_embeds()
|
| 46 |
-
requested = [
|
| 47 |
-
(str(path or "").strip(), self._display_input_path(str(path or "").strip(), idx + 1))
|
| 48 |
-
for idx, path in enumerate(paths)
|
| 49 |
-
]
|
| 50 |
-
limited_paths = requested if max_embeds <= 0 else requested[-max_embeds:]
|
| 51 |
-
self.skipped_paths = (
|
| 52 |
-
[display for _, display in requested[:-max_embeds]]
|
| 53 |
-
if max_embeds > 0 and len(requested) > max_embeds
|
| 54 |
-
else []
|
| 55 |
-
)
|
| 56 |
-
|
| 57 |
-
for idx, (path, display_path) in enumerate(limited_paths):
|
| 58 |
-
if not path:
|
| 59 |
-
continue
|
| 60 |
-
if ephemeral_images.is_ref(path):
|
| 61 |
-
image = ephemeral_images.consume_image(
|
| 62 |
-
path,
|
| 63 |
-
context_id=self._context_id(),
|
| 64 |
-
)
|
| 65 |
-
if image is None:
|
| 66 |
-
continue
|
| 67 |
-
display = image.display_name or display_path
|
| 68 |
-
stored_ref = self._store_ephemeral_image(image)
|
| 69 |
-
if stored_ref:
|
| 70 |
-
self.images_dict[display] = stored_ref
|
| 71 |
-
self.loaded_paths.append(display)
|
| 72 |
-
continue
|
| 73 |
-
if self._is_data_image_url(path):
|
| 74 |
-
stored_ref = self._store_data_url(path, preferred_name=f"vision-load-{idx + 1}.png")
|
| 75 |
-
if stored_ref:
|
| 76 |
-
self.images_dict[display_path] = stored_ref
|
| 77 |
-
self.loaded_paths.append(display_path)
|
| 78 |
-
continue
|
| 79 |
-
if not await runtime.call_development_function(files.exists, str(path)):
|
| 80 |
-
continue
|
| 81 |
-
|
| 82 |
-
if path not in self.images_dict:
|
| 83 |
-
mime_type, _ = guess_type(str(path))
|
| 84 |
-
if mime_type and mime_type.startswith("image/"):
|
| 85 |
-
try:
|
| 86 |
-
stored_ref = self._store_local_image(path, preferred_name=files.basename(path))
|
| 87 |
-
self.images_dict[display_path] = stored_ref
|
| 88 |
-
self.loaded_paths.append(display_path)
|
| 89 |
-
except (FileNotFoundError, OSError, ValueError):
|
| 90 |
-
continue
|
| 91 |
-
|
| 92 |
-
message = self._summary() if self.images_dict or self.skipped_paths else "No images processed"
|
| 93 |
-
if self.vision_config and self.images_dict:
|
| 94 |
-
try:
|
| 95 |
-
capsule = await self._call_vision_model(
|
| 96 |
-
list(self.images_dict.values()),
|
| 97 |
-
query,
|
| 98 |
-
)
|
| 99 |
-
message = (
|
| 100 |
-
f"Analyzed {len(self.images_dict)} image(s)"
|
| 101 |
-
f"; {len(self.skipped_paths)} skipped.\n\n{capsule.strip()}"
|
| 102 |
-
)
|
| 103 |
-
except Exception as exc:
|
| 104 |
-
message = f"Image analysis error: {str(exc)[:1000]}"
|
| 105 |
-
return Response(message=message, break_loop=False)
|
| 106 |
-
|
| 107 |
-
def _get_max_embeds(self) -> int:
|
| 108 |
-
cfg = self.vision_config or get_chat_model_config(self.agent)
|
| 109 |
-
return int(cfg.get("max_embeds", 10) or 0)
|
| 110 |
-
|
| 111 |
-
def _context_id(self) -> str:
|
| 112 |
-
context = getattr(self.agent, "context", None)
|
| 113 |
-
get_data = getattr(context, "get_data", None)
|
| 114 |
-
parent_id = (
|
| 115 |
-
get_data(parallel_tools.PARALLEL_WORKER_PARENT_CONTEXT_KEY)
|
| 116 |
-
if get_data
|
| 117 |
-
else ""
|
| 118 |
-
)
|
| 119 |
-
return str(parent_id or getattr(context, "id", "") or "").strip()
|
| 120 |
-
|
| 121 |
-
async def _call_vision_model(self, image_paths: list[str], query: str) -> str:
|
| 122 |
-
user_message = getattr(self.agent, "last_user_message", None)
|
| 123 |
-
output_text = getattr(user_message, "output_text", None)
|
| 124 |
-
request = str(output_text() if callable(output_text) else "").strip()
|
| 125 |
-
content = [
|
| 126 |
-
{
|
| 127 |
-
"type": "text",
|
| 128 |
-
"text": self.agent.read_prompt(
|
| 129 |
-
"fw.vision_load.md",
|
| 130 |
-
request=request,
|
| 131 |
-
query=str(query or "").strip(),
|
| 132 |
-
),
|
| 133 |
-
}
|
| 134 |
-
]
|
| 135 |
-
content.extend(
|
| 136 |
-
{"type": "image_url", "image_url": {"url": path}}
|
| 137 |
-
for path in image_paths
|
| 138 |
-
)
|
| 139 |
-
response, _ = await build_vision_model(self.agent).unified_call(
|
| 140 |
-
messages=[HumanMessage(content=content)],
|
| 141 |
-
)
|
| 142 |
-
if not str(response or "").strip():
|
| 143 |
-
raise RuntimeError("Vision Model returned an empty response.")
|
| 144 |
-
return str(response)
|
| 145 |
-
|
| 146 |
-
def _store_ephemeral_image(self, image: ephemeral_images.EphemeralImage) -> str:
|
| 147 |
-
context_id = self._context_id()
|
| 148 |
-
if not context_id:
|
| 149 |
-
return image.data_url
|
| 150 |
-
source = chat_media.infer_source(image.ref, image.display_name)
|
| 151 |
-
category = chat_media.category_for_source(source)
|
| 152 |
-
saved = chat_media.save_image_base64(
|
| 153 |
-
context_id=context_id,
|
| 154 |
-
data=image.data,
|
| 155 |
-
mime_type=image.mime,
|
| 156 |
-
category=category,
|
| 157 |
-
source=source,
|
| 158 |
-
preferred_name=image.display_name,
|
| 159 |
-
)
|
| 160 |
-
return saved.a0_path
|
| 161 |
-
|
| 162 |
-
def _store_data_url(self, data_url: str, *, preferred_name: str = "") -> str:
|
| 163 |
-
context_id = self._context_id()
|
| 164 |
-
if not context_id:
|
| 165 |
-
return data_url
|
| 166 |
-
source = chat_media.infer_source(data_url, preferred_name)
|
| 167 |
-
category = chat_media.category_for_source(source)
|
| 168 |
-
saved = chat_media.save_image_data_url(
|
| 169 |
-
context_id=context_id,
|
| 170 |
-
data_url=data_url,
|
| 171 |
-
category=category,
|
| 172 |
-
source=source,
|
| 173 |
-
preferred_name=preferred_name,
|
| 174 |
-
)
|
| 175 |
-
return saved.a0_path
|
| 176 |
-
|
| 177 |
-
def _store_local_image(self, path: str, *, preferred_name: str = "") -> str:
|
| 178 |
-
context_id = self._context_id()
|
| 179 |
-
if not context_id:
|
| 180 |
-
return images.to_data_url(path)
|
| 181 |
-
return chat_media.materialize_image_ref(
|
| 182 |
-
context_id=context_id,
|
| 183 |
-
url=path,
|
| 184 |
-
source=chat_media.infer_source(path, preferred_name),
|
| 185 |
-
preferred_name=preferred_name,
|
| 186 |
-
)
|
| 187 |
-
|
| 188 |
-
def _summary(self) -> str:
|
| 189 |
-
loaded = "\n".join(self.loaded_paths) if self.loaded_paths else "none"
|
| 190 |
-
summary = f"Loaded images ({len(self.loaded_paths)}):\n{loaded}"
|
| 191 |
-
if self.skipped_paths:
|
| 192 |
-
summary += (
|
| 193 |
-
f"\n\nSkipped images ({len(self.skipped_paths)}, max {self._get_max_embeds()}):\n"
|
| 194 |
-
+ "\n".join(self.skipped_paths)
|
| 195 |
-
)
|
| 196 |
-
return summary
|
| 197 |
-
|
| 198 |
-
@staticmethod
|
| 199 |
-
def _is_data_image_url(value: str) -> bool:
|
| 200 |
-
normalized = str(value or "").strip().lower()
|
| 201 |
-
return normalized.startswith("data:image/") and ";base64," in normalized
|
| 202 |
-
|
| 203 |
-
@classmethod
|
| 204 |
-
def _display_input_path(cls, value: str, index: int) -> str:
|
| 205 |
-
if ephemeral_images.is_ref(value):
|
| 206 |
-
return ephemeral_images.display_ref(value)
|
| 207 |
-
if cls._is_data_image_url(value):
|
| 208 |
-
prefix = value.split(",", 1)[0]
|
| 209 |
-
return f"{prefix},<ephemeral-image-{index}>"
|
| 210 |
-
return value
|
| 211 |
-
|
| 212 |
-
async def after_execution(self, response: Response, **kwargs):
|
| 213 |
-
await super().after_execution(response, **kwargs)
|
| 214 |
-
if self.images_dict and not self.vision_config:
|
| 215 |
-
content = [
|
| 216 |
-
{"type": "image_url", "image_url": {"url": image_path}}
|
| 217 |
-
for image_path in self.images_dict.values()
|
| 218 |
-
]
|
| 219 |
-
raw_message = history.RawMessage(
|
| 220 |
-
raw_content=content,
|
| 221 |
-
preview="<Image attachments loaded by path>",
|
| 222 |
-
)
|
| 223 |
-
tokens = TOKENS_ESTIMATE * len(content)
|
| 224 |
-
if not parallel_tools.queue_parallel_parent_history(
|
| 225 |
-
self.agent,
|
| 226 |
-
content=raw_message,
|
| 227 |
-
tokens=tokens,
|
| 228 |
-
):
|
| 229 |
-
self.agent.hist_add_message(
|
| 230 |
-
False,
|
| 231 |
-
content=raw_message,
|
| 232 |
-
tokens=tokens,
|
| 233 |
-
)
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c32e18ed5acc62ee2a24ec59a24c8ad8f55013d4f47b2aaacf2d968e1b835267
|
| 3 |
+
size 8861
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tools/vision_load.py.dox.md
CHANGED
|
@@ -1,62 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
- Own the `vision_load.py` agent tool.
|
| 6 |
-
- This module routes images either into Main model-visible content or through the preset's optional Vision Model.
|
| 7 |
-
- Keep this file-level DOX profile synchronized with `vision_load.py` because this directory is intentionally flat.
|
| 8 |
-
|
| 9 |
-
## Ownership
|
| 10 |
-
|
| 11 |
-
- `vision_load.py` owns the runtime implementation.
|
| 12 |
-
- `vision_load.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
|
| 13 |
-
- Classes:
|
| 14 |
-
- `VisionLoad` (`Tool`)
|
| 15 |
-
- `async execute(self, paths, query="", **kwargs) -> Response`
|
| 16 |
-
- `async after_execution(self, response: Response, **kwargs)`
|
| 17 |
-
- Notable constants/configuration names: `TOKENS_ESTIMATE`.
|
| 18 |
-
|
| 19 |
-
## Runtime Contracts
|
| 20 |
-
|
| 21 |
-
- Tool modules must define `helpers.tool.Tool` subclasses and return `helpers.tool.Response` from `execute(...)`.
|
| 22 |
-
- One call may contain multiple paths; a bare string is treated as one path. The Vision Model route sends every selected path in one request and returns one textual capsule.
|
| 23 |
-
- Model configuration exposes a Vision Model only when the effective preset selects that route; otherwise this tool follows Main's native vision path.
|
| 24 |
-
- The public tool contract is route-agnostic and accepts an optional focused `query`. A Vision Model receives both that query and the current user request through `fw.vision_load.md`; direct parallel workers inherit the request from their parent.
|
| 25 |
-
- Native Main vision already retains the query in its authored tool-call transcript, so native raw history remains image-only and does not repeat model-authored instructions as user content.
|
| 26 |
-
- Delegation completes during `execute(...)` so native Responses function output contains the real capsule before `after_execution(...)` persists it.
|
| 27 |
-
- Delegated history contains the text capsule only. Native history contains the tool result followed by one raw message holding all loaded image blocks.
|
| 28 |
-
- In a direct parallel worker, native image content is queued for the parent and promoted immediately after the outer `parallel` result; the disposable worker never owns the only copy of model-visible pixels.
|
| 29 |
-
- Direct parallel workers inherit the parent's model override generically. This tool uses their recorded parent context only to resolve ephemeral refs and durable chat media.
|
| 30 |
-
- `max_embeds` comes from the model that actually receives the images.
|
| 31 |
-
- Native tool-result summaries omit the skipped-images section when no images were skipped; when the limit drops inputs, the section reports the skipped paths and active maximum.
|
| 32 |
-
- Vision Model calls use the selected model's Advanced `kwargs`; this tool does not impose a separate timeout or output-token limit.
|
| 33 |
-
- An empty Vision Model response is reported as an image-analysis error instead of a successful empty capsule.
|
| 34 |
-
- Update this file whenever tool arguments, output shape, `break_loop` behavior, intervention handling, prompt instructions, or side effects change.
|
| 35 |
-
- `VisionLoad` is a `Tool`.
|
| 36 |
-
- `VisionLoad` defines `execute(...)`.
|
| 37 |
-
- Observed side-effect areas: filesystem writes, model calls, plugin state, settings/state persistence, secret handling.
|
| 38 |
-
- Imported dependency areas include: `helpers`, `helpers.tool`, `langchain_core.messages`, `mimetypes`, and `_model_config`.
|
| 39 |
-
|
| 40 |
-
## Key Concepts
|
| 41 |
-
|
| 42 |
-
- Important called helpers/classes observed in the source: `build_vision_model`, `get_vision_model_config`, `self._get_max_embeds`, `Response`, `self._context_id`, `chat_media.save_image_base64`, `chat_media.save_image_data_url`, `chat_media.materialize_image_ref`, `ephemeral_images.consume_image`, `images.to_data_url`, `history.RawMessage`, `super().after_execution`, `model.unified_call`.
|
| 43 |
-
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
|
| 44 |
-
|
| 45 |
-
## Work Guidance
|
| 46 |
-
|
| 47 |
-
- Keep tool output concise, model-readable, and safe for history persistence.
|
| 48 |
-
- Coordinate argument or behavior changes with prompt tool instructions and skill guidance.
|
| 49 |
-
- Respect intervention flow for long-running, external, or user-visible operations.
|
| 50 |
-
|
| 51 |
-
## Verification
|
| 52 |
-
|
| 53 |
-
- Run targeted tool and prompt-contract tests for changed behavior; smoke-test agent execution when no focused test exists.
|
| 54 |
-
- Related tests observed by source search:
|
| 55 |
-
- `tests/test_browser_agent_regressions.py`
|
| 56 |
-
- `tests/test_host_browser_connector.py`
|
| 57 |
-
- `tests/test_office_desktop_state.py`
|
| 58 |
-
- `tests/test_vision_load_image_refs.py`
|
| 59 |
-
|
| 60 |
-
## Child DOX Index
|
| 61 |
-
|
| 62 |
-
No child DOX files.
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:826f3b31ec512db1d099dee2cc93ab15933e7699bf7749102f4efdcd7be5c374
|
| 3 |
+
size 4459
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tools/wait.py
CHANGED
|
@@ -1,89 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
from helpers.print_style import PrintStyle
|
| 5 |
-
from helpers.wait import managed_wait
|
| 6 |
-
from helpers.localization import Localization
|
| 7 |
-
|
| 8 |
-
class WaitTool(Tool):
|
| 9 |
-
|
| 10 |
-
async def execute(self, **kwargs) -> Response:
|
| 11 |
-
await self.agent.handle_intervention()
|
| 12 |
-
|
| 13 |
-
seconds = self.args.get("seconds", 0)
|
| 14 |
-
minutes = self.args.get("minutes", 0)
|
| 15 |
-
hours = self.args.get("hours", 0)
|
| 16 |
-
days = self.args.get("days", 0)
|
| 17 |
-
until_timestamp_str = self.args.get("until")
|
| 18 |
-
|
| 19 |
-
is_duration_wait = not bool(until_timestamp_str)
|
| 20 |
-
|
| 21 |
-
now = Localization.get().now()
|
| 22 |
-
target_time = None
|
| 23 |
-
|
| 24 |
-
if until_timestamp_str:
|
| 25 |
-
try:
|
| 26 |
-
target_time = Localization.get().localtime_str_to_utc_dt(until_timestamp_str)
|
| 27 |
-
if not target_time:
|
| 28 |
-
raise ValueError(f"Invalid timestamp format: {until_timestamp_str}")
|
| 29 |
-
except ValueError as e:
|
| 30 |
-
return Response(
|
| 31 |
-
message=str(e),
|
| 32 |
-
break_loop=False,
|
| 33 |
-
)
|
| 34 |
-
else:
|
| 35 |
-
wait_duration = timedelta(
|
| 36 |
-
days=int(days),
|
| 37 |
-
hours=int(hours),
|
| 38 |
-
minutes=int(minutes),
|
| 39 |
-
seconds=int(seconds),
|
| 40 |
-
)
|
| 41 |
-
if wait_duration.total_seconds() <= 0:
|
| 42 |
-
return Response(
|
| 43 |
-
message="Wait duration must be positive.",
|
| 44 |
-
break_loop=False,
|
| 45 |
-
)
|
| 46 |
-
target_time = now + wait_duration
|
| 47 |
-
|
| 48 |
-
if target_time <= now:
|
| 49 |
-
return Response(
|
| 50 |
-
message=f"Target time {Localization.get().serialize_datetime(target_time)} is in the past.",
|
| 51 |
-
break_loop=False,
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
PrintStyle.info(f"Waiting until {Localization.get().serialize_datetime(target_time)}...")
|
| 55 |
-
|
| 56 |
-
target_time = await managed_wait(
|
| 57 |
-
agent=self.agent,
|
| 58 |
-
target_time=target_time,
|
| 59 |
-
is_duration_wait=is_duration_wait,
|
| 60 |
-
log=self.log,
|
| 61 |
-
get_heading_callback=self.get_heading
|
| 62 |
-
)
|
| 63 |
-
|
| 64 |
-
if self.log:
|
| 65 |
-
self.log.update(heading=self.get_heading("Done", done=True))
|
| 66 |
-
|
| 67 |
-
message = self.agent.read_prompt(
|
| 68 |
-
"fw.wait_complete.md",
|
| 69 |
-
target_time=Localization.get().serialize_datetime(target_time)
|
| 70 |
-
)
|
| 71 |
-
|
| 72 |
-
return Response(
|
| 73 |
-
message=message,
|
| 74 |
-
break_loop=False,
|
| 75 |
-
)
|
| 76 |
-
|
| 77 |
-
def get_log_object(self):
|
| 78 |
-
return self.agent.context.log.log(
|
| 79 |
-
type="progress",
|
| 80 |
-
heading=self.get_heading(),
|
| 81 |
-
content="",
|
| 82 |
-
kvps=self.args,
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
def get_heading(self, text: str = "", done: bool = False):
|
| 86 |
-
done_icon = " icon://done_all" if done else ""
|
| 87 |
-
if not text:
|
| 88 |
-
text = f"Waiting..."
|
| 89 |
-
return f"icon://timer Wait: {text}{done_icon}"
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:72989a7fece9f2aa74316f2964c56d996810ff1e08324101f80a599f04cc7fba
|
| 3 |
+
size 2930
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tools/wait.py.dox.md
CHANGED
|
@@ -1,54 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
- Own the `wait.py` agent tool.
|
| 6 |
-
- This module lets the agent wait for a managed duration while respecting intervention flow.
|
| 7 |
-
- Keep this file-level DOX profile synchronized with `wait.py` because this directory is intentionally flat.
|
| 8 |
-
|
| 9 |
-
## Ownership
|
| 10 |
-
|
| 11 |
-
- `wait.py` owns the runtime implementation.
|
| 12 |
-
- `wait.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation.
|
| 13 |
-
- Classes:
|
| 14 |
-
- `WaitTool` (`Tool`)
|
| 15 |
-
- `async execute(self, **kwargs) -> Response`
|
| 16 |
-
- `get_log_object(self)`
|
| 17 |
-
- `get_heading(self, text: str=..., done: bool=...)`
|
| 18 |
-
|
| 19 |
-
## Runtime Contracts
|
| 20 |
-
|
| 21 |
-
- Tool modules must define `helpers.tool.Tool` subclasses and return `helpers.tool.Response` from `execute(...)`.
|
| 22 |
-
- Update this file whenever tool arguments, output shape, `break_loop` behavior, intervention handling, prompt instructions, or side effects change.
|
| 23 |
-
- `WaitTool` is a `Tool`.
|
| 24 |
-
- `WaitTool` defines `execute(...)`.
|
| 25 |
-
- Observed side-effect areas: settings/state persistence.
|
| 26 |
-
- Imported dependency areas include: `asyncio`, `datetime`, `helpers.localization`, `helpers.print_style`, `helpers.tool`, `helpers.wait`.
|
| 27 |
-
|
| 28 |
-
## Key Concepts
|
| 29 |
-
|
| 30 |
-
- Important called helpers/classes observed in the source: `Localization.get.now`, `PrintStyle.info`, `self.agent.read_prompt`, `Response`, `self.agent.context.log.log`, `self.agent.handle_intervention`, `timedelta`, `managed_wait`, `Localization.get.localtime_str_to_utc_dt`, `wait_duration.total_seconds`, `Localization.get.serialize_datetime`, `self.get_heading`, `ValueError`.
|
| 31 |
-
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
|
| 32 |
-
|
| 33 |
-
## Work Guidance
|
| 34 |
-
|
| 35 |
-
- Keep tool output concise, model-readable, and safe for history persistence.
|
| 36 |
-
- Coordinate argument or behavior changes with prompt tool instructions and skill guidance.
|
| 37 |
-
- Respect intervention flow for long-running, external, or user-visible operations.
|
| 38 |
-
|
| 39 |
-
## Verification
|
| 40 |
-
|
| 41 |
-
- Run targeted tool and prompt-contract tests for changed behavior; smoke-test agent execution when no focused test exists.
|
| 42 |
-
- Related tests observed by source search:
|
| 43 |
-
- `tests/email_parser_test.py`
|
| 44 |
-
- `tests/rate_limiter_test.py`
|
| 45 |
-
- `tests/test_api_chat_lifetime.py`
|
| 46 |
-
- `tests/test_browser_agent_regressions.py`
|
| 47 |
-
- `tests/test_chat_compaction.py`
|
| 48 |
-
- `tests/test_default_prompt_budget.py`
|
| 49 |
-
- `tests/test_document_query_plugin.py`
|
| 50 |
-
- `tests/test_download_toast_regressions.py`
|
| 51 |
-
|
| 52 |
-
## Child DOX Index
|
| 53 |
-
|
| 54 |
-
No child DOX files.
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7de7c43d3c40bdf266841d4c554d15e759b883f4b066dd758b4b7afa078558d1
|
| 3 |
+
size 2477
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
update_reqs.py
CHANGED
|
@@ -1,38 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
def get_installed_version(package_name):
|
| 5 |
-
try:
|
| 6 |
-
return pkg_resources.get_distribution(package_name).version
|
| 7 |
-
except pkg_resources.DistributionNotFound:
|
| 8 |
-
return None
|
| 9 |
-
|
| 10 |
-
def update_requirements():
|
| 11 |
-
with open('requirements.txt', 'r') as f:
|
| 12 |
-
requirements = f.readlines()
|
| 13 |
-
|
| 14 |
-
updated_requirements = []
|
| 15 |
-
for req in requirements:
|
| 16 |
-
req = req.strip()
|
| 17 |
-
if not req or req.startswith('#'):
|
| 18 |
-
updated_requirements.append(req)
|
| 19 |
-
continue
|
| 20 |
-
|
| 21 |
-
# Extract package name
|
| 22 |
-
match = re.match(r'^([^=<>]+)==', req)
|
| 23 |
-
if match:
|
| 24 |
-
package_name = match.group(1)
|
| 25 |
-
current_version = get_installed_version(package_name)
|
| 26 |
-
if current_version:
|
| 27 |
-
updated_requirements.append(f'{package_name}=={current_version}')
|
| 28 |
-
else:
|
| 29 |
-
updated_requirements.append(req) # Keep original if package not found
|
| 30 |
-
else:
|
| 31 |
-
updated_requirements.append(req) # Keep original if pattern doesn't match
|
| 32 |
-
|
| 33 |
-
# Write updated requirements
|
| 34 |
-
with open('requirements.txt', 'w') as f:
|
| 35 |
-
f.write('\n'.join(updated_requirements) + '\n')
|
| 36 |
-
|
| 37 |
-
if __name__ == '__main__':
|
| 38 |
-
update_requirements()
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d6f700fc5dfbb40827732adb650e07b080cf5796443849f3acd1a81be1556968
|
| 3 |
+
size 1248
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
usr/agents/.gitkeep
ADDED
|
File without changes
|
usr/knowledge/.gitkeep
ADDED
|
File without changes
|
usr/knowledge/main/.gitkeep
ADDED
|
File without changes
|
usr/knowledge/solutions/.gitkeep
ADDED
|
File without changes
|
usr/plugins/.gitkeep
ADDED
|
File without changes
|
usr/skills/.gitkeep
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
|
| 3 |
+
size 1
|
usr/workdir/.gitkeep
ADDED
|
File without changes
|
webui/AGENTS.md
CHANGED
|
@@ -1,60 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
- Own the Flask-served Alpine.js WebUI shell, frontend modules, components, CSS, static assets, and vendored browser libraries.
|
| 6 |
-
- Keep the UI coherent with backend APIs, WebSocket state, plugin extension points, and documented frontend patterns.
|
| 7 |
-
|
| 8 |
-
## Ownership
|
| 9 |
-
|
| 10 |
-
- `splash.html` owns the self-contained cache/bootstrap document served at `/`; `safe.html` owns the self-contained service-worker escape hatch served at `/safe`; `index.html`, `index.js`, and `index.css` define the rendered main UI shell served directly at `/index.html` and `/safe`, and fetched from `/ui/index` for in-place installation at `/`.
|
| 11 |
-
- `components/` owns self-contained Alpine components and component stores.
|
| 12 |
-
- `js/` owns shared frontend modules, API clients, WebSocket clients, stores, extension loaders, and utility code.
|
| 13 |
-
- `css/` owns shared stylesheet modules.
|
| 14 |
-
- `public/` owns first-party static image/icon assets.
|
| 15 |
-
- `vendor/` owns vendored third-party browser libraries.
|
| 16 |
-
|
| 17 |
-
## Local Contracts
|
| 18 |
-
|
| 19 |
-
- Store-dependent UI must be gated with `x-data` and `x-if="$store.storeName"` before using the store.
|
| 20 |
-
- Use `createStore` from `/js/AlpineStore.js` for frontend stores.
|
| 21 |
-
- Use `openModal(path)` and `closeModal()` from `/js/modals.js` for modal flows.
|
| 22 |
-
- Use `/js/api.js` helpers so CSRF and auth behavior stays consistent.
|
| 23 |
-
- Component tags use `<x-component path="...">`; paths are resolved under `webui/components/` when not already prefixed.
|
| 24 |
-
- Frontend extension breakpoints use `<x-extension id="...">` and are loaded through `/js/extensions.js`.
|
| 25 |
-
- `sw.js` owns same-origin HTML/CSS/JavaScript caching. The self-contained splash fetches `/ui/index` and the gzip-compressed `/ui/asset-bundle` in parallel, installs the versioned worker, waits until the bundle is usable in memory, and then installs the rendered index into the current `/` document without navigation. The worker persists the whole prepared bundle as one Cache Storage response in the background and removes obsolete version caches. Eligible text bodies up to 512 KiB each may be embedded; unknown or runtime-computed text URLs use one ordinary backend request and are cached as the native response when their transferred body is at most 256 KiB. The worker does not parse imports, call a secondary graph endpoint, or perform compression. Worker, cache, bundle, and timeout failures fall through to the already-started backend document or ordinary backend asset request. Media, images, fonts, and manifests use normal browser HTTP caching.
|
| 26 |
-
- `splash.html` keeps its logo, styling, and bootstrap logic self-contained. Before replacing the document, it carries its current overlay markup into the rendered index. `index.html` owns the matching inline critical overlay style and fades that same visual overlay after initial component/extension readiness, with a bounded failure timeout; it must not contain duplicate static overlay markup. Icon-font preloading begins when the index is placed behind the overlay, continues independently behind square transparent placeholders, and must neither compete with the startup bundle nor delay the application reveal. `safe.html` must remain self-contained: its first phase unregisters all service workers for the origin, then replaces itself with the directly rendered `/safe` phase without fetching the bundle or registering a worker. The direct phase repeats unregistration defensively but does not initialize a worker; returning to `/` may install the current worker again. The rendered index removes only the internal safe-mode query marker from `/safe` in browser history. Except for the tiny blocking icon guard stylesheet needed to prevent ligature layout shifts, initial application stylesheets remain non-render-blocking.
|
| 27 |
-
- `js/icons.js` owns the native `<x-icon>` element. First-party WebUI and bundled-plugin markup must use `<x-icon name="lowercase_snake_case"></x-icon>` for static Material Symbols and `:name="expression"` for Alpine-driven names; do not put ligature text inside the element or author new `.material-symbols-outlined` spans. The element uses the single local WOFF2 font rather than per-icon SVG requests, validates names, defaults unlabeled icons to `aria-hidden="true"`, and retains the legacy Material class internally so established selectors continue to apply. The shared vendor CSS constrains both `<x-icon>` and legacy ligature spans to a clipped 1:1 em square and keeps them transparent until the icon face is confirmed after the installed application document finishes parsing. `.material-symbols-outlined` and `.material-icons-outlined` spans remain supported for third-party plugin compatibility but are not the first-party authoring API.
|
| 28 |
-
- Component HTML loaded by the shared loader may include `<title>`, module scripts, body content, and scoped styles; modal content uses the same loader path.
|
| 29 |
-
- Do not bypass WebSocket origin/auth/CSRF assumptions from frontend code.
|
| 30 |
-
- Avoid editing vendored files unless intentionally updating the vendor asset.
|
| 31 |
-
- Startup scripts in `index.html` must be local and non-parser-blocking: use ES modules, `defer`, or `async` for every script with `src`.
|
| 32 |
-
- Baseline main and login pages must load scripts, styles, fonts, and images from same-origin vendored or first-party assets so they remain usable without internet access.
|
| 33 |
-
- Rubik (`--font-family-main`) is the default WebUI text and control font; use the code/mono font tokens only for code, logs, paths, and fixed-width data.
|
| 34 |
-
- Hover, focus, and active border treatments should follow existing neutral border/background patterns; avoid hard-coded blue border highlights unless matching an established specialized surface.
|
| 35 |
-
- Keep transient UI affordances such as Bootstrap tooltips and notification toasts above normal and legacy modal layers while confirmation dialogs remain on top.
|
| 36 |
-
|
| 37 |
-
## Work Guidance
|
| 38 |
-
|
| 39 |
-
- Put component-specific markup and styles under `components/`; put reusable frontend infrastructure under `js/`; put shared visual primitives under `css/`.
|
| 40 |
-
- Keep UI text and controls consistent with existing components.
|
| 41 |
-
- Use notifications for user-facing success, warning, and error feedback where the app already uses notification flows.
|
| 42 |
-
- Coordinate API payload changes with backend handlers and tests.
|
| 43 |
-
|
| 44 |
-
## Verification
|
| 45 |
-
|
| 46 |
-
- Run targeted WebUI/frontend tests when available.
|
| 47 |
-
- Manually smoke-test visible UI changes with `python run_ui.py` when behavior cannot be covered by tests.
|
| 48 |
-
- Verify desktop and mobile layout for substantial UI changes.
|
| 49 |
-
|
| 50 |
-
## Child DOX Index
|
| 51 |
-
|
| 52 |
-
Direct child DOX files:
|
| 53 |
-
|
| 54 |
-
| Child | Scope |
|
| 55 |
-
| --- | --- |
|
| 56 |
-
| [components/AGENTS.md](components/AGENTS.md) | Alpine component HTML, component stores, and component-local styles. |
|
| 57 |
-
| [css/AGENTS.md](css/AGENTS.md) | Shared WebUI stylesheet modules. |
|
| 58 |
-
| [js/AGENTS.md](js/AGENTS.md) | Shared frontend JavaScript modules and client-side infrastructure. |
|
| 59 |
-
| [public/AGENTS.md](public/AGENTS.md) | First-party static WebUI images, icons, splash art, and PWA assets. |
|
| 60 |
-
| [vendor/AGENTS.md](vendor/AGENTS.md) | Vendored third-party browser libraries. |
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:905c7dbc80e8df70a9d275d78506c5f88673fff8a686562d3f4566b69eddc22c
|
| 3 |
+
size 7052
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/AGENTS.md
CHANGED
|
@@ -1,77 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
- Own self-contained Alpine.js components, component stores, and component-local styles.
|
| 6 |
-
- Keep component lifecycle, store registration, and nested component loading consistent.
|
| 7 |
-
|
| 8 |
-
## Ownership
|
| 9 |
-
|
| 10 |
-
- Each subdirectory owns a UI feature area such as sidebar, settings, projects, notifications, plugins, canvas, or welcome screens.
|
| 11 |
-
- `*-store.js` files own component state and actions.
|
| 12 |
-
- Component HTML files own their markup, local module imports, and local scoped styles.
|
| 13 |
-
- `_examples/` contains reference implementations for component and store shape.
|
| 14 |
-
- Modal components normally live under `modals/<name>/<name>.html`; settings components live under `settings/`.
|
| 15 |
-
|
| 16 |
-
## Local Contracts
|
| 17 |
-
|
| 18 |
-
- Module imports belong in the component `<head>`.
|
| 19 |
-
- Module scripts must use `type="module"` and import stores before Alpine evaluates bindings.
|
| 20 |
-
- Rendered content belongs in the component `<body>`.
|
| 21 |
-
- Wrap store-dependent content with `x-data` and a `template x-if="$store.name"` gate.
|
| 22 |
-
- A gated `<template>` must contain one root element.
|
| 23 |
-
- Keep component-specific styles in the component `<style>` block; use shared CSS only for primitives that are genuinely reused.
|
| 24 |
-
- Use CSS variables from `index.css` for color, spacing, typography, borders, and transitions.
|
| 25 |
-
- Use `x-create` for per-mount setup and `x-destroy` for cleanup; do not call long-lived store `init()` from `x-init`.
|
| 26 |
-
- Store `init()` must be idempotent and guarded when it registers global listeners, intervals, or one-time data.
|
| 27 |
-
- Store state can be read as `$store.name` in templates and through direct module imports in JavaScript; avoid `window.Alpine.store()` lookups in component code.
|
| 28 |
-
- Use `globalThis.xAttrs(element)` only for explicit parent `<x-component>` attribute inheritance.
|
| 29 |
-
- Use shared API, modal, notification, confirmation, and store helpers instead of ad hoc globals.
|
| 30 |
-
- Author Material Symbols as empty `<x-icon name="lowercase_snake_case"></x-icon>` elements. Bind changing icons through `:name`, keep accessible text on the owning control, and do not add new ligature-text spans or `x-text` icon bindings.
|
| 31 |
-
- Components that use polling directives may use `x-every-second`, `x-every-minute`, or `x-every-hour` only while mounted.
|
| 32 |
-
- Use `$confirmClick` for destructive two-click confirmations where the existing UI pattern fits.
|
| 33 |
-
- Name component files `feature-name.html` and stores `feature-store.js` or `feature-name-store.js`.
|
| 34 |
-
|
| 35 |
-
## Work Guidance
|
| 36 |
-
|
| 37 |
-
- Keep component stores cohesive and avoid cross-component state mutation unless a shared store owns that state.
|
| 38 |
-
- Prefer nested `<x-component path="...">` for reusable UI pieces.
|
| 39 |
-
- Use absolute imports such as `/components/...` and `/js/...` so loader-generated module URLs resolve reliably.
|
| 40 |
-
- Preserve flex layouts through loader wrappers with `display: contents` on the specific wrapper chain when needed.
|
| 41 |
-
- Use `callJsonApi()` for JSON-in/JSON-out calls and `fetchApi()` for raw fetches that still need CSRF handling.
|
| 42 |
-
- Use `openModal(path)` for modal entry points; modal titles come from component `<title>`.
|
| 43 |
-
- Add modal footers with `data-modal-footer` so `modals.js` can move them to the pinned footer slot.
|
| 44 |
-
- Keep modal footer elements stable after load; conditional footers should not be repeatedly created and destroyed.
|
| 45 |
-
- Use `.modal-floating` only for non-blocking utility panels where page clicks must pass through; do not use it for destructive confirmations, settings, auth, import/export, or required workflows.
|
| 46 |
-
- Do not add `document.addEventListener("alpine:init", ...)` blocks inside component HTML.
|
| 47 |
-
- Do not use legacy `x-teleport` plus `.modal-overlay` modal patterns for new work.
|
| 48 |
-
- Do not introduce a second modal overlay system or manually reshape `.modal-inner`, `.modal-scroll`, or `.modal-footer-slot`.
|
| 49 |
-
- Do not hide fetch, import, or lifecycle errors with broad `.catch(() => null)` handlers; surface failures through notifications or console errors as appropriate.
|
| 50 |
-
- Do not store sensitive modal values in long-lived stores without explicit cleanup on close.
|
| 51 |
-
|
| 52 |
-
## Verification
|
| 53 |
-
|
| 54 |
-
- Manually exercise the affected component in the WebUI for visible or lifecycle changes.
|
| 55 |
-
- Run targeted tests for settings, plugins, notifications, projects, state sync, or other touched flows when available.
|
| 56 |
-
- For modal changes, verify open, close button, Escape, click-outside, stacked modal, scroll, and pinned footer behavior.
|
| 57 |
-
|
| 58 |
-
## Child DOX Index
|
| 59 |
-
|
| 60 |
-
Direct child DOX files:
|
| 61 |
-
|
| 62 |
-
| Child | Scope |
|
| 63 |
-
| --- | --- |
|
| 64 |
-
| [_examples/AGENTS.md](_examples/AGENTS.md) | Reference component and store examples. |
|
| 65 |
-
| [canvas/AGENTS.md](canvas/AGENTS.md) | Right-canvas component surface and store. |
|
| 66 |
-
| [chat/AGENTS.md](chat/AGENTS.md) | Chat composer, attachments, queue, navigation, and top-section components. |
|
| 67 |
-
| [dropdown/AGENTS.md](dropdown/AGENTS.md) | Shared dropdown component. |
|
| 68 |
-
| [messages/AGENTS.md](messages/AGENTS.md) | Message rendering helpers, action buttons, process groups, and resize behavior. |
|
| 69 |
-
| [modals/AGENTS.md](modals/AGENTS.md) | Modal component workflows loaded through the shared modal stack. |
|
| 70 |
-
| [notifications/AGENTS.md](notifications/AGENTS.md) | Toasts, notification modal, icons, and notification store. |
|
| 71 |
-
| [plugins/AGENTS.md](plugins/AGENTS.md) | Plugin settings, info, list, execution, and toggle components. |
|
| 72 |
-
| [projects/AGENTS.md](projects/AGENTS.md) | Project creation, selection, edit, secrets, model, skill, and file-structure components. |
|
| 73 |
-
| [settings/AGENTS.md](settings/AGENTS.md) | Settings shell and built-in settings subsections. |
|
| 74 |
-
| [sidebar/AGENTS.md](sidebar/AGENTS.md) | Left sidebar layout, chat/task lists, top actions, and preferences. |
|
| 75 |
-
| [sync/AGENTS.md](sync/AGENTS.md) | WebUI sync status component and store. |
|
| 76 |
-
| [tooltips/AGENTS.md](tooltips/AGENTS.md) | Shared tooltip store and behavior. |
|
| 77 |
-
| [welcome/AGENTS.md](welcome/AGENTS.md) | Welcome screen, discovery cards, banners, and welcome state. |
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4b4ee454a09ecc7546e8c759ed222b279208ee9cc5f053beb3e5c0ae352d48c7
|
| 3 |
+
size 5902
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/_examples/AGENTS.md
CHANGED
|
@@ -1,27 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
- Own reference component and store examples for WebUI component authors.
|
| 6 |
-
|
| 7 |
-
## Ownership
|
| 8 |
-
|
| 9 |
-
- `_example-component.html` owns the component markup example.
|
| 10 |
-
- `_example-store.js` owns the store pattern example.
|
| 11 |
-
|
| 12 |
-
## Local Contracts
|
| 13 |
-
|
| 14 |
-
- Keep examples aligned with current component loader, Store Gating, and `createStore` patterns.
|
| 15 |
-
- Do not add product behavior or dependencies here.
|
| 16 |
-
|
| 17 |
-
## Work Guidance
|
| 18 |
-
|
| 19 |
-
- Prefer small examples that demonstrate current conventions clearly.
|
| 20 |
-
|
| 21 |
-
## Verification
|
| 22 |
-
|
| 23 |
-
- Manually inspect examples after component contract changes.
|
| 24 |
-
|
| 25 |
-
## Child DOX Index
|
| 26 |
-
|
| 27 |
-
No child DOX files.
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1b7033487bae6a3700601a3a13ee754d764cae76c0175a4539b12c961557b803
|
| 3 |
+
size 624
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/_examples/_example-component.html
CHANGED
|
@@ -1,36 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
<title>Example component or modal</title>
|
| 5 |
-
|
| 6 |
-
<!-- Import the alpine store -->
|
| 7 |
-
<script type="module">
|
| 8 |
-
import { store } from "/components/_examples/_example-store.js";
|
| 9 |
-
</script>
|
| 10 |
-
</head>
|
| 11 |
-
|
| 12 |
-
<body>
|
| 13 |
-
|
| 14 |
-
<!-- This construct of x-data + x-if is used to ensure the component is only rendered when the store is available -->
|
| 15 |
-
<div x-data>
|
| 16 |
-
<template x-if="$store.exampleStore">
|
| 17 |
-
|
| 18 |
-
<!-- Keep in mind that <template> can have only one root element inside -->
|
| 19 |
-
<div>
|
| 20 |
-
<p x-text="$store.exampleStore.example1"></p>
|
| 21 |
-
<p x-text="$store.exampleStore.example2"></p>
|
| 22 |
-
</div>
|
| 23 |
-
|
| 24 |
-
</template>
|
| 25 |
-
</div>
|
| 26 |
-
|
| 27 |
-
<!-- Optional style for the component -->
|
| 28 |
-
<style>
|
| 29 |
-
#example-component {
|
| 30 |
-
width: 100%;
|
| 31 |
-
}
|
| 32 |
-
</style>
|
| 33 |
-
|
| 34 |
-
</body>
|
| 35 |
-
|
| 36 |
-
</html>
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:004c8364524a2809211ce97806aafbfe357cd1e283a025208f746d5a6ab82f98
|
| 3 |
+
size 862
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/_examples/_example-store.js
CHANGED
|
@@ -1,23 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
const model = {
|
| 5 |
-
example1:"Example 1",
|
| 6 |
-
example2:"Example 2",
|
| 7 |
-
|
| 8 |
-
// gets called when the store is created
|
| 9 |
-
init(){
|
| 10 |
-
console.log("Example store initialized");
|
| 11 |
-
},
|
| 12 |
-
|
| 13 |
-
clickHandler(event){
|
| 14 |
-
console.log(event)
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
};
|
| 18 |
-
|
| 19 |
-
// convert it to alpine store
|
| 20 |
-
const store = createStore("_exampleStore", model);
|
| 21 |
-
|
| 22 |
-
// export for use in other files
|
| 23 |
-
export { store };
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bc4574407b5841fdcc2e14f85c0272c3f96770e7c666470d848c6dad0bb66769
|
| 3 |
+
size 485
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/canvas/AGENTS.md
CHANGED
|
@@ -1,33 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
- Own the right-canvas component surface and its state.
|
| 6 |
-
|
| 7 |
-
## Ownership
|
| 8 |
-
|
| 9 |
-
- `right-canvas.html` owns canvas markup.
|
| 10 |
-
- `right-canvas-store.js` owns canvas state, surface registration, and actions.
|
| 11 |
-
- `right-canvas.css` owns canvas-specific styling.
|
| 12 |
-
|
| 13 |
-
## Local Contracts
|
| 14 |
-
|
| 15 |
-
- Keep registered surfaces compatible with WebUI extension hooks.
|
| 16 |
-
- Preserve responsive layout and avoid overlapping the chat/sidebar shells.
|
| 17 |
-
- Right-canvas rail and tab buttons are explicit canvas entry points above the mobile breakpoint; at mobile widths, keep the rail visible but route non-action surfaces into floating modals instead of the side canvas shell.
|
| 18 |
-
- Keep the right-canvas rail and docked shell hidden while the welcome screen is active; non-action surface opens during welcome must route into floating/modal surfaces instead of docking beside the welcome screen.
|
| 19 |
-
- Preserve docked canvas open state across same-tab reloads with session-scoped state, but do not treat it as durable cross-session UI state.
|
| 20 |
-
- In mobile mode, keep the rail below blocking modal layers and compact it on very narrow screens instead of letting it cover modal content.
|
| 21 |
-
- Keep the canvas shell functional when the instance-level visibility preference hides only the right-canvas rail.
|
| 22 |
-
|
| 23 |
-
## Work Guidance
|
| 24 |
-
|
| 25 |
-
- Use existing surface and extension helpers before adding new canvas infrastructure.
|
| 26 |
-
|
| 27 |
-
## Verification
|
| 28 |
-
|
| 29 |
-
- Smoke-test right-canvas open, close, resize, and registered surfaces after changes.
|
| 30 |
-
|
| 31 |
-
## Child DOX Index
|
| 32 |
-
|
| 33 |
-
No child DOX files.
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1cfb44c6adfd734461e1d51790c5b79bf01ea4a67cf269dc63f32b905ae88574
|
| 3 |
+
size 1530
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/canvas/right-canvas-store.js
CHANGED
|
@@ -1,620 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import {
|
| 5 |
-
SURFACE_MODE_DOCKED,
|
| 6 |
-
SURFACE_MODE_FLOATING,
|
| 7 |
-
closeSurfaceGroupModals,
|
| 8 |
-
getRegisteredSurfaces,
|
| 9 |
-
migratePersistedSurfaceState,
|
| 10 |
-
normalizeSurfaceId,
|
| 11 |
-
normalizeSurfaceMode,
|
| 12 |
-
registerSurface as registerSurfaceDefinition,
|
| 13 |
-
} from "/js/surfaces.js";
|
| 14 |
-
|
| 15 |
-
const STORAGE_KEY = "a0.rightCanvas";
|
| 16 |
-
const SESSION_STORAGE_KEY = "a0.rightCanvas.session";
|
| 17 |
-
const DEFAULT_WIDTH = 720;
|
| 18 |
-
const MIN_WIDTH = 0;
|
| 19 |
-
const DESKTOP_BREAKPOINT = 1200;
|
| 20 |
-
const MOBILE_BREAKPOINT = 768;
|
| 21 |
-
|
| 22 |
-
function clamp(value, min, max) {
|
| 23 |
-
return Math.min(Math.max(value, min), max);
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
function viewportWidth() {
|
| 27 |
-
return Math.max(document.documentElement.clientWidth || 0, globalThis.innerWidth || 0);
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
function normalizeWidth(value, fallback = DEFAULT_WIDTH) {
|
| 31 |
-
if (value === null || value === undefined || value === "") return fallback;
|
| 32 |
-
const width = Number(value);
|
| 33 |
-
return Number.isFinite(width) ? Math.max(MIN_WIDTH, Math.round(width)) : fallback;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
function isReloadNavigation() {
|
| 37 |
-
const navigation = performance.getEntriesByType?.("navigation")?.[0];
|
| 38 |
-
if (navigation?.type) return navigation.type === "reload";
|
| 39 |
-
if (!performance.navigation) return false;
|
| 40 |
-
return performance.navigation?.type === performance.navigation?.TYPE_RELOAD;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
const model = {
|
| 44 |
-
surfaces: [],
|
| 45 |
-
activeSurfaceId: "",
|
| 46 |
-
surfaceModes: {},
|
| 47 |
-
mountedSurfaces: {},
|
| 48 |
-
isOpen: false,
|
| 49 |
-
width: DEFAULT_WIDTH,
|
| 50 |
-
isOverlayMode: false,
|
| 51 |
-
isMobileMode: false,
|
| 52 |
-
_initialized: false,
|
| 53 |
-
_registering: false,
|
| 54 |
-
_rootElement: null,
|
| 55 |
-
_resizeCleanup: null,
|
| 56 |
-
_lastPayloadBySurface: {},
|
| 57 |
-
_restoreOpenRequested: false,
|
| 58 |
-
_restoreOpenTimer: null,
|
| 59 |
-
_restoringSurface: false,
|
| 60 |
-
|
| 61 |
-
async init(element = null) {
|
| 62 |
-
if (element) this._rootElement = element;
|
| 63 |
-
if (this._initialized) {
|
| 64 |
-
this.applyLayoutState();
|
| 65 |
-
this.scheduleRestoreOpenSurface();
|
| 66 |
-
return;
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
this._initialized = true;
|
| 70 |
-
this.restore();
|
| 71 |
-
this.updateLayoutMode();
|
| 72 |
-
this.applyLayoutState();
|
| 73 |
-
globalThis.addEventListener("resize", () => {
|
| 74 |
-
this.updateLayoutMode();
|
| 75 |
-
this.setWidth(this.width, { persist: false });
|
| 76 |
-
this.applyLayoutState();
|
| 77 |
-
});
|
| 78 |
-
|
| 79 |
-
if (!this._registering) {
|
| 80 |
-
this._registering = true;
|
| 81 |
-
await callJsExtensions("surfaces_register", this);
|
| 82 |
-
await callJsExtensions("right_canvas_register_surfaces", this);
|
| 83 |
-
this._registering = false;
|
| 84 |
-
this.ensureActiveSurface();
|
| 85 |
-
this.scheduleRestoreOpenSurface();
|
| 86 |
-
}
|
| 87 |
-
},
|
| 88 |
-
|
| 89 |
-
registerSurface(surface) {
|
| 90 |
-
if (!surface?.id) return;
|
| 91 |
-
const surfaceId = normalizeSurfaceId(surface.id);
|
| 92 |
-
const normalized = {
|
| 93 |
-
title: surface.id,
|
| 94 |
-
icon: "web_asset",
|
| 95 |
-
image: "",
|
| 96 |
-
order: 100,
|
| 97 |
-
canOpen: () => true,
|
| 98 |
-
open: () => {},
|
| 99 |
-
close: () => {},
|
| 100 |
-
modalPath: "",
|
| 101 |
-
actionOnly: false,
|
| 102 |
-
...surface,
|
| 103 |
-
id: surfaceId,
|
| 104 |
-
};
|
| 105 |
-
|
| 106 |
-
const index = this.surfaces.findIndex((item) => item.id === normalized.id);
|
| 107 |
-
if (index >= 0) {
|
| 108 |
-
this.surfaces.splice(index, 1, normalized);
|
| 109 |
-
} else {
|
| 110 |
-
this.surfaces.push(normalized);
|
| 111 |
-
}
|
| 112 |
-
if (!this.surfaceModes[normalized.id]) {
|
| 113 |
-
this.surfaceModes[normalized.id] = SURFACE_MODE_DOCKED;
|
| 114 |
-
}
|
| 115 |
-
registerSurfaceDefinition(normalized);
|
| 116 |
-
this.surfaces.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
| 117 |
-
if (!this._registering) {
|
| 118 |
-
this.ensureActiveSurface();
|
| 119 |
-
}
|
| 120 |
-
},
|
| 121 |
-
|
| 122 |
-
ensureActiveSurface() {
|
| 123 |
-
const panelSurfaces = this.panelSurfaces;
|
| 124 |
-
if (!panelSurfaces.length) {
|
| 125 |
-
this.activeSurfaceId = "";
|
| 126 |
-
return;
|
| 127 |
-
}
|
| 128 |
-
if (!panelSurfaces.some((surface) => surface.id === this.activeSurfaceId)) {
|
| 129 |
-
this.activeSurfaceId = panelSurfaces[0].id;
|
| 130 |
-
}
|
| 131 |
-
},
|
| 132 |
-
|
| 133 |
-
async open(surfaceId = "", payload = {}) {
|
| 134 |
-
const targetId = normalizeSurfaceId(surfaceId || this.activeSurfaceId || this.panelSurfaces[0]?.id || "");
|
| 135 |
-
const surface = this.getSurface(targetId);
|
| 136 |
-
if (!surface) {
|
| 137 |
-
return false;
|
| 138 |
-
}
|
| 139 |
-
if (typeof surface.canOpen === "function" && surface.canOpen(payload) === false) {
|
| 140 |
-
return false;
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
this._restoreOpenRequested = false;
|
| 144 |
-
this.cancelRestoreOpenSurface();
|
| 145 |
-
|
| 146 |
-
if (surface.actionOnly) {
|
| 147 |
-
try {
|
| 148 |
-
await surface.open?.(payload || {});
|
| 149 |
-
} catch (error) {
|
| 150 |
-
console.error(`Canvas action ${targetId} failed`, error);
|
| 151 |
-
}
|
| 152 |
-
return true;
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
if (!this.shouldRender()) {
|
| 156 |
-
return await this.openModalSurface(targetId, payload);
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
if (this.isMobileMode) {
|
| 160 |
-
this.activeSurfaceId = targetId;
|
| 161 |
-
this.isOpen = false;
|
| 162 |
-
return await this.openModalSurface(targetId, payload);
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
this.activeSurfaceId = targetId;
|
| 166 |
-
this.markSurfaceMounted(targetId);
|
| 167 |
-
this.isOpen = true;
|
| 168 |
-
this.recordSurfaceMode(targetId, SURFACE_MODE_DOCKED, { persist: false });
|
| 169 |
-
this._lastPayloadBySurface[targetId] = payload || {};
|
| 170 |
-
this.persist();
|
| 171 |
-
this.applyLayoutState();
|
| 172 |
-
|
| 173 |
-
try {
|
| 174 |
-
await surface.open?.(payload || {});
|
| 175 |
-
} catch (error) {
|
| 176 |
-
console.error(`Canvas surface ${targetId} failed to open`, error);
|
| 177 |
-
}
|
| 178 |
-
return true;
|
| 179 |
-
},
|
| 180 |
-
|
| 181 |
-
markSurfaceMounted(surfaceId) {
|
| 182 |
-
const targetId = normalizeSurfaceId(surfaceId);
|
| 183 |
-
if (!targetId) return;
|
| 184 |
-
this.mountedSurfaces = {
|
| 185 |
-
...this.mountedSurfaces,
|
| 186 |
-
[targetId]: true,
|
| 187 |
-
};
|
| 188 |
-
},
|
| 189 |
-
|
| 190 |
-
markSurfaceUnmounted(surfaceId) {
|
| 191 |
-
const targetId = normalizeSurfaceId(surfaceId);
|
| 192 |
-
if (!targetId || !this.mountedSurfaces[targetId]) return;
|
| 193 |
-
const next = { ...this.mountedSurfaces };
|
| 194 |
-
delete next[targetId];
|
| 195 |
-
this.mountedSurfaces = next;
|
| 196 |
-
},
|
| 197 |
-
|
| 198 |
-
mountedSurfaceIds() {
|
| 199 |
-
return Object.entries(this.mountedSurfaces)
|
| 200 |
-
.filter(([, mounted]) => mounted)
|
| 201 |
-
.map(([surfaceId]) => surfaceId);
|
| 202 |
-
},
|
| 203 |
-
|
| 204 |
-
isSurfaceMounted(id) {
|
| 205 |
-
return Boolean(this.mountedSurfaces[normalizeSurfaceId(id)]);
|
| 206 |
-
},
|
| 207 |
-
|
| 208 |
-
isSurfaceRendered(id) {
|
| 209 |
-
return Boolean(this.isOpen && this.isSurfaceMounted(id));
|
| 210 |
-
},
|
| 211 |
-
|
| 212 |
-
isSurfaceVisible(id) {
|
| 213 |
-
const targetId = normalizeSurfaceId(id);
|
| 214 |
-
return Boolean(this.isOpen && this.activeSurfaceId === targetId && this.isSurfaceMounted(targetId));
|
| 215 |
-
},
|
| 216 |
-
|
| 217 |
-
recordSurfaceMode(surfaceId, mode = SURFACE_MODE_DOCKED, options = {}) {
|
| 218 |
-
const targetId = normalizeSurfaceId(surfaceId);
|
| 219 |
-
if (!targetId) return;
|
| 220 |
-
this.surfaceModes = {
|
| 221 |
-
...this.surfaceModes,
|
| 222 |
-
[targetId]: normalizeSurfaceMode(mode),
|
| 223 |
-
};
|
| 224 |
-
if (options.persist !== false) this.persist();
|
| 225 |
-
},
|
| 226 |
-
|
| 227 |
-
latestSurfaceMode(surfaceId) {
|
| 228 |
-
const targetId = normalizeSurfaceId(surfaceId);
|
| 229 |
-
return normalizeSurfaceMode(this.surfaceModes[targetId]);
|
| 230 |
-
},
|
| 231 |
-
|
| 232 |
-
async openLatest(surfaceId = "", payload = {}) {
|
| 233 |
-
const targetId = normalizeSurfaceId(surfaceId || this.activeSurfaceId || this.panelSurfaces[0]?.id || "");
|
| 234 |
-
if (!targetId) return false;
|
| 235 |
-
if (this.isMobileMode) {
|
| 236 |
-
return await this.open(targetId, payload);
|
| 237 |
-
}
|
| 238 |
-
if (this.latestSurfaceMode(targetId) === SURFACE_MODE_FLOATING) {
|
| 239 |
-
return await this.openModalSurface(targetId, payload);
|
| 240 |
-
}
|
| 241 |
-
return await this.open(targetId, payload);
|
| 242 |
-
},
|
| 243 |
-
|
| 244 |
-
async close() {
|
| 245 |
-
this._restoreOpenRequested = false;
|
| 246 |
-
this.cancelRestoreOpenSurface();
|
| 247 |
-
this.isOpen = false;
|
| 248 |
-
this.persist();
|
| 249 |
-
this.applyLayoutState();
|
| 250 |
-
return true;
|
| 251 |
-
},
|
| 252 |
-
|
| 253 |
-
async dockSurface(surfaceId, payload = {}) {
|
| 254 |
-
surfaceId = normalizeSurfaceId(surfaceId);
|
| 255 |
-
if (this.isMobileMode) {
|
| 256 |
-
return false;
|
| 257 |
-
}
|
| 258 |
-
const surface = this.getSurface(surfaceId);
|
| 259 |
-
if (!surface) {
|
| 260 |
-
return false;
|
| 261 |
-
}
|
| 262 |
-
const modalPath = payload.modalPath || surface.modalPath || "";
|
| 263 |
-
let handoffStarted = false;
|
| 264 |
-
try {
|
| 265 |
-
await surface.beginDockHandoff?.(payload);
|
| 266 |
-
handoffStarted = true;
|
| 267 |
-
|
| 268 |
-
const closed = await this.closeDockSourceModal(payload, modalPath);
|
| 269 |
-
if (closed === false) {
|
| 270 |
-
await surface.cancelDockHandoff?.(payload);
|
| 271 |
-
return false;
|
| 272 |
-
}
|
| 273 |
-
|
| 274 |
-
const openPayload = { ...payload, source: "modal" };
|
| 275 |
-
delete openPayload.closeSourceModal;
|
| 276 |
-
const opened = await this.open(surfaceId, openPayload);
|
| 277 |
-
await surface.finishDockHandoff?.({ ...openPayload, opened });
|
| 278 |
-
return opened;
|
| 279 |
-
} catch (error) {
|
| 280 |
-
if (handoffStarted) {
|
| 281 |
-
await surface.cancelDockHandoff?.(payload);
|
| 282 |
-
}
|
| 283 |
-
console.error(`Canvas surface ${surfaceId} failed to dock`, error);
|
| 284 |
-
return false;
|
| 285 |
-
}
|
| 286 |
-
},
|
| 287 |
-
|
| 288 |
-
async closeDockSourceModal(payload = {}, modalPath = "") {
|
| 289 |
-
if (typeof payload.closeSourceModal === "function") {
|
| 290 |
-
return (await payload.closeSourceModal()) !== false;
|
| 291 |
-
}
|
| 292 |
-
|
| 293 |
-
const sourceModalPath = payload.sourceModalPath || modalPath;
|
| 294 |
-
if (sourceModalPath || modalPath) {
|
| 295 |
-
const closed = await closeSurfaceGroupModals();
|
| 296 |
-
if (closed === false) return false;
|
| 297 |
-
if (!sourceModalPath || !globalThis.isModalOpen?.(sourceModalPath)) return true;
|
| 298 |
-
}
|
| 299 |
-
if (sourceModalPath && globalThis.isModalOpen?.(sourceModalPath)) {
|
| 300 |
-
return (await globalThis.closeModal?.(sourceModalPath)) !== false;
|
| 301 |
-
}
|
| 302 |
-
if (modalPath && modalPath !== sourceModalPath && globalThis.isModalOpen?.(modalPath)) {
|
| 303 |
-
return (await globalThis.closeModal?.(modalPath)) !== false;
|
| 304 |
-
}
|
| 305 |
-
return true;
|
| 306 |
-
},
|
| 307 |
-
|
| 308 |
-
async undockSurface(surfaceId = "", payload = {}) {
|
| 309 |
-
const targetId = normalizeSurfaceId(surfaceId || this.activeSurfaceId);
|
| 310 |
-
const surface = this.getSurface(targetId);
|
| 311 |
-
const modalPath = payload.modalPath || surface?.modalPath || "";
|
| 312 |
-
if (!surface || !modalPath) return false;
|
| 313 |
-
const openModal = globalThis.ensureModalOpen || globalThis.openModal;
|
| 314 |
-
if (!openModal) return false;
|
| 315 |
-
if (this.activeSurfaceId === targetId) {
|
| 316 |
-
this.isOpen = false;
|
| 317 |
-
this.persist();
|
| 318 |
-
this.applyLayoutState();
|
| 319 |
-
}
|
| 320 |
-
this.recordSurfaceMode(targetId, SURFACE_MODE_FLOATING);
|
| 321 |
-
const modalPromise = openModal(modalPath);
|
| 322 |
-
if (modalPromise?.catch) {
|
| 323 |
-
modalPromise.catch((error) => console.error(`Canvas surface ${targetId} failed to undock`, error));
|
| 324 |
-
}
|
| 325 |
-
return true;
|
| 326 |
-
},
|
| 327 |
-
|
| 328 |
-
async openModalSurface(surfaceId = "", payload = {}) {
|
| 329 |
-
const targetId = normalizeSurfaceId(surfaceId || this.activeSurfaceId);
|
| 330 |
-
const surface = this.getSurface(targetId);
|
| 331 |
-
const modalPath = payload.modalPath || surface?.modalPath || "";
|
| 332 |
-
if (!surface || !modalPath) return false;
|
| 333 |
-
const openModal = globalThis.ensureModalOpen || globalThis.openModal;
|
| 334 |
-
if (!openModal) return false;
|
| 335 |
-
|
| 336 |
-
this._restoreOpenRequested = false;
|
| 337 |
-
this.cancelRestoreOpenSurface();
|
| 338 |
-
|
| 339 |
-
if (this.isOpen && this.activeSurfaceId === targetId) {
|
| 340 |
-
this.isOpen = false;
|
| 341 |
-
this.persist();
|
| 342 |
-
this.applyLayoutState();
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
this.recordSurfaceMode(targetId, SURFACE_MODE_FLOATING);
|
| 346 |
-
const modalPromise = openModal(modalPath);
|
| 347 |
-
if (modalPromise?.catch) {
|
| 348 |
-
modalPromise.catch((error) => console.error(`Canvas surface ${targetId} failed to open as modal`, error));
|
| 349 |
-
}
|
| 350 |
-
return true;
|
| 351 |
-
},
|
| 352 |
-
|
| 353 |
-
async undockActiveSurface() {
|
| 354 |
-
return await this.undockSurface(this.activeSurfaceId);
|
| 355 |
-
},
|
| 356 |
-
|
| 357 |
-
currentSurfaceCanUndock() {
|
| 358 |
-
return Boolean(this.currentSurface()?.modalPath);
|
| 359 |
-
},
|
| 360 |
-
|
| 361 |
-
async toggle(surfaceId = "", payload = {}) {
|
| 362 |
-
const targetId = normalizeSurfaceId(surfaceId || this.activeSurfaceId || this.panelSurfaces[0]?.id || "");
|
| 363 |
-
if (this.isOpen && targetId === this.activeSurfaceId) {
|
| 364 |
-
await this.close();
|
| 365 |
-
return false;
|
| 366 |
-
}
|
| 367 |
-
return await this.open(targetId, payload);
|
| 368 |
-
},
|
| 369 |
-
|
| 370 |
-
async toggleCanvas() {
|
| 371 |
-
if (!this.shouldRender()) return false;
|
| 372 |
-
if (this.isMobileMode) {
|
| 373 |
-
return await this.open(this.activeSurfaceId || this.panelSurfaces[0]?.id || "", { source: "mobile-toggle" });
|
| 374 |
-
}
|
| 375 |
-
if (this.isOpen) {
|
| 376 |
-
await this.close();
|
| 377 |
-
return false;
|
| 378 |
-
}
|
| 379 |
-
return await this.open(this.activeSurfaceId || this.panelSurfaces[0]?.id || "");
|
| 380 |
-
},
|
| 381 |
-
|
| 382 |
-
setWidth(px, options = {}) {
|
| 383 |
-
const { persist = true } = options;
|
| 384 |
-
const next = clamp(normalizeWidth(px), MIN_WIDTH, this.maxWidth());
|
| 385 |
-
this.width = next;
|
| 386 |
-
this.applyLayoutState();
|
| 387 |
-
if (persist) this.persist();
|
| 388 |
-
},
|
| 389 |
-
|
| 390 |
-
maxWidth() {
|
| 391 |
-
if (this.isOverlayMode) {
|
| 392 |
-
return Math.max(MIN_WIDTH, viewportWidth() - 44);
|
| 393 |
-
}
|
| 394 |
-
|
| 395 |
-
const container = this._rootElement?.closest(".container");
|
| 396 |
-
const rightPanel = document.getElementById("right-panel");
|
| 397 |
-
const containerRight = container?.getBoundingClientRect().right ?? viewportWidth();
|
| 398 |
-
const panelLeft = rightPanel?.getBoundingClientRect().left ?? 0;
|
| 399 |
-
return Math.max(MIN_WIDTH, Math.floor(containerRight - panelLeft));
|
| 400 |
-
},
|
| 401 |
-
|
| 402 |
-
defaultWidth() {
|
| 403 |
-
return Math.min(DEFAULT_WIDTH, Math.floor(viewportWidth() * 0.45));
|
| 404 |
-
},
|
| 405 |
-
|
| 406 |
-
startResize(event) {
|
| 407 |
-
if (this.isOverlayMode || this.isMobileMode || !this.isOpen) return;
|
| 408 |
-
if (event.button !== 0) return;
|
| 409 |
-
event.preventDefault();
|
| 410 |
-
this.dispatchResizeEvent("right-canvas-resize-start");
|
| 411 |
-
|
| 412 |
-
const onPointerMove = (moveEvent) => {
|
| 413 |
-
const nextWidth = viewportWidth() - moveEvent.clientX;
|
| 414 |
-
this.setWidth(nextWidth);
|
| 415 |
-
};
|
| 416 |
-
const onPointerUp = () => {
|
| 417 |
-
globalThis.removeEventListener("pointermove", onPointerMove);
|
| 418 |
-
globalThis.removeEventListener("pointerup", onPointerUp);
|
| 419 |
-
globalThis.removeEventListener("pointercancel", onPointerUp);
|
| 420 |
-
document.body.classList.remove("right-canvas-resizing");
|
| 421 |
-
this.persist();
|
| 422 |
-
this.dispatchResizeEvent("right-canvas-resize-end");
|
| 423 |
-
};
|
| 424 |
-
|
| 425 |
-
document.body.classList.add("right-canvas-resizing");
|
| 426 |
-
globalThis.addEventListener("pointermove", onPointerMove);
|
| 427 |
-
globalThis.addEventListener("pointerup", onPointerUp);
|
| 428 |
-
globalThis.addEventListener("pointercancel", onPointerUp);
|
| 429 |
-
},
|
| 430 |
-
|
| 431 |
-
dispatchResizeEvent(name) {
|
| 432 |
-
try {
|
| 433 |
-
globalThis.dispatchEvent(new CustomEvent(name, {
|
| 434 |
-
detail: {
|
| 435 |
-
width: this.width,
|
| 436 |
-
activeSurfaceId: this.activeSurfaceId,
|
| 437 |
-
},
|
| 438 |
-
}));
|
| 439 |
-
} catch {
|
| 440 |
-
// Resize events are an optimization hook for embedded surfaces.
|
| 441 |
-
}
|
| 442 |
-
},
|
| 443 |
-
|
| 444 |
-
cancelRestoreOpenSurface() {
|
| 445 |
-
if (!this._restoreOpenTimer) return;
|
| 446 |
-
globalThis.clearTimeout?.(this._restoreOpenTimer);
|
| 447 |
-
this._restoreOpenTimer = null;
|
| 448 |
-
},
|
| 449 |
-
|
| 450 |
-
scheduleRestoreOpenSurface() {
|
| 451 |
-
if (!this._restoreOpenRequested || this._restoreOpenTimer) return;
|
| 452 |
-
if (!this.shouldRender() || this.isMobileMode) return;
|
| 453 |
-
this._restoreOpenTimer = globalThis.setTimeout?.(() => {
|
| 454 |
-
this._restoreOpenTimer = null;
|
| 455 |
-
this.restoreOpenSurface();
|
| 456 |
-
}, 0) || null;
|
| 457 |
-
},
|
| 458 |
-
|
| 459 |
-
async restoreOpenSurface() {
|
| 460 |
-
if (!this._restoreOpenRequested || this._restoringSurface) return false;
|
| 461 |
-
if (!this.shouldRender() || this.isMobileMode) {
|
| 462 |
-
return false;
|
| 463 |
-
}
|
| 464 |
-
|
| 465 |
-
const targetId = normalizeSurfaceId(this.activeSurfaceId || this.panelSurfaces[0]?.id || "");
|
| 466 |
-
if (!targetId || !this.getSurface(targetId)) {
|
| 467 |
-
return false;
|
| 468 |
-
}
|
| 469 |
-
|
| 470 |
-
this._restoreOpenRequested = false;
|
| 471 |
-
this._restoringSurface = true;
|
| 472 |
-
try {
|
| 473 |
-
return await this.open(targetId, { source: "reload-restore" });
|
| 474 |
-
} finally {
|
| 475 |
-
this._restoringSurface = false;
|
| 476 |
-
}
|
| 477 |
-
},
|
| 478 |
-
|
| 479 |
-
persist() {
|
| 480 |
-
const state = {
|
| 481 |
-
isOpen: this.isOpen,
|
| 482 |
-
activeSurfaceId: this.activeSurfaceId,
|
| 483 |
-
surfaceModes: this.surfaceModes,
|
| 484 |
-
width: this.width,
|
| 485 |
-
};
|
| 486 |
-
try {
|
| 487 |
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
| 488 |
-
} catch (error) {
|
| 489 |
-
console.warn("Could not persist right canvas state", error);
|
| 490 |
-
}
|
| 491 |
-
try {
|
| 492 |
-
sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(state));
|
| 493 |
-
} catch (error) {
|
| 494 |
-
console.warn("Could not persist right canvas session state", error);
|
| 495 |
-
}
|
| 496 |
-
},
|
| 497 |
-
|
| 498 |
-
restore() {
|
| 499 |
-
this.width = this.defaultWidth();
|
| 500 |
-
try {
|
| 501 |
-
const saved = migratePersistedSurfaceState(JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}"));
|
| 502 |
-
this.isOpen = false;
|
| 503 |
-
this.activeSurfaceId = String(saved.activeSurfaceId || "");
|
| 504 |
-
this.surfaceModes = Object.fromEntries(
|
| 505 |
-
Object.entries(saved.surfaceModes || {}).map(([surfaceId, mode]) => [
|
| 506 |
-
surfaceId,
|
| 507 |
-
normalizeSurfaceMode(mode),
|
| 508 |
-
]),
|
| 509 |
-
);
|
| 510 |
-
if (Number.isFinite(Number(saved.width))) this.width = Number(saved.width);
|
| 511 |
-
} catch (error) {
|
| 512 |
-
console.warn("Could not restore right canvas state", error);
|
| 513 |
-
}
|
| 514 |
-
|
| 515 |
-
if (isReloadNavigation()) {
|
| 516 |
-
try {
|
| 517 |
-
const savedSession = migratePersistedSurfaceState(
|
| 518 |
-
JSON.parse(sessionStorage.getItem(SESSION_STORAGE_KEY) || "{}"),
|
| 519 |
-
);
|
| 520 |
-
if (savedSession?.activeSurfaceId) this.activeSurfaceId = String(savedSession.activeSurfaceId);
|
| 521 |
-
if (savedSession?.surfaceModes) {
|
| 522 |
-
this.surfaceModes = Object.fromEntries(
|
| 523 |
-
Object.entries(savedSession.surfaceModes || {}).map(([surfaceId, mode]) => [
|
| 524 |
-
surfaceId,
|
| 525 |
-
normalizeSurfaceMode(mode),
|
| 526 |
-
]),
|
| 527 |
-
);
|
| 528 |
-
}
|
| 529 |
-
if (Number.isFinite(Number(savedSession?.width))) this.width = Number(savedSession.width);
|
| 530 |
-
this._restoreOpenRequested = Boolean(savedSession?.isOpen && savedSession?.activeSurfaceId);
|
| 531 |
-
} catch (error) {
|
| 532 |
-
console.warn("Could not restore right canvas session state", error);
|
| 533 |
-
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
| 534 |
-
}
|
| 535 |
-
} else {
|
| 536 |
-
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
| 537 |
-
}
|
| 538 |
-
|
| 539 |
-
this.setWidth(this.width, { persist: false });
|
| 540 |
-
},
|
| 541 |
-
|
| 542 |
-
updateLayoutMode() {
|
| 543 |
-
const width = viewportWidth();
|
| 544 |
-
const wasMobileMode = this.isMobileMode;
|
| 545 |
-
this.isOverlayMode = width < DESKTOP_BREAKPOINT;
|
| 546 |
-
this.isMobileMode = width <= MOBILE_BREAKPOINT;
|
| 547 |
-
if (this.isMobileMode) {
|
| 548 |
-
const wasOpen = this.isOpen;
|
| 549 |
-
const mountedIds = this.mountedSurfaceIds();
|
| 550 |
-
this.isOpen = false;
|
| 551 |
-
this.mountedSurfaces = {};
|
| 552 |
-
if ((wasOpen || mountedIds.length > 0) && mountedIds.length > 0) {
|
| 553 |
-
globalThis.setTimeout?.(() => {
|
| 554 |
-
for (const surfaceId of mountedIds) {
|
| 555 |
-
const surface = this.getSurface(surfaceId);
|
| 556 |
-
const payload = this._lastPayloadBySurface[surfaceId] || {};
|
| 557 |
-
surface?.close?.({ ...payload, reason: "mobile" });
|
| 558 |
-
}
|
| 559 |
-
}, 0);
|
| 560 |
-
}
|
| 561 |
-
} else if (wasMobileMode && this.width < MIN_WIDTH) {
|
| 562 |
-
this.width = this.defaultWidth();
|
| 563 |
-
}
|
| 564 |
-
},
|
| 565 |
-
|
| 566 |
-
applyLayoutState() {
|
| 567 |
-
this.updateLayoutMode();
|
| 568 |
-
document.documentElement.style.setProperty("--right-canvas-width", `${this.width}px`);
|
| 569 |
-
document.body.classList.toggle("right-canvas-open", this.isOpen && !this.isMobileMode && this.shouldRender());
|
| 570 |
-
document.body.classList.toggle("right-canvas-overlay-mode", this.isOverlayMode);
|
| 571 |
-
document.body.classList.toggle("right-canvas-mobile-mode", this.isMobileMode);
|
| 572 |
-
this.scheduleRestoreOpenSurface();
|
| 573 |
-
},
|
| 574 |
-
|
| 575 |
-
widthStyle() {
|
| 576 |
-
if (this.isMobileMode) return "";
|
| 577 |
-
if (!this.isOpen) return "width: 0;";
|
| 578 |
-
if (this.isOverlayMode) {
|
| 579 |
-
return `width: min(${this.width}px, calc(100vw - 44px));`;
|
| 580 |
-
}
|
| 581 |
-
return `width: ${this.width}px;`;
|
| 582 |
-
},
|
| 583 |
-
|
| 584 |
-
getSurface(id) {
|
| 585 |
-
const targetId = normalizeSurfaceId(id);
|
| 586 |
-
return this.surfaces.find((surface) => surface.id === targetId)
|
| 587 |
-
|| getRegisteredSurfaces().find((surface) => surface.id === targetId)
|
| 588 |
-
|| null;
|
| 589 |
-
},
|
| 590 |
-
|
| 591 |
-
get railSurfaces() {
|
| 592 |
-
return this.surfaces;
|
| 593 |
-
},
|
| 594 |
-
|
| 595 |
-
get panelSurfaces() {
|
| 596 |
-
return this.surfaces.filter((surface) => !surface.actionOnly);
|
| 597 |
-
},
|
| 598 |
-
|
| 599 |
-
currentSurface() {
|
| 600 |
-
return this.getSurface(this.activeSurfaceId);
|
| 601 |
-
},
|
| 602 |
-
|
| 603 |
-
isSurfaceActive(id) {
|
| 604 |
-
return this.activeSurfaceId === normalizeSurfaceId(id);
|
| 605 |
-
},
|
| 606 |
-
|
| 607 |
-
activeTitle() {
|
| 608 |
-
return this.currentSurface()?.title || "Canvas";
|
| 609 |
-
},
|
| 610 |
-
|
| 611 |
-
isWelcomeVisible() {
|
| 612 |
-
return !chatsStore.selected;
|
| 613 |
-
},
|
| 614 |
-
|
| 615 |
-
shouldRender() {
|
| 616 |
-
return !this.isWelcomeVisible();
|
| 617 |
-
},
|
| 618 |
-
};
|
| 619 |
-
|
| 620 |
-
export const store = createStore("rightCanvas", model);
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:594000d14b05173a185721add68a5bfc02e6b71de6ec5d72b1e915190f826931
|
| 3 |
+
size 19660
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/canvas/right-canvas.css
CHANGED
|
@@ -1,384 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
}
|
| 5 |
-
|
| 6 |
-
#right-panel {
|
| 7 |
-
position: relative;
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
.container > x-component[path="canvas/right-canvas.html"],
|
| 11 |
-
.container > x-component[path="canvas/right-canvas.html"] > div[x-data] {
|
| 12 |
-
display: flex;
|
| 13 |
-
flex: 0 0 auto;
|
| 14 |
-
height: 100%;
|
| 15 |
-
min-width: 0;
|
| 16 |
-
min-height: 0;
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
body.right-canvas-resizing {
|
| 20 |
-
cursor: col-resize;
|
| 21 |
-
user-select: none;
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
.right-canvas {
|
| 25 |
-
--right-canvas-chrome: color-mix(in srgb, var(--color-background) 94%, #000 6%);
|
| 26 |
-
--right-canvas-surface: color-mix(in srgb, var(--color-panel) 88%, var(--color-background) 12%);
|
| 27 |
-
--right-canvas-border: color-mix(in srgb, var(--color-border) 70%, transparent);
|
| 28 |
-
position: relative;
|
| 29 |
-
z-index: 1200;
|
| 30 |
-
display: flex;
|
| 31 |
-
flex: 0 0 auto;
|
| 32 |
-
height: 100%;
|
| 33 |
-
min-width: 0;
|
| 34 |
-
max-width: none;
|
| 35 |
-
overflow: hidden;
|
| 36 |
-
border-left: 1px solid var(--right-canvas-border);
|
| 37 |
-
background: var(--right-canvas-chrome);
|
| 38 |
-
transition: width 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
.right-canvas-rail {
|
| 42 |
-
position: absolute;
|
| 43 |
-
top: 33%;
|
| 44 |
-
right: 100%;
|
| 45 |
-
z-index: 4;
|
| 46 |
-
display: flex;
|
| 47 |
-
flex: 0 0 52px;
|
| 48 |
-
flex-direction: column;
|
| 49 |
-
align-items: center;
|
| 50 |
-
gap: 7px;
|
| 51 |
-
height: auto;
|
| 52 |
-
max-height: min(70vh, calc(100vh - 32px));
|
| 53 |
-
padding: 10px 7px;
|
| 54 |
-
transform: translateY(-50%);
|
| 55 |
-
border-right: 0;
|
| 56 |
-
border-radius: 8px 0 0 8px;
|
| 57 |
-
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.24);
|
| 58 |
-
background: var(--color-chat-background);
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
.right-canvas-rail-separator {
|
| 62 |
-
width: 24px;
|
| 63 |
-
height: 1px;
|
| 64 |
-
margin: 1px 0 3px;
|
| 65 |
-
background: color-mix(in srgb, var(--right-canvas-border) 76%, transparent);
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
.right-canvas.is-closed {
|
| 69 |
-
width: 0 !important;
|
| 70 |
-
min-width: 0;
|
| 71 |
-
overflow: visible;
|
| 72 |
-
border-left: 0;
|
| 73 |
-
background: transparent;
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
.right-canvas.is-open {
|
| 77 |
-
overflow: visible;
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
.right-canvas.is-closed .right-canvas-shell,
|
| 81 |
-
.right-canvas.is-closed .right-canvas-resize-handle {
|
| 82 |
-
display: none;
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
.right-canvas-rail-button,
|
| 86 |
-
.right-canvas-icon-button,
|
| 87 |
-
.right-canvas-tab {
|
| 88 |
-
appearance: none;
|
| 89 |
-
border: 1px solid transparent;
|
| 90 |
-
color: var(--color-text);
|
| 91 |
-
font: inherit;
|
| 92 |
-
cursor: pointer;
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
.right-canvas-rail-button,
|
| 96 |
-
.right-canvas-icon-button {
|
| 97 |
-
display: inline-flex;
|
| 98 |
-
align-items: center;
|
| 99 |
-
justify-content: center;
|
| 100 |
-
width: 34px;
|
| 101 |
-
height: 34px;
|
| 102 |
-
min-width: 34px;
|
| 103 |
-
min-height: 34px;
|
| 104 |
-
padding: 0;
|
| 105 |
-
border-radius: 7px;
|
| 106 |
-
background: transparent;
|
| 107 |
-
opacity: 0.76;
|
| 108 |
-
transition: background-color 0.16s ease, border-color 0.16s ease, opacity 0.16s ease;
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
.right-canvas-toggle-button {
|
| 112 |
-
color: color-mix(in srgb, var(--color-primary) 70%, var(--color-text));
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
.right-canvas-rail-button:hover,
|
| 116 |
-
.right-canvas-icon-button:hover,
|
| 117 |
-
.right-canvas-rail-button.is-active {
|
| 118 |
-
opacity: 1;
|
| 119 |
-
border-color: color-mix(in srgb, var(--color-primary) 28%, var(--right-canvas-border));
|
| 120 |
-
background: color-mix(in srgb, var(--color-background-hover) 72%, transparent);
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
.right-canvas-rail-button .material-symbols-outlined,
|
| 124 |
-
.right-canvas-icon-button .material-symbols-outlined {
|
| 125 |
-
font-size: 19px;
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
.right-canvas-surface-image {
|
| 129 |
-
display: block;
|
| 130 |
-
width: 22px;
|
| 131 |
-
height: 22px;
|
| 132 |
-
border-radius: 6px;
|
| 133 |
-
object-fit: cover;
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
.right-canvas-shell {
|
| 137 |
-
display: flex;
|
| 138 |
-
flex: 1 1 auto;
|
| 139 |
-
min-width: 0;
|
| 140 |
-
min-height: 0;
|
| 141 |
-
flex-direction: column;
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
.right-canvas-header {
|
| 145 |
-
position: relative;
|
| 146 |
-
z-index: 10;
|
| 147 |
-
display: grid;
|
| 148 |
-
grid-template-columns: minmax(0, 1fr) auto;
|
| 149 |
-
align-items: center;
|
| 150 |
-
gap: 8px;
|
| 151 |
-
min-height: 45px;
|
| 152 |
-
padding: 7px 8px 0 10px;
|
| 153 |
-
border-bottom: 1px solid var(--right-canvas-border);
|
| 154 |
-
background: color-mix(in srgb, var(--color-background) 91%, #000 9%);
|
| 155 |
-
overflow: visible;
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
.right-canvas-tabs {
|
| 159 |
-
display: flex;
|
| 160 |
-
align-items: end;
|
| 161 |
-
gap: 4px;
|
| 162 |
-
min-width: 0;
|
| 163 |
-
overflow-x: auto;
|
| 164 |
-
overflow-y: hidden;
|
| 165 |
-
scrollbar-width: thin;
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
.right-canvas-tabs::-webkit-scrollbar {
|
| 169 |
-
height: 4px;
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
.right-canvas-tabs::-webkit-scrollbar-track {
|
| 173 |
-
background: transparent;
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
.right-canvas-tabs::-webkit-scrollbar-thumb {
|
| 177 |
-
background: color-mix(in srgb, var(--color-border) 76%, transparent);
|
| 178 |
-
border-radius: 999px;
|
| 179 |
-
}
|
| 180 |
-
|
| 181 |
-
.right-canvas-tab {
|
| 182 |
-
display: inline-flex;
|
| 183 |
-
flex: 0 1 auto;
|
| 184 |
-
align-items: center;
|
| 185 |
-
gap: 7px;
|
| 186 |
-
min-width: 0;
|
| 187 |
-
padding: 0 10px;
|
| 188 |
-
background: transparent;
|
| 189 |
-
opacity: 0.4;
|
| 190 |
-
white-space: nowrap;
|
| 191 |
-
transition: border-color 0.16s ease, background-color 0.16s ease, opacity 0.16s ease;
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
.right-canvas-tab:hover,
|
| 195 |
-
.right-canvas-tab.is-active {
|
| 196 |
-
opacity: 1;
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
.right-canvas-tab .material-symbols-outlined {
|
| 200 |
-
flex: 0 0 auto;
|
| 201 |
-
font-size: 18px;
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
.right-canvas-tab .right-canvas-surface-image {
|
| 205 |
-
flex: 0 0 auto;
|
| 206 |
-
width: 18px;
|
| 207 |
-
height: 18px;
|
| 208 |
-
border-radius: 5px;
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
.right-canvas-tab-label {
|
| 212 |
-
min-width: 0;
|
| 213 |
-
overflow: hidden;
|
| 214 |
-
text-overflow: ellipsis;
|
| 215 |
-
font-size: 0.84rem;
|
| 216 |
-
font-weight: 650;
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
.right-canvas-toolbar {
|
| 220 |
-
position: relative;
|
| 221 |
-
z-index: 11;
|
| 222 |
-
display: flex;
|
| 223 |
-
align-items: center;
|
| 224 |
-
gap: 5px;
|
| 225 |
-
padding-bottom: 6px;
|
| 226 |
-
overflow: visible;
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
-
.right-canvas-panels {
|
| 230 |
-
position: relative;
|
| 231 |
-
z-index: 1;
|
| 232 |
-
display: flex;
|
| 233 |
-
flex: 1 1 auto;
|
| 234 |
-
min-width: 0;
|
| 235 |
-
min-height: 0;
|
| 236 |
-
overflow: hidden;
|
| 237 |
-
background: var(--right-canvas-surface);
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
.right-canvas-panels > x-extension {
|
| 241 |
-
display: contents;
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
.right-canvas-panels > x-extension > x-component {
|
| 245 |
-
display: contents;
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
.right-canvas-surface-panel {
|
| 249 |
-
position: absolute;
|
| 250 |
-
inset: 0;
|
| 251 |
-
display: flex;
|
| 252 |
-
flex: 0 0 auto;
|
| 253 |
-
width: 100%;
|
| 254 |
-
height: 100%;
|
| 255 |
-
min-width: 0;
|
| 256 |
-
min-height: 0;
|
| 257 |
-
overflow: hidden;
|
| 258 |
-
opacity: 0;
|
| 259 |
-
pointer-events: none;
|
| 260 |
-
z-index: 0;
|
| 261 |
-
}
|
| 262 |
-
|
| 263 |
-
.right-canvas-surface-panel.is-active {
|
| 264 |
-
opacity: 1;
|
| 265 |
-
pointer-events: auto;
|
| 266 |
-
z-index: 2;
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
.right-canvas-surface-panel > x-component,
|
| 270 |
-
.right-canvas-surface-panel > x-component > div[x-data] {
|
| 271 |
-
display: flex;
|
| 272 |
-
flex: 1 1 auto;
|
| 273 |
-
width: 100%;
|
| 274 |
-
height: 100%;
|
| 275 |
-
min-width: 0;
|
| 276 |
-
min-height: 0;
|
| 277 |
-
}
|
| 278 |
-
|
| 279 |
-
.right-canvas-empty-state {
|
| 280 |
-
display: grid;
|
| 281 |
-
flex: 1 1 auto;
|
| 282 |
-
place-items: center;
|
| 283 |
-
align-content: center;
|
| 284 |
-
gap: 8px;
|
| 285 |
-
color: color-mix(in srgb, var(--color-text) 60%, transparent);
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
.right-canvas-empty-state .material-symbols-outlined {
|
| 289 |
-
font-size: 28px;
|
| 290 |
-
}
|
| 291 |
-
|
| 292 |
-
.right-canvas-resize-handle {
|
| 293 |
-
position: absolute;
|
| 294 |
-
top: 0;
|
| 295 |
-
bottom: 0;
|
| 296 |
-
left: -3px;
|
| 297 |
-
z-index: 3;
|
| 298 |
-
width: 7px;
|
| 299 |
-
cursor: col-resize;
|
| 300 |
-
}
|
| 301 |
-
|
| 302 |
-
body.right-canvas-overlay-mode .right-canvas {
|
| 303 |
-
position: fixed;
|
| 304 |
-
top: 0;
|
| 305 |
-
right: 0;
|
| 306 |
-
bottom: 0;
|
| 307 |
-
height: 100%;
|
| 308 |
-
max-width: calc(100vw - 44px);
|
| 309 |
-
box-shadow: -16px 0 38px rgba(0, 0, 0, 0.34);
|
| 310 |
-
transform: translateX(100%);
|
| 311 |
-
}
|
| 312 |
-
|
| 313 |
-
body.right-canvas-overlay-mode .right-canvas.is-open {
|
| 314 |
-
transform: translateX(0);
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
body.right-canvas-overlay-mode .right-canvas-resize-handle {
|
| 318 |
-
display: none;
|
| 319 |
-
}
|
| 320 |
-
|
| 321 |
-
body.right-canvas-mobile-mode .right-canvas {
|
| 322 |
-
position: fixed;
|
| 323 |
-
top: 0;
|
| 324 |
-
right: 0;
|
| 325 |
-
bottom: 0;
|
| 326 |
-
z-index: 3400;
|
| 327 |
-
display: flex !important;
|
| 328 |
-
width: 0 !important;
|
| 329 |
-
min-width: 0;
|
| 330 |
-
height: 100%;
|
| 331 |
-
overflow: visible;
|
| 332 |
-
transform: none;
|
| 333 |
-
border-left: 0;
|
| 334 |
-
background: transparent;
|
| 335 |
-
box-shadow: none;
|
| 336 |
-
pointer-events: none;
|
| 337 |
-
}
|
| 338 |
-
|
| 339 |
-
body.right-canvas-mobile-mode .right-canvas.is-open {
|
| 340 |
-
transform: none;
|
| 341 |
-
}
|
| 342 |
-
|
| 343 |
-
body.right-canvas-mobile-mode .right-canvas-rail {
|
| 344 |
-
position: fixed;
|
| 345 |
-
top: 50%;
|
| 346 |
-
right: max(0px, env(safe-area-inset-right));
|
| 347 |
-
display: flex;
|
| 348 |
-
transform: translateY(-50%);
|
| 349 |
-
pointer-events: auto;
|
| 350 |
-
}
|
| 351 |
-
|
| 352 |
-
body.right-canvas-mobile-mode .right-canvas-shell,
|
| 353 |
-
body.right-canvas-mobile-mode .right-canvas-resize-handle {
|
| 354 |
-
display: none !important;
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
@media (max-width: 420px) {
|
| 358 |
-
body.right-canvas-mobile-mode .right-canvas-rail {
|
| 359 |
-
gap: 5px;
|
| 360 |
-
padding: 8px 6px;
|
| 361 |
-
transform: translateY(-50%) scale(0.92);
|
| 362 |
-
transform-origin: right center;
|
| 363 |
-
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.22);
|
| 364 |
-
}
|
| 365 |
-
|
| 366 |
-
body.right-canvas-mobile-mode .right-canvas-rail-button,
|
| 367 |
-
body.right-canvas-mobile-mode .right-canvas-icon-button {
|
| 368 |
-
width: 32px;
|
| 369 |
-
height: 32px;
|
| 370 |
-
min-width: 32px;
|
| 371 |
-
min-height: 32px;
|
| 372 |
-
}
|
| 373 |
-
|
| 374 |
-
body.right-canvas-mobile-mode .right-canvas-rail-button .material-symbols-outlined,
|
| 375 |
-
body.right-canvas-mobile-mode .right-canvas-icon-button .material-symbols-outlined {
|
| 376 |
-
font-size: 18px;
|
| 377 |
-
}
|
| 378 |
-
}
|
| 379 |
-
|
| 380 |
-
@media (max-width: 480px) {
|
| 381 |
-
.right-canvas-tab-label {
|
| 382 |
-
display: none;
|
| 383 |
-
}
|
| 384 |
-
}
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f2075f93cfeb6151b7bf50eaca76aed323ba3fa8a0fc24c491e8f12da19fc405
|
| 3 |
+
size 8046
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/canvas/right-canvas.html
CHANGED
|
@@ -1,132 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import { store } from "/components/canvas/right-canvas-store.js";
|
| 5 |
-
import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
|
| 6 |
-
</script>
|
| 7 |
-
</head>
|
| 8 |
-
<body>
|
| 9 |
-
<div x-data>
|
| 10 |
-
<template x-if="$store.rightCanvas">
|
| 11 |
-
<aside
|
| 12 |
-
id="right-canvas"
|
| 13 |
-
class="right-canvas"
|
| 14 |
-
:class="{
|
| 15 |
-
'is-open': $store.rightCanvas.isOpen,
|
| 16 |
-
'is-closed': !$store.rightCanvas.isOpen,
|
| 17 |
-
'is-overlay': $store.rightCanvas.isOverlayMode,
|
| 18 |
-
'is-mobile': $store.rightCanvas.isMobileMode
|
| 19 |
-
}"
|
| 20 |
-
:style="$store.rightCanvas.widthStyle()"
|
| 21 |
-
x-show="$store.rightCanvas.shouldRender()"
|
| 22 |
-
x-init="$store.rightCanvas.init($el)"
|
| 23 |
-
x-effect="$store.rightCanvas.applyLayoutState()"
|
| 24 |
-
aria-label="Universal Canvas"
|
| 25 |
-
>
|
| 26 |
-
<div class="right-canvas-resize-handle" title="Resize canvas" @pointerdown="$store.rightCanvas.startResize($event)"></div>
|
| 27 |
-
|
| 28 |
-
<nav class="right-canvas-rail" aria-label="Canvas surfaces" x-show="$store.preferences.isUiControlVisible('rightCanvasRail')">
|
| 29 |
-
<button
|
| 30 |
-
type="button"
|
| 31 |
-
class="right-canvas-rail-button right-canvas-toggle-button"
|
| 32 |
-
:class="{ 'is-active': $store.rightCanvas.isOpen }"
|
| 33 |
-
:title="$store.rightCanvas.isOpen ? 'Hide canvas' : 'Show canvas'"
|
| 34 |
-
:aria-label="$store.rightCanvas.isOpen ? 'Hide canvas' : 'Show canvas'"
|
| 35 |
-
:aria-pressed="$store.rightCanvas.isOpen.toString()"
|
| 36 |
-
@click="$store.rightCanvas.toggleCanvas()"
|
| 37 |
-
>
|
| 38 |
-
<x-icon name="dock_to_right"></x-icon>
|
| 39 |
-
</button>
|
| 40 |
-
|
| 41 |
-
<div class="right-canvas-rail-separator" aria-hidden="true"></div>
|
| 42 |
-
|
| 43 |
-
<template x-for="surface in $store.rightCanvas.railSurfaces" :key="surface.id">
|
| 44 |
-
<button
|
| 45 |
-
type="button"
|
| 46 |
-
class="right-canvas-rail-button"
|
| 47 |
-
:class="{ 'is-active': $store.rightCanvas.isSurfaceActive(surface.id) && $store.rightCanvas.isOpen }"
|
| 48 |
-
:title="surface.title"
|
| 49 |
-
:aria-label="surface.title"
|
| 50 |
-
@click="$store.rightCanvas.open(surface.id)"
|
| 51 |
-
>
|
| 52 |
-
<template x-if="surface.image">
|
| 53 |
-
<img class="right-canvas-surface-image" :src="surface.image" alt="" aria-hidden="true">
|
| 54 |
-
</template>
|
| 55 |
-
<template x-if="!surface.image">
|
| 56 |
-
<x-icon :name="surface.icon"></x-icon>
|
| 57 |
-
</template>
|
| 58 |
-
</button>
|
| 59 |
-
</template>
|
| 60 |
-
</nav>
|
| 61 |
-
|
| 62 |
-
<section class="right-canvas-shell">
|
| 63 |
-
<x-extension id="right-canvas-shell-start"></x-extension>
|
| 64 |
-
|
| 65 |
-
<header class="right-canvas-header">
|
| 66 |
-
<div class="right-canvas-tabs" role="tablist" aria-label="Canvas tabs">
|
| 67 |
-
<x-extension id="right-canvas-tabs-start"></x-extension>
|
| 68 |
-
<template x-for="surface in $store.rightCanvas.panelSurfaces" :key="surface.id">
|
| 69 |
-
<button
|
| 70 |
-
type="button"
|
| 71 |
-
class="right-canvas-tab"
|
| 72 |
-
role="tab"
|
| 73 |
-
:aria-selected="$store.rightCanvas.isSurfaceActive(surface.id).toString()"
|
| 74 |
-
:title="surface.title"
|
| 75 |
-
:class="{ 'is-active': $store.rightCanvas.isSurfaceActive(surface.id) }"
|
| 76 |
-
@click="$store.rightCanvas.open(surface.id)"
|
| 77 |
-
>
|
| 78 |
-
<template x-if="surface.image">
|
| 79 |
-
<img class="right-canvas-surface-image" :src="surface.image" alt="" aria-hidden="true">
|
| 80 |
-
</template>
|
| 81 |
-
<template x-if="!surface.image">
|
| 82 |
-
<x-icon aria-hidden="true" :name="surface.icon"></x-icon>
|
| 83 |
-
</template>
|
| 84 |
-
<span class="right-canvas-tab-label" x-text="surface.title"></span>
|
| 85 |
-
</button>
|
| 86 |
-
</template>
|
| 87 |
-
<x-extension id="right-canvas-tabs-end"></x-extension>
|
| 88 |
-
</div>
|
| 89 |
-
|
| 90 |
-
<div class="right-canvas-toolbar" aria-label="Canvas toolbar">
|
| 91 |
-
<x-extension id="right-canvas-toolbar-start"></x-extension>
|
| 92 |
-
<button
|
| 93 |
-
type="button"
|
| 94 |
-
class="right-canvas-icon-button"
|
| 95 |
-
title="Open as window"
|
| 96 |
-
aria-label="Open as window"
|
| 97 |
-
x-show="$store.rightCanvas.currentSurfaceCanUndock()"
|
| 98 |
-
@click="$store.rightCanvas.undockActiveSurface()"
|
| 99 |
-
>
|
| 100 |
-
<x-icon name="open_in_new"></x-icon>
|
| 101 |
-
</button>
|
| 102 |
-
<button
|
| 103 |
-
type="button"
|
| 104 |
-
class="right-canvas-icon-button right-canvas-close-button"
|
| 105 |
-
title="Close canvas"
|
| 106 |
-
aria-label="Close canvas"
|
| 107 |
-
@click="$store.rightCanvas.close()"
|
| 108 |
-
>
|
| 109 |
-
<x-icon name="close"></x-icon>
|
| 110 |
-
</button>
|
| 111 |
-
<x-extension id="right-canvas-toolbar-end"></x-extension>
|
| 112 |
-
</div>
|
| 113 |
-
</header>
|
| 114 |
-
|
| 115 |
-
<main class="right-canvas-panels">
|
| 116 |
-
<x-extension id="right-canvas-panels"></x-extension>
|
| 117 |
-
<template x-if="$store.rightCanvas.panelSurfaces.length === 0">
|
| 118 |
-
<div class="right-canvas-empty-state">
|
| 119 |
-
<x-extension id="right-canvas-empty-state"></x-extension>
|
| 120 |
-
<x-icon name="space_dashboard"></x-icon>
|
| 121 |
-
<span>Canvas</span>
|
| 122 |
-
</div>
|
| 123 |
-
</template>
|
| 124 |
-
</main>
|
| 125 |
-
|
| 126 |
-
<x-extension id="right-canvas-shell-end"></x-extension>
|
| 127 |
-
</section>
|
| 128 |
-
</aside>
|
| 129 |
-
</template>
|
| 130 |
-
</div>
|
| 131 |
-
</body>
|
| 132 |
-
</html>
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:325a3338a45d7df92b147f236725c639834a12c4ab1ee0ae603c4ede9e8dd624
|
| 3 |
+
size 5617
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/AGENTS.md
CHANGED
|
@@ -1,47 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
- Own chat composer, attachments, message queue, navigation, and top-section component groups.
|
| 6 |
-
|
| 7 |
-
## Ownership
|
| 8 |
-
|
| 9 |
-
- `input/` owns composer input, progress, banners, and bottom action bars.
|
| 10 |
-
- `attachments/` owns drag/drop and attachment preview UI.
|
| 11 |
-
- `message-queue/` owns queued message display and store state.
|
| 12 |
-
- `navigation/` owns chat navigation state.
|
| 13 |
-
- `top-section/` owns chat header/top area.
|
| 14 |
-
- `model-gate-store.js` and `model-setup-gate.html` own the deferred in-thread model setup gate.
|
| 15 |
-
|
| 16 |
-
## Local Contracts
|
| 17 |
-
|
| 18 |
-
- Preserve Store Gating for all store-backed chat components.
|
| 19 |
-
- Use shared API, WebSocket, notification, and attachment helpers where available.
|
| 20 |
-
- Do not bypass CSRF or WebSocket state-sync expectations.
|
| 21 |
-
- The shared composer can be mounted on the Welcome screen with no selected chat; sending from that state must create and select a chat context before dispatch.
|
| 22 |
-
- Text-only and attachment sends must render the first user turn immediately with the request message ID so the backend log merges into the same row.
|
| 23 |
-
- Unsent composer text is kept as a separate browser-session draft for each selected chat and restored when switching contexts; a Welcome-screen prompt must follow the chat created for its first send.
|
| 24 |
-
- Composer text uses the main UI font by default; typing a triple-backtick fence and pressing Enter turns that line into a visual code block that serializes back to fenced Markdown, while pasted fenced Markdown stays plain text.
|
| 25 |
-
- Missing model setup is gated at send intent: the first unconfigured send renders an in-thread setup card, keeps the pending prompt in browser session storage for refresh recovery, and must not call `/message_async` until a chat model is configured.
|
| 26 |
-
- While the setup gate is open, the composer remains typeable but send is blocked until setup succeeds.
|
| 27 |
-
- The setup gate must delegate Cloud/Local setup, account connections, and advanced model configuration to the existing onboarding and model-preset editor modals; do not duplicate provider/model/key forms inline.
|
| 28 |
-
- A connected OAuth account without Main/Utility model selection is its own gate state; route to model configuration and do not select models automatically.
|
| 29 |
-
- Model setup surfaces that change readiness must notify the gate with `model-setup-changed`, `model-configured`, or an existing modal/onboarding completion signal so the pending prompt can retry automatically.
|
| 30 |
-
- The top-section project selector, clock, and connection indicator must respect the instance-level mobile/desktop visibility preferences.
|
| 31 |
-
- While the selected context is running, an empty composer makes the primary button stop the active run; typed text still adds to the queue, and Enter with an empty composer still sends queued messages.
|
| 32 |
-
- Chat navigation controls must cross virtual message-window boundaries; top and bottom target the full cached history rather than only the mounted DOM slice.
|
| 33 |
-
- The page-wide attachment drop overlay must activate only for external file drags so internal WebUI drag interactions keep their own targets.
|
| 34 |
-
|
| 35 |
-
## Work Guidance
|
| 36 |
-
|
| 37 |
-
- Keep composer and attachment changes responsive across desktop and mobile.
|
| 38 |
-
- Coordinate payload changes with backend chat, upload, and WebSocket handlers.
|
| 39 |
-
|
| 40 |
-
## Verification
|
| 41 |
-
|
| 42 |
-
- Smoke-test sending, queued messages, attachments, drag/drop, and navigation after visible changes.
|
| 43 |
-
- Smoke-test the unconfigured first-send gate and automatic dispatch after model setup when touching model setup or send interception.
|
| 44 |
-
|
| 45 |
-
## Child DOX Index
|
| 46 |
-
|
| 47 |
-
No child DOX files.
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:145d134096c801ba7884cdf7ca6a494950af6f4f9afbb704ac5f97e7d7eac5ac
|
| 3 |
+
size 3545
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/attachments/attachmentsStore.js
CHANGED
|
@@ -1,511 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
const model = {
|
| 6 |
-
// State properties
|
| 7 |
-
attachments: [],
|
| 8 |
-
hasAttachments: false,
|
| 9 |
-
dragDropOverlayVisible: false,
|
| 10 |
-
|
| 11 |
-
async init() {
|
| 12 |
-
await this.initialize();
|
| 13 |
-
},
|
| 14 |
-
|
| 15 |
-
// Initialize the store
|
| 16 |
-
async initialize() {
|
| 17 |
-
// Setup event listeners for drag and drop
|
| 18 |
-
this.setupDragDropHandlers();
|
| 19 |
-
// Setup paste event listener for clipboard images
|
| 20 |
-
this.setupPasteHandler();
|
| 21 |
-
},
|
| 22 |
-
|
| 23 |
-
// Basic attachment management methods
|
| 24 |
-
addAttachment(attachment) {
|
| 25 |
-
// Validate for duplicates
|
| 26 |
-
if (this.validateDuplicates(attachment)) {
|
| 27 |
-
this.attachments.push(attachment);
|
| 28 |
-
this.updateAttachmentState();
|
| 29 |
-
}
|
| 30 |
-
},
|
| 31 |
-
|
| 32 |
-
removeAttachment(index) {
|
| 33 |
-
if (index >= 0 && index < this.attachments.length) {
|
| 34 |
-
this.attachments.splice(index, 1);
|
| 35 |
-
this.updateAttachmentState();
|
| 36 |
-
}
|
| 37 |
-
},
|
| 38 |
-
|
| 39 |
-
clearAttachments() {
|
| 40 |
-
this.attachments = [];
|
| 41 |
-
this.updateAttachmentState();
|
| 42 |
-
},
|
| 43 |
-
|
| 44 |
-
validateDuplicates(newAttachment) {
|
| 45 |
-
// Check if attachment already exists based on name and size
|
| 46 |
-
const isDuplicate = this.attachments.some(
|
| 47 |
-
(existing) =>
|
| 48 |
-
existing.name === newAttachment.name &&
|
| 49 |
-
existing.file &&
|
| 50 |
-
newAttachment.file &&
|
| 51 |
-
existing.file.size === newAttachment.file.size
|
| 52 |
-
);
|
| 53 |
-
return !isDuplicate;
|
| 54 |
-
},
|
| 55 |
-
|
| 56 |
-
updateAttachmentState() {
|
| 57 |
-
this.hasAttachments = this.attachments.length > 0;
|
| 58 |
-
},
|
| 59 |
-
|
| 60 |
-
// Drag drop overlay control methods
|
| 61 |
-
showDragDropOverlay() {
|
| 62 |
-
this.dragDropOverlayVisible = true;
|
| 63 |
-
},
|
| 64 |
-
|
| 65 |
-
hideDragDropOverlay() {
|
| 66 |
-
this.dragDropOverlayVisible = false;
|
| 67 |
-
},
|
| 68 |
-
|
| 69 |
-
isExternalFileDrag(event) {
|
| 70 |
-
return Array.from(event?.dataTransfer?.types || []).includes("Files");
|
| 71 |
-
},
|
| 72 |
-
|
| 73 |
-
// Setup drag and drop event handlers
|
| 74 |
-
setupDragDropHandlers() {
|
| 75 |
-
console.log("Setting up drag and drop handlers...");
|
| 76 |
-
let dragCounter = 0;
|
| 77 |
-
|
| 78 |
-
// Prevent default drag behaviors
|
| 79 |
-
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
| 80 |
-
document.addEventListener(
|
| 81 |
-
eventName,
|
| 82 |
-
(e) => {
|
| 83 |
-
if (!this.isExternalFileDrag(e)) return;
|
| 84 |
-
e.preventDefault();
|
| 85 |
-
e.stopPropagation();
|
| 86 |
-
},
|
| 87 |
-
false
|
| 88 |
-
);
|
| 89 |
-
});
|
| 90 |
-
|
| 91 |
-
// Handle drag enter
|
| 92 |
-
document.addEventListener(
|
| 93 |
-
"dragenter",
|
| 94 |
-
(e) => {
|
| 95 |
-
if (!this.isExternalFileDrag(e)) return;
|
| 96 |
-
console.log("Drag enter detected");
|
| 97 |
-
dragCounter++;
|
| 98 |
-
if (dragCounter === 1) {
|
| 99 |
-
console.log("Showing drag drop overlay");
|
| 100 |
-
this.showDragDropOverlay();
|
| 101 |
-
}
|
| 102 |
-
},
|
| 103 |
-
false
|
| 104 |
-
);
|
| 105 |
-
|
| 106 |
-
// Handle drag leave
|
| 107 |
-
document.addEventListener(
|
| 108 |
-
"dragleave",
|
| 109 |
-
(e) => {
|
| 110 |
-
if (!this.isExternalFileDrag(e) && dragCounter === 0) return;
|
| 111 |
-
dragCounter--;
|
| 112 |
-
if (dragCounter === 0) {
|
| 113 |
-
this.hideDragDropOverlay();
|
| 114 |
-
}
|
| 115 |
-
},
|
| 116 |
-
false
|
| 117 |
-
);
|
| 118 |
-
|
| 119 |
-
// Handle drop
|
| 120 |
-
document.addEventListener(
|
| 121 |
-
"drop",
|
| 122 |
-
async (e) => {
|
| 123 |
-
if (!this.isExternalFileDrag(e)) return;
|
| 124 |
-
const dataTransfer = e.dataTransfer;
|
| 125 |
-
console.log("Drop detected with files:", dataTransfer?.files?.length || 0);
|
| 126 |
-
dragCounter = 0;
|
| 127 |
-
this.hideDragDropOverlay();
|
| 128 |
-
|
| 129 |
-
let files = [];
|
| 130 |
-
try {
|
| 131 |
-
files = await this.getDroppedFiles(dataTransfer);
|
| 132 |
-
} catch (error) {
|
| 133 |
-
console.error("Failed to read dropped files:", error);
|
| 134 |
-
files = Array.from(dataTransfer?.files || []);
|
| 135 |
-
}
|
| 136 |
-
this.handleFiles(files);
|
| 137 |
-
},
|
| 138 |
-
false
|
| 139 |
-
);
|
| 140 |
-
},
|
| 141 |
-
|
| 142 |
-
// Setup paste event handler for clipboard images
|
| 143 |
-
setupPasteHandler() {
|
| 144 |
-
console.log("Setting up paste handler...");
|
| 145 |
-
document.addEventListener("paste", (e) => {
|
| 146 |
-
// console.log("Paste event detected, target:", e.target.id);
|
| 147 |
-
if (
|
| 148 |
-
e.target.id !== "chat-input" &&
|
| 149 |
-
e.target.id !== "full-screen-input"
|
| 150 |
-
) return;
|
| 151 |
-
|
| 152 |
-
const items = e.clipboardData.items;
|
| 153 |
-
let imageFound = false;
|
| 154 |
-
console.log("Checking clipboard items:", items.length);
|
| 155 |
-
|
| 156 |
-
// First, check if there are any images in the clipboard
|
| 157 |
-
for (let i = 0; i < items.length; i++) {
|
| 158 |
-
const item = items[i];
|
| 159 |
-
if (item.type.indexOf("image") !== -1) {
|
| 160 |
-
imageFound = true;
|
| 161 |
-
const blob = item.getAsFile();
|
| 162 |
-
if (blob) {
|
| 163 |
-
e.preventDefault(); // Prevent default paste behavior for images
|
| 164 |
-
this.handleClipboardImage(blob);
|
| 165 |
-
console.log("Image detected in clipboard, processing...");
|
| 166 |
-
}
|
| 167 |
-
break; // Only handle the first image found
|
| 168 |
-
}
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
// If no images found and we're in an input field, let normal text paste happen
|
| 172 |
-
if (
|
| 173 |
-
!imageFound &&
|
| 174 |
-
(e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")
|
| 175 |
-
) {
|
| 176 |
-
console.log(
|
| 177 |
-
"No images in clipboard, allowing normal text paste in input field"
|
| 178 |
-
);
|
| 179 |
-
return;
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
// If no images found and not in input field, do nothing
|
| 183 |
-
if (!imageFound) {
|
| 184 |
-
console.log("No images in clipboard");
|
| 185 |
-
}
|
| 186 |
-
});
|
| 187 |
-
},
|
| 188 |
-
|
| 189 |
-
// Handle clipboard image pasting
|
| 190 |
-
async handleClipboardImage(blob) {
|
| 191 |
-
try {
|
| 192 |
-
// Generate unique filename
|
| 193 |
-
const guid = this.generateGUID();
|
| 194 |
-
const filename = `clipboard-${guid}.png`;
|
| 195 |
-
|
| 196 |
-
// Create file object from blob
|
| 197 |
-
const file = new File([blob], filename, { type: "image/png" });
|
| 198 |
-
|
| 199 |
-
// Create attachment object
|
| 200 |
-
const attachment = {
|
| 201 |
-
file: file,
|
| 202 |
-
type: "image",
|
| 203 |
-
name: filename,
|
| 204 |
-
extension: "png",
|
| 205 |
-
displayInfo: this.getAttachmentDisplayInfo(file),
|
| 206 |
-
};
|
| 207 |
-
|
| 208 |
-
// Read as data URL for preview
|
| 209 |
-
const reader = new FileReader();
|
| 210 |
-
reader.onload = (e) => {
|
| 211 |
-
attachment.url = e.target.result;
|
| 212 |
-
this.addAttachment(attachment);
|
| 213 |
-
};
|
| 214 |
-
reader.readAsDataURL(file);
|
| 215 |
-
|
| 216 |
-
// Show success feedback
|
| 217 |
-
console.log("Clipboard image pasted successfully:", filename);
|
| 218 |
-
} catch (error) {
|
| 219 |
-
console.error("Failed to handle clipboard image:", error);
|
| 220 |
-
}
|
| 221 |
-
},
|
| 222 |
-
|
| 223 |
-
// Update handleFileUpload to use the attachments store
|
| 224 |
-
handleFileUpload(event) {
|
| 225 |
-
const files = event.target.files;
|
| 226 |
-
this.handleFiles(files);
|
| 227 |
-
event.target.value = ""; // clear uploader selection to fix issue where same file is ignored the second time
|
| 228 |
-
},
|
| 229 |
-
|
| 230 |
-
async getDroppedFiles(dataTransfer) {
|
| 231 |
-
const items = Array.from(dataTransfer?.items || []);
|
| 232 |
-
const entries = items
|
| 233 |
-
.map((item) =>
|
| 234 |
-
typeof item.webkitGetAsEntry === "function"
|
| 235 |
-
? item.webkitGetAsEntry()
|
| 236 |
-
: null
|
| 237 |
-
)
|
| 238 |
-
.filter(Boolean);
|
| 239 |
-
|
| 240 |
-
if (!entries.length) {
|
| 241 |
-
return Array.from(dataTransfer?.files || []);
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
const nested = await Promise.all(
|
| 245 |
-
entries.map((entry) => this.readEntryFiles(entry))
|
| 246 |
-
);
|
| 247 |
-
const files = nested.flat();
|
| 248 |
-
return files.length ? files : Array.from(dataTransfer?.files || []);
|
| 249 |
-
},
|
| 250 |
-
|
| 251 |
-
async readEntryFiles(entry) {
|
| 252 |
-
if (entry.isFile) {
|
| 253 |
-
return await new Promise((resolve, reject) => {
|
| 254 |
-
entry.file(
|
| 255 |
-
(file) => resolve([file]),
|
| 256 |
-
(error) => reject(error)
|
| 257 |
-
);
|
| 258 |
-
});
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
if (!entry.isDirectory) return [];
|
| 262 |
-
|
| 263 |
-
const reader = entry.createReader();
|
| 264 |
-
const childEntries = await this.readAllDirectoryEntries(reader);
|
| 265 |
-
const nested = await Promise.all(
|
| 266 |
-
childEntries.map((child) => this.readEntryFiles(child))
|
| 267 |
-
);
|
| 268 |
-
return nested.flat();
|
| 269 |
-
},
|
| 270 |
-
|
| 271 |
-
async readAllDirectoryEntries(reader) {
|
| 272 |
-
const entries = [];
|
| 273 |
-
|
| 274 |
-
return await new Promise((resolve, reject) => {
|
| 275 |
-
const readBatch = () => {
|
| 276 |
-
reader.readEntries(
|
| 277 |
-
(batch) => {
|
| 278 |
-
if (!batch.length) {
|
| 279 |
-
resolve(entries);
|
| 280 |
-
return;
|
| 281 |
-
}
|
| 282 |
-
entries.push(...batch);
|
| 283 |
-
readBatch();
|
| 284 |
-
},
|
| 285 |
-
(error) => reject(error)
|
| 286 |
-
);
|
| 287 |
-
};
|
| 288 |
-
|
| 289 |
-
readBatch();
|
| 290 |
-
});
|
| 291 |
-
},
|
| 292 |
-
|
| 293 |
-
// File handling logic (moved from index.js)
|
| 294 |
-
handleFiles(files) {
|
| 295 |
-
const fileList = Array.from(files || []);
|
| 296 |
-
console.log("handleFiles called with", fileList.length, "files");
|
| 297 |
-
fileList.forEach((file) => {
|
| 298 |
-
if (!file?.name) return;
|
| 299 |
-
|
| 300 |
-
console.log("Processing file:", file.name, file.type);
|
| 301 |
-
const ext = file.name.split(".").pop().toLowerCase();
|
| 302 |
-
const isImage = ["jpg", "jpeg", "png", "bmp", "gif", "webp", "svg"].includes(
|
| 303 |
-
ext
|
| 304 |
-
);
|
| 305 |
-
|
| 306 |
-
const attachment = {
|
| 307 |
-
file: file,
|
| 308 |
-
type: isImage ? "image" : "file",
|
| 309 |
-
name: file.name,
|
| 310 |
-
extension: ext,
|
| 311 |
-
displayInfo: this.getAttachmentDisplayInfo(file),
|
| 312 |
-
};
|
| 313 |
-
|
| 314 |
-
if (isImage) {
|
| 315 |
-
// Read image as data URL for preview
|
| 316 |
-
const reader = new FileReader();
|
| 317 |
-
reader.onload = (e) => {
|
| 318 |
-
attachment.url = e.target.result;
|
| 319 |
-
this.addAttachment(attachment);
|
| 320 |
-
};
|
| 321 |
-
reader.readAsDataURL(file);
|
| 322 |
-
} else {
|
| 323 |
-
// For non-image files, add directly
|
| 324 |
-
this.addAttachment(attachment);
|
| 325 |
-
}
|
| 326 |
-
});
|
| 327 |
-
},
|
| 328 |
-
|
| 329 |
-
// Get attachments for sending message
|
| 330 |
-
getAttachmentsForSending() {
|
| 331 |
-
return this.attachments.map((attachment) => {
|
| 332 |
-
if (attachment.type === "image") {
|
| 333 |
-
return {
|
| 334 |
-
...attachment,
|
| 335 |
-
url: URL.createObjectURL(attachment.file),
|
| 336 |
-
};
|
| 337 |
-
} else {
|
| 338 |
-
return {
|
| 339 |
-
...attachment,
|
| 340 |
-
};
|
| 341 |
-
}
|
| 342 |
-
});
|
| 343 |
-
},
|
| 344 |
-
|
| 345 |
-
// Generate server-side API URL for file (for device sync)
|
| 346 |
-
getServerImgUrl(filename) {
|
| 347 |
-
return `/api/image_get?path=/a0/usr/uploads/${encodeURIComponent(filename)}`;
|
| 348 |
-
},
|
| 349 |
-
|
| 350 |
-
getServerFileUrl(filename) {
|
| 351 |
-
return `/a0/usr/uploads/${encodeURIComponent(filename)}`;
|
| 352 |
-
},
|
| 353 |
-
|
| 354 |
-
// Check if file is an image based on extension
|
| 355 |
-
isImageFile(filename) {
|
| 356 |
-
const imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"];
|
| 357 |
-
const extension = filename.split(".").pop().toLowerCase();
|
| 358 |
-
return imageExtensions.includes(extension);
|
| 359 |
-
},
|
| 360 |
-
|
| 361 |
-
// Get attachment preview URL (server URL for persistence, blob URL for current session)
|
| 362 |
-
getAttachmentPreviewUrl(attachment) {
|
| 363 |
-
// If attachment has a name and we're dealing with a server-stored file
|
| 364 |
-
if (typeof attachment === "string") {
|
| 365 |
-
// attachment is just a filename (from loaded chat)
|
| 366 |
-
return this.getServerImgUrl(attachment);
|
| 367 |
-
} else if (attachment.name && attachment.file) {
|
| 368 |
-
// attachment is an object from current session
|
| 369 |
-
if (attachment.type === "image") {
|
| 370 |
-
// For images, use blob URL for current session preview
|
| 371 |
-
return attachment.url || URL.createObjectURL(attachment.file);
|
| 372 |
-
} else {
|
| 373 |
-
// For non-image files, use server URL to get appropriate icon
|
| 374 |
-
return this.getServerImgUrl(attachment.name);
|
| 375 |
-
}
|
| 376 |
-
}
|
| 377 |
-
return null;
|
| 378 |
-
},
|
| 379 |
-
|
| 380 |
-
getFilePreviewUrl(filename) {
|
| 381 |
-
const extension = filename.split(".").pop().toLowerCase();
|
| 382 |
-
const types = {
|
| 383 |
-
// Archive files
|
| 384 |
-
zip: "archive",
|
| 385 |
-
rar: "archive",
|
| 386 |
-
"7z": "archive",
|
| 387 |
-
tar: "archive",
|
| 388 |
-
gz: "archive",
|
| 389 |
-
// Document files
|
| 390 |
-
pdf: "document",
|
| 391 |
-
doc: "document",
|
| 392 |
-
docx: "document",
|
| 393 |
-
txt: "document",
|
| 394 |
-
rtf: "document",
|
| 395 |
-
odt: "document",
|
| 396 |
-
// Code files
|
| 397 |
-
py: "code",
|
| 398 |
-
js: "code",
|
| 399 |
-
html: "code",
|
| 400 |
-
css: "code",
|
| 401 |
-
json: "code",
|
| 402 |
-
xml: "code",
|
| 403 |
-
md: "code",
|
| 404 |
-
yml: "code",
|
| 405 |
-
yaml: "code",
|
| 406 |
-
sql: "code",
|
| 407 |
-
sh: "code",
|
| 408 |
-
bat: "code",
|
| 409 |
-
// Spreadsheet files
|
| 410 |
-
xls: "document",
|
| 411 |
-
xlsx: "document",
|
| 412 |
-
csv: "document",
|
| 413 |
-
// Presentation files
|
| 414 |
-
ppt: "document",
|
| 415 |
-
pptx: "document",
|
| 416 |
-
odp: "document",
|
| 417 |
-
};
|
| 418 |
-
const type = types[extension] || "file";
|
| 419 |
-
return `/public/${type}.svg`;
|
| 420 |
-
},
|
| 421 |
-
|
| 422 |
-
// Enhanced method to get attachment display info for UI
|
| 423 |
-
getAttachmentDisplayInfo(attachment) {
|
| 424 |
-
if (typeof attachment === "string") {
|
| 425 |
-
// attachment is filename only (from persistent storage)
|
| 426 |
-
const filename = attachment;
|
| 427 |
-
const extension = filename.split(".").pop();
|
| 428 |
-
const isImage = this.isImageFile(filename);
|
| 429 |
-
const previewUrl = isImage
|
| 430 |
-
? this.getServerImgUrl(filename)
|
| 431 |
-
: this.getFilePreviewUrl(filename);
|
| 432 |
-
|
| 433 |
-
return {
|
| 434 |
-
filename: filename,
|
| 435 |
-
extension: extension.toUpperCase(),
|
| 436 |
-
isImage: isImage,
|
| 437 |
-
previewUrl: previewUrl,
|
| 438 |
-
clickHandler: () => {
|
| 439 |
-
if (this.isImageFile(filename)) {
|
| 440 |
-
imageViewerStore.open(this.getServerImgUrl(filename), { name: filename });
|
| 441 |
-
} else {
|
| 442 |
-
this.downloadAttachment(filename);
|
| 443 |
-
}
|
| 444 |
-
},
|
| 445 |
-
};
|
| 446 |
-
} else {
|
| 447 |
-
// attachment is object (from current session)
|
| 448 |
-
const isImage = this.isImageFile(attachment.name);
|
| 449 |
-
const filename = attachment.name;
|
| 450 |
-
const extension = filename.split(".").pop() || "";
|
| 451 |
-
const previewUrl = isImage
|
| 452 |
-
? this.getServerImgUrl(attachment.name)
|
| 453 |
-
: this.getFilePreviewUrl(attachment.name);
|
| 454 |
-
return {
|
| 455 |
-
filename: filename,
|
| 456 |
-
extension: extension.toUpperCase(),
|
| 457 |
-
isImage: attachment.type === "image",
|
| 458 |
-
previewUrl: previewUrl,
|
| 459 |
-
clickHandler: () => {
|
| 460 |
-
if (attachment.type === "image") {
|
| 461 |
-
const imageUrl = this.getServerImgUrl(attachment.name);
|
| 462 |
-
imageViewerStore.open(imageUrl, { name: attachment.name });
|
| 463 |
-
} else {
|
| 464 |
-
this.downloadAttachment(attachment.name);
|
| 465 |
-
}
|
| 466 |
-
},
|
| 467 |
-
};
|
| 468 |
-
}
|
| 469 |
-
},
|
| 470 |
-
|
| 471 |
-
async downloadAttachment(filename) {
|
| 472 |
-
try {
|
| 473 |
-
const path = this.getServerFileUrl(filename);
|
| 474 |
-
const response = await fetchApi("/download_work_dir_file?path=" + path);
|
| 475 |
-
|
| 476 |
-
if (!response.ok) {
|
| 477 |
-
throw new Error("Network response was not ok");
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
const blob = await response.blob();
|
| 481 |
-
|
| 482 |
-
const link = document.createElement("a");
|
| 483 |
-
link.href = window.URL.createObjectURL(blob);
|
| 484 |
-
link.download = filename;
|
| 485 |
-
document.body.appendChild(link);
|
| 486 |
-
link.click();
|
| 487 |
-
document.body.removeChild(link);
|
| 488 |
-
window.URL.revokeObjectURL(link.href);
|
| 489 |
-
} catch (error) {
|
| 490 |
-
window.toastFetchError("Error downloading file", error);
|
| 491 |
-
alert("Error downloading file");
|
| 492 |
-
}
|
| 493 |
-
},
|
| 494 |
-
|
| 495 |
-
// Generate GUID for unique filenames
|
| 496 |
-
generateGUID() {
|
| 497 |
-
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
|
| 498 |
-
/[xy]/g,
|
| 499 |
-
function (c) {
|
| 500 |
-
const r = (Math.random() * 16) | 0;
|
| 501 |
-
const v = c == "x" ? r : (r & 0x3) | 0x8;
|
| 502 |
-
return v.toString(16);
|
| 503 |
-
}
|
| 504 |
-
);
|
| 505 |
-
},
|
| 506 |
-
|
| 507 |
-
};
|
| 508 |
-
|
| 509 |
-
const store = createStore("chatAttachments", model);
|
| 510 |
-
|
| 511 |
-
export { store };
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:58ee9c5c4f3edd414df6a73c646170e88069cd365820df9f8de13646bbdcbf74
|
| 3 |
+
size 14540
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/attachments/dragDropOverlay.html
CHANGED
|
@@ -1,28 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
<title>Drag Drop Overlay</title>
|
| 5 |
-
|
| 6 |
-
<script type="module">
|
| 7 |
-
import { store } from "/components/chat/attachments/attachmentsStore.js";
|
| 8 |
-
</script>
|
| 9 |
-
</head>
|
| 10 |
-
|
| 11 |
-
<body>
|
| 12 |
-
<!-- Drag and Drop Overlay -->
|
| 13 |
-
<div x-data>
|
| 14 |
-
<template x-if="$store.chatAttachments">
|
| 15 |
-
<div x-cloak x-show="$store.chatAttachments.dragDropOverlayVisible" id="dragdrop-overlay"
|
| 16 |
-
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0"
|
| 17 |
-
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-300"
|
| 18 |
-
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" class="dragdrop-overlay">
|
| 19 |
-
<img src="public/dragndrop.svg" alt="Drop files" class="dragdrop-icon">
|
| 20 |
-
<div class="dragdrop-text">Drop files or folders to attach them to your message</div>
|
| 21 |
-
<div class="dragdrop-subtext"></div>
|
| 22 |
-
</div>
|
| 23 |
-
</template>
|
| 24 |
-
</div>
|
| 25 |
-
|
| 26 |
-
</body>
|
| 27 |
-
|
| 28 |
-
</html>
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7171cdc28bf4c422adaef686aa8502f87871582b2eb4ab4e8f0a8495f29e6fb0
|
| 3 |
+
size 1034
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/attachments/inputPreview.html
CHANGED
|
@@ -1,30 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
</script>
|
| 5 |
-
|
| 6 |
-
<div x-data>
|
| 7 |
-
<template x-if="$store.chatAttachments">
|
| 8 |
-
|
| 9 |
-
<div x-show="$store.chatAttachments.hasAttachments" class="preview-section scrollbar">
|
| 10 |
-
<template x-for="(attachment, index) in $store.chatAttachments.attachments" :key="index">
|
| 11 |
-
<div class="attachment-item"
|
| 12 |
-
:class="{'image-type': attachment.type === 'image', 'file-type': attachment.type === 'file'}">
|
| 13 |
-
<template x-if="attachment.type === 'image'">
|
| 14 |
-
<img :src="attachment.url" class="attachment-preview" :alt="attachment.name" style="cursor: pointer;"
|
| 15 |
-
@click="$store.imageViewer.open(attachment.url, { name: attachment.name })">
|
| 16 |
-
</template>
|
| 17 |
-
<template x-if="attachment.type === 'file'">
|
| 18 |
-
<div>
|
| 19 |
-
<img :src="attachment.displayInfo.previewUrl" class="file-icon" :alt="attachment.extension">
|
| 20 |
-
<span class="file-title" x-text="attachment.name"></span>
|
| 21 |
-
</div>
|
| 22 |
-
</template>
|
| 23 |
-
<button @click="$store.chatAttachments.removeAttachment(index)"
|
| 24 |
-
class="remove-attachment">×</button>
|
| 25 |
-
</div>
|
| 26 |
-
</template>
|
| 27 |
-
</div>
|
| 28 |
-
|
| 29 |
-
</template>
|
| 30 |
-
</div>
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0255aaa44ae7179e3f8460a7e265f1671ee8f6d5ae362a658a402f90e8a0d00d
|
| 3 |
+
size 1561
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/input/bottom-actions-bar.html
CHANGED
|
@@ -1,98 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import { store } from "/components/chat/input/input-store.js";
|
| 5 |
-
</script>
|
| 6 |
-
</head>
|
| 7 |
-
<body>
|
| 8 |
-
<template x-if="$store.chatInput">
|
| 9 |
-
<div class="text-buttons-row chat-bottom-actions-bar">
|
| 10 |
-
<x-extension id="chat-input-bottom-actions-start"></x-extension>
|
| 11 |
-
<button type="button" class="text-button" @click="$store.chatInput.pauseAgent(!$store.chatInput.paused)">
|
| 12 |
-
<template x-if="!$store.chatInput.paused">
|
| 13 |
-
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="14" height="14" aria-hidden="true">
|
| 14 |
-
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path>
|
| 15 |
-
</svg>
|
| 16 |
-
</template>
|
| 17 |
-
<template x-if="$store.chatInput.paused">
|
| 18 |
-
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" width="14" height="14" aria-hidden="true">
|
| 19 |
-
<path d="M8 5v14l11-7z"></path>
|
| 20 |
-
</svg>
|
| 21 |
-
</template>
|
| 22 |
-
<span x-text="$store.chatInput.paused ? 'Resume Agent' : 'Pause Agent'"></span>
|
| 23 |
-
</button>
|
| 24 |
-
|
| 25 |
-
<button type="button" class="text-button" id="nudges_window" @click="$store.chatInput.nudge()">
|
| 26 |
-
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49 58" fill="currentColor" width="14" height="14" aria-hidden="true">
|
| 27 |
-
<path d="m11.97,16.32c-.46,0-.91-.25-1.15-.68-.9-1.63-1.36-3.34-1.36-5.1C9.45,4.73,14.18,0,20,0s10.55,4.73,10.55,10.55c0,.87-.13,1.75-.41,2.76-.19.7-.9,1.13-1.62.93-.7-.19-1.12-.92-.93-1.62.21-.79.31-1.44.31-2.07,0-4.36-3.55-7.91-7.91-7.91s-7.91,3.55-7.91,7.91c0,1.3.35,2.59,1.03,3.82.36.64.13,1.44-.51,1.79-.21.11-.42.17-.64.17Z" stroke-width="0.5" stroke="currentColor"/>
|
| 28 |
-
<path d="M34.5,58h-6.18c-3.17,0-6.15-1.23-8.39-3.47L1.16,35.75c-1.54-1.54-1.54-4.05,0-5.59,2.4-2.4,6.27-2.68,8.99-.64l4.58,3.44V10.55c0-2.91,2.36-5.27,5.27-5.27s5.27,2.36,5.27,5.27v8.62c.78-.45,1.68-.71,2.64-.71,2.3,0,4.26,1.48,4.98,3.53.84-.56,1.85-.89,2.93-.89,2.3,0,4.26,1.48,4.98,3.53.84-.56,1.85-.89,2.93-.89,2.91,0,5.27,2.36,5.27,5.27v14.5c0,8-6.51,14.5-14.5,14.5ZM6.03,30.79c-1.1,0-2.19.42-3.01,1.23-.51.51-.51,1.35,0,1.86l18.77,18.78c1.74,1.74,4.06,2.7,6.53,2.7h6.18c6.54,0,11.86-5.32,11.86-11.86v-14.5c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v1.32c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32v-3.95c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v3.95c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32v-6.59c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v6.59c0,.73-.59,1.32-1.32,1.32s-1.32-.59-1.32-1.32V10.55c0-1.45-1.18-2.64-2.64-2.64s-2.64,1.18-2.64,2.64v25.05c0,.5-.28.95-.73,1.18s-.98.18-1.38-.12l-6.69-5.02c-.75-.56-1.65-.84-2.54-.84Z" stroke-width="0.5" stroke="currentColor"/>
|
| 29 |
-
</svg>
|
| 30 |
-
<p>Nudge</p>
|
| 31 |
-
</button>
|
| 32 |
-
<x-extension id="chat-input-bottom-actions-end"></x-extension>
|
| 33 |
-
</div>
|
| 34 |
-
</template>
|
| 35 |
-
|
| 36 |
-
<style>
|
| 37 |
-
.text-buttons-row {
|
| 38 |
-
width: 100%;
|
| 39 |
-
display: flex;
|
| 40 |
-
flex-wrap: wrap;
|
| 41 |
-
gap: var(--spacing-xs);
|
| 42 |
-
padding-top: var(--spacing-xs);
|
| 43 |
-
margin-left: var(--spacing-xs);
|
| 44 |
-
}
|
| 45 |
-
.text-buttons-row > x-extension:not(:empty) { display: contents; }
|
| 46 |
-
.text-buttons-row > x-extension:empty { display: none; }
|
| 47 |
-
.text-buttons-row > x-extension > x-component,
|
| 48 |
-
.text-buttons-row > x-extension > x-component > div[x-data] { display: contents; }
|
| 49 |
-
.text-button {
|
| 50 |
-
background-color: transparent;
|
| 51 |
-
border: none;
|
| 52 |
-
border-radius: 5px;
|
| 53 |
-
color: var(--color-text);
|
| 54 |
-
font-family: "Rubik", Arial, Helvetica, sans-serif;
|
| 55 |
-
font-size: 0.6rem;
|
| 56 |
-
padding: 6px var(--spacing-sm);
|
| 57 |
-
cursor: pointer;
|
| 58 |
-
opacity: 0.8;
|
| 59 |
-
transition: all 0.3s;
|
| 60 |
-
display: flex;
|
| 61 |
-
align-items: center;
|
| 62 |
-
gap: var(--spacing-xs);
|
| 63 |
-
}
|
| 64 |
-
.text-button:hover:not(:disabled) {
|
| 65 |
-
opacity: 1;
|
| 66 |
-
background-color: var(--color-background-hover);
|
| 67 |
-
border-radius: 4px;
|
| 68 |
-
}
|
| 69 |
-
.text-button:active:not(:disabled) { opacity: 0.5; }
|
| 70 |
-
.text-button:disabled {
|
| 71 |
-
opacity: 0.4;
|
| 72 |
-
cursor: not-allowed;
|
| 73 |
-
}
|
| 74 |
-
.text-button svg {
|
| 75 |
-
width: 14px;
|
| 76 |
-
height: 14px;
|
| 77 |
-
flex-shrink: 0;
|
| 78 |
-
}
|
| 79 |
-
.text-button p { margin-block: 0; }
|
| 80 |
-
|
| 81 |
-
@media (max-width: 768px) {
|
| 82 |
-
.text-buttons-row {
|
| 83 |
-
width: 90%;
|
| 84 |
-
white-space: pre-wrap;
|
| 85 |
-
}
|
| 86 |
-
.text-button svg { width: 18px; height: 18px; }
|
| 87 |
-
}
|
| 88 |
-
@media (max-width: 640px) {
|
| 89 |
-
.text-buttons-row {
|
| 90 |
-
width: 90%;
|
| 91 |
-
white-space: pre-wrap;
|
| 92 |
-
}
|
| 93 |
-
.text-button { max-height: 25px; }
|
| 94 |
-
.text-button p { display: none; }
|
| 95 |
-
}
|
| 96 |
-
</style>
|
| 97 |
-
</body>
|
| 98 |
-
</html>
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:50f132af7eee05b29dd84d7ab248b34e85c81ae9f79e43dc5a48694b4bc2cc6f
|
| 3 |
+
size 4662
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/input/bottom-actions.html
CHANGED
|
@@ -1,161 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import { store } from "/components/chat/input/input-store.js";
|
| 5 |
-
import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
|
| 6 |
-
import { store as historyStore } from "/components/modals/history/history-store.js";
|
| 7 |
-
import { store as contextStore } from "/components/modals/context/context-store.js";
|
| 8 |
-
import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
|
| 9 |
-
import { store as mcpServersStore } from "/components/settings/mcp/client/mcp-servers-store.js";
|
| 10 |
-
</script>
|
| 11 |
-
</head>
|
| 12 |
-
<body>
|
| 13 |
-
<template x-if="$store.chatInput">
|
| 14 |
-
<div class="chat-bottom-actions-menu">
|
| 15 |
-
<input
|
| 16 |
-
type="file"
|
| 17 |
-
x-ref="chatFileInput"
|
| 18 |
-
class="chat-bottom-menu-file-input"
|
| 19 |
-
accept="*"
|
| 20 |
-
multiple
|
| 21 |
-
@change="$store.chatAttachments.handleFileUpload($event); $store.chatInput.closeChatMoreMenu()"
|
| 22 |
-
>
|
| 23 |
-
<button
|
| 24 |
-
type="button"
|
| 25 |
-
class="chat-bottom-menu-item"
|
| 26 |
-
@click.stop="$refs.chatFileInput?.click()"
|
| 27 |
-
>
|
| 28 |
-
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
| 29 |
-
<path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" />
|
| 30 |
-
</svg>
|
| 31 |
-
<span>Attach files</span>
|
| 32 |
-
</button>
|
| 33 |
-
|
| 34 |
-
<input
|
| 35 |
-
type="file"
|
| 36 |
-
x-ref="chatFolderInput"
|
| 37 |
-
class="chat-bottom-menu-file-input"
|
| 38 |
-
webkitdirectory
|
| 39 |
-
directory
|
| 40 |
-
multiple
|
| 41 |
-
@change="$store.chatAttachments.handleFileUpload($event); $store.chatInput.closeChatMoreMenu()"
|
| 42 |
-
>
|
| 43 |
-
<button
|
| 44 |
-
type="button"
|
| 45 |
-
class="chat-bottom-menu-item"
|
| 46 |
-
@click.stop="$refs.chatFolderInput?.click()"
|
| 47 |
-
>
|
| 48 |
-
<x-icon aria-hidden="true" name="drive_folder_upload"></x-icon>
|
| 49 |
-
<span>Attach folder</span>
|
| 50 |
-
</button>
|
| 51 |
-
|
| 52 |
-
<button
|
| 53 |
-
type="button"
|
| 54 |
-
class="chat-bottom-menu-item"
|
| 55 |
-
@click="$store.mcpServersStore.openFromComposer($store.chats.selectedContext?.project?.name || ''); $store.chatInput.closeChatMoreMenu()"
|
| 56 |
-
>
|
| 57 |
-
<x-icon aria-hidden="true" name="hub"></x-icon>
|
| 58 |
-
<span>MCP Servers</span>
|
| 59 |
-
</button>
|
| 60 |
-
|
| 61 |
-
<button
|
| 62 |
-
type="button"
|
| 63 |
-
class="chat-bottom-menu-item"
|
| 64 |
-
@click="$confirmClick($event, () => { $store.chats.resetChat(); $store.chatInput.closeChatMoreMenu() })"
|
| 65 |
-
:disabled="!$store.chats.selected"
|
| 66 |
-
>
|
| 67 |
-
<x-icon aria-hidden="true" name="delete_sweep"></x-icon>
|
| 68 |
-
<span>Clear Chat</span>
|
| 69 |
-
</button>
|
| 70 |
-
|
| 71 |
-
<button
|
| 72 |
-
type="button"
|
| 73 |
-
class="chat-bottom-menu-item"
|
| 74 |
-
id="history_inspect"
|
| 75 |
-
@click="$store.history.open(); $store.chatInput.closeChatMoreMenu()"
|
| 76 |
-
:disabled="!$store.chats.selected"
|
| 77 |
-
>
|
| 78 |
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="5 10 85 85" width="18" height="18" aria-hidden="true">
|
| 79 |
-
<path fill="currentColor" d="m59.572,57.949c-.41,0-.826-.105-1.207-.325l-9.574-5.528c-.749-.432-1.21-1.231-1.21-2.095v-14.923c0-1.336,1.083-2.419,2.419-2.419s2.419,1.083,2.419,2.419v13.526l8.364,4.829c1.157.668,1.554,2.148.886,3.305-.448.776-1.261,1.21-2.097,1.21Zm30.427-7.947c0,10.684-4.161,20.728-11.716,28.283-6.593,6.59-15.325,10.69-24.59,11.544-1.223.113-2.448.169-3.669.169-7.492,0-14.878-2.102-21.22-6.068l-15.356,5.733c-.888.331-1.887.114-2.557-.556s-.887-1.669-.556-2.557l5.733-15.351c-4.613-7.377-6.704-16.165-5.899-24.891.854-9.266,4.954-17.998,11.544-24.588,7.555-7.555,17.6-11.716,28.285-11.716s20.73,4.161,28.285,11.716c7.555,7.555,11.716,17.599,11.716,28.283Zm-15.137-24.861c-13.71-13.71-36.018-13.71-49.728,0-11.846,11.846-13.682,30.526-4.365,44.417.434.647.53,1.464.257,2.194l-4.303,11.523,11.528-4.304c.274-.102.561-.153.846-.153.474,0,.944.139,1.348.41,13.888,9.315,32.568,7.479,44.417-4.365,13.707-13.708,13.706-36.014,0-49.723Zm-24.861-4.13c-15.989,0-28.996,13.006-28.996,28.992s13.008,28.992,28.996,28.992c1.336,0,2.419-1.083,2.419-2.419s-1.083-2.419-2.419-2.419c-13.32,0-24.157-10.835-24.157-24.153s10.837-24.153,24.157-24.153,24.153,10.835,24.153,24.153c0,1.336,1.083,2.419,2.419,2.419c0-15.986-13.006-28.992-28.992-28.992Zm25.041,33.531c-1.294.347-2.057,1.673-1.71,2.963.343,1.289,1.669,2.057,2.963,1.71,1.289-.343,2.053-1.669,1.71-2.963-.347-1.289-1.673-2.057-2.963-1.71Zm-2.03,6.328c-1.335,0-2.419,1.084-2.419,2.419s1.084,2.419,2.419,2.419,2.419-1.084,2.419-2.419-1.084-2.419-2.419-2.419Zm-3.598,5.587c-1.289-.347-2.615.416-2.963,1.71-.343,1.289.421,2.615,1.71,2.963,1.294.347,2.62-.421,2.963-1.71.347-1.294-.416-2.62-1.71-2.963Zm-4.919,4.462c-1.157-.667-2.638-.27-3.306.887-.667,1.157-.27,2.638.887,3.305,1.157.668,2.638.27,3.306-.887.667-1.157.27-2.638-.887-3.306Zm-9.327,3.04c-.946.946-.946,2.478,0,3.42.942.946,2.473.946,3.42,0,.946-.942.946-2.473,0-3.42-.946-.942-2.478-.946-3.42,0Z"></path>
|
| 80 |
-
</svg>
|
| 81 |
-
<span>History</span>
|
| 82 |
-
</button>
|
| 83 |
-
|
| 84 |
-
<button
|
| 85 |
-
type="button"
|
| 86 |
-
class="chat-bottom-menu-item"
|
| 87 |
-
id="ctx_window"
|
| 88 |
-
@click="$store.context.open(); $store.chatInput.closeChatMoreMenu()"
|
| 89 |
-
:disabled="!$store.chats.selected"
|
| 90 |
-
>
|
| 91 |
-
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="17 15 70 70" width="18" height="18" fill="currentColor" aria-hidden="true">
|
| 92 |
-
<path d="m63 25c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z"></path>
|
| 93 |
-
<path d="m63 79c1.1016 0 2-0.89844 2-2s-0.89844-2-2-2h-26c-1.1016 0-2 0.89844-2 2s0.89844 2 2 2z"></path>
|
| 94 |
-
<path d="m68 39h-36c-6.0703 0-11 4.9297-11 11s4.9297 11 11 11h36c6.0703 0 11-4.9297 11-11s-4.9297-11-11-11zm0 18h-36c-3.8594 0-7-3.1406-7-7s3.1406-7 7-7h36c3.8594 0 7 3.1406 7 7s-3.1406 7-7 7z"></path>
|
| 95 |
-
</svg>
|
| 96 |
-
<span>Context</span>
|
| 97 |
-
</button>
|
| 98 |
-
</div>
|
| 99 |
-
</template>
|
| 100 |
-
|
| 101 |
-
<style>
|
| 102 |
-
.chat-bottom-actions-menu {
|
| 103 |
-
display: flex;
|
| 104 |
-
flex-direction: column;
|
| 105 |
-
width: 100%;
|
| 106 |
-
font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
|
| 107 |
-
font-size: 0.8125rem;
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
.chat-bottom-menu-item {
|
| 111 |
-
display: flex;
|
| 112 |
-
align-items: center;
|
| 113 |
-
gap: 0.4rem;
|
| 114 |
-
width: 100%;
|
| 115 |
-
margin: 0;
|
| 116 |
-
padding: var(--spacing-xs) var(--spacing-sm);
|
| 117 |
-
border-radius: 0.25rem;
|
| 118 |
-
background: transparent;
|
| 119 |
-
color: var(--color-text);
|
| 120 |
-
font-family: 'Rubik', Arial, Helvetica, sans-serif;
|
| 121 |
-
text-align: left;
|
| 122 |
-
cursor: pointer;
|
| 123 |
-
opacity: 0.88;
|
| 124 |
-
transition: background-color 0.12s ease, opacity 0.12s ease;
|
| 125 |
-
white-space: nowrap;
|
| 126 |
-
box-sizing: border-box;
|
| 127 |
-
}
|
| 128 |
-
button.chat-bottom-menu-item {
|
| 129 |
-
border: none;
|
| 130 |
-
}
|
| 131 |
-
.chat-bottom-menu-file-input {
|
| 132 |
-
position: absolute;
|
| 133 |
-
width: 1px;
|
| 134 |
-
height: 1px;
|
| 135 |
-
padding: 0;
|
| 136 |
-
margin: -1px;
|
| 137 |
-
overflow: hidden;
|
| 138 |
-
clip: rect(0, 0, 0, 0);
|
| 139 |
-
white-space: nowrap;
|
| 140 |
-
border: 0;
|
| 141 |
-
opacity: 0;
|
| 142 |
-
}
|
| 143 |
-
button.chat-bottom-menu-item:hover:not(:disabled) {
|
| 144 |
-
opacity: 1;
|
| 145 |
-
background-color: var(--color-background-hover);
|
| 146 |
-
}
|
| 147 |
-
button.chat-bottom-menu-item:active:not(:disabled) { opacity: 0.72; }
|
| 148 |
-
button.chat-bottom-menu-item:disabled {
|
| 149 |
-
opacity: 0.38;
|
| 150 |
-
cursor: not-allowed;
|
| 151 |
-
pointer-events: none;
|
| 152 |
-
}
|
| 153 |
-
.chat-bottom-menu-item .material-symbols-outlined {
|
| 154 |
-
font-size: 18px;
|
| 155 |
-
flex-shrink: 0;
|
| 156 |
-
opacity: 0.9;
|
| 157 |
-
}
|
| 158 |
-
.chat-bottom-menu-item svg { flex-shrink: 0; opacity: 0.88; }
|
| 159 |
-
</style>
|
| 160 |
-
</body>
|
| 161 |
-
</html>
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:acb3a816b76c3bf32a318d6f297e7b0f57752b191e3af49b3e66702019044738
|
| 3 |
+
size 7639
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/input/chat-bar-input.html
CHANGED
|
@@ -1,457 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
<script type="module">
|
| 5 |
-
import { store as fullScreenStore } from "/components/modals/full-screen-input/full-screen-store.js";
|
| 6 |
-
import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js";
|
| 7 |
-
import { store as chatInputStore } from "/components/chat/input/input-store.js";
|
| 8 |
-
</script>
|
| 9 |
-
</head>
|
| 10 |
-
|
| 11 |
-
<body>
|
| 12 |
-
<div x-data>
|
| 13 |
-
<x-extension id="chat-input-box-start"></x-extension>
|
| 14 |
-
<div class="input-row">
|
| 15 |
-
<!-- More actions (+): opens upward; content lives in bottom-actions.html -->
|
| 16 |
-
<div class="attachment-wrapper chat-more-dropdown" @click.outside="$store.chatInput.closeChatMoreMenu()"
|
| 17 |
-
@keydown.escape.window="$store.chatInput.closeChatMoreMenu()">
|
| 18 |
-
<button type="button" class="chat-more-trigger" @click.stop="$store.chatInput.toggleChatMoreMenu()"
|
| 19 |
-
:aria-expanded="$store.chatInput.chatMoreMenuOpen.toString()" aria-label="More actions">
|
| 20 |
-
<x-icon aria-hidden="true" name="add"></x-icon>
|
| 21 |
-
</button>
|
| 22 |
-
<div class="chat-more-panel" x-show="$store.chatInput.chatMoreMenuOpen" x-transition style="display: none;">
|
| 23 |
-
<x-component path="chat/input/bottom-actions.html"></x-component>
|
| 24 |
-
</div>
|
| 25 |
-
</div>
|
| 26 |
-
|
| 27 |
-
<!-- Container for composer and expand button -->
|
| 28 |
-
<div id="chat-input-container">
|
| 29 |
-
<div id="chat-input" role="textbox" aria-label="Message" aria-multiline="true" contenteditable="true"
|
| 30 |
-
spellcheck="true" :data-placeholder="$store.chatInput.inputPlaceholder"
|
| 31 |
-
@keydown="$store.chatInput.handleKeydown($event)"
|
| 32 |
-
@input="$store.chatInput.handleInput($event)"
|
| 33 |
-
@paste="$store.chatInput.handlePaste($event)"
|
| 34 |
-
:class="{ 'progress-active': $store.chatInput.progressActive, 'is-empty': !$store.chatInput.message }"
|
| 35 |
-
x-create="$store.chatInput.mountEditor($el)"
|
| 36 |
-
x-destroy="$store.chatInput.unmountEditor($el)"></div>
|
| 37 |
-
<div id="chat-input-progress-placeholder" x-show="$store.chatInput.showProgressPlaceholder"
|
| 38 |
-
x-html="$store.chatInput.progressPlaceholderHtml"
|
| 39 |
-
:class="{ 'progress-active': $store.chatInput.progressActive }" aria-hidden="true" style="display: none;">
|
| 40 |
-
</div>
|
| 41 |
-
<button id="expand-button" @click="$store.fullScreenInputModal.openModal()" aria-label="Expand input">
|
| 42 |
-
<x-icon aria-hidden="true" name="fullscreen"></x-icon>
|
| 43 |
-
</button>
|
| 44 |
-
</div>
|
| 45 |
-
|
| 46 |
-
<div id="chat-buttons-wrapper">
|
| 47 |
-
<!-- Send button -->
|
| 48 |
-
<button class="chat-button" id="send-button" @click="$store.chatInput.activateSendButton()"
|
| 49 |
-
:class="$store.chatInput.sendButtonClass" :title="$store.chatInput.sendButtonTitle"
|
| 50 |
-
:aria-label="$store.chatInput.sendButtonTitle">
|
| 51 |
-
<x-icon :name="$store.chatInput.sendButtonIcon"></x-icon>
|
| 52 |
-
</button>
|
| 53 |
-
</div>
|
| 54 |
-
</div>
|
| 55 |
-
|
| 56 |
-
<!-- Full Screen Input Modal -->
|
| 57 |
-
<x-component path="modals/full-screen-input/full-screen-input.html"></x-component>
|
| 58 |
-
<x-extension id="chat-input-box-end"></x-extension>
|
| 59 |
-
</div>
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
<style>
|
| 63 |
-
/* Layout rows */
|
| 64 |
-
.input-row {
|
| 65 |
-
display: flex;
|
| 66 |
-
align-items: center;
|
| 67 |
-
gap: 0.72rem;
|
| 68 |
-
width: 100%;
|
| 69 |
-
min-height: 4.25rem;
|
| 70 |
-
padding: 0.42rem 0.52rem 0.42rem 0.72rem;
|
| 71 |
-
border: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
|
| 72 |
-
border-radius: 8px;
|
| 73 |
-
background: color-mix(in srgb, var(--color-panel) 72%, var(--color-background) 28%);
|
| 74 |
-
box-shadow:
|
| 75 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.035),
|
| 76 |
-
0 18px 46px rgba(0, 0, 0, 0.18);
|
| 77 |
-
white-space: nowrap;
|
| 78 |
-
transition: border-color 0.16s ease, background-color 0.16s ease, box-shadow 0.16s ease;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
.input-row:focus-within {
|
| 82 |
-
border-color: color-mix(in srgb, var(--color-border) 88%, var(--color-text) 12%);
|
| 83 |
-
box-shadow:
|
| 84 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.045),
|
| 85 |
-
0 22px 54px rgba(0, 0, 0, 0.22);
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
.attachment-wrapper {
|
| 89 |
-
position: relative;
|
| 90 |
-
flex-shrink: 0;
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
/* Composer + menu: custom panel (edit here; not shared with file-browser dropdown) */
|
| 94 |
-
.chat-more-dropdown {
|
| 95 |
-
z-index: 1;
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
.chat-more-trigger {
|
| 99 |
-
cursor: pointer;
|
| 100 |
-
color: var(--color-text);
|
| 101 |
-
opacity: 0.82;
|
| 102 |
-
transition: opacity 0.16s ease, background-color 0.16s ease, color 0.16s ease;
|
| 103 |
-
display: flex;
|
| 104 |
-
align-items: center;
|
| 105 |
-
justify-content: center;
|
| 106 |
-
width: 2.5rem;
|
| 107 |
-
height: 2.5rem;
|
| 108 |
-
padding: 0;
|
| 109 |
-
border: none;
|
| 110 |
-
border-radius: 10px;
|
| 111 |
-
background: transparent;
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
.chat-more-trigger:hover {
|
| 115 |
-
opacity: 1;
|
| 116 |
-
background: color-mix(in srgb, var(--color-border) 36%, transparent);
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
.chat-more-trigger:active {
|
| 120 |
-
opacity: 0.5;
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
.chat-more-trigger .material-symbols-outlined {
|
| 124 |
-
font-size: 1.7rem;
|
| 125 |
-
line-height: 1;
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
.chat-more-panel {
|
| 129 |
-
position: absolute;
|
| 130 |
-
left: 0;
|
| 131 |
-
bottom: 100%;
|
| 132 |
-
margin-bottom: 0.25rem;
|
| 133 |
-
min-width: 11rem;
|
| 134 |
-
max-width: min(18rem, 92vw);
|
| 135 |
-
max-height: min(70vh, 20rem);
|
| 136 |
-
overflow-x: hidden;
|
| 137 |
-
overflow-y: auto;
|
| 138 |
-
background-color: color-mix(in srgb, var(--color-panel) 90%, black 10%);
|
| 139 |
-
border: 1px solid var(--color-border);
|
| 140 |
-
border-radius: 12px;
|
| 141 |
-
box-shadow: 0 16px 38px rgba(0, 0, 0, 0.3);
|
| 142 |
-
z-index: 1050;
|
| 143 |
-
padding: 0.3rem;
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
.light-mode .chat-more-panel {
|
| 147 |
-
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
/* Text input */
|
| 151 |
-
#chat-input-container {
|
| 152 |
-
position: relative;
|
| 153 |
-
width: 100%;
|
| 154 |
-
min-width: 0;
|
| 155 |
-
display: flex;
|
| 156 |
-
align-items: center;
|
| 157 |
-
align-self: stretch;
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
#chat-input {
|
| 161 |
-
background-color: transparent;
|
| 162 |
-
border: 0;
|
| 163 |
-
border-radius: 0;
|
| 164 |
-
color: var(--color-text);
|
| 165 |
-
flex-grow: 1;
|
| 166 |
-
font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
|
| 167 |
-
font-optical-sizing: auto;
|
| 168 |
-
-webkit-font-optical-sizing: auto;
|
| 169 |
-
font-size: 1rem;
|
| 170 |
-
line-height: 1.45;
|
| 171 |
-
max-height: 30vh;
|
| 172 |
-
min-height: 2.8rem;
|
| 173 |
-
width: 100%;
|
| 174 |
-
padding: 0.68rem 2.2rem 0.68rem 0.1rem;
|
| 175 |
-
margin: 0;
|
| 176 |
-
overflow-y: auto;
|
| 177 |
-
white-space: pre-wrap;
|
| 178 |
-
word-break: break-word;
|
| 179 |
-
scroll-behavior: smooth;
|
| 180 |
-
resize: none;
|
| 181 |
-
align-content: center;
|
| 182 |
-
background-clip: border-box;
|
| 183 |
-
outline: none;
|
| 184 |
-
transition: color 0.16s ease, background-color 0.16s ease;
|
| 185 |
-
-webkit-transition: color 0.16s ease, background-color 0.16s ease;
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
#chat-input.is-empty::before {
|
| 189 |
-
content: attr(data-placeholder);
|
| 190 |
-
color: color-mix(in srgb, var(--color-text) 62%, transparent);
|
| 191 |
-
opacity: 0.78;
|
| 192 |
-
pointer-events: none;
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
#chat-input .composer-code-block {
|
| 196 |
-
display: block;
|
| 197 |
-
margin: 0.48rem 0;
|
| 198 |
-
padding: 0.56rem 0.7rem;
|
| 199 |
-
border-radius: 6px;
|
| 200 |
-
background: color-mix(in srgb, var(--color-border) 28%, transparent);
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
#chat-input .composer-code-content {
|
| 204 |
-
min-height: 1.35rem;
|
| 205 |
-
margin: 0;
|
| 206 |
-
outline: none;
|
| 207 |
-
white-space: pre-wrap;
|
| 208 |
-
word-break: break-word;
|
| 209 |
-
font-family: var(--font-family-code, "Roboto Mono", monospace);
|
| 210 |
-
font-size: 0.94rem;
|
| 211 |
-
line-height: 1.45;
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
-
#chat-input::-webkit-scrollbar {
|
| 215 |
-
width: 6px;
|
| 216 |
-
height: 6px;
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
#chat-input::-webkit-scrollbar-track {
|
| 220 |
-
background: transparent;
|
| 221 |
-
margin: 4px 0;
|
| 222 |
-
border-radius: 6px;
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
#chat-input::-webkit-scrollbar-thumb {
|
| 226 |
-
background-color: rgba(155, 155, 155, 0.5);
|
| 227 |
-
border-radius: 6px;
|
| 228 |
-
-webkit-transition: background-color 0.2s ease;
|
| 229 |
-
transition: background-color 0.2s ease;
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
#chat-input::-webkit-scrollbar-thumb:hover {
|
| 233 |
-
background-color: rgba(155, 155, 155, 0.7);
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
#chat-input:focus {
|
| 237 |
-
outline: none;
|
| 238 |
-
background-color: transparent;
|
| 239 |
-
}
|
| 240 |
-
|
| 241 |
-
/* Progress ghost text animation — gentle pulse on placeholder */
|
| 242 |
-
#chat-input.progress-active.is-empty::before {
|
| 243 |
-
animation: placeholder-pulse 2s ease-in-out infinite;
|
| 244 |
-
}
|
| 245 |
-
|
| 246 |
-
@keyframes placeholder-pulse {
|
| 247 |
-
|
| 248 |
-
0%,
|
| 249 |
-
100% {
|
| 250 |
-
opacity: 0.45;
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
50% {
|
| 254 |
-
opacity: 0.85;
|
| 255 |
-
}
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
#chat-input.progress-active {
|
| 259 |
-
border-color: color-mix(in srgb, var(--color-highlight) 20%, transparent);
|
| 260 |
-
outline-color: color-mix(in srgb, var(--color-highlight) 15%, transparent);
|
| 261 |
-
}
|
| 262 |
-
|
| 263 |
-
#chat-input-progress-placeholder {
|
| 264 |
-
position: absolute;
|
| 265 |
-
top: 0;
|
| 266 |
-
bottom: 0;
|
| 267 |
-
left: 0.12rem;
|
| 268 |
-
right: 2rem;
|
| 269 |
-
color: var(--color-text-muted);
|
| 270 |
-
opacity: 0.7;
|
| 271 |
-
font-family: var(--font-family-code, "Roboto Mono", monospace);
|
| 272 |
-
font-size: 0.9rem;
|
| 273 |
-
line-height: 1.25;
|
| 274 |
-
white-space: nowrap;
|
| 275 |
-
overflow: hidden;
|
| 276 |
-
text-overflow: ellipsis;
|
| 277 |
-
pointer-events: none;
|
| 278 |
-
display: flex;
|
| 279 |
-
align-items: center;
|
| 280 |
-
gap: 0.35rem;
|
| 281 |
-
}
|
| 282 |
-
|
| 283 |
-
#chat-input-progress-placeholder.progress-active {
|
| 284 |
-
animation: placeholder-pulse 2s ease-in-out infinite;
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
#chat-input-progress-placeholder .chat-input-progress-icon {
|
| 288 |
-
display: inline-flex;
|
| 289 |
-
align-items: center;
|
| 290 |
-
justify-content: center;
|
| 291 |
-
font-size: 1.15rem;
|
| 292 |
-
line-height: 1;
|
| 293 |
-
color: var(--color-highlight);
|
| 294 |
-
}
|
| 295 |
-
|
| 296 |
-
#expand-button {
|
| 297 |
-
position: absolute;
|
| 298 |
-
top: 50%;
|
| 299 |
-
right: 0;
|
| 300 |
-
width: 2rem;
|
| 301 |
-
height: 2rem;
|
| 302 |
-
background: transparent;
|
| 303 |
-
border: none;
|
| 304 |
-
cursor: pointer;
|
| 305 |
-
transform: translateY(-50%) scale(0.8);
|
| 306 |
-
transform-origin: center;
|
| 307 |
-
font-size: 1.2rem;
|
| 308 |
-
color: var(--color-text);
|
| 309 |
-
opacity: 0.2;
|
| 310 |
-
transition: opacity 0.2s;
|
| 311 |
-
display: flex;
|
| 312 |
-
align-items: center;
|
| 313 |
-
justify-content: center;
|
| 314 |
-
padding: 0;
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
#expand-button:hover {
|
| 318 |
-
opacity: 0.7;
|
| 319 |
-
}
|
| 320 |
-
|
| 321 |
-
#expand-button:active {
|
| 322 |
-
opacity: 1;
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
#expand-button .material-symbols-outlined {
|
| 326 |
-
display: block;
|
| 327 |
-
font-size: 1.3rem;
|
| 328 |
-
line-height: 1;
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
/* Chat buttons (Send/Mic) */
|
| 332 |
-
#chat-buttons-wrapper {
|
| 333 |
-
gap: 0.58rem;
|
| 334 |
-
padding-left: 0;
|
| 335 |
-
line-height: 1;
|
| 336 |
-
display: -webkit-flex;
|
| 337 |
-
display: flex;
|
| 338 |
-
align-items: center;
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
.chat-button {
|
| 342 |
-
border: 1px solid transparent;
|
| 343 |
-
border-radius: 50%;
|
| 344 |
-
color: #fff;
|
| 345 |
-
cursor: pointer;
|
| 346 |
-
font-size: var(--font-size-normal);
|
| 347 |
-
height: 3.15rem;
|
| 348 |
-
width: 3.15rem;
|
| 349 |
-
margin: 0;
|
| 350 |
-
display: -webkit-flex;
|
| 351 |
-
display: flex;
|
| 352 |
-
align-items: center;
|
| 353 |
-
justify-content: center;
|
| 354 |
-
flex-shrink: 0;
|
| 355 |
-
flex-grow: 0;
|
| 356 |
-
min-width: 3.15rem;
|
| 357 |
-
-webkit-transition: background-color var(--transition-speed), border-color 0.16s ease, box-shadow 0.16s ease, filter 0.12s ease-in-out, transform 0.12s ease;
|
| 358 |
-
transition: background-color var(--transition-speed), border-color 0.16s ease, box-shadow 0.16s ease, filter 0.12s ease-in-out, transform 0.12s ease;
|
| 359 |
-
}
|
| 360 |
-
|
| 361 |
-
#send-button {
|
| 362 |
-
order: 2;
|
| 363 |
-
border-radius: 12px;
|
| 364 |
-
background-color: #4248f1;
|
| 365 |
-
box-shadow: none;
|
| 366 |
-
}
|
| 367 |
-
|
| 368 |
-
#send-button.send-queue {
|
| 369 |
-
background-color: #e67e22;
|
| 370 |
-
box-shadow: none;
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
#send-button.stop {
|
| 374 |
-
background-color: var(--color-error-text);
|
| 375 |
-
box-shadow: none;
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
#send-button:hover {
|
| 379 |
-
background-color: #353bc5;
|
| 380 |
-
filter: none;
|
| 381 |
-
box-shadow: none;
|
| 382 |
-
}
|
| 383 |
-
|
| 384 |
-
#send-button.send-queue:hover {
|
| 385 |
-
background-color: #d35400;
|
| 386 |
-
filter: none;
|
| 387 |
-
}
|
| 388 |
-
|
| 389 |
-
#send-button.stop:hover {
|
| 390 |
-
background-color: color-mix(in srgb, var(--color-error-text) 82%, black);
|
| 391 |
-
filter: none;
|
| 392 |
-
}
|
| 393 |
-
|
| 394 |
-
#send-button:active {
|
| 395 |
-
background-color: #2b309c;
|
| 396 |
-
transform: translateY(1px) scale(0.98);
|
| 397 |
-
filter: brightness(0.96);
|
| 398 |
-
}
|
| 399 |
-
|
| 400 |
-
#send-button.stop:active {
|
| 401 |
-
background-color: color-mix(in srgb, var(--color-error-text) 68%, black);
|
| 402 |
-
}
|
| 403 |
-
|
| 404 |
-
.chat-button svg {
|
| 405 |
-
width: 1.5rem;
|
| 406 |
-
height: 1.5rem;
|
| 407 |
-
}
|
| 408 |
-
|
| 409 |
-
#send-button .material-symbols-outlined {
|
| 410 |
-
font-size: 1.78rem;
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
/* Responsive tweaks */
|
| 414 |
-
@media (max-width: 640px) {
|
| 415 |
-
#chat-input {
|
| 416 |
-
align-content: center;
|
| 417 |
-
}
|
| 418 |
-
|
| 419 |
-
#chat-buttons-wrapper {
|
| 420 |
-
display: flex;
|
| 421 |
-
gap: 0.42rem;
|
| 422 |
-
padding: 0 !important;
|
| 423 |
-
width: auto;
|
| 424 |
-
flex-wrap: nowrap;
|
| 425 |
-
-webkit-transition: all 0.3s ease;
|
| 426 |
-
transition: all 0.3s ease;
|
| 427 |
-
}
|
| 428 |
-
|
| 429 |
-
.input-row {
|
| 430 |
-
gap: 0.42rem;
|
| 431 |
-
min-height: 4rem;
|
| 432 |
-
padding: 0.36rem 0.42rem 0.36rem 0.48rem;
|
| 433 |
-
}
|
| 434 |
-
|
| 435 |
-
.chat-more-trigger {
|
| 436 |
-
width: 2.15rem;
|
| 437 |
-
height: 2.15rem;
|
| 438 |
-
}
|
| 439 |
-
|
| 440 |
-
.chat-button {
|
| 441 |
-
width: 2.75rem;
|
| 442 |
-
height: 2.75rem;
|
| 443 |
-
min-width: 2.75rem;
|
| 444 |
-
}
|
| 445 |
-
}
|
| 446 |
-
|
| 447 |
-
@media (min-width: 768px) {
|
| 448 |
-
#chat-buttons-wrapper {
|
| 449 |
-
flex-wrap: nowrap;
|
| 450 |
-
-webkit-flex-wrap: nowrap;
|
| 451 |
-
flex-wrap: nowrap;
|
| 452 |
-
}
|
| 453 |
-
}
|
| 454 |
-
</style>
|
| 455 |
-
</body>
|
| 456 |
-
|
| 457 |
-
</html>
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c828481f4740f859a1157593e3c67a48a28696c7df876ab3e133d102cc369822
|
| 3 |
+
size 12869
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/input/chat-bar.html
CHANGED
|
@@ -1,52 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import { store } from "/components/chat/input/input-store.js";
|
| 5 |
-
import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js";
|
| 6 |
-
</script>
|
| 7 |
-
</head>
|
| 8 |
-
<body>
|
| 9 |
-
<div id="input-section" x-data>
|
| 10 |
-
<x-extension id="chat-input-start"></x-extension>
|
| 11 |
-
<template x-if="$store.chatInput">
|
| 12 |
-
<div style="width: 100%; display: contents;">
|
| 13 |
-
<!-- Message Queue section -->
|
| 14 |
-
<x-component path="chat/message-queue/message-queue.html"></x-component>
|
| 15 |
-
|
| 16 |
-
<!-- Attachment Preview section -->
|
| 17 |
-
<div>
|
| 18 |
-
<x-component path="chat/attachments/inputPreview.html" />
|
| 19 |
-
</div>
|
| 20 |
-
|
| 21 |
-
<x-component path="chat/input/chat-bar-input.html"></x-component>
|
| 22 |
-
<x-component path="chat/input/bottom-actions-bar.html"></x-component>
|
| 23 |
-
</div>
|
| 24 |
-
</template>
|
| 25 |
-
<x-extension id="chat-input-end"></x-extension>
|
| 26 |
-
</div>
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
<style>
|
| 30 |
-
/* Input section wrapper */
|
| 31 |
-
#input-section {
|
| 32 |
-
position: relative;
|
| 33 |
-
background-color: var(--color-panel);
|
| 34 |
-
display: -webkit-flex;
|
| 35 |
-
display: flex;
|
| 36 |
-
flex-direction: column;
|
| 37 |
-
gap: var(--spacing-xs);
|
| 38 |
-
padding: var(--spacing-xxs) var(--spacing-md) var(--spacing-sm) 0.8rem;
|
| 39 |
-
align-items: start;
|
| 40 |
-
place-items: normal;
|
| 41 |
-
flex-shrink: 0;
|
| 42 |
-
overflow: visible;
|
| 43 |
-
z-index: 1001;
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
@media (max-width: 768px) {
|
| 47 |
-
#input-section { align-items: normal !important; }
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
</style>
|
| 51 |
-
</body>
|
| 52 |
-
</html>
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7b9ccb53f0c73e706ec6c81d539e42adb8803c904bd7926055afbd0298d6d34a
|
| 3 |
+
size 1609
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/input/composer-banner-store.js
CHANGED
|
@@ -1,69 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
function buildBannersContext() {
|
| 6 |
-
return {
|
| 7 |
-
url: window.location.href,
|
| 8 |
-
protocol: window.location.protocol,
|
| 9 |
-
hostname: window.location.hostname,
|
| 10 |
-
port: window.location.port,
|
| 11 |
-
browser: navigator.userAgent,
|
| 12 |
-
timestamp: getCurrentUserISOString(),
|
| 13 |
-
};
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
export const store = createStore("composerBanner", {
|
| 17 |
-
missingApiKeys: [],
|
| 18 |
-
hasMissingApiKeyBanner: false,
|
| 19 |
-
loading: false,
|
| 20 |
-
lastRefresh: 0,
|
| 21 |
-
_modalCloseBound: false,
|
| 22 |
-
|
| 23 |
-
get hasMissingApiKeys() {
|
| 24 |
-
return this.hasMissingApiKeyBanner;
|
| 25 |
-
},
|
| 26 |
-
|
| 27 |
-
get missingApiKeysSummaryText() {
|
| 28 |
-
if (!this.hasMissingApiKeys) return "";
|
| 29 |
-
if (!Array.isArray(this.missingApiKeys) || this.missingApiKeys.length === 0) {
|
| 30 |
-
return "Configure your model provider API keys to continue.";
|
| 31 |
-
}
|
| 32 |
-
return this.missingApiKeys
|
| 33 |
-
.map((p) => `${p.model_type} (${p.provider})`)
|
| 34 |
-
.join(", ");
|
| 35 |
-
},
|
| 36 |
-
|
| 37 |
-
init() {
|
| 38 |
-
if (this._modalCloseBound) return;
|
| 39 |
-
this._modalCloseBound = true;
|
| 40 |
-
document.addEventListener("modal-closed", () => {
|
| 41 |
-
this.refresh(true);
|
| 42 |
-
});
|
| 43 |
-
},
|
| 44 |
-
|
| 45 |
-
async refresh(force = false) {
|
| 46 |
-
const now = Date.now();
|
| 47 |
-
if (!force && now - this.lastRefresh < 1000) return;
|
| 48 |
-
this.lastRefresh = now;
|
| 49 |
-
this.loading = true;
|
| 50 |
-
try {
|
| 51 |
-
const response = await callJsonApi("/banners", {
|
| 52 |
-
banners: [],
|
| 53 |
-
context: buildBannersContext(),
|
| 54 |
-
});
|
| 55 |
-
const banners = Array.isArray(response?.banners) ? response.banners : [];
|
| 56 |
-
const missingApiKeyBanner = banners.find((banner) => banner?.id === "missing-api-key");
|
| 57 |
-
this.hasMissingApiKeyBanner = !!missingApiKeyBanner;
|
| 58 |
-
this.missingApiKeys = Array.isArray(missingApiKeyBanner?.missing_providers)
|
| 59 |
-
? missingApiKeyBanner.missing_providers
|
| 60 |
-
: [];
|
| 61 |
-
} catch (e) {
|
| 62 |
-
console.error("composerBanner refresh failed", e);
|
| 63 |
-
this.hasMissingApiKeyBanner = false;
|
| 64 |
-
this.missingApiKeys = [];
|
| 65 |
-
} finally {
|
| 66 |
-
this.loading = false;
|
| 67 |
-
}
|
| 68 |
-
},
|
| 69 |
-
});
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f75dbff9419edd621acc868de8a904fd8e910e89555a68a9eadedb8e9562a8c2
|
| 3 |
+
size 2088
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/input/input-store.js
CHANGED
|
@@ -1,780 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import { openLatest as openLatestSurface } from "/js/surfaces.js";
|
| 5 |
-
import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js";
|
| 6 |
-
import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
|
| 7 |
-
import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
|
| 8 |
-
|
| 9 |
-
const ICON_MARKER_RE = /icon:\/\/([a-zA-Z0-9_]+)(\[(?:\\.|[^\]])*\])?/g;
|
| 10 |
-
const FENCE_LINE_RE = /^```([A-Za-z0-9_-]*)?$/;
|
| 11 |
-
const BLOCK_TAGS = new Set(["DIV", "P", "LI"]);
|
| 12 |
-
const DRAFT_STORAGE_PREFIX = "a0:chat-draft:";
|
| 13 |
-
|
| 14 |
-
function escapeHTML(value) {
|
| 15 |
-
return String(value ?? "")
|
| 16 |
-
.replace(/&/g, "&")
|
| 17 |
-
.replace(/</g, "<")
|
| 18 |
-
.replace(/>/g, ">")
|
| 19 |
-
.replace(/"/g, """)
|
| 20 |
-
.replace(/'/g, "'");
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
function unescapeIconTooltip(block) {
|
| 24 |
-
if (!block) return "";
|
| 25 |
-
return block
|
| 26 |
-
.slice(1, -1)
|
| 27 |
-
.replace(/\\\[/g, "[")
|
| 28 |
-
.replace(/\\\]/g, "]")
|
| 29 |
-
.replace(/\\\\/g, "\\");
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
function convertIconMarkersToHtml(value) {
|
| 33 |
-
const text = String(value ?? "");
|
| 34 |
-
let html = "";
|
| 35 |
-
let lastIndex = 0;
|
| 36 |
-
|
| 37 |
-
text.replace(ICON_MARKER_RE, (match, iconName, tooltipBlock, offset) => {
|
| 38 |
-
html += escapeHTML(text.slice(lastIndex, offset));
|
| 39 |
-
const tooltip = unescapeIconTooltip(tooltipBlock) || iconName;
|
| 40 |
-
html += (
|
| 41 |
-
`<x-icon class="icon chat-input-progress-icon" ` +
|
| 42 |
-
`title="${escapeHTML(tooltip)}" name="${escapeHTML(iconName)}"></x-icon>`
|
| 43 |
-
);
|
| 44 |
-
lastIndex = offset + match.length;
|
| 45 |
-
return match;
|
| 46 |
-
});
|
| 47 |
-
|
| 48 |
-
html += escapeHTML(text.slice(lastIndex));
|
| 49 |
-
return html;
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
const model = {
|
| 53 |
-
paused: false,
|
| 54 |
-
_message: "",
|
| 55 |
-
_editorEl: null,
|
| 56 |
-
_history: [],
|
| 57 |
-
_historyIndex: null,
|
| 58 |
-
_draft: "",
|
| 59 |
-
_historyCtxid: null,
|
| 60 |
-
_draftCtxid: null,
|
| 61 |
-
/** Composer + menu (bottom actions moved into dropdown) */
|
| 62 |
-
chatMoreMenuOpen: false,
|
| 63 |
-
progressText: "",
|
| 64 |
-
progressActive: false,
|
| 65 |
-
|
| 66 |
-
get message() {
|
| 67 |
-
return this._message;
|
| 68 |
-
},
|
| 69 |
-
|
| 70 |
-
set message(value) {
|
| 71 |
-
this._message = String(value ?? "");
|
| 72 |
-
this._renderEditorFromText(this._message);
|
| 73 |
-
this._saveDraft();
|
| 74 |
-
},
|
| 75 |
-
|
| 76 |
-
toggleChatMoreMenu() {
|
| 77 |
-
this.chatMoreMenuOpen = !this.chatMoreMenuOpen;
|
| 78 |
-
},
|
| 79 |
-
|
| 80 |
-
closeChatMoreMenu() {
|
| 81 |
-
this.chatMoreMenuOpen = false;
|
| 82 |
-
},
|
| 83 |
-
|
| 84 |
-
_getSendState() {
|
| 85 |
-
const hasInput = this.message.trim() || attachmentsStore?.attachments?.length > 0;
|
| 86 |
-
const hasQueue = !!messageQueueStore?.hasQueue;
|
| 87 |
-
const running = !!chatsStore.selectedContext?.running;
|
| 88 |
-
|
| 89 |
-
if (running && !hasInput) return "stop";
|
| 90 |
-
if (hasQueue && !hasInput) return "all";
|
| 91 |
-
if ((running || hasQueue) && hasInput) return "queue";
|
| 92 |
-
return "normal";
|
| 93 |
-
},
|
| 94 |
-
|
| 95 |
-
get inputPlaceholder() {
|
| 96 |
-
if (!chatsStore.selected) return "Ask anything to start a new chat";
|
| 97 |
-
const state = this._getSendState();
|
| 98 |
-
if ((state === "all" || state === "stop") && messageQueueStore?.hasQueue) {
|
| 99 |
-
return "Press Enter to send queued messages";
|
| 100 |
-
}
|
| 101 |
-
if (this.showProgressPlaceholder) return "";
|
| 102 |
-
return "Type your message here...";
|
| 103 |
-
},
|
| 104 |
-
|
| 105 |
-
get showProgressPlaceholder() {
|
| 106 |
-
const state = this._getSendState();
|
| 107 |
-
return (
|
| 108 |
-
!!chatsStore.selected &&
|
| 109 |
-
state !== "all" &&
|
| 110 |
-
!(state === "stop" && messageQueueStore?.hasQueue) &&
|
| 111 |
-
!!this.progressText &&
|
| 112 |
-
!this.message
|
| 113 |
-
);
|
| 114 |
-
},
|
| 115 |
-
|
| 116 |
-
get progressPlaceholderHtml() {
|
| 117 |
-
return convertIconMarkersToHtml(this.progressText);
|
| 118 |
-
},
|
| 119 |
-
|
| 120 |
-
// Computed: send button icon type
|
| 121 |
-
get sendButtonIcon() {
|
| 122 |
-
const state = this._getSendState();
|
| 123 |
-
if (state === "stop") return "stop";
|
| 124 |
-
if (state === "all") return "send_and_archive";
|
| 125 |
-
if (state === "queue") return "schedule_send";
|
| 126 |
-
return "arrow_forward";
|
| 127 |
-
},
|
| 128 |
-
|
| 129 |
-
// Computed: send button CSS class
|
| 130 |
-
get sendButtonClass() {
|
| 131 |
-
const state = this._getSendState();
|
| 132 |
-
if (state === "stop") return "stop";
|
| 133 |
-
if (state === "all") return "send-queue send-all";
|
| 134 |
-
if (state === "queue") return "send-queue queue";
|
| 135 |
-
return "";
|
| 136 |
-
},
|
| 137 |
-
|
| 138 |
-
// Computed: send button title
|
| 139 |
-
get sendButtonTitle() {
|
| 140 |
-
const state = this._getSendState();
|
| 141 |
-
if (state === "stop") return "Stop agent";
|
| 142 |
-
if (state === "all") return "Send all queued messages";
|
| 143 |
-
if (state === "queue") return "Add to queue";
|
| 144 |
-
return "Send message";
|
| 145 |
-
},
|
| 146 |
-
|
| 147 |
-
init() {
|
| 148 |
-
console.log("Input store initialized");
|
| 149 |
-
// Event listeners are now handled via Alpine directives in the component
|
| 150 |
-
},
|
| 151 |
-
|
| 152 |
-
async sendMessage() {
|
| 153 |
-
this._syncMessageFromEditor();
|
| 154 |
-
const pendingMessage = this.message;
|
| 155 |
-
|
| 156 |
-
if (!chatsStore.selected && (this.message.trim() || attachmentsStore?.attachments?.length > 0)) {
|
| 157 |
-
const ctxid = await chatsStore.newChat();
|
| 158 |
-
if (!ctxid && !chatsStore.selected) return;
|
| 159 |
-
this.message = pendingMessage;
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
// Capture sent prompt to per-chat history (bash-style)
|
| 163 |
-
try { this._pushHistory(this.message); } catch (_e) { /* ignore */ }
|
| 164 |
-
|
| 165 |
-
// Delegate to the global function
|
| 166 |
-
if (globalThis.sendMessage) {
|
| 167 |
-
await globalThis.sendMessage();
|
| 168 |
-
}
|
| 169 |
-
},
|
| 170 |
-
|
| 171 |
-
async activateSendButton() {
|
| 172 |
-
this._syncMessageFromEditor();
|
| 173 |
-
if (this._getSendState() === "stop") {
|
| 174 |
-
await this.stopAgent();
|
| 175 |
-
return;
|
| 176 |
-
}
|
| 177 |
-
await this.sendMessage();
|
| 178 |
-
},
|
| 179 |
-
|
| 180 |
-
mountEditor(editor) {
|
| 181 |
-
this._editorEl = editor;
|
| 182 |
-
this.setDraftContext(shortcuts.getCurrentContextId());
|
| 183 |
-
this._renderEditorFromText(this._message);
|
| 184 |
-
this.adjustTextareaHeight({ target: editor });
|
| 185 |
-
},
|
| 186 |
-
|
| 187 |
-
unmountEditor(editor) {
|
| 188 |
-
if (this._editorEl === editor) this._editorEl = null;
|
| 189 |
-
},
|
| 190 |
-
|
| 191 |
-
_composerTextareas(target = null) {
|
| 192 |
-
const editor = target?.closest?.("#chat-input") || this._editorEl || document.getElementById("chat-input");
|
| 193 |
-
return editor ? [editor] : [];
|
| 194 |
-
},
|
| 195 |
-
|
| 196 |
-
_activeTextarea() {
|
| 197 |
-
const active = document.activeElement;
|
| 198 |
-
const activeEditor = active?.closest?.("#chat-input");
|
| 199 |
-
if (activeEditor) {
|
| 200 |
-
return activeEditor;
|
| 201 |
-
}
|
| 202 |
-
return this._editorEl || document.getElementById("chat-input");
|
| 203 |
-
},
|
| 204 |
-
|
| 205 |
-
_renderEditorFromText(text) {
|
| 206 |
-
const editor = this._editorEl;
|
| 207 |
-
if (!editor) return;
|
| 208 |
-
if (editor.textContent !== text || editor.querySelector("[data-code-block]")) {
|
| 209 |
-
editor.textContent = text;
|
| 210 |
-
}
|
| 211 |
-
this._setEditorEmptyState();
|
| 212 |
-
},
|
| 213 |
-
|
| 214 |
-
_setEditorEmptyState() {
|
| 215 |
-
const editor = this._editorEl;
|
| 216 |
-
if (editor) editor.classList.toggle("is-empty", !this._message);
|
| 217 |
-
},
|
| 218 |
-
|
| 219 |
-
_createCodeBlock(lang = "") {
|
| 220 |
-
const block = document.createElement("div");
|
| 221 |
-
block.className = "composer-code-block";
|
| 222 |
-
block.dataset.codeBlock = "true";
|
| 223 |
-
block.dataset.lang = lang;
|
| 224 |
-
block.contentEditable = "false";
|
| 225 |
-
|
| 226 |
-
const code = document.createElement("pre");
|
| 227 |
-
code.className = "composer-code-content";
|
| 228 |
-
code.dataset.codeContent = "true";
|
| 229 |
-
code.contentEditable = "true";
|
| 230 |
-
code.spellcheck = false;
|
| 231 |
-
code.setAttribute("aria-label", "Code block");
|
| 232 |
-
block.append(code);
|
| 233 |
-
|
| 234 |
-
return block;
|
| 235 |
-
},
|
| 236 |
-
|
| 237 |
-
_plainText(node) {
|
| 238 |
-
if (!node) return "";
|
| 239 |
-
if (node.nodeType === Node.TEXT_NODE) return node.nodeValue || "";
|
| 240 |
-
if (node.nodeType !== Node.ELEMENT_NODE) return "";
|
| 241 |
-
if (node.matches?.("[data-code-block]")) {
|
| 242 |
-
const lang = node.dataset.lang || "";
|
| 243 |
-
const code = this._plainText(node.querySelector("[data-code-content]")).replace(/\n$/, "");
|
| 244 |
-
return "```" + lang + "\n" + code + "\n```";
|
| 245 |
-
}
|
| 246 |
-
if (node.tagName === "BR") return "\n";
|
| 247 |
-
|
| 248 |
-
let text = "";
|
| 249 |
-
for (const child of node.childNodes) text += this._plainText(child);
|
| 250 |
-
if (node !== this._editorEl && BLOCK_TAGS.has(node.tagName) && text && !text.endsWith("\n")) {
|
| 251 |
-
text += "\n";
|
| 252 |
-
}
|
| 253 |
-
return text;
|
| 254 |
-
},
|
| 255 |
-
|
| 256 |
-
_editorToMarkdown() {
|
| 257 |
-
return this._plainText(this._editorEl).replace(/\n$/, "");
|
| 258 |
-
},
|
| 259 |
-
|
| 260 |
-
_syncMessageFromEditor() {
|
| 261 |
-
if (!this._editorEl) return;
|
| 262 |
-
this._message = this._editorToMarkdown();
|
| 263 |
-
this._setEditorEmptyState();
|
| 264 |
-
this._saveDraft();
|
| 265 |
-
},
|
| 266 |
-
|
| 267 |
-
_isInCodeBlock(target) {
|
| 268 |
-
return Boolean(target?.closest?.("[data-code-content]"));
|
| 269 |
-
},
|
| 270 |
-
|
| 271 |
-
_selectionOffsets(editor) {
|
| 272 |
-
const selection = document.getSelection?.();
|
| 273 |
-
if (!selection || selection.rangeCount === 0 || !editor) {
|
| 274 |
-
return { start: this._message.length, end: this._message.length };
|
| 275 |
-
}
|
| 276 |
-
const range = selection.getRangeAt(0);
|
| 277 |
-
if (!editor.contains(range.startContainer) || !editor.contains(range.endContainer)) {
|
| 278 |
-
return { start: this._message.length, end: this._message.length };
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
const before = range.cloneRange();
|
| 282 |
-
before.selectNodeContents(editor);
|
| 283 |
-
before.setEnd(range.startContainer, range.startOffset);
|
| 284 |
-
|
| 285 |
-
const selected = range.cloneRange();
|
| 286 |
-
return {
|
| 287 |
-
start: before.toString().length,
|
| 288 |
-
end: before.toString().length + selected.toString().length,
|
| 289 |
-
};
|
| 290 |
-
},
|
| 291 |
-
|
| 292 |
-
_setEditorCaret(offset) {
|
| 293 |
-
const editor = this._activeTextarea();
|
| 294 |
-
const selection = document.getSelection?.();
|
| 295 |
-
if (!editor || !selection) return;
|
| 296 |
-
|
| 297 |
-
editor.focus();
|
| 298 |
-
const range = document.createRange();
|
| 299 |
-
let remaining = Math.max(0, Number(offset) || 0);
|
| 300 |
-
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
|
| 301 |
-
let node = walker.nextNode();
|
| 302 |
-
|
| 303 |
-
while (node) {
|
| 304 |
-
const length = node.nodeValue.length;
|
| 305 |
-
if (remaining <= length) {
|
| 306 |
-
range.setStart(node, remaining);
|
| 307 |
-
range.collapse(true);
|
| 308 |
-
selection.removeAllRanges();
|
| 309 |
-
selection.addRange(range);
|
| 310 |
-
return;
|
| 311 |
-
}
|
| 312 |
-
remaining -= length;
|
| 313 |
-
node = walker.nextNode();
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
range.selectNodeContents(editor);
|
| 317 |
-
range.collapse(false);
|
| 318 |
-
selection.removeAllRanges();
|
| 319 |
-
selection.addRange(range);
|
| 320 |
-
},
|
| 321 |
-
|
| 322 |
-
_focusCodeBlock(block) {
|
| 323 |
-
queueMicrotask(() => {
|
| 324 |
-
const code = block.querySelector("[data-code-content]");
|
| 325 |
-
const selection = document.getSelection?.();
|
| 326 |
-
if (!code || !selection) return;
|
| 327 |
-
code.focus();
|
| 328 |
-
const range = document.createRange();
|
| 329 |
-
range.selectNodeContents(code);
|
| 330 |
-
range.collapse(false);
|
| 331 |
-
selection.removeAllRanges();
|
| 332 |
-
selection.addRange(range);
|
| 333 |
-
});
|
| 334 |
-
},
|
| 335 |
-
|
| 336 |
-
_insertPlainText(text) {
|
| 337 |
-
const selection = document.getSelection?.();
|
| 338 |
-
if (!selection || selection.rangeCount === 0) return false;
|
| 339 |
-
const range = selection.getRangeAt(0);
|
| 340 |
-
range.deleteContents();
|
| 341 |
-
const node = document.createTextNode(text);
|
| 342 |
-
range.insertNode(node);
|
| 343 |
-
range.setStart(node, node.nodeValue.length);
|
| 344 |
-
range.collapse(true);
|
| 345 |
-
selection.removeAllRanges();
|
| 346 |
-
selection.addRange(range);
|
| 347 |
-
return true;
|
| 348 |
-
},
|
| 349 |
-
|
| 350 |
-
_tryCreateCodeBlock($event) {
|
| 351 |
-
const editor = this._activeTextarea();
|
| 352 |
-
const selection = document.getSelection?.();
|
| 353 |
-
if (!editor || !selection || selection.rangeCount === 0) return false;
|
| 354 |
-
|
| 355 |
-
const range = selection.getRangeAt(0);
|
| 356 |
-
if (!range.collapsed || !editor.contains(range.startContainer)) return false;
|
| 357 |
-
if (this._isInCodeBlock(range.startContainer.parentElement)) return false;
|
| 358 |
-
|
| 359 |
-
const before = range.cloneRange();
|
| 360 |
-
before.selectNodeContents(editor);
|
| 361 |
-
before.setEnd(range.startContainer, range.startOffset);
|
| 362 |
-
|
| 363 |
-
const after = range.cloneRange();
|
| 364 |
-
after.selectNodeContents(editor);
|
| 365 |
-
after.setStart(range.startContainer, range.startOffset);
|
| 366 |
-
|
| 367 |
-
const lineBefore = before.toString().split("\n").pop() || "";
|
| 368 |
-
const lineAfter = (after.toString().split("\n")[0] || "");
|
| 369 |
-
const match = (lineBefore + lineAfter).match(FENCE_LINE_RE);
|
| 370 |
-
if (!match || lineAfter) return false;
|
| 371 |
-
if (range.startContainer.nodeType !== Node.TEXT_NODE || range.startOffset < lineBefore.length) return false;
|
| 372 |
-
|
| 373 |
-
$event.preventDefault();
|
| 374 |
-
const editRange = range.cloneRange();
|
| 375 |
-
editRange.setStart(range.startContainer, range.startOffset - lineBefore.length);
|
| 376 |
-
editRange.deleteContents();
|
| 377 |
-
|
| 378 |
-
const block = this._createCodeBlock(match[1] || "");
|
| 379 |
-
editRange.insertNode(block);
|
| 380 |
-
block.after(document.createTextNode("\n"));
|
| 381 |
-
this._focusCodeBlock(block);
|
| 382 |
-
this._syncMessageFromEditor();
|
| 383 |
-
this.adjustTextareaHeight({ target: editor });
|
| 384 |
-
return true;
|
| 385 |
-
},
|
| 386 |
-
|
| 387 |
-
handleInput($event) {
|
| 388 |
-
this._syncMessageFromEditor();
|
| 389 |
-
this.adjustTextareaHeight($event);
|
| 390 |
-
},
|
| 391 |
-
|
| 392 |
-
handlePaste($event) {
|
| 393 |
-
const text = $event.clipboardData?.getData("text/plain");
|
| 394 |
-
if (text === undefined) return;
|
| 395 |
-
$event.preventDefault();
|
| 396 |
-
this._insertPlainText(text);
|
| 397 |
-
this.handleInput($event);
|
| 398 |
-
},
|
| 399 |
-
|
| 400 |
-
handleKeydown($event) {
|
| 401 |
-
if ($event.isComposing || $event.keyCode === 229) return;
|
| 402 |
-
|
| 403 |
-
if ($event.key === "Enter") {
|
| 404 |
-
if (this._isInCodeBlock($event.target)) return;
|
| 405 |
-
if (!$event.shiftKey) {
|
| 406 |
-
if (this._tryCreateCodeBlock($event)) return;
|
| 407 |
-
$event.preventDefault();
|
| 408 |
-
this.sendMessage();
|
| 409 |
-
}
|
| 410 |
-
return;
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
if ($event.key === "ArrowUp") this.historyPrev($event);
|
| 414 |
-
if ($event.key === "ArrowDown") this.historyNext($event);
|
| 415 |
-
},
|
| 416 |
-
|
| 417 |
-
adjustTextareaHeight($event = null) {
|
| 418 |
-
const target = $event?.target || null;
|
| 419 |
-
for (const chatInput of this._composerTextareas(target)) {
|
| 420 |
-
chatInput.style.height = "auto";
|
| 421 |
-
chatInput.style.height = chatInput.scrollHeight + "px";
|
| 422 |
-
// pick up any layout shift triggered by the height assignment
|
| 423 |
-
chatInput.style.height = Math.max(chatInput.scrollHeight, parseInt(chatInput.style.height)) + "px";
|
| 424 |
-
}
|
| 425 |
-
},
|
| 426 |
-
|
| 427 |
-
async pauseAgent(paused) {
|
| 428 |
-
const prev = this.paused;
|
| 429 |
-
this.paused = paused;
|
| 430 |
-
try {
|
| 431 |
-
const context = globalThis.getContext?.();
|
| 432 |
-
if (!globalThis.sendJsonData)
|
| 433 |
-
throw new Error("sendJsonData not available");
|
| 434 |
-
await globalThis.sendJsonData("/pause", { paused, context });
|
| 435 |
-
} catch (e) {
|
| 436 |
-
this.paused = prev;
|
| 437 |
-
if (globalThis.toastFetchError) {
|
| 438 |
-
globalThis.toastFetchError("Error pausing agent", e);
|
| 439 |
-
}
|
| 440 |
-
}
|
| 441 |
-
},
|
| 442 |
-
|
| 443 |
-
async stopAgent() {
|
| 444 |
-
try {
|
| 445 |
-
const context = globalThis.getContext?.();
|
| 446 |
-
if (!context || !globalThis.sendJsonData) return;
|
| 447 |
-
await globalThis.sendJsonData("/stop", { context });
|
| 448 |
-
} catch (e) {
|
| 449 |
-
if (globalThis.toastFetchError) {
|
| 450 |
-
globalThis.toastFetchError("Error stopping agent", e);
|
| 451 |
-
}
|
| 452 |
-
}
|
| 453 |
-
},
|
| 454 |
-
|
| 455 |
-
async nudge() {
|
| 456 |
-
try {
|
| 457 |
-
const context = globalThis.getContext();
|
| 458 |
-
await globalThis.sendJsonData("/nudge", { ctxid: context });
|
| 459 |
-
} catch (e) {
|
| 460 |
-
if (globalThis.toastFetchError) {
|
| 461 |
-
globalThis.toastFetchError("Error nudging agent", e);
|
| 462 |
-
}
|
| 463 |
-
}
|
| 464 |
-
},
|
| 465 |
-
|
| 466 |
-
async loadKnowledge() {
|
| 467 |
-
try {
|
| 468 |
-
const resp = await shortcuts.callJsonApi(
|
| 469 |
-
"/plugins/_memory/knowledge_path_get",
|
| 470 |
-
{ ctxid: shortcuts.getCurrentContextId() }
|
| 471 |
-
);
|
| 472 |
-
if (!resp.ok) throw new Error("Error getting knowledge path");
|
| 473 |
-
const path = resp.path;
|
| 474 |
-
|
| 475 |
-
// open file browser and wait for it to close
|
| 476 |
-
await fileBrowserStore.open(path);
|
| 477 |
-
|
| 478 |
-
// progress notification
|
| 479 |
-
shortcuts.frontendNotification({
|
| 480 |
-
type: shortcuts.NotificationType.PROGRESS,
|
| 481 |
-
message: "Loading knowledge...",
|
| 482 |
-
priority: shortcuts.NotificationPriority.NORMAL,
|
| 483 |
-
displayTime: 999,
|
| 484 |
-
group: "knowledge_load",
|
| 485 |
-
frontendOnly: true,
|
| 486 |
-
});
|
| 487 |
-
|
| 488 |
-
// then reindex knowledge
|
| 489 |
-
await globalThis.sendJsonData("/plugins/_memory/knowledge_reindex", {
|
| 490 |
-
ctxid: shortcuts.getCurrentContextId(),
|
| 491 |
-
});
|
| 492 |
-
|
| 493 |
-
// finished notification
|
| 494 |
-
shortcuts.frontendNotification({
|
| 495 |
-
type: shortcuts.NotificationType.SUCCESS,
|
| 496 |
-
message: "Knowledge loaded successfully",
|
| 497 |
-
priority: shortcuts.NotificationPriority.NORMAL,
|
| 498 |
-
displayTime: 2,
|
| 499 |
-
group: "knowledge_load",
|
| 500 |
-
frontendOnly: true,
|
| 501 |
-
});
|
| 502 |
-
} catch (e) {
|
| 503 |
-
// error notification
|
| 504 |
-
shortcuts.frontendNotification({
|
| 505 |
-
type: shortcuts.NotificationType.ERROR,
|
| 506 |
-
message: "Error loading knowledge",
|
| 507 |
-
priority: shortcuts.NotificationPriority.NORMAL,
|
| 508 |
-
displayTime: 5,
|
| 509 |
-
group: "knowledge_load",
|
| 510 |
-
frontendOnly: true,
|
| 511 |
-
});
|
| 512 |
-
}
|
| 513 |
-
},
|
| 514 |
-
|
| 515 |
-
// previous implementation without projects
|
| 516 |
-
async _loadKnowledge() {
|
| 517 |
-
const input = document.createElement("input");
|
| 518 |
-
input.type = "file";
|
| 519 |
-
input.accept = ".txt,.pdf,.csv,.html,.json,.md";
|
| 520 |
-
input.multiple = true;
|
| 521 |
-
|
| 522 |
-
input.onchange = async () => {
|
| 523 |
-
try {
|
| 524 |
-
const formData = new FormData();
|
| 525 |
-
for (let file of input.files) {
|
| 526 |
-
formData.append("files[]", file);
|
| 527 |
-
}
|
| 528 |
-
|
| 529 |
-
formData.append("ctxid", globalThis.getContext());
|
| 530 |
-
|
| 531 |
-
const response = await globalThis.fetchApi("/import_knowledge", {
|
| 532 |
-
method: "POST",
|
| 533 |
-
body: formData,
|
| 534 |
-
});
|
| 535 |
-
|
| 536 |
-
if (!response.ok) {
|
| 537 |
-
if (globalThis.toast)
|
| 538 |
-
globalThis.toast(await response.text(), "error");
|
| 539 |
-
} else {
|
| 540 |
-
const data = await response.json();
|
| 541 |
-
if (globalThis.toast) {
|
| 542 |
-
globalThis.toast(
|
| 543 |
-
"Knowledge files imported: " + data.filenames.join(", "),
|
| 544 |
-
"success"
|
| 545 |
-
);
|
| 546 |
-
}
|
| 547 |
-
}
|
| 548 |
-
} catch (e) {
|
| 549 |
-
if (globalThis.toastFetchError) {
|
| 550 |
-
globalThis.toastFetchError("Error loading knowledge", e);
|
| 551 |
-
}
|
| 552 |
-
}
|
| 553 |
-
};
|
| 554 |
-
|
| 555 |
-
input.click();
|
| 556 |
-
},
|
| 557 |
-
|
| 558 |
-
async browseFiles(path) {
|
| 559 |
-
if (!path) {
|
| 560 |
-
const ctxid = shortcuts.getCurrentContextId();
|
| 561 |
-
|
| 562 |
-
if (ctxid) {
|
| 563 |
-
try {
|
| 564 |
-
const resp = await shortcuts.callJsonApi("/chat_files_path_get", {
|
| 565 |
-
ctxid,
|
| 566 |
-
});
|
| 567 |
-
if (resp.ok) path = resp.path;
|
| 568 |
-
} catch (_e) {
|
| 569 |
-
console.error("Error getting chat files path", _e);
|
| 570 |
-
}
|
| 571 |
-
}
|
| 572 |
-
}
|
| 573 |
-
let opened = false;
|
| 574 |
-
try {
|
| 575 |
-
opened = await openLatestSurface("files", { path, source: "sidebar" });
|
| 576 |
-
} catch (error) {
|
| 577 |
-
console.error("Error opening Files surface", error);
|
| 578 |
-
}
|
| 579 |
-
if (!opened) await fileBrowserStore.open(path);
|
| 580 |
-
},
|
| 581 |
-
|
| 582 |
-
focus() {
|
| 583 |
-
const chatInput = this._activeTextarea();
|
| 584 |
-
if (chatInput) {
|
| 585 |
-
chatInput.focus();
|
| 586 |
-
}
|
| 587 |
-
},
|
| 588 |
-
|
| 589 |
-
setDraftContext(ctxid) {
|
| 590 |
-
const nextCtxid = String(ctxid || "");
|
| 591 |
-
if (nextCtxid === this._draftCtxid) return;
|
| 592 |
-
if (this._draftCtxid !== null) this._syncMessageFromEditor();
|
| 593 |
-
|
| 594 |
-
this._draftCtxid = nextCtxid;
|
| 595 |
-
this._historyIndex = null;
|
| 596 |
-
this._draft = "";
|
| 597 |
-
|
| 598 |
-
let draft = "";
|
| 599 |
-
if (nextCtxid) {
|
| 600 |
-
try { draft = sessionStorage.getItem(DRAFT_STORAGE_PREFIX + nextCtxid) || ""; } catch (_e) { /* ignore */ }
|
| 601 |
-
}
|
| 602 |
-
this._message = draft;
|
| 603 |
-
this._renderEditorFromText(draft);
|
| 604 |
-
queueMicrotask(() => this.adjustTextareaHeight());
|
| 605 |
-
},
|
| 606 |
-
|
| 607 |
-
_saveDraft() {
|
| 608 |
-
if (!this._draftCtxid) return;
|
| 609 |
-
try {
|
| 610 |
-
const key = DRAFT_STORAGE_PREFIX + this._draftCtxid;
|
| 611 |
-
if (this._message) sessionStorage.setItem(key, this._message);
|
| 612 |
-
else sessionStorage.removeItem(key);
|
| 613 |
-
} catch (_e) { /* ignore unavailable storage */ }
|
| 614 |
-
},
|
| 615 |
-
|
| 616 |
-
_loadHistory() {
|
| 617 |
-
let ctxid = null;
|
| 618 |
-
try { ctxid = shortcuts.getCurrentContextId(); } catch (_e) { ctxid = null; }
|
| 619 |
-
this._historyCtxid = ctxid;
|
| 620 |
-
this._history = [];
|
| 621 |
-
this._historyIndex = null;
|
| 622 |
-
this._draft = "";
|
| 623 |
-
if (!ctxid) return;
|
| 624 |
-
let raw = null;
|
| 625 |
-
try { raw = localStorage.getItem("a0:chat-history:" + ctxid); } catch (_e) { raw = null; }
|
| 626 |
-
if (raw !== null) {
|
| 627 |
-
try {
|
| 628 |
-
const arr = JSON.parse(raw);
|
| 629 |
-
if (Array.isArray(arr)) {
|
| 630 |
-
this._history = arr.filter((s) => typeof s === "string");
|
| 631 |
-
}
|
| 632 |
-
} catch (_e) { /* ignore */ }
|
| 633 |
-
return;
|
| 634 |
-
}
|
| 635 |
-
// No entry yet for this chat: seed from rendered chat DOM (one-time bootstrap)
|
| 636 |
-
try {
|
| 637 |
-
const seeded = this._seedFromChatDom();
|
| 638 |
-
if (seeded.length > 0) {
|
| 639 |
-
this._history = seeded;
|
| 640 |
-
this._saveHistory();
|
| 641 |
-
} else {
|
| 642 |
-
// Persist an empty array so we don't re-seed on every nav; respects user clearing
|
| 643 |
-
this._saveHistory();
|
| 644 |
-
}
|
| 645 |
-
} catch (_e) { /* ignore */ }
|
| 646 |
-
},
|
| 647 |
-
|
| 648 |
-
_seedFromChatDom() {
|
| 649 |
-
const out = [];
|
| 650 |
-
let nodes;
|
| 651 |
-
try {
|
| 652 |
-
nodes = document.querySelectorAll(".user-container .message-user .message-text pre");
|
| 653 |
-
} catch (_e) {
|
| 654 |
-
return out;
|
| 655 |
-
}
|
| 656 |
-
for (const pre of nodes) {
|
| 657 |
-
const text = (pre.textContent || "").trim();
|
| 658 |
-
if (!text) continue;
|
| 659 |
-
if (out.length > 0 && out[out.length - 1] === text) continue; // ignoredups
|
| 660 |
-
out.push(text);
|
| 661 |
-
}
|
| 662 |
-
if (out.length > 50) return out.slice(-50);
|
| 663 |
-
return out;
|
| 664 |
-
},
|
| 665 |
-
|
| 666 |
-
_saveHistory() {
|
| 667 |
-
if (!this._historyCtxid) return;
|
| 668 |
-
try {
|
| 669 |
-
localStorage.setItem(
|
| 670 |
-
"a0:chat-history:" + this._historyCtxid,
|
| 671 |
-
JSON.stringify(this._history)
|
| 672 |
-
);
|
| 673 |
-
} catch (_e) { /* ignore quota / disabled */ }
|
| 674 |
-
},
|
| 675 |
-
|
| 676 |
-
_ensureHistoryLoaded() {
|
| 677 |
-
let ctxid = null;
|
| 678 |
-
try { ctxid = shortcuts.getCurrentContextId(); } catch (_e) { ctxid = null; }
|
| 679 |
-
if (ctxid !== this._historyCtxid) {
|
| 680 |
-
this._loadHistory();
|
| 681 |
-
}
|
| 682 |
-
},
|
| 683 |
-
|
| 684 |
-
_pushHistory(text) {
|
| 685 |
-
if (typeof text !== "string") return;
|
| 686 |
-
const trimmed = text.trim();
|
| 687 |
-
if (!trimmed) return;
|
| 688 |
-
this._ensureHistoryLoaded();
|
| 689 |
-
if (this._history.length > 0 && this._history[this._history.length - 1] === trimmed) {
|
| 690 |
-
this._historyIndex = null;
|
| 691 |
-
this._draft = "";
|
| 692 |
-
return;
|
| 693 |
-
}
|
| 694 |
-
this._history.push(trimmed);
|
| 695 |
-
if (this._history.length > 50) {
|
| 696 |
-
this._history = this._history.slice(-50);
|
| 697 |
-
}
|
| 698 |
-
this._saveHistory();
|
| 699 |
-
this._historyIndex = null;
|
| 700 |
-
this._draft = "";
|
| 701 |
-
},
|
| 702 |
-
|
| 703 |
-
_setCaretStart() {
|
| 704 |
-
queueMicrotask(() => {
|
| 705 |
-
const editor = this._activeTextarea();
|
| 706 |
-
if (editor) {
|
| 707 |
-
this._setEditorCaret(0);
|
| 708 |
-
try { editor.scrollTop = 0; } catch (_e) { /* ignore */ }
|
| 709 |
-
}
|
| 710 |
-
this.adjustTextareaHeight();
|
| 711 |
-
});
|
| 712 |
-
},
|
| 713 |
-
|
| 714 |
-
_setCaretEnd() {
|
| 715 |
-
queueMicrotask(() => {
|
| 716 |
-
const editor = this._activeTextarea();
|
| 717 |
-
if (editor) {
|
| 718 |
-
this._setEditorCaret(editor.innerText.length);
|
| 719 |
-
try { editor.scrollTop = editor.scrollHeight; } catch (_e) { /* ignore */ }
|
| 720 |
-
}
|
| 721 |
-
this.adjustTextareaHeight();
|
| 722 |
-
});
|
| 723 |
-
},
|
| 724 |
-
|
| 725 |
-
historyPrev($event) {
|
| 726 |
-
if ($event && ($event.isComposing || $event.keyCode === 229)) return;
|
| 727 |
-
if (this._isInCodeBlock($event?.target)) return;
|
| 728 |
-
const editor = this._activeTextarea();
|
| 729 |
-
if (!editor) return;
|
| 730 |
-
const { start, end } = this._selectionOffsets(editor);
|
| 731 |
-
if (start !== 0 || end !== 0) return;
|
| 732 |
-
$event.preventDefault();
|
| 733 |
-
this._ensureHistoryLoaded();
|
| 734 |
-
if (this._history.length === 0) return;
|
| 735 |
-
if (this._historyIndex === null) {
|
| 736 |
-
this._draft = this.message || "";
|
| 737 |
-
this._historyIndex = this._history.length - 1;
|
| 738 |
-
} else if (this._historyIndex > 0) {
|
| 739 |
-
this._historyIndex -= 1;
|
| 740 |
-
} else {
|
| 741 |
-
return;
|
| 742 |
-
}
|
| 743 |
-
this.message = this._history[this._historyIndex];
|
| 744 |
-
this._setCaretStart();
|
| 745 |
-
},
|
| 746 |
-
|
| 747 |
-
historyNext($event) {
|
| 748 |
-
if ($event && ($event.isComposing || $event.keyCode === 229)) return;
|
| 749 |
-
if (this._isInCodeBlock($event?.target)) return;
|
| 750 |
-
const editor = this._activeTextarea();
|
| 751 |
-
if (!editor) return;
|
| 752 |
-
const { start, end } = this._selectionOffsets(editor);
|
| 753 |
-
const valueLength = editor.innerText.length;
|
| 754 |
-
if (start !== valueLength || end !== valueLength) return;
|
| 755 |
-
$event.preventDefault();
|
| 756 |
-
if (this._historyIndex === null) return;
|
| 757 |
-
if (this._historyIndex < this._history.length - 1) {
|
| 758 |
-
this._historyIndex += 1;
|
| 759 |
-
this.message = this._history[this._historyIndex];
|
| 760 |
-
} else {
|
| 761 |
-
this._historyIndex = null;
|
| 762 |
-
this.message = this._draft || "";
|
| 763 |
-
this._draft = "";
|
| 764 |
-
}
|
| 765 |
-
this._setCaretEnd();
|
| 766 |
-
},
|
| 767 |
-
|
| 768 |
-
reset() {
|
| 769 |
-
this.message = "";
|
| 770 |
-
attachmentsStore.clearAttachments();
|
| 771 |
-
this.chatMoreMenuOpen = false;
|
| 772 |
-
this._historyIndex = null;
|
| 773 |
-
this._draft = "";
|
| 774 |
-
this.adjustTextareaHeight();
|
| 775 |
-
}
|
| 776 |
-
};
|
| 777 |
-
|
| 778 |
-
const store = createStore("chatInput", model);
|
| 779 |
-
|
| 780 |
-
export { store };
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9f9248f9813e804fce49f05b0f922faf8638220648c5f1b399742a5a90babc56
|
| 3 |
+
size 23500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/input/progress.html
CHANGED
|
@@ -1,57 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import { store as chatNavStore } from "/components/chat/navigation/chat-navigation-store.js";
|
| 5 |
-
</script>
|
| 6 |
-
</head>
|
| 7 |
-
<body>
|
| 8 |
-
<div id="progress-bar-box" x-data>
|
| 9 |
-
<x-extension id="chat-input-progress-start"></x-extension>
|
| 10 |
-
|
| 11 |
-
<!-- Hidden legacy progress-bar element (keeps JS updater happy, not displayed) -->
|
| 12 |
-
<span id="progress-bar" style="display:none;"></span>
|
| 13 |
-
|
| 14 |
-
<div id="progress-bar-right">
|
| 15 |
-
<h4
|
| 16 |
-
id="progress-bar-stop-speech"
|
| 17 |
-
x-data="{
|
| 18 |
-
speaking: window.ttsService?.isSpeaking?.() || false,
|
| 19 |
-
_listener: null,
|
| 20 |
-
init() {
|
| 21 |
-
this._listener = (event) => {
|
| 22 |
-
this.speaking = !!event?.detail?.isSpeaking;
|
| 23 |
-
};
|
| 24 |
-
window.ttsService?.addEventListener?.('statechange', this._listener);
|
| 25 |
-
},
|
| 26 |
-
cleanup() {
|
| 27 |
-
if (this._listener) {
|
| 28 |
-
window.ttsService?.removeEventListener?.('statechange', this._listener);
|
| 29 |
-
}
|
| 30 |
-
},
|
| 31 |
-
}"
|
| 32 |
-
x-init="init()"
|
| 33 |
-
x-destroy="cleanup()"
|
| 34 |
-
x-cloak
|
| 35 |
-
x-show="speaking"
|
| 36 |
-
>
|
| 37 |
-
<span id="stop-speech" @click="window.ttsService?.stop?.()" style="cursor: pointer" title="Stop Speech" aria-label="Stop Speech">
|
| 38 |
-
<x-icon class="icon" name="volume_off"></x-icon>
|
| 39 |
-
</span>
|
| 40 |
-
</h4>
|
| 41 |
-
<x-extension id="chat-nav-after"></x-extension>
|
| 42 |
-
</div>
|
| 43 |
-
<x-extension id="chat-input-progress-end"></x-extension>
|
| 44 |
-
</div>
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
<style>
|
| 48 |
-
#progress-bar-box { background-color: var(--color-panel); padding: var(--spacing-xs) var(--spacing-md) 0 var(--spacing-md); display: flex; flex-wrap: nowrap; justify-content: flex-start; align-items: center; z-index: 1001; gap: var(--spacing-sm); min-height: 0; }
|
| 49 |
-
#progress-bar-box > x-extension { display: contents; }
|
| 50 |
-
#progress-bar-box h4 { margin: 0; }
|
| 51 |
-
#progress-bar-right { display: flex; align-items: center; gap: var(--spacing-sm); flex-shrink: 0; margin-left: auto;}
|
| 52 |
-
#progress-bar-right > x-extension { display: contents; }
|
| 53 |
-
.shiny-text { background: linear-gradient(to right, var(--color-primary-dark) 20%, var(--color-text) 40%, var(--color-text) 60%, var(--color-primary-dark) 60%); background-size: 200% auto; color: transparent; -webkit-background-clip: text; background-clip: text; animation: shine 1s linear infinite; }
|
| 54 |
-
@keyframes shine { to { background-position: -200% center; } }
|
| 55 |
-
</style>
|
| 56 |
-
</body>
|
| 57 |
-
</html>
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a3090e2fa54cf5a9b86c50056486d5958eecbb28d88d1fb1b1d1195aeb6092f4
|
| 3 |
+
size 2596
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/message-queue/message-queue-store.js
CHANGED
|
@@ -1,245 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import {
|
| 5 |
-
toastFrontendInfo,
|
| 6 |
-
NotificationPriority,
|
| 7 |
-
} from "/components/notifications/notification-store.js";
|
| 8 |
-
import { sleep } from "/js/sleep.js";
|
| 9 |
-
import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
|
| 10 |
-
|
| 11 |
-
const model = {
|
| 12 |
-
// items: [],
|
| 13 |
-
get items() {
|
| 14 |
-
return chatsStore.selectedContext?.message_queue || [];
|
| 15 |
-
},
|
| 16 |
-
|
| 17 |
-
pendingItems: [], // Local pending items (uploading to queue)
|
| 18 |
-
|
| 19 |
-
_pendingAddOps: {},
|
| 20 |
-
|
| 21 |
-
_lastAddToQueuePromise: Promise.resolve(),
|
| 22 |
-
|
| 23 |
-
_getQueueScrollerEl() {
|
| 24 |
-
return document.querySelector(".queue-preview .queue-items");
|
| 25 |
-
},
|
| 26 |
-
|
| 27 |
-
scrollQueueToBottom() {
|
| 28 |
-
const el = this._getQueueScrollerEl();
|
| 29 |
-
if (!el) return;
|
| 30 |
-
|
| 31 |
-
const scroll = () => {
|
| 32 |
-
el.scrollTop = el.scrollHeight;
|
| 33 |
-
};
|
| 34 |
-
|
| 35 |
-
requestAnimationFrame(() => {
|
| 36 |
-
scroll();
|
| 37 |
-
requestAnimationFrame(scroll);
|
| 38 |
-
});
|
| 39 |
-
},
|
| 40 |
-
|
| 41 |
-
get hasQueue() {
|
| 42 |
-
return this.items.length > 0 || this.pendingItems.length > 0;
|
| 43 |
-
},
|
| 44 |
-
|
| 45 |
-
get count() {
|
| 46 |
-
return this.items.length + this.pendingItems.length;
|
| 47 |
-
},
|
| 48 |
-
|
| 49 |
-
// Combined items for display: confirmed first, then pending at the end
|
| 50 |
-
get allItems() {
|
| 51 |
-
return [...this.items, ...this.pendingItems];
|
| 52 |
-
},
|
| 53 |
-
|
| 54 |
-
async addToQueue(text, attachments = []) {
|
| 55 |
-
const context = globalThis.getContext?.();
|
| 56 |
-
if (!context) return false;
|
| 57 |
-
|
| 58 |
-
// Generate a temporary ID for pending item
|
| 59 |
-
const tempId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
| 60 |
-
const pendingItem = {
|
| 61 |
-
id: tempId,
|
| 62 |
-
text: text.substring(0, 200) || "(attachment only)",
|
| 63 |
-
attachments: attachments.map((a) => a.name || a.file?.name || "file"),
|
| 64 |
-
pending: true,
|
| 65 |
-
};
|
| 66 |
-
|
| 67 |
-
const controller =
|
| 68 |
-
typeof AbortController !== "undefined" ? new AbortController() : null;
|
| 69 |
-
this._pendingAddOps = {
|
| 70 |
-
...this._pendingAddOps,
|
| 71 |
-
[tempId]: {
|
| 72 |
-
canceled: false,
|
| 73 |
-
controller,
|
| 74 |
-
},
|
| 75 |
-
};
|
| 76 |
-
|
| 77 |
-
// Add to pending immediately for UI feedback
|
| 78 |
-
this.pendingItems = [...this.pendingItems, pendingItem];
|
| 79 |
-
this.scrollQueueToBottom();
|
| 80 |
-
|
| 81 |
-
const run = async () => {
|
| 82 |
-
const op = this._pendingAddOps?.[tempId];
|
| 83 |
-
if (!op || op.canceled) {
|
| 84 |
-
this._pendingAddOps = { ...this._pendingAddOps };
|
| 85 |
-
delete this._pendingAddOps[tempId];
|
| 86 |
-
return false;
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
try {
|
| 90 |
-
let filenames = [];
|
| 91 |
-
if (attachments.length > 0) {
|
| 92 |
-
const formData = new FormData();
|
| 93 |
-
for (const att of attachments) {
|
| 94 |
-
formData.append("file", att.file || att);
|
| 95 |
-
}
|
| 96 |
-
const resp = await api.fetchApi("/upload", {
|
| 97 |
-
method: "POST",
|
| 98 |
-
body: formData,
|
| 99 |
-
signal: op.controller ? op.controller.signal : undefined,
|
| 100 |
-
});
|
| 101 |
-
if (resp.ok) {
|
| 102 |
-
const result = await resp.json();
|
| 103 |
-
filenames = result.filenames || [];
|
| 104 |
-
}
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
const resp = await api.fetchApi("/message_queue_add", {
|
| 108 |
-
method: "POST",
|
| 109 |
-
headers: {
|
| 110 |
-
"Content-Type": "application/json",
|
| 111 |
-
},
|
| 112 |
-
credentials: "same-origin",
|
| 113 |
-
body: JSON.stringify({
|
| 114 |
-
context,
|
| 115 |
-
text,
|
| 116 |
-
attachments: filenames,
|
| 117 |
-
item_id: tempId,
|
| 118 |
-
}),
|
| 119 |
-
signal: op.controller ? op.controller.signal : undefined,
|
| 120 |
-
});
|
| 121 |
-
|
| 122 |
-
if (!resp || !resp.ok) {
|
| 123 |
-
return false;
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
const response = await resp.json();
|
| 127 |
-
|
| 128 |
-
return response?.ok || false;
|
| 129 |
-
} catch (e) {
|
| 130 |
-
if (e?.name !== "AbortError") {
|
| 131 |
-
console.error("Failed to queue message:", e);
|
| 132 |
-
}
|
| 133 |
-
return false;
|
| 134 |
-
} finally {
|
| 135 |
-
this._pendingAddOps = { ...this._pendingAddOps };
|
| 136 |
-
delete this._pendingAddOps[tempId];
|
| 137 |
-
}
|
| 138 |
-
};
|
| 139 |
-
|
| 140 |
-
// Chain promises to ensure sequential execution
|
| 141 |
-
const previous = this._lastAddToQueuePromise || Promise.resolve();
|
| 142 |
-
const chained = previous.catch(() => false).then(run);
|
| 143 |
-
this._lastAddToQueuePromise = chained.catch(() => false);
|
| 144 |
-
return await chained;
|
| 145 |
-
},
|
| 146 |
-
|
| 147 |
-
async removeItem(itemId) {
|
| 148 |
-
const context = globalThis.getContext?.();
|
| 149 |
-
if (!context) return;
|
| 150 |
-
|
| 151 |
-
const isPending = this.pendingItems.some((p) => p.id === itemId);
|
| 152 |
-
if (isPending) {
|
| 153 |
-
const op = this._pendingAddOps?.[itemId];
|
| 154 |
-
if (op) {
|
| 155 |
-
op.canceled = true;
|
| 156 |
-
if (op.controller) {
|
| 157 |
-
op.controller.abort();
|
| 158 |
-
}
|
| 159 |
-
}
|
| 160 |
-
this.pendingItems = this.pendingItems.filter((p) => p.id !== itemId);
|
| 161 |
-
return;
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
try {
|
| 165 |
-
await api.callJsonApi("/message_queue_remove", {
|
| 166 |
-
context,
|
| 167 |
-
item_id: itemId,
|
| 168 |
-
});
|
| 169 |
-
} catch (e) {
|
| 170 |
-
console.error("Failed to remove from queue:", e);
|
| 171 |
-
}
|
| 172 |
-
},
|
| 173 |
-
|
| 174 |
-
async clearQueue() {
|
| 175 |
-
const context = globalThis.getContext?.();
|
| 176 |
-
if (!context) return;
|
| 177 |
-
try {
|
| 178 |
-
await api.callJsonApi("/message_queue_remove", { context });
|
| 179 |
-
} catch (e) {
|
| 180 |
-
console.error("Failed to clear queue:", e);
|
| 181 |
-
}
|
| 182 |
-
},
|
| 183 |
-
|
| 184 |
-
async sendItem(itemId) {
|
| 185 |
-
const context = globalThis.getContext?.();
|
| 186 |
-
if (!context) return;
|
| 187 |
-
try {
|
| 188 |
-
await api.callJsonApi("/message_queue_send", {
|
| 189 |
-
context,
|
| 190 |
-
item_id: itemId,
|
| 191 |
-
});
|
| 192 |
-
} catch (e) {
|
| 193 |
-
console.error("Failed to send queued message:", e);
|
| 194 |
-
}
|
| 195 |
-
},
|
| 196 |
-
|
| 197 |
-
async sendAll() {
|
| 198 |
-
const context = globalThis.getContext?.();
|
| 199 |
-
if (!context || !this.hasQueue) return;
|
| 200 |
-
|
| 201 |
-
// check for pending uploads and notify user
|
| 202 |
-
if (this.pendingItems.length > 0) {
|
| 203 |
-
await sleep(1000);
|
| 204 |
-
if (this.pendingItems.length > 0) {
|
| 205 |
-
toastFrontendInfo(
|
| 206 |
-
"There are pending uploads in the queue. You can wait for them to finish or remove them.",
|
| 207 |
-
"Pending uploads",
|
| 208 |
-
3,
|
| 209 |
-
"pending-uploads",
|
| 210 |
-
NotificationPriority.NORMAL,
|
| 211 |
-
true,
|
| 212 |
-
);
|
| 213 |
-
return;
|
| 214 |
-
}
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
if (!this.hasQueue) return;
|
| 218 |
-
try {
|
| 219 |
-
navStore.scrollToBottom();
|
| 220 |
-
await api.callJsonApi("/message_queue_send", { context, send_all: true });
|
| 221 |
-
} catch (e) {
|
| 222 |
-
console.error("Failed to send all queued:", e);
|
| 223 |
-
}
|
| 224 |
-
},
|
| 225 |
-
|
| 226 |
-
updateFromPoll() {
|
| 227 |
-
// this.items = queue || [];
|
| 228 |
-
|
| 229 |
-
if (this.pendingItems.length > 0) {
|
| 230 |
-
const serverIds = new Set(this.items.map((i) => i.id).filter(Boolean));
|
| 231 |
-
this.pendingItems = this.pendingItems.filter((p) => {
|
| 232 |
-
if (!p.id) return true;
|
| 233 |
-
return !serverIds.has(p.id);
|
| 234 |
-
});
|
| 235 |
-
}
|
| 236 |
-
// this.scrollQueueToBottom();
|
| 237 |
-
},
|
| 238 |
-
|
| 239 |
-
getAttachmentUrl(filename) {
|
| 240 |
-
return `/api/image_get?path=/a0/usr/uploads/${encodeURIComponent(filename)}`;
|
| 241 |
-
},
|
| 242 |
-
};
|
| 243 |
-
|
| 244 |
-
const store = createStore("messageQueue", model);
|
| 245 |
-
export { store };
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aa736dcee1b8124b42207dd332a2c6215d54e260276bd5f42439a3baddf2b422
|
| 3 |
+
size 6718
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/message-queue/message-queue.html
CHANGED
|
@@ -1,245 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import { store } from "/components/chat/message-queue/message-queue-store.js";
|
| 5 |
-
</script>
|
| 6 |
-
</head>
|
| 7 |
-
<body>
|
| 8 |
-
<div x-data>
|
| 9 |
-
<template x-if="$store.messageQueue?.hasQueue">
|
| 10 |
-
<div class="queue-preview">
|
| 11 |
-
<div class="queue-header">
|
| 12 |
-
<span class="queue-title">
|
| 13 |
-
Queued Messages (<span x-text="$store.messageQueue.count"></span>)
|
| 14 |
-
</span>
|
| 15 |
-
<div class="queue-header-actions">
|
| 16 |
-
<button class="queue-header-btn" @click="$store.messageQueue.sendAll()" title="Send all">
|
| 17 |
-
<x-icon name="keyboard_double_arrow_right"></x-icon>
|
| 18 |
-
</button>
|
| 19 |
-
<button class="queue-header-btn delete" @click="$store.messageQueue.clearQueue()" title="Clear all">
|
| 20 |
-
<x-icon name="delete_sweep"></x-icon>
|
| 21 |
-
</button>
|
| 22 |
-
</div>
|
| 23 |
-
</div>
|
| 24 |
-
<div class="queue-items no-scrollbar">
|
| 25 |
-
<template x-for="(item, index) in $store.messageQueue.allItems" :key="item.id">
|
| 26 |
-
<div class="queue-item" :class="{ 'queue-item-pending': item.pending }">
|
| 27 |
-
<div class="queue-item-content">
|
| 28 |
-
<template x-if="item.pending">
|
| 29 |
-
<span class="queue-item-pending-icon"></span>
|
| 30 |
-
</template>
|
| 31 |
-
<template x-if="!item.pending">
|
| 32 |
-
<span class="queue-item-seq" x-text="index + 1"></span>
|
| 33 |
-
</template>
|
| 34 |
-
<span class="queue-item-text" x-text="item.text || '(attachment only)'"></span>
|
| 35 |
-
<template x-if="item.attachments?.length > 0">
|
| 36 |
-
<div class="queue-attachments">
|
| 37 |
-
<template x-for="att in item.attachments" :key="att">
|
| 38 |
-
<img
|
| 39 |
-
:src="$store.messageQueue.getAttachmentUrl(att)"
|
| 40 |
-
:alt="att"
|
| 41 |
-
class="queue-attachment-thumb"
|
| 42 |
-
:title="att"
|
| 43 |
-
/>
|
| 44 |
-
</template>
|
| 45 |
-
</div>
|
| 46 |
-
</template>
|
| 47 |
-
</div>
|
| 48 |
-
<div class="queue-item-actions">
|
| 49 |
-
<button class="queue-action-btn send" @click="$store.messageQueue.sendItem(item.id)" title="Send now">
|
| 50 |
-
<x-icon name="send"></x-icon>
|
| 51 |
-
</button>
|
| 52 |
-
<button class="queue-action-btn delete" @click="$store.messageQueue.removeItem(item.id)" title="Remove">
|
| 53 |
-
<x-icon name="close"></x-icon>
|
| 54 |
-
</button>
|
| 55 |
-
</div>
|
| 56 |
-
</div>
|
| 57 |
-
</template>
|
| 58 |
-
</div>
|
| 59 |
-
</div>
|
| 60 |
-
</template>
|
| 61 |
-
</div>
|
| 62 |
-
|
| 63 |
-
<style>
|
| 64 |
-
.queue-preview {
|
| 65 |
-
border: 1px solid var(--color-border);
|
| 66 |
-
border-radius: 8px;
|
| 67 |
-
padding: var(--spacing-sm);
|
| 68 |
-
margin-bottom: var(--spacing-xs);
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
.queue-header {
|
| 72 |
-
display: flex;
|
| 73 |
-
justify-content: space-between;
|
| 74 |
-
align-items: center;
|
| 75 |
-
margin-bottom: var(--spacing-xs);
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
.queue-title {
|
| 79 |
-
font-size: var(--font-size-xs);
|
| 80 |
-
color: var(--color-text-secondary);
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
.queue-header-actions {
|
| 84 |
-
display: flex;
|
| 85 |
-
gap: 2px;
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
.queue-header-btn {
|
| 89 |
-
background: transparent;
|
| 90 |
-
border: none;
|
| 91 |
-
color: var(--color-text-secondary);
|
| 92 |
-
cursor: pointer;
|
| 93 |
-
padding: 2px;
|
| 94 |
-
border-radius: 4px;
|
| 95 |
-
display: flex;
|
| 96 |
-
align-items: center;
|
| 97 |
-
opacity: 0.6;
|
| 98 |
-
transition: opacity 0.1s ease-in-out, color 0.1s ease-in-out,
|
| 99 |
-
box-shadow 0.1s ease-in-out;
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
.queue-header-btn:hover {
|
| 103 |
-
opacity: 1;
|
| 104 |
-
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 18%, transparent);
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
.queue-header-btn:active {
|
| 108 |
-
opacity: 0.5;
|
| 109 |
-
box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.06);
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
.queue-header-btn .material-symbols-outlined {
|
| 113 |
-
font-size: var(--font-size-large);
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
.queue-items {
|
| 117 |
-
display: flex;
|
| 118 |
-
flex-direction: column;
|
| 119 |
-
gap: 2px;
|
| 120 |
-
max-height: 120px;
|
| 121 |
-
overflow-y: auto;
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
.queue-item {
|
| 125 |
-
display: flex;
|
| 126 |
-
align-items: center;
|
| 127 |
-
justify-content: space-between;
|
| 128 |
-
gap: var(--spacing-sm);
|
| 129 |
-
padding: 6px 8px;
|
| 130 |
-
border-radius: 4px;
|
| 131 |
-
cursor: default;
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
.queue-item:hover {
|
| 135 |
-
background-color: var(--color-background-hover);
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
.queue-item-content {
|
| 139 |
-
display: flex;
|
| 140 |
-
align-items: center;
|
| 141 |
-
gap: var(--spacing-sm);
|
| 142 |
-
flex: 1;
|
| 143 |
-
min-width: 0;
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
.queue-item-seq {
|
| 147 |
-
display: inline-flex;
|
| 148 |
-
align-items: center;
|
| 149 |
-
justify-content: center;
|
| 150 |
-
width: 1rem;
|
| 151 |
-
height: 1rem;
|
| 152 |
-
font-size: 0.5rem;
|
| 153 |
-
color: var(--color-text-secondary);
|
| 154 |
-
background: var(--color-background);
|
| 155 |
-
border-radius: 50%;
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
.queue-item-text {
|
| 159 |
-
font-size: var(--font-size-small);
|
| 160 |
-
color: var(--color-text);
|
| 161 |
-
white-space: nowrap;
|
| 162 |
-
overflow: hidden;
|
| 163 |
-
text-overflow: ellipsis;
|
| 164 |
-
flex: 1;
|
| 165 |
-
min-width: 0;
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
.queue-attachments {
|
| 169 |
-
display: flex;
|
| 170 |
-
gap: 4px;
|
| 171 |
-
flex-shrink: 0;
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
.queue-attachment-thumb {
|
| 175 |
-
width: 20px;
|
| 176 |
-
height: 20px;
|
| 177 |
-
object-fit: contain;
|
| 178 |
-
border-radius: 3px;
|
| 179 |
-
border: 1px solid var(--color-border);
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
.queue-item-actions {
|
| 183 |
-
display: flex;
|
| 184 |
-
gap: 2px;
|
| 185 |
-
flex-shrink: 0;
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
.device-pointer .queue-item-actions {
|
| 189 |
-
opacity: 0;
|
| 190 |
-
visibility: hidden;
|
| 191 |
-
pointer-events: none;
|
| 192 |
-
transition: opacity 0.15s;
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
.device-pointer .queue-item:hover .queue-item-actions {
|
| 196 |
-
opacity: 1;
|
| 197 |
-
visibility: visible;
|
| 198 |
-
pointer-events: auto;
|
| 199 |
-
}
|
| 200 |
-
|
| 201 |
-
.device-touch .queue-item-actions {
|
| 202 |
-
opacity: 1;
|
| 203 |
-
visibility: visible;
|
| 204 |
-
pointer-events: auto;
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
.queue-action-btn {
|
| 208 |
-
background: transparent;
|
| 209 |
-
border: none;
|
| 210 |
-
color: var(--color-primary);
|
| 211 |
-
cursor: pointer;
|
| 212 |
-
padding: 4px;
|
| 213 |
-
border-radius: 4px;
|
| 214 |
-
display: flex;
|
| 215 |
-
align-items: center;
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
.queue-action-btn .material-symbols-outlined {
|
| 219 |
-
font-size: var(--font-size-smaller);
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
.queue-item-pending {
|
| 223 |
-
opacity: 0.6;
|
| 224 |
-
}
|
| 225 |
-
|
| 226 |
-
.queue-item-pending .queue-action-btn.send {
|
| 227 |
-
visibility: hidden !important;
|
| 228 |
-
pointer-events: none !important;
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
.queue-item-pending-icon {
|
| 232 |
-
width: 1rem;
|
| 233 |
-
height: 1rem;
|
| 234 |
-
border: 2px solid var(--color-border);
|
| 235 |
-
border-top-color: var(--color-primary);
|
| 236 |
-
border-radius: 50%;
|
| 237 |
-
animation: pending-spin 0.8s linear infinite;
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
@keyframes pending-spin {
|
| 241 |
-
to { transform: rotate(360deg); }
|
| 242 |
-
}
|
| 243 |
-
</style>
|
| 244 |
-
</body>
|
| 245 |
-
</html>
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a0fb9b33d9df338f506b05d012aa262ed37ed7d69e69366dd104ebe62c22b1c8
|
| 3 |
+
size 6601
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/model-gate-store.js
CHANGED
|
@@ -1,233 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import { store as onboardingStore } from "/plugins/_onboarding/webui/onboarding-store.js";
|
| 5 |
-
import { toastFrontendError } from "/components/notifications/notification-store.js";
|
| 6 |
-
|
| 7 |
-
const ONBOARDING_MODAL_PATH = "/plugins/_onboarding/webui/onboarding.html";
|
| 8 |
-
const STORAGE_KEY = "a0:model-gate-pending:v1";
|
| 9 |
-
const SYNTHETIC_MESSAGE_NO_BASE = Number.MAX_SAFE_INTEGER - 2;
|
| 10 |
-
|
| 11 |
-
export const store = createStore("modelGate", {
|
| 12 |
-
active: false,
|
| 13 |
-
connected: false,
|
| 14 |
-
connectedLabel: "",
|
| 15 |
-
accountConnected: false,
|
| 16 |
-
accountLabel: "",
|
| 17 |
-
pending: null,
|
| 18 |
-
gateMessageId: "",
|
| 19 |
-
choice: "",
|
| 20 |
-
dispatching: false,
|
| 21 |
-
_initialized: false,
|
| 22 |
-
|
| 23 |
-
get introText() {
|
| 24 |
-
if (this.connected) {
|
| 25 |
-
return `Model connected: ${this.connectedLabel || "ready"}`;
|
| 26 |
-
}
|
| 27 |
-
if (this.accountConnected) {
|
| 28 |
-
return `${this.accountLabel || "Your AI account"} is connected. Choose Main and Utility models and I'll answer right away.`;
|
| 29 |
-
}
|
| 30 |
-
return "I'm ready to work on this — I just need a model to think with. Pick one and I'll answer right away.";
|
| 31 |
-
},
|
| 32 |
-
|
| 33 |
-
init() {
|
| 34 |
-
if (this._initialized) return;
|
| 35 |
-
this._initialized = true;
|
| 36 |
-
this.restorePending();
|
| 37 |
-
document.addEventListener("onboarding-configured", () => {
|
| 38 |
-
void this.dispatchPendingIfConfigured();
|
| 39 |
-
});
|
| 40 |
-
document.addEventListener("model-configured", () => {
|
| 41 |
-
void this.dispatchPendingIfConfigured();
|
| 42 |
-
});
|
| 43 |
-
document.addEventListener("model-setup-changed", () => {
|
| 44 |
-
void this.dispatchPendingIfConfigured();
|
| 45 |
-
});
|
| 46 |
-
document.addEventListener("modal-closed", () => {
|
| 47 |
-
if (this.active && !this.connected) {
|
| 48 |
-
this.choice = "";
|
| 49 |
-
this.savePending();
|
| 50 |
-
void this.dispatchPendingIfConfigured();
|
| 51 |
-
}
|
| 52 |
-
});
|
| 53 |
-
},
|
| 54 |
-
|
| 55 |
-
async canSendToModel() {
|
| 56 |
-
try {
|
| 57 |
-
const data = await callJsonApi("/plugins/_model_config/model_config_get", {});
|
| 58 |
-
this.applyModelStatus(data);
|
| 59 |
-
if (!data?.model_configured) await this.refreshConnectedAccountState();
|
| 60 |
-
return !!data?.model_configured;
|
| 61 |
-
} catch (error) {
|
| 62 |
-
console.error("Could not check model configuration:", error);
|
| 63 |
-
return true;
|
| 64 |
-
}
|
| 65 |
-
},
|
| 66 |
-
|
| 67 |
-
async refreshConnectedAccountState() {
|
| 68 |
-
try {
|
| 69 |
-
const { store: oauthConfigStore } = await import("/plugins/_oauth/webui/oauth-config-store.js");
|
| 70 |
-
await oauthConfigStore.loadStatus();
|
| 71 |
-
const account = oauthConfigStore.connectedProviderCards()[0] || null;
|
| 72 |
-
this.accountConnected = Boolean(account);
|
| 73 |
-
this.accountLabel = account?.short_name || account?.display_name || account?.provider_id || "";
|
| 74 |
-
return this.accountConnected;
|
| 75 |
-
} catch (error) {
|
| 76 |
-
console.error("Could not check connected accounts:", error);
|
| 77 |
-
this.accountConnected = false;
|
| 78 |
-
this.accountLabel = "";
|
| 79 |
-
return false;
|
| 80 |
-
}
|
| 81 |
-
},
|
| 82 |
-
|
| 83 |
-
applyModelStatus(data = {}) {
|
| 84 |
-
modelConfigStore.modelConfigured = !!data.model_configured;
|
| 85 |
-
modelConfigStore.modelConfiguredLabel = data.model_configured_label || "";
|
| 86 |
-
this.connectedLabel = data.model_configured_label || this.connectedLabel || "";
|
| 87 |
-
},
|
| 88 |
-
|
| 89 |
-
start({ message, attachments, messageId, context }) {
|
| 90 |
-
this.init();
|
| 91 |
-
this.active = true;
|
| 92 |
-
this.connected = false;
|
| 93 |
-
this.pending = { message, attachments, messageId, context };
|
| 94 |
-
this.gateMessageId = this.gateMessageId || `model-gate-${messageId}`;
|
| 95 |
-
this.savePending();
|
| 96 |
-
},
|
| 97 |
-
|
| 98 |
-
syntheticMessages(currentContext) {
|
| 99 |
-
this.init();
|
| 100 |
-
if (!this.active || !this.pending || this.pending.context !== currentContext) return [];
|
| 101 |
-
return [
|
| 102 |
-
{
|
| 103 |
-
no: SYNTHETIC_MESSAGE_NO_BASE,
|
| 104 |
-
id: this.pending.messageId,
|
| 105 |
-
type: "user",
|
| 106 |
-
content: this.pending.message,
|
| 107 |
-
kvps: { attachments: this.pending.attachments },
|
| 108 |
-
},
|
| 109 |
-
{
|
| 110 |
-
no: SYNTHETIC_MESSAGE_NO_BASE + 1,
|
| 111 |
-
id: this.gateMessageId,
|
| 112 |
-
type: "model_setup_gate",
|
| 113 |
-
},
|
| 114 |
-
];
|
| 115 |
-
},
|
| 116 |
-
|
| 117 |
-
mergeSyntheticMessages(logs, currentContext) {
|
| 118 |
-
this.init();
|
| 119 |
-
const synthetic = this.syntheticMessages(currentContext);
|
| 120 |
-
return synthetic.length ? [...(logs || []), ...synthetic] : logs;
|
| 121 |
-
},
|
| 122 |
-
|
| 123 |
-
onCardCreate() {
|
| 124 |
-
this.init();
|
| 125 |
-
void this.dispatchPendingIfConfigured();
|
| 126 |
-
},
|
| 127 |
-
|
| 128 |
-
openOnboarding(choice) {
|
| 129 |
-
this.choice = ["local", "account"].includes(choice) ? choice : "cloud";
|
| 130 |
-
this.savePending();
|
| 131 |
-
onboardingStore.presetMode = this.choice;
|
| 132 |
-
const modalPromise = window.openModal?.(ONBOARDING_MODAL_PATH);
|
| 133 |
-
void Promise.resolve(modalPromise).then(() => this.dispatchPendingIfConfigured());
|
| 134 |
-
},
|
| 135 |
-
|
| 136 |
-
async openAdvancedModelConfiguration() {
|
| 137 |
-
try {
|
| 138 |
-
const data = await callJsonApi("/plugins/_model_config/model_config_get", {});
|
| 139 |
-
await modelConfigStore.openPresetEditor(
|
| 140 |
-
data?.selected_preset || data?.configured_preset || "Default"
|
| 141 |
-
);
|
| 142 |
-
await this.dispatchPendingIfConfigured();
|
| 143 |
-
} catch (error) {
|
| 144 |
-
console.error("Could not open model preset editor:", error);
|
| 145 |
-
void toastFrontendError(
|
| 146 |
-
error?.message || "Could not open model presets.",
|
| 147 |
-
"Advanced model configuration"
|
| 148 |
-
);
|
| 149 |
-
}
|
| 150 |
-
},
|
| 151 |
-
|
| 152 |
-
async dispatchPendingIfConfigured() {
|
| 153 |
-
this.init();
|
| 154 |
-
if (this.dispatching || !this.pending) return;
|
| 155 |
-
const configured = await this.canSendToModel();
|
| 156 |
-
if (!configured) return;
|
| 157 |
-
|
| 158 |
-
const pending = this.pending;
|
| 159 |
-
this.pending = null;
|
| 160 |
-
this.connected = true;
|
| 161 |
-
this.dispatching = true;
|
| 162 |
-
this.clearSavedPending();
|
| 163 |
-
try {
|
| 164 |
-
await globalThis.sendMessage?.({
|
| 165 |
-
bypassModelGate: true,
|
| 166 |
-
skipExtensions: true,
|
| 167 |
-
preserveInput: true,
|
| 168 |
-
message: pending.message,
|
| 169 |
-
attachments: pending.attachments,
|
| 170 |
-
messageId: pending.messageId,
|
| 171 |
-
context: pending.context,
|
| 172 |
-
});
|
| 173 |
-
} finally {
|
| 174 |
-
this.dispatching = false;
|
| 175 |
-
}
|
| 176 |
-
},
|
| 177 |
-
|
| 178 |
-
savePending() {
|
| 179 |
-
const message = String(this.pending?.message || "").trim();
|
| 180 |
-
const context = String(this.pending?.context || "");
|
| 181 |
-
const messageId = String(this.pending?.messageId || "");
|
| 182 |
-
const attachments = Array.isArray(this.pending?.attachments) ? this.pending.attachments : [];
|
| 183 |
-
if (!message || !context || !messageId || attachments.length) {
|
| 184 |
-
this.clearSavedPending();
|
| 185 |
-
return;
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
try {
|
| 189 |
-
sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
|
| 190 |
-
pending: { message, attachments: [], messageId, context },
|
| 191 |
-
gateMessageId: this.gateMessageId || `model-gate-${messageId}`,
|
| 192 |
-
choice: this.choice,
|
| 193 |
-
}));
|
| 194 |
-
} catch (_error) {
|
| 195 |
-
// Session storage can be unavailable in private or locked-down browser modes.
|
| 196 |
-
}
|
| 197 |
-
},
|
| 198 |
-
|
| 199 |
-
restorePending() {
|
| 200 |
-
if (this.pending) return;
|
| 201 |
-
|
| 202 |
-
let saved = null;
|
| 203 |
-
try {
|
| 204 |
-
saved = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "null");
|
| 205 |
-
} catch (_error) {
|
| 206 |
-
this.clearSavedPending();
|
| 207 |
-
return;
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
const message = String(saved?.pending?.message || "").trim();
|
| 211 |
-
const context = String(saved?.pending?.context || "");
|
| 212 |
-
const messageId = String(saved?.pending?.messageId || "");
|
| 213 |
-
if (!message || !context || !messageId) {
|
| 214 |
-
this.clearSavedPending();
|
| 215 |
-
return;
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
this.active = true;
|
| 219 |
-
this.connected = false;
|
| 220 |
-
this.pending = { message, attachments: [], messageId, context };
|
| 221 |
-
this.gateMessageId = String(saved?.gateMessageId || `model-gate-${messageId}`);
|
| 222 |
-
this.choice = ["local", "cloud", "account"].includes(saved?.choice) ? saved.choice : "";
|
| 223 |
-
},
|
| 224 |
-
|
| 225 |
-
clearSavedPending() {
|
| 226 |
-
try {
|
| 227 |
-
sessionStorage.removeItem(STORAGE_KEY);
|
| 228 |
-
} catch (_error) {
|
| 229 |
-
// Ignore unavailable storage; the in-memory gate state is still authoritative.
|
| 230 |
-
}
|
| 231 |
-
},
|
| 232 |
-
|
| 233 |
-
});
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:508d0f27865b8aa46fcb8c99788caba98a1b5258d6f9c8569a3e46a75eb8c53d
|
| 3 |
+
size 7685
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/model-setup-gate.html
CHANGED
|
@@ -1,203 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import { store as modelGateStore } from "/components/chat/model-gate-store.js";
|
| 5 |
-
</script>
|
| 6 |
-
</head>
|
| 7 |
-
<body>
|
| 8 |
-
<div x-data>
|
| 9 |
-
<template x-if="$store.modelGate">
|
| 10 |
-
<section class="model-gate" x-create="$store.modelGate.onCardCreate()">
|
| 11 |
-
<template x-if="$store.modelGate.connected">
|
| 12 |
-
<div class="model-gate-chip">
|
| 13 |
-
<x-icon aria-hidden="true" name="check_circle"></x-icon>
|
| 14 |
-
<span x-text="$store.modelGate.introText"></span>
|
| 15 |
-
</div>
|
| 16 |
-
</template>
|
| 17 |
-
|
| 18 |
-
<template x-if="!$store.modelGate.connected">
|
| 19 |
-
<div class="model-gate-body">
|
| 20 |
-
<p class="model-gate-intro" x-text="$store.modelGate.introText"></p>
|
| 21 |
-
|
| 22 |
-
<div class="model-gate-card">
|
| 23 |
-
<div class="model-gate-fork is-single" x-show="$store.modelGate.accountConnected">
|
| 24 |
-
<button type="button"
|
| 25 |
-
class="model-gate-option"
|
| 26 |
-
@click="$store.modelGate.openAdvancedModelConfiguration()">
|
| 27 |
-
<span class="model-gate-option-title">
|
| 28 |
-
<x-icon aria-hidden="true" name="tune"></x-icon>
|
| 29 |
-
<span>Choose models</span>
|
| 30 |
-
</span>
|
| 31 |
-
<span class="model-gate-option-sub">Select Main and Utility models</span>
|
| 32 |
-
</button>
|
| 33 |
-
</div>
|
| 34 |
-
|
| 35 |
-
<div class="model-gate-fork" x-show="!$store.modelGate.accountConnected">
|
| 36 |
-
<button type="button"
|
| 37 |
-
class="model-gate-option"
|
| 38 |
-
:class="{ selected: $store.modelGate.choice === 'cloud' }"
|
| 39 |
-
@click="$store.modelGate.openOnboarding('cloud')">
|
| 40 |
-
<span class="model-gate-option-title">
|
| 41 |
-
<x-icon aria-hidden="true" name="cloud"></x-icon>
|
| 42 |
-
<span>Cloud provider</span>
|
| 43 |
-
</span>
|
| 44 |
-
<span class="model-gate-option-sub">Connect with an API key</span>
|
| 45 |
-
</button>
|
| 46 |
-
<button type="button"
|
| 47 |
-
class="model-gate-option"
|
| 48 |
-
:class="{ selected: $store.modelGate.choice === 'account' }"
|
| 49 |
-
@click="$store.modelGate.openOnboarding('account')">
|
| 50 |
-
<span class="model-gate-option-title">
|
| 51 |
-
<x-icon aria-hidden="true" name="account_circle"></x-icon>
|
| 52 |
-
<span>AI account</span>
|
| 53 |
-
</span>
|
| 54 |
-
<span class="model-gate-option-sub">Codex, Copilot, Gemini, Grok</span>
|
| 55 |
-
</button>
|
| 56 |
-
<button type="button"
|
| 57 |
-
class="model-gate-option"
|
| 58 |
-
:class="{ selected: $store.modelGate.choice === 'local' }"
|
| 59 |
-
@click="$store.modelGate.openOnboarding('local')">
|
| 60 |
-
<span class="model-gate-option-title">
|
| 61 |
-
<x-icon aria-hidden="true" name="memory"></x-icon>
|
| 62 |
-
<span>Local model</span>
|
| 63 |
-
</span>
|
| 64 |
-
<span class="model-gate-option-sub">Runs on your machine</span>
|
| 65 |
-
</button>
|
| 66 |
-
</div>
|
| 67 |
-
</div>
|
| 68 |
-
|
| 69 |
-
<p class="model-gate-footnote">
|
| 70 |
-
<span>Your message sends automatically once a model is connected.</span>
|
| 71 |
-
<button type="button" @click="$store.modelGate.openAdvancedModelConfiguration()">Advanced model configuration</button>
|
| 72 |
-
</p>
|
| 73 |
-
</div>
|
| 74 |
-
</template>
|
| 75 |
-
</section>
|
| 76 |
-
</template>
|
| 77 |
-
</div>
|
| 78 |
-
|
| 79 |
-
<style>
|
| 80 |
-
.model-gate {
|
| 81 |
-
width: min(100%, 36rem);
|
| 82 |
-
color: var(--color-text);
|
| 83 |
-
font-family: var(--font-family-main, "Rubik", Arial, Helvetica, sans-serif);
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
.model-gate-body {
|
| 87 |
-
display: grid;
|
| 88 |
-
gap: 0.65rem;
|
| 89 |
-
container-type: inline-size;
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
.model-gate-intro {
|
| 93 |
-
margin: 0;
|
| 94 |
-
color: color-mix(in srgb, var(--color-text) 84%, transparent);
|
| 95 |
-
line-height: 1.45;
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
.model-gate-card {
|
| 99 |
-
display: grid;
|
| 100 |
-
gap: 0.75rem;
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
.model-gate-fork {
|
| 104 |
-
display: flex;
|
| 105 |
-
gap: 0.6rem;
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
.model-gate-fork .model-gate-option {
|
| 109 |
-
flex: 1 1 0;
|
| 110 |
-
min-width: 0;
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
@container (max-width: 30rem) {
|
| 114 |
-
.model-gate-fork {
|
| 115 |
-
flex-direction: column;
|
| 116 |
-
}
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
.model-gate-option {
|
| 120 |
-
border: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent);
|
| 121 |
-
border-radius: 8px;
|
| 122 |
-
background: color-mix(in srgb, var(--color-background) 52%, transparent);
|
| 123 |
-
color: inherit;
|
| 124 |
-
font: inherit;
|
| 125 |
-
text-align: left;
|
| 126 |
-
cursor: pointer;
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
.model-gate-option {
|
| 130 |
-
display: grid;
|
| 131 |
-
gap: 0.25rem;
|
| 132 |
-
padding: 0.72rem;
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
.model-gate-option:hover,
|
| 136 |
-
.model-gate-option:focus-visible,
|
| 137 |
-
.model-gate-option.selected {
|
| 138 |
-
border-color: color-mix(in srgb, var(--color-border) 86%, var(--color-text) 14%);
|
| 139 |
-
background: color-mix(in srgb, var(--color-background-hover) 60%, var(--color-panel));
|
| 140 |
-
outline: none;
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
.model-gate-option-title {
|
| 144 |
-
display: inline-flex;
|
| 145 |
-
align-items: center;
|
| 146 |
-
gap: 0.4rem;
|
| 147 |
-
font-weight: 560;
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
.model-gate-option-title .material-symbols-outlined {
|
| 151 |
-
font-size: 1.15rem;
|
| 152 |
-
color: color-mix(in srgb, var(--color-text) 72%, transparent);
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
.model-gate-option-sub,
|
| 156 |
-
.model-gate-footnote {
|
| 157 |
-
color: color-mix(in srgb, var(--color-text) 58%, transparent);
|
| 158 |
-
font-size: 0.78rem;
|
| 159 |
-
line-height: 1.35;
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
.model-gate-footnote {
|
| 163 |
-
display: flex;
|
| 164 |
-
align-items: center;
|
| 165 |
-
justify-content: space-between;
|
| 166 |
-
gap: 0.7rem;
|
| 167 |
-
flex-wrap: wrap;
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
.model-gate-footnote button {
|
| 171 |
-
border: 0;
|
| 172 |
-
background: transparent;
|
| 173 |
-
color: color-mix(in srgb, #6f8cff 82%, var(--color-text));
|
| 174 |
-
font: inherit;
|
| 175 |
-
font-size: 0.8rem;
|
| 176 |
-
cursor: pointer;
|
| 177 |
-
padding: 0;
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
.model-gate-chip {
|
| 181 |
-
display: inline-flex;
|
| 182 |
-
align-items: center;
|
| 183 |
-
gap: 0.4rem;
|
| 184 |
-
border: 1px solid color-mix(in srgb, #7bc995 44%, var(--color-border));
|
| 185 |
-
border-radius: 999px;
|
| 186 |
-
padding: 0.35rem 0.65rem;
|
| 187 |
-
color: color-mix(in srgb, var(--color-text) 88%, #7bc995);
|
| 188 |
-
background: color-mix(in srgb, #7bc995 10%, var(--color-panel));
|
| 189 |
-
font-size: 0.84rem;
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
.model-gate-chip .material-symbols-outlined {
|
| 193 |
-
font-size: 1rem;
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
@media (max-width: 480px) {
|
| 197 |
-
.model-gate-fork {
|
| 198 |
-
flex-direction: column;
|
| 199 |
-
}
|
| 200 |
-
}
|
| 201 |
-
</style>
|
| 202 |
-
</body>
|
| 203 |
-
</html>
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:84203c0efe43657c9506ab678938f133cfd2d2738363cf77449da3a106558c61
|
| 3 |
+
size 6522
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/navigation/chat-navigation-store.js
CHANGED
|
@@ -1,127 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
loadAdjacentMessageWindow,
|
| 5 |
-
scrollMessageWindowToEdge,
|
| 6 |
-
} from "/js/messages.js";
|
| 7 |
-
|
| 8 |
-
const model = {
|
| 9 |
-
// Configuration
|
| 10 |
-
scrollMargin: 60,
|
| 11 |
-
prevTolerance: 35,
|
| 12 |
-
nextTolerance: 5,
|
| 13 |
-
|
| 14 |
-
init() {
|
| 15 |
-
// Any initialization if needed
|
| 16 |
-
},
|
| 17 |
-
|
| 18 |
-
async scrollToTop() {
|
| 19 |
-
await scrollMessageWindowToEdge("start");
|
| 20 |
-
},
|
| 21 |
-
|
| 22 |
-
async scrollToBottom() {
|
| 23 |
-
await scrollMessageWindowToEdge("end");
|
| 24 |
-
},
|
| 25 |
-
|
| 26 |
-
async scrollToPrevUserMessage() {
|
| 27 |
-
const scroller = this._getChatHistoryEl();
|
| 28 |
-
if (!scroller) return;
|
| 29 |
-
|
| 30 |
-
const positions = this._getUserMessagePositions(scroller);
|
| 31 |
-
const scrollerRect = scroller.getBoundingClientRect();
|
| 32 |
-
|
| 33 |
-
const prevThreshold = this.scrollMargin - this.prevTolerance; // 25px
|
| 34 |
-
|
| 35 |
-
const currentIndex = positions.findIndex((p) => {
|
| 36 |
-
const relativeTop = p.el.getBoundingClientRect().top - scrollerRect.top;
|
| 37 |
-
return relativeTop >= prevThreshold;
|
| 38 |
-
});
|
| 39 |
-
|
| 40 |
-
if (currentIndex > 0) {
|
| 41 |
-
// Go to previous message
|
| 42 |
-
positions[currentIndex - 1].el.scrollIntoView({ block: "start", behavior: "smooth" });
|
| 43 |
-
} else if (currentIndex === 0) {
|
| 44 |
-
if (getMessageWindowState().hasOlder) {
|
| 45 |
-
await loadAdjacentMessageWindow("older");
|
| 46 |
-
this._scrollToLastUserAboveThreshold(scroller, prevThreshold);
|
| 47 |
-
} else {
|
| 48 |
-
scroller.scrollTo({ top: 0, behavior: "instant" });
|
| 49 |
-
}
|
| 50 |
-
} else if (currentIndex === -1 && positions.length > 0) {
|
| 51 |
-
// All messages are above the threshold (scrolled past), scroll to bottom
|
| 52 |
-
positions[positions.length - 1].el.scrollIntoView({ block: "start", behavior: "smooth" });
|
| 53 |
-
} else if (positions.length === 0 && getMessageWindowState().hasOlder) {
|
| 54 |
-
await loadAdjacentMessageWindow("older");
|
| 55 |
-
this._scrollToLastUserAboveThreshold(scroller, prevThreshold);
|
| 56 |
-
}
|
| 57 |
-
},
|
| 58 |
-
|
| 59 |
-
async scrollToNextUserMessage() {
|
| 60 |
-
const scroller = this._getChatHistoryEl();
|
| 61 |
-
if (!scroller) return;
|
| 62 |
-
|
| 63 |
-
const positions = this._getUserMessagePositions(scroller);
|
| 64 |
-
const scrollerRect = scroller.getBoundingClientRect();
|
| 65 |
-
|
| 66 |
-
const nextThreshold = this.scrollMargin + this.nextTolerance; // 65px
|
| 67 |
-
|
| 68 |
-
// Find first message below the threshold
|
| 69 |
-
const targetIndex = positions.findIndex((p) => {
|
| 70 |
-
const relativeTop = p.el.getBoundingClientRect().top - scrollerRect.top;
|
| 71 |
-
return relativeTop > nextThreshold;
|
| 72 |
-
});
|
| 73 |
-
|
| 74 |
-
if (targetIndex !== -1) {
|
| 75 |
-
// Go to that message
|
| 76 |
-
positions[targetIndex].el.scrollIntoView({ block: "start", behavior: "smooth" });
|
| 77 |
-
} else {
|
| 78 |
-
if (getMessageWindowState().hasNewer) {
|
| 79 |
-
await loadAdjacentMessageWindow("newer");
|
| 80 |
-
this._scrollToFirstUserBelowThreshold(scroller, nextThreshold);
|
| 81 |
-
} else {
|
| 82 |
-
await this.scrollToBottom();
|
| 83 |
-
}
|
| 84 |
-
}
|
| 85 |
-
},
|
| 86 |
-
|
| 87 |
-
_scrollToLastUserAboveThreshold(scroller, threshold) {
|
| 88 |
-
const scrollerRect = scroller.getBoundingClientRect();
|
| 89 |
-
const positions = this._getUserMessagePositions(scroller);
|
| 90 |
-
const candidates = positions.filter(
|
| 91 |
-
({ el }) => el.getBoundingClientRect().top - scrollerRect.top < threshold
|
| 92 |
-
);
|
| 93 |
-
candidates.at(-1)?.el.scrollIntoView({ block: "start", behavior: "smooth" });
|
| 94 |
-
},
|
| 95 |
-
|
| 96 |
-
_scrollToFirstUserBelowThreshold(scroller, threshold) {
|
| 97 |
-
const scrollerRect = scroller.getBoundingClientRect();
|
| 98 |
-
const target = this._getUserMessagePositions(scroller).find(
|
| 99 |
-
({ el }) => el.getBoundingClientRect().top - scrollerRect.top > threshold
|
| 100 |
-
);
|
| 101 |
-
target?.el.scrollIntoView({ block: "start", behavior: "smooth" });
|
| 102 |
-
},
|
| 103 |
-
|
| 104 |
-
// Helpers
|
| 105 |
-
_getChatHistoryEl() {
|
| 106 |
-
return document.getElementById("chat-history");
|
| 107 |
-
},
|
| 108 |
-
|
| 109 |
-
_getUserMessagePositions(scroller) {
|
| 110 |
-
const userMessageEls = Array.from(
|
| 111 |
-
scroller.querySelectorAll(".message-container.user-container")
|
| 112 |
-
);
|
| 113 |
-
// Helper for getElementTopInScroller
|
| 114 |
-
const getTop = (el) => {
|
| 115 |
-
const elRect = el.getBoundingClientRect();
|
| 116 |
-
const scrollerRect = scroller.getBoundingClientRect();
|
| 117 |
-
return elRect.top - scrollerRect.top + scroller.scrollTop;
|
| 118 |
-
};
|
| 119 |
-
|
| 120 |
-
return userMessageEls
|
| 121 |
-
.map((el) => ({ el, top: getTop(el) }))
|
| 122 |
-
.sort((a, b) => a.top - b.top);
|
| 123 |
-
}
|
| 124 |
-
};
|
| 125 |
-
|
| 126 |
-
const store = createStore("chatNavigation", model);
|
| 127 |
-
export { store };
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:319bdd51040b0b65ab5219f34d951556736c799c5755b942ff0f527707d18046
|
| 3 |
+
size 4661
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/top-section/chat-top-store.js
CHANGED
|
@@ -1,13 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
const model = {
|
| 5 |
-
connected: false,
|
| 6 |
-
progressActive: false, // true when progress bar is active
|
| 7 |
-
};
|
| 8 |
-
|
| 9 |
-
// convert it to alpine store
|
| 10 |
-
const store = createStore("chatTop", model);
|
| 11 |
-
|
| 12 |
-
// export for use in other files
|
| 13 |
-
export { store };
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b72133213443c983195c6cd378516d26c74835719dec7b8e2a16603a9a514b1b
|
| 3 |
+
size 334
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/chat/top-section/chat-top.html
CHANGED
|
@@ -1,42 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
<title>Example component or modal</title>
|
| 5 |
-
|
| 6 |
-
<!-- Import the alpine store -->
|
| 7 |
-
<script type="module">
|
| 8 |
-
import { store } from "/components/chat/top-section/chat-top-store.js";
|
| 9 |
-
import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
|
| 10 |
-
</script>
|
| 11 |
-
</head>
|
| 12 |
-
|
| 13 |
-
<body>
|
| 14 |
-
|
| 15 |
-
<!-- This construct of x-data + x-if is used to ensure the component is only rendered when the store is available -->
|
| 16 |
-
<div x-data>
|
| 17 |
-
<template x-if="$store.chatTop">
|
| 18 |
-
<!-- Time and Date -->
|
| 19 |
-
<div id="time-date-container">
|
| 20 |
-
<x-extension id="chat-top-start"></x-extension>
|
| 21 |
-
<div id="time-date" x-show="$store.preferences.isUiControlVisible('time')"></div>
|
| 22 |
-
<div x-show="$store.preferences.isUiControlVisible('connectionStatus')">
|
| 23 |
-
<x-component path="sync/sync-status.html"></x-component>
|
| 24 |
-
</div>
|
| 25 |
-
<!-- Notification Toggle positioned next to time-date -->
|
| 26 |
-
<x-component path="notifications/notification-icons.html"></x-component>
|
| 27 |
-
<!-- Project Selector -->
|
| 28 |
-
<div x-show="$store.preferences.isUiControlVisible('projectSelector')">
|
| 29 |
-
<x-component path="projects/project-selector.html"></x-component>
|
| 30 |
-
</div>
|
| 31 |
-
<x-extension id="chat-top-end"></x-extension>
|
| 32 |
-
</div>
|
| 33 |
-
</template>
|
| 34 |
-
</div>
|
| 35 |
-
|
| 36 |
-
<style>
|
| 37 |
-
#time-date-container > x-extension { display: contents; }
|
| 38 |
-
</style>
|
| 39 |
-
|
| 40 |
-
</body>
|
| 41 |
-
|
| 42 |
-
</html>
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:80a1150109c8e4c7fbb4a5be3d759536a6c1bb813af1edae4c13ef1945fc7074
|
| 3 |
+
size 1579
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/dropdown/AGENTS.md
CHANGED
|
@@ -1,26 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
- Own the shared dropdown component used by WebUI controls.
|
| 6 |
-
|
| 7 |
-
## Ownership
|
| 8 |
-
|
| 9 |
-
- `dropdown.html` owns dropdown markup, behavior hooks, and component-local styling.
|
| 10 |
-
|
| 11 |
-
## Local Contracts
|
| 12 |
-
|
| 13 |
-
- Keep keyboard, focus, and click-outside behavior compatible with existing callers.
|
| 14 |
-
- Avoid caller-specific assumptions in the shared dropdown.
|
| 15 |
-
|
| 16 |
-
## Work Guidance
|
| 17 |
-
|
| 18 |
-
- Prefer component attributes and slots over hardcoded option sets.
|
| 19 |
-
|
| 20 |
-
## Verification
|
| 21 |
-
|
| 22 |
-
- Smoke-test each caller that uses the dropdown after behavior changes.
|
| 23 |
-
|
| 24 |
-
## Child DOX Index
|
| 25 |
-
|
| 26 |
-
No child DOX files.
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0f3838921b0e98ec0b562a2a7d32f5e9f5b962cd6f4e4ac9b6cd9ac2e9243caf
|
| 3 |
+
size 582
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/dropdown/dropdown.html
CHANGED
|
@@ -1,69 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
Usage:
|
| 5 |
-
<x-component data-component="ui/dropdown" data-props='{"id": "my-dropdown"}'>
|
| 6 |
-
<template x-slot:trigger>
|
| 7 |
-
<button>Click me</button>
|
| 8 |
-
</template>
|
| 9 |
-
<template x-slot:menu>
|
| 10 |
-
<button class="dropdown-item" @click="...">Item 1</button>
|
| 11 |
-
<div class="dropdown-separator"></div>
|
| 12 |
-
<button class="dropdown-item" @click="...">Item 2</button>
|
| 13 |
-
</template>
|
| 14 |
-
</x-component>
|
| 15 |
-
|
| 16 |
-
Or use the simpler inline approach with x-data:
|
| 17 |
-
<div class="dropdown" x-data="{ open: false }" @click.outside="open = false">
|
| 18 |
-
<button class="dropdown-trigger" @click="open = !open">...</button>
|
| 19 |
-
<div class="dropdown-menu" x-show="open" x-transition>...</div>
|
| 20 |
-
</div>
|
| 21 |
-
-->
|
| 22 |
-
<html>
|
| 23 |
-
<head>
|
| 24 |
-
<!-- Dropdown uses global styles from index.css -->
|
| 25 |
-
</head>
|
| 26 |
-
<body>
|
| 27 |
-
<div
|
| 28 |
-
class="dropdown"
|
| 29 |
-
x-data="{
|
| 30 |
-
open: false,
|
| 31 |
-
toggle() { this.open = !this.open; },
|
| 32 |
-
close() { this.open = false; }
|
| 33 |
-
}"
|
| 34 |
-
@click.outside="close()"
|
| 35 |
-
@keydown.escape.window="close()"
|
| 36 |
-
>
|
| 37 |
-
<!-- Trigger slot -->
|
| 38 |
-
<div class="dropdown-trigger-wrapper" @click="toggle()">
|
| 39 |
-
<slot name="trigger">
|
| 40 |
-
<button class="dropdown-trigger" type="button">
|
| 41 |
-
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 42 |
-
<path d="M6 9l6 6 6-6"/>
|
| 43 |
-
</svg>
|
| 44 |
-
</button>
|
| 45 |
-
</slot>
|
| 46 |
-
</div>
|
| 47 |
-
|
| 48 |
-
<!-- Menu slot -->
|
| 49 |
-
<div
|
| 50 |
-
class="dropdown-menu"
|
| 51 |
-
x-show="open"
|
| 52 |
-
@click="close()"
|
| 53 |
-
>
|
| 54 |
-
<slot name="menu">
|
| 55 |
-
<!-- Default empty menu -->
|
| 56 |
-
</slot>
|
| 57 |
-
</div>
|
| 58 |
-
</div>
|
| 59 |
-
|
| 60 |
-
<style>
|
| 61 |
-
/* Component-specific styles (layout helpers) */
|
| 62 |
-
.dropdown-trigger-wrapper {
|
| 63 |
-
display: contents;
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
</style>
|
| 67 |
-
</body>
|
| 68 |
-
</html>
|
| 69 |
-
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e3a1b08991339508618332fc2909fd59fe3c6ea8958c1e46103f8303eebbf0ed
|
| 3 |
+
size 1810
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/messages/AGENTS.md
CHANGED
|
@@ -1,38 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
- Own message rendering helpers, action buttons, process groups, and resize behavior.
|
| 6 |
-
|
| 7 |
-
## Ownership
|
| 8 |
-
|
| 9 |
-
- `action-buttons/` owns simple message action controls.
|
| 10 |
-
- `process-group/` owns grouped process-step DOM and styling helpers.
|
| 11 |
-
- `resize/` owns message resize state.
|
| 12 |
-
|
| 13 |
-
## Local Contracts
|
| 14 |
-
|
| 15 |
-
- Keep message DOM helpers compatible with extension points that modify rendered messages.
|
| 16 |
-
- Sanitize or safely render model/user-provided content through shared rendering paths.
|
| 17 |
-
- Avoid layout shifts that break long-running message streaming.
|
| 18 |
-
- Keep message action chrome out of text selection so copy/paste captures message content without button labels or icons.
|
| 19 |
-
- Reconcile standard action buttons in place during streamed updates so hover, focus, tooltips, and click feedback survive while handlers receive the latest message data; preserve extension-owned buttons in the same action bar.
|
| 20 |
-
- Order standard message actions as Detail, Copy, then Speak; omit unavailable actions without changing the relative order of the remaining controls. Plugin-rendered message actions must follow the same order.
|
| 21 |
-
- Keep collapsed process-step detail text out of the DOM; opening a step may materialize its current cached log data and collapsing it must discard that heavy detail again without removing extension action hooks.
|
| 22 |
-
- Preference-driven process detail modes must await the same materialization path as manual expansion and accept an explicit chat-history target for off-screen window staging. `STEP` opens only the current non-utility step at the live tail; historical windows must not invent a current step at their boundary.
|
| 23 |
-
- Keep oversized standalone replay bodies and key/value tables in a bounded preview state until the user expands them; collapsing must remove the full body again.
|
| 24 |
-
- Message-window boundaries must not split process groups. Groups with more than 50 steps initially render their newest 50 steps and prepend earlier steps in 50-step increments through the group-local `Show more` control while retaining stable full-group header metrics.
|
| 25 |
-
- The process-group `Show more` paging control uses the same understated, non-underlined typography and hover-opacity treatment as message-body expansion controls.
|
| 26 |
-
- A root response may attach only to a substantive process render unit. Utility-prefixed units remain visible even while utility steps are hidden; standalone utility-only groups remain separate and hidden while utility messages are disabled, and completed groups must not absorb later utility records. Determine this from full-log render metadata, not partially mounted DOM children.
|
| 27 |
-
|
| 28 |
-
## Work Guidance
|
| 29 |
-
|
| 30 |
-
- Coordinate changes with `webui/js/messages.js` and frontend extension hooks.
|
| 31 |
-
|
| 32 |
-
## Verification
|
| 33 |
-
|
| 34 |
-
- Smoke-test message rendering, action buttons, process groups, and resizing after changes.
|
| 35 |
-
|
| 36 |
-
## Child DOX Index
|
| 37 |
-
|
| 38 |
-
No child DOX files.
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0c8e79e3f5634742fab1915654221fd49f925c9ea29d9ae79590dd1dc9543722
|
| 3 |
+
size 2884
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/messages/action-buttons/simple-action-buttons.css
CHANGED
|
@@ -1,153 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
=========================================== */
|
| 5 |
-
|
| 6 |
-
.step-action-buttons {
|
| 7 |
-
display: flex;
|
| 8 |
-
flex-direction: row;
|
| 9 |
-
align-items: center;
|
| 10 |
-
gap: var(--spacing-sm);
|
| 11 |
-
padding: var(--spacing-sm) 0;
|
| 12 |
-
opacity: 0;
|
| 13 |
-
transition: opacity 0.2s ease-in-out;
|
| 14 |
-
pointer-events: none;
|
| 15 |
-
user-select: none;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.step-action-buttons .expand-btn {
|
| 19 |
-
display: none;
|
| 20 |
-
align-items: center;
|
| 21 |
-
background: transparent;
|
| 22 |
-
border: none;
|
| 23 |
-
color: var(--color-text-muted);
|
| 24 |
-
font-family: "Rubik", Arial, Helvetica, sans-serif;
|
| 25 |
-
font-size: var(--font-size-xs);
|
| 26 |
-
cursor: pointer;
|
| 27 |
-
padding: var(--spacing-xs) 0;
|
| 28 |
-
opacity: 0.7;
|
| 29 |
-
transition: opacity 0.15s ease;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
.step-action-buttons .expand-btn:hover {
|
| 33 |
-
opacity: 1;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
.step-action-buttons .action-button,
|
| 37 |
-
.action-button {
|
| 38 |
-
display: flex;
|
| 39 |
-
align-items: center;
|
| 40 |
-
justify-content: center;
|
| 41 |
-
background: transparent;
|
| 42 |
-
border: none;
|
| 43 |
-
padding: 2px;
|
| 44 |
-
cursor: pointer;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
.step-action-buttons .action-button .material-symbols-outlined,
|
| 48 |
-
.action-button .material-symbols-outlined {
|
| 49 |
-
font-size: 0.85rem;
|
| 50 |
-
line-height: 1;
|
| 51 |
-
color: var(--color-message-text);
|
| 52 |
-
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
|
| 53 |
-
transition: color 0.15s ease;
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
.step-action-buttons .action-button:hover .material-symbols-outlined,
|
| 57 |
-
.action-button:hover .material-symbols-outlined {
|
| 58 |
-
color: var(--color-text);
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
/* Success state */
|
| 62 |
-
.step-action-buttons .action-button.success .material-symbols-outlined,
|
| 63 |
-
.action-button.success .material-symbols-outlined {
|
| 64 |
-
color: #4CAF50;
|
| 65 |
-
font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20;
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
/* Error state */
|
| 69 |
-
.step-action-buttons .action-button.error .material-symbols-outlined,
|
| 70 |
-
.action-button.error .material-symbols-outlined {
|
| 71 |
-
color: var(--color-accent);
|
| 72 |
-
font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 20;
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
.show-more-btn,
|
| 76 |
-
.show-less-btn {
|
| 77 |
-
display: none;
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
.message.message-collapsible.has-overflow:not(.expanded) > .step-action-buttons {
|
| 81 |
-
opacity: 1;
|
| 82 |
-
pointer-events: auto;
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
.message.message-collapsible.has-overflow:not(.expanded)
|
| 86 |
-
> .step-action-buttons
|
| 87 |
-
.show-more-btn {
|
| 88 |
-
display: inline-flex;
|
| 89 |
-
pointer-events: auto;
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
.device-pointer
|
| 93 |
-
.message.message-collapsible.has-overflow:not(.expanded)
|
| 94 |
-
> .step-action-buttons
|
| 95 |
-
.action-button {
|
| 96 |
-
opacity: 0;
|
| 97 |
-
pointer-events: none;
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
.device-pointer
|
| 101 |
-
.message.message-collapsible.has-overflow:not(.expanded):hover
|
| 102 |
-
> .step-action-buttons
|
| 103 |
-
.action-button {
|
| 104 |
-
opacity: 1;
|
| 105 |
-
pointer-events: auto;
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
.device-pointer
|
| 109 |
-
.message.message-collapsible.has-overflow.expanded:hover
|
| 110 |
-
> .step-action-buttons
|
| 111 |
-
.show-less-btn {
|
| 112 |
-
display: inline-flex;
|
| 113 |
-
pointer-events: auto;
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
.device-touch
|
| 117 |
-
.message.message-collapsible.has-overflow.expanded
|
| 118 |
-
> .step-action-buttons
|
| 119 |
-
.show-less-btn {
|
| 120 |
-
display: inline-flex;
|
| 121 |
-
pointer-events: auto;
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
/* User messages: right-aligned */
|
| 125 |
-
.message-user .step-action-buttons {
|
| 126 |
-
justify-content: flex-end;
|
| 127 |
-
width: 100%;
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
.message-user .step-action-buttons .expand-btn {
|
| 131 |
-
order: 1;
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
/* ===========================================
|
| 137 |
-
Hover behavior - Pointer devices
|
| 138 |
-
=========================================== */
|
| 139 |
-
|
| 140 |
-
.device-pointer .step-action-buttons:hover,
|
| 141 |
-
.device-pointer :hover > .step-action-buttons {
|
| 142 |
-
opacity: 1;
|
| 143 |
-
pointer-events: auto;
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
/* ===========================================
|
| 147 |
-
Touch devices - Always visible
|
| 148 |
-
=========================================== */
|
| 149 |
-
|
| 150 |
-
.device-touch .step-action-buttons {
|
| 151 |
-
opacity: 1;
|
| 152 |
-
pointer-events: auto;
|
| 153 |
-
}
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ba3db6841e04158cf6798cfebc7bbc878cd885a9935ba6e64938371327ef8f3c
|
| 3 |
+
size 3627
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/messages/action-buttons/simple-action-buttons.js
CHANGED
|
@@ -1,134 +1,3 @@
|
|
| 1 |
-
//
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
const ACTION_ICON_MAP = {
|
| 6 |
-
detail: "open_in_full",
|
| 7 |
-
speak: "volume_up",
|
| 8 |
-
copy: "content_copy",
|
| 9 |
-
};
|
| 10 |
-
|
| 11 |
-
const ACTION_LABELS = {
|
| 12 |
-
detail: "View details",
|
| 13 |
-
speak: "Speak",
|
| 14 |
-
copy: "Copy",
|
| 15 |
-
};
|
| 16 |
-
|
| 17 |
-
function resolveActionIcon(icon) {
|
| 18 |
-
if (!icon) return "";
|
| 19 |
-
return ACTION_ICON_MAP[icon] || icon;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
function buildActionLabel(icon, text) {
|
| 23 |
-
const baseLabel = ACTION_LABELS[icon] || text || icon;
|
| 24 |
-
if (text && ACTION_LABELS[icon]) return `${ACTION_LABELS[icon]} ${text}`;
|
| 25 |
-
return baseLabel;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
/**
|
| 29 |
-
* Copy text to clipboard with fallback for non-secure contexts
|
| 30 |
-
*/
|
| 31 |
-
export async function copyToClipboard(text) {
|
| 32 |
-
if (navigator.clipboard && window.isSecureContext) {
|
| 33 |
-
await navigator.clipboard.writeText(text);
|
| 34 |
-
} else {
|
| 35 |
-
// Fallback for local dev / non-secure contexts
|
| 36 |
-
const textarea = document.createElement("textarea");
|
| 37 |
-
textarea.value = text;
|
| 38 |
-
textarea.style.cssText = "position:fixed;left:-9999px";
|
| 39 |
-
document.body.appendChild(textarea);
|
| 40 |
-
textarea.select();
|
| 41 |
-
document.execCommand("copy");
|
| 42 |
-
document.body.removeChild(textarea);
|
| 43 |
-
}
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
/**
|
| 47 |
-
* Show visual feedback on a button (success/error state)
|
| 48 |
-
*/
|
| 49 |
-
export function showButtonFeedback(button, success, originalIcon) {
|
| 50 |
-
const icon = button.querySelector(ICON_SELECTOR);
|
| 51 |
-
if (!icon) return;
|
| 52 |
-
|
| 53 |
-
setIconName(icon, success ? "check" : "error");
|
| 54 |
-
button.classList.add(success ? "success" : "error");
|
| 55 |
-
|
| 56 |
-
setTimeout(() => {
|
| 57 |
-
setIconName(icon, originalIcon);
|
| 58 |
-
button.classList.remove("success", "error");
|
| 59 |
-
}, 1000);
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
export function syncActionButtons(container, actionButtons = []) {
|
| 63 |
-
const previous = container.__managedActionButtons || [];
|
| 64 |
-
const next = actionButtons.filter(Boolean).map((button, index) => {
|
| 65 |
-
const existing = previous[index];
|
| 66 |
-
if (
|
| 67 |
-
existing?.isConnected &&
|
| 68 |
-
existing.dataset.actionKey === button.dataset.actionKey
|
| 69 |
-
) {
|
| 70 |
-
existing.__actionHandler = button.__actionHandler;
|
| 71 |
-
return existing;
|
| 72 |
-
}
|
| 73 |
-
existing?.remove();
|
| 74 |
-
return button;
|
| 75 |
-
});
|
| 76 |
-
const anchor = [...container.children].find(
|
| 77 |
-
(child) =>
|
| 78 |
-
!previous.includes(child) && !child.classList.contains("expand-btn"),
|
| 79 |
-
);
|
| 80 |
-
|
| 81 |
-
previous.slice(next.length).forEach((button) => button.remove());
|
| 82 |
-
next.forEach((button) => container.insertBefore(button, anchor || null));
|
| 83 |
-
container.__managedActionButtons = next;
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
/**
|
| 87 |
-
* Create action button element
|
| 88 |
-
*
|
| 89 |
-
* @param {string} icon
|
| 90 |
-
* @param {string} [text]
|
| 91 |
-
* @param {(() => (any | Promise<any>)) | null} [handler]
|
| 92 |
-
* @returns {HTMLButtonElement}
|
| 93 |
-
*/
|
| 94 |
-
export function createActionButton(icon, text = "", handler = null) {
|
| 95 |
-
const iconName = resolveActionIcon(icon);
|
| 96 |
-
|
| 97 |
-
const button = document.createElement("button");
|
| 98 |
-
button.type = "button";
|
| 99 |
-
button.className = `action-button action-${icon}`;
|
| 100 |
-
button.dataset.actionKey = `${icon}:${text}`;
|
| 101 |
-
button.__actionHandler = handler;
|
| 102 |
-
const label = buildActionLabel(icon, text);
|
| 103 |
-
if (label) {
|
| 104 |
-
button.setAttribute("aria-label", label);
|
| 105 |
-
button.setAttribute("title", label);
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
if (iconName) {
|
| 109 |
-
const iconElement = document.createElement("x-icon");
|
| 110 |
-
iconElement.name = iconName;
|
| 111 |
-
button.appendChild(iconElement);
|
| 112 |
-
} else if (text) {
|
| 113 |
-
button.textContent = text;
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
button.addEventListener("click", async (event) => {
|
| 117 |
-
event.stopPropagation();
|
| 118 |
-
if (typeof button.__actionHandler !== "function") return;
|
| 119 |
-
const shouldShowFeedback = Boolean(iconName); // icon === "copy" || icon === "speak";
|
| 120 |
-
try {
|
| 121 |
-
await button.__actionHandler();
|
| 122 |
-
if (shouldShowFeedback) {
|
| 123 |
-
showButtonFeedback(button, true, iconName);
|
| 124 |
-
}
|
| 125 |
-
} catch (err) {
|
| 126 |
-
console.error("Action button failed:", err);
|
| 127 |
-
if (shouldShowFeedback) {
|
| 128 |
-
showButtonFeedback(button, false, iconName);
|
| 129 |
-
}
|
| 130 |
-
}
|
| 131 |
-
});
|
| 132 |
-
|
| 133 |
-
return button;
|
| 134 |
-
}
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:906e3cf12b86ff41aa8ee6c3059a1dffb98c258f3765c7d41e42eb1f448a9778
|
| 3 |
+
size 3881
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/messages/process-group/process-group-dom.js
CHANGED
|
@@ -1,76 +1,3 @@
|
|
| 1 |
-
/
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
import { store as preferencesStore } from "/components/sidebar/bottom/preferences/preferences-store.js";
|
| 5 |
-
|
| 6 |
-
export async function applyModeSteps(
|
| 7 |
-
detailMode,
|
| 8 |
-
showUtils,
|
| 9 |
-
chatHistory = document.getElementById("chat-history"),
|
| 10 |
-
) {
|
| 11 |
-
const mode =
|
| 12 |
-
detailMode ||
|
| 13 |
-
preferencesStore.detailMode ||
|
| 14 |
-
"current";
|
| 15 |
-
|
| 16 |
-
if (!chatHistory) return;
|
| 17 |
-
|
| 18 |
-
chatHistory.dataset.detailMode = mode;
|
| 19 |
-
|
| 20 |
-
const shouldExpand = mode !== "collapsed";
|
| 21 |
-
const allMode = mode === "expanded";
|
| 22 |
-
const showUtilsFlag =
|
| 23 |
-
typeof showUtils === "boolean"
|
| 24 |
-
? showUtils
|
| 25 |
-
: preferencesStore.showUtils || false;
|
| 26 |
-
const pending = [];
|
| 27 |
-
const messages = Array.from(chatHistory.querySelectorAll(".process-group"));
|
| 28 |
-
const windowEnd = Number(chatHistory.dataset.messageWindowEnd);
|
| 29 |
-
const windowTotal = Number(chatHistory.dataset.messageWindowTotal);
|
| 30 |
-
const isAtTail =
|
| 31 |
-
Number.isFinite(windowEnd) &&
|
| 32 |
-
Number.isFinite(windowTotal) &&
|
| 33 |
-
windowEnd >= windowTotal;
|
| 34 |
-
for (let i = 0; i < messages.length; i += 1) {
|
| 35 |
-
const group = messages[i];
|
| 36 |
-
if (typeof group.__setExpanded === "function") {
|
| 37 |
-
pending.push(Promise.resolve(group.__setExpanded(shouldExpand)));
|
| 38 |
-
} else {
|
| 39 |
-
group.classList.toggle("expanded", shouldExpand);
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
const steps = Array.from(group.querySelectorAll(".process-step"));
|
| 43 |
-
const isComplete =
|
| 44 |
-
group.hasAttribute("data-group-complete") ||
|
| 45 |
-
Boolean(group.querySelector(".process-group-response"));
|
| 46 |
-
let currentStep = null;
|
| 47 |
-
if (
|
| 48 |
-
mode === "current" &&
|
| 49 |
-
isAtTail &&
|
| 50 |
-
group === messages[messages.length - 1] &&
|
| 51 |
-
!isComplete
|
| 52 |
-
) {
|
| 53 |
-
for (let si = steps.length - 1; si >= 0; si -= 1) {
|
| 54 |
-
if (
|
| 55 |
-
showUtilsFlag ||
|
| 56 |
-
!steps[si].classList.contains("message-util")
|
| 57 |
-
) {
|
| 58 |
-
currentStep = steps[si];
|
| 59 |
-
break;
|
| 60 |
-
}
|
| 61 |
-
}
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
for (let si = 0; si < steps.length; si += 1) {
|
| 65 |
-
const step = steps[si];
|
| 66 |
-
const shouldExpandStep = allMode || step === currentStep;
|
| 67 |
-
if (typeof step.__setExpanded === "function") {
|
| 68 |
-
pending.push(Promise.resolve(step.__setExpanded(shouldExpandStep)));
|
| 69 |
-
} else {
|
| 70 |
-
step.classList.toggle("expanded", shouldExpandStep);
|
| 71 |
-
}
|
| 72 |
-
}
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
await Promise.allSettled(pending);
|
| 76 |
-
}
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ef180e4cc6f6feb0498139015cb64ce7ae4fbf09083d3be92dc8ebb3e9a8e4f9
|
| 3 |
+
size 2305
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/messages/process-group/process-group.css
CHANGED
|
@@ -1,914 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
position: relative;
|
| 5 |
-
z-index: 1;
|
| 6 |
-
margin: var(--spacing-xs) 0;
|
| 7 |
-
padding: var(--spacing-xs) 0;
|
| 8 |
-
min-width: 200px;
|
| 9 |
-
max-width: 100%;
|
| 10 |
-
box-sizing: border-box;
|
| 11 |
-
flex-shrink: 0;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
.process-group.utility-only {
|
| 15 |
-
display: none !important;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.show-utility-messages .process-group.utility-only {
|
| 19 |
-
display: flex !important;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
/* Embedded Process Group inside Response */
|
| 23 |
-
.process-group.embedded {
|
| 24 |
-
display: flex;
|
| 25 |
-
flex-direction: column;
|
| 26 |
-
width: 100%;
|
| 27 |
-
margin: 0;
|
| 28 |
-
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
| 29 |
-
/* border-bottom: 1px solid rgba(255, 255, 255, 0.08); */
|
| 30 |
-
background: transparent;
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
/* Message container with embedded process group */
|
| 34 |
-
.message-container.has-process-group {
|
| 35 |
-
display: inline-flex;
|
| 36 |
-
flex-direction: column;
|
| 37 |
-
padding: 0;
|
| 38 |
-
overflow: hidden;
|
| 39 |
-
min-width: 200px;
|
| 40 |
-
max-width: 100%;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
.message-container.has-process-group > .message {
|
| 44 |
-
border: none;
|
| 45 |
-
background: transparent;
|
| 46 |
-
margin: 0;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
/* Process Group Header */
|
| 50 |
-
.process-group-header {
|
| 51 |
-
display: flex;
|
| 52 |
-
align-items: center;
|
| 53 |
-
padding: 0;
|
| 54 |
-
cursor: pointer;
|
| 55 |
-
user-select: none;
|
| 56 |
-
transition: opacity 0.15s ease;
|
| 57 |
-
min-height: 22px;
|
| 58 |
-
gap: 6px;
|
| 59 |
-
white-space: nowrap;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
.process-group-header:hover {
|
| 63 |
-
opacity: 0.85;
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
/* Expand/collapse triangle - CSS-only (no Material Icons) */
|
| 67 |
-
.process-group-header .expand-icon {
|
| 68 |
-
display: inline-block;
|
| 69 |
-
width: 0;
|
| 70 |
-
height: 0;
|
| 71 |
-
border-style: solid;
|
| 72 |
-
border-width: 4px 0 4px 6px;
|
| 73 |
-
border-color: transparent transparent transparent var(--color-text);
|
| 74 |
-
opacity: 0.5;
|
| 75 |
-
transition: transform 0.2s ease, opacity 0.15s ease;
|
| 76 |
-
flex-shrink: 0;
|
| 77 |
-
font-size: 0; /* Hide any text content */
|
| 78 |
-
margin-right: 2px;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
.process-group-header:hover .expand-icon {
|
| 82 |
-
opacity: 0.8;
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
.process-group.expanded .process-group-header .expand-icon {
|
| 86 |
-
transform: rotate(90deg);
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
/* Group icon removed - using status badges */
|
| 90 |
-
.process-group-header .group-icon {
|
| 91 |
-
display: none;
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
/* Group title - prominent display */
|
| 95 |
-
.process-group-header .group-title {
|
| 96 |
-
flex: 0 1 auto;
|
| 97 |
-
font-size: var(--font-size-medium);
|
| 98 |
-
font-weight: 500;
|
| 99 |
-
color: var(--color-text-muted);
|
| 100 |
-
opacity: 0.9;
|
| 101 |
-
white-space: nowrap;
|
| 102 |
-
overflow: hidden;
|
| 103 |
-
text-overflow: ellipsis;
|
| 104 |
-
max-width: 350px;
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
/* Step count badge */
|
| 108 |
-
.process-group-header .step-count {
|
| 109 |
-
font-size: 0.65rem;
|
| 110 |
-
color: var(--color-text);
|
| 111 |
-
opacity: 0.5;
|
| 112 |
-
flex-shrink: 0;
|
| 113 |
-
padding: 1px 6px;
|
| 114 |
-
font-family: var(--font-family-code);
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
/* ===========================================
|
| 118 |
-
3-Letter Status Code Badges
|
| 119 |
-
=========================================== */
|
| 120 |
-
|
| 121 |
-
/* Base status badge */
|
| 122 |
-
.step-badge {
|
| 123 |
-
display: inline-flex;
|
| 124 |
-
align-items: center;
|
| 125 |
-
gap: 3px;
|
| 126 |
-
padding: 2px 6px;
|
| 127 |
-
border-radius: 3px;
|
| 128 |
-
font-size: 0.65rem;
|
| 129 |
-
font-weight: 600;
|
| 130 |
-
font-family: var(--font-family-code);
|
| 131 |
-
text-transform: uppercase;
|
| 132 |
-
letter-spacing: 0.5px;
|
| 133 |
-
flex-shrink: 0;
|
| 134 |
-
line-height: 1;
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
/* Status badge with icon */
|
| 138 |
-
.step-badge .material-symbols-outlined {
|
| 139 |
-
font-size: 0.8rem;
|
| 140 |
-
line-height: 1;
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
/* Badge icon styling */
|
| 144 |
-
.step-badge .badge-icon {
|
| 145 |
-
font-size: var(--font-size-xs);
|
| 146 |
-
margin-right: 2px;
|
| 147 |
-
opacity: 0.9;
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
/* Status colors */
|
| 151 |
-
/* Each status defines --step-accent for cascading to internal icons */
|
| 152 |
-
|
| 153 |
-
/* GEN - agent type (blue/cyan) */
|
| 154 |
-
.process-group .GEN {
|
| 155 |
-
--step-accent: #38bdf8;
|
| 156 |
-
}
|
| 157 |
-
.light-mode .process-group .GEN {
|
| 158 |
-
--step-accent: #0284c7;
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
/* END - response/done type (green) */
|
| 162 |
-
.process-group .END {
|
| 163 |
-
--step-accent: #22c55e;
|
| 164 |
-
}
|
| 165 |
-
.light-mode .process-group .END {
|
| 166 |
-
--step-accent: #15803d;
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
/* USE - tool usage (amber/yellow) */
|
| 170 |
-
.process-group .USE {
|
| 171 |
-
--step-accent: #fbbf24;
|
| 172 |
-
}
|
| 173 |
-
.light-mode .process-group .USE {
|
| 174 |
-
--step-accent: #b45309;
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
/* MCP - mcp type (amber/yellow) */
|
| 178 |
-
.process-group .MCP {
|
| 179 |
-
--step-accent: #fbbf24;
|
| 180 |
-
}
|
| 181 |
-
.light-mode .process-group .MCP {
|
| 182 |
-
--step-accent: #b45309;
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
-
/* SUB and RES - subagent type and response (teal) */
|
| 186 |
-
.process-group .SUB {
|
| 187 |
-
--step-accent: #14b8a6;
|
| 188 |
-
}
|
| 189 |
-
.light-mode .process-group .SUB {
|
| 190 |
-
--step-accent: #0f766e;
|
| 191 |
-
}
|
| 192 |
-
.process-group .RES {
|
| 193 |
-
--step-accent: #14b8a6;
|
| 194 |
-
}
|
| 195 |
-
.light-mode .process-group .RES {
|
| 196 |
-
--step-accent: #0f766e;
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
/* EXE - code_exe type (magenta/purple) */
|
| 200 |
-
.process-group .EXE {
|
| 201 |
-
--step-accent: #ba68c8;
|
| 202 |
-
}
|
| 203 |
-
.light-mode .process-group .EXE {
|
| 204 |
-
--step-accent: #7c3aed;
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
/* WWW - browser type (indigo) */
|
| 208 |
-
.process-group .WWW {
|
| 209 |
-
--step-accent: #818cf8;
|
| 210 |
-
}
|
| 211 |
-
.light-mode .process-group .WWW {
|
| 212 |
-
--step-accent: #4f46e5;
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
/* WAIT - progress type (slate) */
|
| 216 |
-
.process-group .HDL {
|
| 217 |
-
--step-accent: #94a3b8;
|
| 218 |
-
}
|
| 219 |
-
.light-mode .process-group .HDL {
|
| 220 |
-
--step-accent: #475569;
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
/* INF - info type (gray) */
|
| 224 |
-
.process-group .INF {
|
| 225 |
-
--step-accent: #94a3b8;
|
| 226 |
-
}
|
| 227 |
-
.light-mode .process-group .INF {
|
| 228 |
-
--step-accent: #475569;
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
/* HNT - hint type (yellow-green) */
|
| 232 |
-
.process-group .HNT {
|
| 233 |
-
--step-accent: #a3e635;
|
| 234 |
-
}
|
| 235 |
-
.light-mode .process-group .HNT {
|
| 236 |
-
--step-accent: #65a30d;
|
| 237 |
-
}
|
| 238 |
-
|
| 239 |
-
/* WRN - warning type (orange) */
|
| 240 |
-
.process-group .WRN {
|
| 241 |
-
--step-accent: #f97316;
|
| 242 |
-
}
|
| 243 |
-
.light-mode .process-group .WRN {
|
| 244 |
-
--step-accent: #c2410c;
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
/* ERR - error type (red) */
|
| 249 |
-
.process-group .ERR {
|
| 250 |
-
--step-accent: var(--color-error-text);
|
| 251 |
-
}
|
| 252 |
-
.light-mode .process-group .ERR {
|
| 253 |
-
--step-accent: var(--color-error-text);
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
/* UTL - util type (gray-blue) */
|
| 257 |
-
.process-group .UTL {
|
| 258 |
-
--step-accent: #64748b;
|
| 259 |
-
}
|
| 260 |
-
.light-mode .process-group .UTL {
|
| 261 |
-
--step-accent: #334155;
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
.process-group .step-badge{
|
| 266 |
-
color: var(--step-accent);
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
.process-group .kvps-key{
|
| 270 |
-
color: var(--step-accent);
|
| 271 |
-
}
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
/* Completed process group styling */
|
| 275 |
-
.process-group-completed {
|
| 276 |
-
opacity: 0.95;
|
| 277 |
-
}
|
| 278 |
-
|
| 279 |
-
.process-group-completed .group-status {
|
| 280 |
-
font-weight: 700;
|
| 281 |
-
}
|
| 282 |
-
|
| 283 |
-
/* Subtle completion indicator on the expand icon */
|
| 284 |
-
.process-group-completed .expand-icon {
|
| 285 |
-
color: #22c55e;
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
/* Group status badge in header */
|
| 289 |
-
.process-group-header .group-status {
|
| 290 |
-
margin-left: var(--spacing-xs);
|
| 291 |
-
}
|
| 292 |
-
|
| 293 |
-
/* Metrics row */
|
| 294 |
-
.process-group-header .group-metrics {
|
| 295 |
-
display: flex;
|
| 296 |
-
align-items: center;
|
| 297 |
-
gap: var(--spacing-sm);
|
| 298 |
-
/* margin-left: auto; */
|
| 299 |
-
margin-left: 2em;
|
| 300 |
-
font-size: 0.65rem;
|
| 301 |
-
font-family: var(--font-family-code);
|
| 302 |
-
color: var(--color-text);
|
| 303 |
-
opacity: 0.8;
|
| 304 |
-
}
|
| 305 |
-
|
| 306 |
-
.process-group-header .group-metrics .material-symbols-outlined {
|
| 307 |
-
font-size: var(--font-size-xs);
|
| 308 |
-
opacity: 0.7;
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
.process-group-header .group-metrics span[class^="metric-"] {
|
| 312 |
-
display: inline-flex;
|
| 313 |
-
align-items: center;
|
| 314 |
-
gap: 2px;
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
.process-group-header .group-metrics .metric-notifications {
|
| 318 |
-
font-weight: 600;
|
| 319 |
-
}
|
| 320 |
-
|
| 321 |
-
.process-group-header .group-metrics .metric-notifications[hidden] {
|
| 322 |
-
display: none;
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
.process-group-header .group-metrics .metric-notifications .material-symbols-outlined {
|
| 326 |
-
opacity: 0.85;
|
| 327 |
-
}
|
| 328 |
-
|
| 329 |
-
/* Legacy timestamp/duration (for backwards compatibility) */
|
| 330 |
-
.process-group-header .group-timestamp {
|
| 331 |
-
font-size: 0.65rem;
|
| 332 |
-
color: rgba(255, 255, 255, 0.65);
|
| 333 |
-
flex-shrink: 0;
|
| 334 |
-
font-family: var(--font-family-code);
|
| 335 |
-
display: none; /* Hidden, replaced by metrics */
|
| 336 |
-
}
|
| 337 |
-
|
| 338 |
-
.process-group-header .group-duration {
|
| 339 |
-
font-size: var(--font-size-xs);
|
| 340 |
-
color: #81c784;
|
| 341 |
-
flex-shrink: 0;
|
| 342 |
-
font-family: var(--font-family-code);
|
| 343 |
-
min-width: 40px;
|
| 344 |
-
text-align: right;
|
| 345 |
-
display: none; /* Hidden, replaced by metrics */
|
| 346 |
-
}
|
| 347 |
-
|
| 348 |
-
/* Process Group Content - Animated expand/collapse */
|
| 349 |
-
.process-group-content {
|
| 350 |
-
display: grid;
|
| 351 |
-
grid-template-rows: 0fr;
|
| 352 |
-
opacity: 0;
|
| 353 |
-
margin-top: 0;
|
| 354 |
-
padding-top: 0;
|
| 355 |
-
border-top: 1px solid transparent;
|
| 356 |
-
transition: grid-template-rows 0.25s ease-out,
|
| 357 |
-
opacity 0.2s ease-out,
|
| 358 |
-
margin-top 0.25s ease-out,
|
| 359 |
-
padding-top 0.25s ease-out,
|
| 360 |
-
border-color 0.2s ease-out;
|
| 361 |
-
overflow: hidden;
|
| 362 |
-
color: var(--color-text-muted);
|
| 363 |
-
max-width: 100%;
|
| 364 |
-
}
|
| 365 |
-
|
| 366 |
-
.process-group-content > .process-steps {
|
| 367 |
-
min-height: 0;
|
| 368 |
-
}
|
| 369 |
-
|
| 370 |
-
.process-group.expanded .process-group-content {
|
| 371 |
-
grid-template-rows: 1fr;
|
| 372 |
-
opacity: 1;
|
| 373 |
-
margin-top: var(--spacing-xs);
|
| 374 |
-
padding-top: var(--spacing-xs);
|
| 375 |
-
/* border-top-color: var(--color-border); */
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
/* Process Steps List */
|
| 379 |
-
.process-steps {
|
| 380 |
-
padding: 0;
|
| 381 |
-
display: flex;
|
| 382 |
-
flex-direction: column;
|
| 383 |
-
gap: 2px;
|
| 384 |
-
max-width: 100%;
|
| 385 |
-
overflow: hidden;
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
.process-group-show-more {
|
| 389 |
-
align-self: flex-start;
|
| 390 |
-
appearance: none;
|
| 391 |
-
display: inline-flex;
|
| 392 |
-
align-items: center;
|
| 393 |
-
border: 0;
|
| 394 |
-
background: transparent;
|
| 395 |
-
color: var(--color-text-muted);
|
| 396 |
-
cursor: pointer;
|
| 397 |
-
font-family: var(--font-family-main);
|
| 398 |
-
font-size: var(--font-size-xs);
|
| 399 |
-
padding: var(--spacing-xs) 0;
|
| 400 |
-
opacity: 0.7;
|
| 401 |
-
text-decoration: none;
|
| 402 |
-
transition: opacity 0.15s ease;
|
| 403 |
-
}
|
| 404 |
-
|
| 405 |
-
.process-group-show-more:hover,
|
| 406 |
-
.process-group-show-more:focus-visible {
|
| 407 |
-
opacity: 1;
|
| 408 |
-
}
|
| 409 |
-
|
| 410 |
-
/* Individual Process Step */
|
| 411 |
-
.process-step {
|
| 412 |
-
display: flex;
|
| 413 |
-
flex-direction: column;
|
| 414 |
-
padding: 0.1em;
|
| 415 |
-
transition: background-color 0.15s ease;
|
| 416 |
-
max-width: 100%;
|
| 417 |
-
overflow: hidden;
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
.process-step.message-util {
|
| 421 |
-
display: none;
|
| 422 |
-
}
|
| 423 |
-
|
| 424 |
-
.show-utility-messages .process-step.message-util {
|
| 425 |
-
display: flex;
|
| 426 |
-
}
|
| 427 |
-
|
| 428 |
-
/* Utility/Info/Hint steps have subtle background tint */
|
| 429 |
-
.process-step[data-type="util"],
|
| 430 |
-
.process-step[data-type="info"],
|
| 431 |
-
.process-step[data-type="hint"] {
|
| 432 |
-
background-color: rgba(120, 115, 100, 0);
|
| 433 |
-
}
|
| 434 |
-
|
| 435 |
-
.process-step[data-type="util"]:hover,
|
| 436 |
-
.process-step[data-type="info"]:hover,
|
| 437 |
-
.process-step[data-type="hint"]:hover {
|
| 438 |
-
background-color: rgba(255, 255, 255, 0.03);
|
| 439 |
-
}
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
.light-mode .process-step[data-type="util"]:hover,
|
| 443 |
-
.light-mode .process-step[data-type="info"]:hover,
|
| 444 |
-
.light-mode .process-step[data-type="hint"]:hover {
|
| 445 |
-
background-color: rgba(0, 0, 0, 0.04);
|
| 446 |
-
}
|
| 447 |
-
|
| 448 |
-
/* Step Header (clickable) */
|
| 449 |
-
.process-step-header {
|
| 450 |
-
display: flex;
|
| 451 |
-
align-items: center;
|
| 452 |
-
cursor: pointer;
|
| 453 |
-
user-select: none;
|
| 454 |
-
gap: 4px;
|
| 455 |
-
min-height: 18px;
|
| 456 |
-
max-width: 100%;
|
| 457 |
-
overflow: hidden;
|
| 458 |
-
}
|
| 459 |
-
|
| 460 |
-
/* Step title */
|
| 461 |
-
.process-step-header .step-title {
|
| 462 |
-
flex: 1;
|
| 463 |
-
font-size: 0.8rem;
|
| 464 |
-
/* color: var(--color-text-muted); */
|
| 465 |
-
white-space: nowrap;
|
| 466 |
-
overflow: hidden;
|
| 467 |
-
text-overflow: ellipsis;
|
| 468 |
-
margin-left: var(--spacing-xs);
|
| 469 |
-
min-width: 0;
|
| 470 |
-
}
|
| 471 |
-
|
| 472 |
-
/* Step expand icon - CSS triangle (no Material Icons) */
|
| 473 |
-
.process-step-header .step-expand-icon {
|
| 474 |
-
display: inline-block;
|
| 475 |
-
width: 0;
|
| 476 |
-
height: 0;
|
| 477 |
-
border-style: solid;
|
| 478 |
-
border-width: 3px 0 3px 5px;
|
| 479 |
-
border-color: transparent transparent transparent var(--color-text);
|
| 480 |
-
opacity: 0.4;
|
| 481 |
-
transition: transform 0.2s ease, opacity 0.15s ease;
|
| 482 |
-
flex-shrink: 0;
|
| 483 |
-
font-size: 0; /* Hide any text content */
|
| 484 |
-
margin-right: 2px;
|
| 485 |
-
}
|
| 486 |
-
|
| 487 |
-
.process-step-header:hover .step-expand-icon {
|
| 488 |
-
opacity: 0.7;
|
| 489 |
-
}
|
| 490 |
-
|
| 491 |
-
/* Use direct child selector to prevent affecting nested steps */
|
| 492 |
-
.process-step.expanded > .process-step-header > .step-expand-icon {
|
| 493 |
-
transform: rotate(90deg);
|
| 494 |
-
}
|
| 495 |
-
|
| 496 |
-
/* Step Detail Content - Animated expand/collapse */
|
| 497 |
-
.process-step-detail {
|
| 498 |
-
/* display: grid; */
|
| 499 |
-
grid-template-rows: 0fr;
|
| 500 |
-
opacity: 0;
|
| 501 |
-
margin-top: 0;
|
| 502 |
-
transition: grid-template-rows 0.2s ease-out, opacity 0.15s ease-out, margin-top 0.2s ease-out;
|
| 503 |
-
overflow: hidden;
|
| 504 |
-
max-width: 100%;
|
| 505 |
-
}
|
| 506 |
-
|
| 507 |
-
.process-step-detail-content{
|
| 508 |
-
white-space: pre-wrap;
|
| 509 |
-
}
|
| 510 |
-
|
| 511 |
-
.process-step-detail,
|
| 512 |
-
.process-step-detail-scroll {
|
| 513 |
-
scrollbar-width: thin; /* Firefox */
|
| 514 |
-
-ms-overflow-style: thin; /* IE/Edge */
|
| 515 |
-
}
|
| 516 |
-
|
| 517 |
-
/* Hide scrollbar for Chrome/Safari/Webkit */
|
| 518 |
-
.process-step-detail::-webkit-scrollbar,
|
| 519 |
-
.process-step-detail-scroll::-webkit-scrollbar {
|
| 520 |
-
width: thin;
|
| 521 |
-
height: thin;
|
| 522 |
-
}
|
| 523 |
-
|
| 524 |
-
.process-step:not(.expanded) > .process-step-detail {
|
| 525 |
-
margin-top: 0;
|
| 526 |
-
}
|
| 527 |
-
|
| 528 |
-
.process-step:not(.expanded) > .process-step-detail > .process-step-detail-scroll {
|
| 529 |
-
padding: 0;
|
| 530 |
-
margin-top: 0;
|
| 531 |
-
max-height: 0;
|
| 532 |
-
overflow: hidden;
|
| 533 |
-
}
|
| 534 |
-
|
| 535 |
-
.process-step-detail > .process-step-detail-scroll {
|
| 536 |
-
min-height: 0;
|
| 537 |
-
}
|
| 538 |
-
|
| 539 |
-
/* Use direct child selector (>) to prevent cascading to nested steps */
|
| 540 |
-
.process-step.expanded > .process-step-detail {
|
| 541 |
-
/* grid-template-rows: 1fr; */
|
| 542 |
-
opacity: 1;
|
| 543 |
-
overflow: hidden;
|
| 544 |
-
margin-top: var(--spacing-xs);
|
| 545 |
-
max-width: 100%;
|
| 546 |
-
padding-left: 17px;
|
| 547 |
-
padding-right: 1em;
|
| 548 |
-
}
|
| 549 |
-
|
| 550 |
-
.process-step.expanded > .process-step-detail > .process-step-detail-scroll {
|
| 551 |
-
max-height: 40em;
|
| 552 |
-
overflow: auto;
|
| 553 |
-
max-width: 100%;
|
| 554 |
-
width: fit-content;
|
| 555 |
-
box-sizing: content-box;
|
| 556 |
-
border-radius: var(--border-radius-sm);
|
| 557 |
-
-webkit-overflow-scrolling: touch;
|
| 558 |
-
scrollbar-width: thin;
|
| 559 |
-
-ms-overflow-style: thin;
|
| 560 |
-
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
| 561 |
-
/* overscroll-behavior: contain; */
|
| 562 |
-
}
|
| 563 |
-
|
| 564 |
-
.process-step.expanded > .process-step-detail > .process-step-detail-scroll > .process-step-detail-scroll {
|
| 565 |
-
scrollbar-width: thin;
|
| 566 |
-
-ms-overflow-style: thin;
|
| 567 |
-
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
| 568 |
-
}
|
| 569 |
-
|
| 570 |
-
.process-step-detail-scroll {
|
| 571 |
-
transition: max-height 0.2s ease-out, padding 0.2s ease-out, margin-top 0.2s ease-out;
|
| 572 |
-
padding: var(--spacing-xs) 0;
|
| 573 |
-
margin-top: var(--spacing-xxs);
|
| 574 |
-
/* margin-left: 28px; */
|
| 575 |
-
background-color: transparent;
|
| 576 |
-
font-size: var(--font-size-xs);
|
| 577 |
-
line-height: 1.5;
|
| 578 |
-
-webkit-overflow-scrolling: touch; /* smooth scrolling on iOS */
|
| 579 |
-
overscroll-behavior-x: contain;
|
| 580 |
-
}
|
| 581 |
-
|
| 582 |
-
/* .process-step-detail-scroll pre {
|
| 583 |
-
margin: 0;
|
| 584 |
-
white-space: pre-wrap;
|
| 585 |
-
word-break: break-word;
|
| 586 |
-
font-family: var(--font-family-code);
|
| 587 |
-
font-size: var(--font-size-xs);
|
| 588 |
-
color: var(--color-text);
|
| 589 |
-
opacity: 0.8;
|
| 590 |
-
} */
|
| 591 |
-
|
| 592 |
-
/* KVPs in step detail - CSS Grid for aligned columns */
|
| 593 |
-
.process-step-detail-scroll .step-kvps {
|
| 594 |
-
display: grid;
|
| 595 |
-
grid-template-columns: auto minmax(0, 1fr);
|
| 596 |
-
gap: var(--spacing-xs) var(--spacing-sm);
|
| 597 |
-
align-items: start;
|
| 598 |
-
}
|
| 599 |
-
|
| 600 |
-
.process-step-detail-scroll .step-kvp {
|
| 601 |
-
display: contents; /* Children participate in parent grid */
|
| 602 |
-
}
|
| 603 |
-
|
| 604 |
-
.process-step-detail-scroll .step-kvp-key {
|
| 605 |
-
color: var(--step-accent, var(--color-primary));
|
| 606 |
-
font-weight: 500;
|
| 607 |
-
opacity: 0.8;
|
| 608 |
-
display: flex;
|
| 609 |
-
align-items: center;
|
| 610 |
-
justify-content: end;
|
| 611 |
-
white-space: nowrap;
|
| 612 |
-
}
|
| 613 |
-
|
| 614 |
-
.process-step-detail-scroll .step-kvp-key .material-symbols-outlined {
|
| 615 |
-
font-size: 0.9rem;
|
| 616 |
-
color: var(--step-accent, var(--color-primary)) !important;
|
| 617 |
-
}
|
| 618 |
-
|
| 619 |
-
.process-step-detail-scroll .step-kvp-value {
|
| 620 |
-
/* color: var(--color-text); */
|
| 621 |
-
opacity: 0.85;
|
| 622 |
-
word-break: break-word;
|
| 623 |
-
overflow-wrap: anywhere;
|
| 624 |
-
white-space: pre-wrap;
|
| 625 |
-
font-size: 0.75rem;
|
| 626 |
-
line-height: 1.5;
|
| 627 |
-
min-width: 0;
|
| 628 |
-
}
|
| 629 |
-
|
| 630 |
-
.process-step-detail-scroll .step-kvp-value p {
|
| 631 |
-
margin: 0;
|
| 632 |
-
word-break: break-word;
|
| 633 |
-
overflow-wrap: anywhere;
|
| 634 |
-
}
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
/* Tool arguments - CSS Grid for aligned columns */
|
| 638 |
-
/* .process-step-detail-scroll .step-tool-args {
|
| 639 |
-
display: grid;
|
| 640 |
-
grid-template-columns: auto 1fr;
|
| 641 |
-
gap: 4px 8px;
|
| 642 |
-
margin: var(--spacing-xs) 0;
|
| 643 |
-
align-items: baseline;
|
| 644 |
-
} */
|
| 645 |
-
|
| 646 |
-
/* .process-step-detail-scroll .tool-arg-row {
|
| 647 |
-
display: contents;
|
| 648 |
-
} */
|
| 649 |
-
|
| 650 |
-
/* .process-step-detail-scroll .tool-arg-label {
|
| 651 |
-
font-size: 0.68rem;
|
| 652 |
-
font-weight: 600;
|
| 653 |
-
color: var(--step-accent, var(--color-primary));
|
| 654 |
-
display: flex;
|
| 655 |
-
align-items: center;
|
| 656 |
-
justify-content: end;
|
| 657 |
-
} */
|
| 658 |
-
|
| 659 |
-
/* .process-step-detail-scroll .tool-arg-label .material-symbols-outlined {
|
| 660 |
-
font-size: 0.9rem;
|
| 661 |
-
opacity: 0.85;
|
| 662 |
-
color: var(--step-accent, var(--color-primary));
|
| 663 |
-
} */
|
| 664 |
-
|
| 665 |
-
/* .process-step-detail-scroll .tool-arg-value {
|
| 666 |
-
font-size: var(--font-size-xs);
|
| 667 |
-
color: var(--color-text);
|
| 668 |
-
opacity: 0.85;
|
| 669 |
-
word-break: break-word;
|
| 670 |
-
font-family: var(--font-family-code);
|
| 671 |
-
} */
|
| 672 |
-
|
| 673 |
-
/* Light mode tool args - transparent like dark mode */
|
| 674 |
-
/* .light-mode .process-step-detail-scroll .step-tool-args {
|
| 675 |
-
background: transparent;
|
| 676 |
-
border-left-color: transparent;
|
| 677 |
-
} */
|
| 678 |
-
|
| 679 |
-
/* .process-step-detail-scroll .step-tool-header .tool-icon {
|
| 680 |
-
font-size: 0.85rem;
|
| 681 |
-
color: var(--step-accent, var(--color-warning));
|
| 682 |
-
opacity: 0.85;
|
| 683 |
-
} */
|
| 684 |
-
|
| 685 |
-
/* .process-step-detail-scroll .step-tool-header .tool-label {
|
| 686 |
-
font-size: 0.68rem;
|
| 687 |
-
font-weight: 600;
|
| 688 |
-
color: var(--step-accent, var(--color-warning));
|
| 689 |
-
opacity: 0.85;
|
| 690 |
-
} */
|
| 691 |
-
|
| 692 |
-
/* .process-step-detail-scroll .step-tool-header .tool-name {
|
| 693 |
-
font-size: 0.72rem;
|
| 694 |
-
color: var(--color-text);
|
| 695 |
-
opacity: 0.85;
|
| 696 |
-
} */
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
/* Terminal output - self-contained scrollable block */
|
| 700 |
-
.process-step-detail .terminal-output {
|
| 701 |
-
margin: var(--spacing-xs) 0 0 0;
|
| 702 |
-
padding: var(--spacing-xs);
|
| 703 |
-
background: rgba(0, 0, 0, 0.8);
|
| 704 |
-
min-height: 5em;
|
| 705 |
-
min-width: 20em;
|
| 706 |
-
width: max-content;
|
| 707 |
-
border-radius: var(--border-radius-sm);
|
| 708 |
-
color: #c9d1d9;
|
| 709 |
-
white-space: pre;
|
| 710 |
-
font-family: monospace;
|
| 711 |
-
/* max-width: fit-content; */
|
| 712 |
-
/* overflow: auto; */
|
| 713 |
-
/* -webkit-overflow-scrolling: touch; */
|
| 714 |
-
/* scrollbar-width: thin; */
|
| 715 |
-
/* scrollbar-color: rgba(255,255,255,0.2) transparent; */
|
| 716 |
-
/* overscroll-behavior: contain; */
|
| 717 |
-
}
|
| 718 |
-
|
| 719 |
-
.process-step-detail .terminal-output::-webkit-scrollbar {
|
| 720 |
-
width: 4px;
|
| 721 |
-
height: 4px;
|
| 722 |
-
}
|
| 723 |
-
|
| 724 |
-
.process-step-detail .terminal-output::-webkit-scrollbar-track {
|
| 725 |
-
background: transparent;
|
| 726 |
-
}
|
| 727 |
-
|
| 728 |
-
.process-step-detail .terminal-output::-webkit-scrollbar-thumb {
|
| 729 |
-
background: rgba(255, 255, 255, 0.2);
|
| 730 |
-
border-radius: 2px;
|
| 731 |
-
}
|
| 732 |
-
|
| 733 |
-
.process-step-detail .terminal-output::-webkit-scrollbar-thumb:hover {
|
| 734 |
-
background: rgba(255, 255, 255, 0.35);
|
| 735 |
-
}
|
| 736 |
-
|
| 737 |
-
/* Light mode terminal */
|
| 738 |
-
.light-mode .process-step-detail .terminal-output {
|
| 739 |
-
background: rgba(0, 0, 0, 0.05);
|
| 740 |
-
color: black;
|
| 741 |
-
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
| 742 |
-
}
|
| 743 |
-
|
| 744 |
-
.light-mode .process-step-detail .terminal-output::-webkit-scrollbar-thumb {
|
| 745 |
-
background: rgba(0, 0, 0, 0.15);
|
| 746 |
-
}
|
| 747 |
-
|
| 748 |
-
.light-mode .process-step-detail .terminal-output::-webkit-scrollbar-thumb:hover {
|
| 749 |
-
background: rgba(0, 0, 0, 0.3);
|
| 750 |
-
}
|
| 751 |
-
|
| 752 |
-
/* Light mode adjustments */
|
| 753 |
-
.light-mode .process-group.expanded .process-group-content {
|
| 754 |
-
border-top-color: rgba(0, 0, 0, 0.06);
|
| 755 |
-
}
|
| 756 |
-
|
| 757 |
-
.light-mode .process-step-detail-scroll {
|
| 758 |
-
background-color: transparent;
|
| 759 |
-
}
|
| 760 |
-
|
| 761 |
-
/* Light mode text colors for process group */
|
| 762 |
-
.light-mode .process-group-header .group-title,
|
| 763 |
-
.light-mode .process-step-header .step-title {
|
| 764 |
-
color: var(--color-text-muted);
|
| 765 |
-
}
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
/* Animation for loading state */
|
| 770 |
-
@keyframes pulse-step {
|
| 771 |
-
0%, 100% { opacity: 0.5; }
|
| 772 |
-
50% { opacity: 0.8; }
|
| 773 |
-
}
|
| 774 |
-
|
| 775 |
-
.step-title.shiny-text { color: transparent !important; -webkit-background-clip: text; background-clip: text; animation: shine 1s linear infinite; }
|
| 776 |
-
@keyframes shine { to { background-position: -100% center; } }
|
| 777 |
-
|
| 778 |
-
/* Responsive adjustments */
|
| 779 |
-
@media (max-width: 768px) {
|
| 780 |
-
.process-group {
|
| 781 |
-
padding: var(--spacing-xs) var(--spacing-sm);
|
| 782 |
-
}
|
| 783 |
-
|
| 784 |
-
.process-step-header .step-title {
|
| 785 |
-
font-size: var(--font-size-xs);
|
| 786 |
-
}
|
| 787 |
-
|
| 788 |
-
.process-step-detail-scroll {
|
| 789 |
-
margin-left: 0;
|
| 790 |
-
}
|
| 791 |
-
}
|
| 792 |
-
|
| 793 |
-
/* ===========================================
|
| 794 |
-
Preferences Visibility Controls
|
| 795 |
-
These rules work with preferences-store toggles
|
| 796 |
-
=========================================== */
|
| 797 |
-
|
| 798 |
-
/* .process-step.message-util {
|
| 799 |
-
display: none;
|
| 800 |
-
}
|
| 801 |
-
|
| 802 |
-
.step-kvp.msg-thoughts {
|
| 803 |
-
display: none;
|
| 804 |
-
}
|
| 805 |
-
|
| 806 |
-
.process-step-detail-scroll pre.msg-json {
|
| 807 |
-
display: none;
|
| 808 |
-
} */
|
| 809 |
-
|
| 810 |
-
/* Nested Process Steps (Subordinate Agents) */
|
| 811 |
-
|
| 812 |
-
.process-nested-container {
|
| 813 |
-
display: grid;
|
| 814 |
-
grid-template-rows: 0fr;
|
| 815 |
-
opacity: 0;
|
| 816 |
-
overflow: hidden;
|
| 817 |
-
transition: grid-template-rows 0.25s ease-out, opacity 0.2s ease-out;
|
| 818 |
-
gap: 2px;
|
| 819 |
-
max-width: 100%;
|
| 820 |
-
}
|
| 821 |
-
|
| 822 |
-
/* Only show nested container when parent step is expanded */
|
| 823 |
-
.process-step.expanded > .process-nested-container {
|
| 824 |
-
grid-template-rows: 1fr;
|
| 825 |
-
opacity: 1;
|
| 826 |
-
margin-top: 2px;
|
| 827 |
-
/* Keep overflow hidden to allow nested steps to control their own expansion */
|
| 828 |
-
}
|
| 829 |
-
|
| 830 |
-
/* Inner wrapper to handle grid transition content */
|
| 831 |
-
.process-nested-inner {
|
| 832 |
-
min-height: 0;
|
| 833 |
-
display: flex;
|
| 834 |
-
flex-direction: column;
|
| 835 |
-
gap: 2px;
|
| 836 |
-
max-width: 100%;
|
| 837 |
-
overflow: hidden;
|
| 838 |
-
}
|
| 839 |
-
|
| 840 |
-
/* All nested containers have same indentation (no cascading) */
|
| 841 |
-
.nested-step .process-nested-container {
|
| 842 |
-
margin-left: 0;
|
| 843 |
-
}
|
| 844 |
-
|
| 845 |
-
/* Response content in process steps */
|
| 846 |
-
.process-step-detail-scroll .step-response-content {
|
| 847 |
-
font-size: 0.75rem;
|
| 848 |
-
line-height: 1.6;
|
| 849 |
-
color: var(--color-text);
|
| 850 |
-
opacity: 0.9;
|
| 851 |
-
margin: var(--spacing-xs) 0;
|
| 852 |
-
word-break: break-word;
|
| 853 |
-
overflow-wrap: anywhere;
|
| 854 |
-
}
|
| 855 |
-
|
| 856 |
-
.process-step-detail-scroll .step-response-content p {
|
| 857 |
-
margin: 0.5em 0;
|
| 858 |
-
}
|
| 859 |
-
|
| 860 |
-
.process-step-detail-scroll .step-response-content ul,
|
| 861 |
-
.process-step-detail-scroll .step-response-content ol {
|
| 862 |
-
margin: 0.5em 0;
|
| 863 |
-
padding-left: 1.5em;
|
| 864 |
-
}
|
| 865 |
-
|
| 866 |
-
/* Warning/error content in process steps */
|
| 867 |
-
.process-step-detail-scroll .step-warning-content {
|
| 868 |
-
font-size: 0.72rem;
|
| 869 |
-
line-height: 1.5;
|
| 870 |
-
color: var(--color-warning);
|
| 871 |
-
opacity: 0.9;
|
| 872 |
-
margin: var(--spacing-xs) 0;
|
| 873 |
-
padding: var(--spacing-xs);
|
| 874 |
-
border-left: 2px solid var(--color-warning);
|
| 875 |
-
border-radius: 3px;
|
| 876 |
-
}
|
| 877 |
-
|
| 878 |
-
/* Browser screenshot content in process steps */
|
| 879 |
-
.process-step-detail-scroll .screenshot-img {
|
| 880 |
-
max-width: 100%;
|
| 881 |
-
max-height: 400px;
|
| 882 |
-
border-radius: 4px;
|
| 883 |
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 884 |
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
| 885 |
-
transition: box-shadow 0.2s ease, filter 0.2s ease;
|
| 886 |
-
object-fit: contain;
|
| 887 |
-
}
|
| 888 |
-
|
| 889 |
-
.process-step-detail-scroll .screenshot-img:hover {
|
| 890 |
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
| 891 |
-
filter: brightness(1.02);
|
| 892 |
-
}
|
| 893 |
-
|
| 894 |
-
/* Light mode screenshot border */
|
| 895 |
-
.light-mode .process-step-detail-scroll .screenshot-img {
|
| 896 |
-
border: 1px solid rgba(0, 0, 0, 0.15);
|
| 897 |
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 898 |
-
}
|
| 899 |
-
|
| 900 |
-
.light-mode .process-step-detail-scroll .screenshot-img:hover {
|
| 901 |
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 902 |
-
}
|
| 903 |
-
|
| 904 |
-
/* View Details button in step detail */
|
| 905 |
-
.step-detail-actions {
|
| 906 |
-
display: flex;
|
| 907 |
-
padding-left: 18px;
|
| 908 |
-
min-height: 0; /* Required for grid collapse animation */
|
| 909 |
-
}
|
| 910 |
-
|
| 911 |
-
/* Hide View Details button when step is collapsed */
|
| 912 |
-
.process-step:not(.expanded) > .process-step-detail > .step-detail-actions {
|
| 913 |
-
display: none;
|
| 914 |
-
}
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:41bbbb6cd4077b17682056b616b401c13223c16b38dd46fa0dd37cb729cb3011
|
| 3 |
+
size 21217
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/messages/resize/message-resize-store.js
CHANGED
|
@@ -1,137 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
const model = {
|
| 5 |
-
settings: {},
|
| 6 |
-
|
| 7 |
-
async init() {
|
| 8 |
-
this.settings =
|
| 9 |
-
JSON.parse(localStorage.getItem("messageResizeSettings") || "null") ||
|
| 10 |
-
this._getDefaultSettings();
|
| 11 |
-
this._applyAllSettings();
|
| 12 |
-
},
|
| 13 |
-
|
| 14 |
-
_getDefaultSettings() {
|
| 15 |
-
return {
|
| 16 |
-
"message": { minimized: false, maximized: false },
|
| 17 |
-
"message-agent": { minimized: true, maximized: false },
|
| 18 |
-
"message-agent-response": { minimized: false, maximized: true },
|
| 19 |
-
};
|
| 20 |
-
},
|
| 21 |
-
|
| 22 |
-
getSetting(className) {
|
| 23 |
-
return this.settings[className] || { minimized: false, maximized: false };
|
| 24 |
-
},
|
| 25 |
-
|
| 26 |
-
_getDefaultSetting() {
|
| 27 |
-
return { minimized: false, maximized: false };
|
| 28 |
-
},
|
| 29 |
-
|
| 30 |
-
_setSetting(className, setting) {
|
| 31 |
-
this.settings[className] = setting;
|
| 32 |
-
localStorage.setItem(
|
| 33 |
-
"messageResizeSettings",
|
| 34 |
-
JSON.stringify(this.settings)
|
| 35 |
-
);
|
| 36 |
-
},
|
| 37 |
-
|
| 38 |
-
_applyAllSettings() {
|
| 39 |
-
for (const [className, setting] of Object.entries(this.settings)) {
|
| 40 |
-
this._applySetting(className, setting);
|
| 41 |
-
}
|
| 42 |
-
},
|
| 43 |
-
|
| 44 |
-
async minimizeMessageClass(className, event) {
|
| 45 |
-
const set = this.getSetting(className);
|
| 46 |
-
set.minimized = !set.minimized;
|
| 47 |
-
this._setSetting(className, set);
|
| 48 |
-
this._applySetting(className, set);
|
| 49 |
-
this._applyScroll(event);
|
| 50 |
-
},
|
| 51 |
-
|
| 52 |
-
async maximizeMessageClass(className, event) {
|
| 53 |
-
const set = this.getSetting(className);
|
| 54 |
-
if (set.minimized) return this.minimizeMessageClass(className, event); // if minimized, unminimize first
|
| 55 |
-
set.maximized = !set.maximized;
|
| 56 |
-
this._setSetting(className, set);
|
| 57 |
-
this._applySetting(className, set);
|
| 58 |
-
this._applyScroll(event);
|
| 59 |
-
},
|
| 60 |
-
|
| 61 |
-
_applyScroll(event) {
|
| 62 |
-
if (!event || !event.target) {
|
| 63 |
-
return;
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
// Store the element reference to avoid issues with event being modified
|
| 67 |
-
const targetElement = event.target;
|
| 68 |
-
const clickY = event.clientY;
|
| 69 |
-
|
| 70 |
-
// Use requestAnimationFrame for smoother timing with browser rendering
|
| 71 |
-
// requestAnimationFrame(() => {
|
| 72 |
-
try {
|
| 73 |
-
// Get fresh measurements after potential re-renders
|
| 74 |
-
const rect = targetElement.getBoundingClientRect();
|
| 75 |
-
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
|
| 76 |
-
|
| 77 |
-
// Get chat history element
|
| 78 |
-
const chatHistory = document.getElementById('chat-history');
|
| 79 |
-
if (!chatHistory) {
|
| 80 |
-
return;
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
// Get chat history position
|
| 84 |
-
const chatRect = chatHistory.getBoundingClientRect();
|
| 85 |
-
|
| 86 |
-
// Calculate element's middle position relative to chat history
|
| 87 |
-
const elementHeight = rect.height;
|
| 88 |
-
const elementMiddle = rect.top + (elementHeight / 2);
|
| 89 |
-
const relativeMiddle = elementMiddle - chatRect.top;
|
| 90 |
-
|
| 91 |
-
// Calculate target scroll position
|
| 92 |
-
let scrollTop;
|
| 93 |
-
|
| 94 |
-
if (typeof clickY === 'number') {
|
| 95 |
-
// Calculate based on click position
|
| 96 |
-
const clickRelativeToChat = clickY - chatRect.top;
|
| 97 |
-
// Add current scroll position and adjust to keep element middle at click position
|
| 98 |
-
scrollTop = chatHistory.scrollTop + relativeMiddle - clickRelativeToChat;
|
| 99 |
-
} else {
|
| 100 |
-
// Position element middle at 50% from the top of chat history viewport (center)
|
| 101 |
-
const targetPosition = chatHistory.clientHeight * 0.5;
|
| 102 |
-
scrollTop = chatHistory.scrollTop + relativeMiddle - targetPosition;
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
// Apply scroll with instant behavior
|
| 106 |
-
chatHistory.scrollTo({
|
| 107 |
-
top: scrollTop,
|
| 108 |
-
behavior: "auto"
|
| 109 |
-
});
|
| 110 |
-
} catch (e) {
|
| 111 |
-
// Silent error handling
|
| 112 |
-
}
|
| 113 |
-
// });
|
| 114 |
-
},
|
| 115 |
-
|
| 116 |
-
_applySetting(className, setting) {
|
| 117 |
-
toggleCssProperty(
|
| 118 |
-
`.${className} .message-body`,
|
| 119 |
-
"max-height",
|
| 120 |
-
setting.maximized ? "unset" : "30em"
|
| 121 |
-
);
|
| 122 |
-
toggleCssProperty(
|
| 123 |
-
`.${className} .message-body`,
|
| 124 |
-
"overflow-y",
|
| 125 |
-
setting.maximized ? "hidden" : "auto"
|
| 126 |
-
);
|
| 127 |
-
toggleCssProperty(
|
| 128 |
-
`.${className} .message-body`,
|
| 129 |
-
"display",
|
| 130 |
-
setting.minimized ? "none" : "block"
|
| 131 |
-
);
|
| 132 |
-
},
|
| 133 |
-
};
|
| 134 |
-
|
| 135 |
-
const store = createStore("messageResize", model);
|
| 136 |
-
|
| 137 |
-
export { store };
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3bc738ec8ebc2bc63e2dd68ede24dac25575f5f088a6e7e0a0f5bce139ac237d
|
| 3 |
+
size 4271
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/modals/AGENTS.md
CHANGED
|
@@ -1,34 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
- Own WebUI modal component content loaded through the shared modal stack.
|
| 6 |
-
|
| 7 |
-
## Ownership
|
| 8 |
-
|
| 9 |
-
- Each direct child folder owns one modal workflow and its store.
|
| 10 |
-
- Modal HTML files own body content, titles, scoped styles, and `data-modal-footer` content.
|
| 11 |
-
- Modal store files own modal-local state and cleanup.
|
| 12 |
-
|
| 13 |
-
## Local Contracts
|
| 14 |
-
|
| 15 |
-
- Use `openModal(path)` and `closeModal()` from `/js/modals.js`.
|
| 16 |
-
- Keep footer content marked with `data-modal-footer` when using pinned modal actions.
|
| 17 |
-
- Do not introduce a parallel overlay, backdrop, or teleport modal system.
|
| 18 |
-
|
| 19 |
-
## Work Guidance
|
| 20 |
-
|
| 21 |
-
- Keep modal state cleanup explicit because stores may outlive DOM nodes.
|
| 22 |
-
- Preserve stacked modal, Escape, click-outside, scroll, and footer behavior.
|
| 23 |
-
|
| 24 |
-
## Verification
|
| 25 |
-
|
| 26 |
-
- Smoke-test open, close, Escape, click-outside, scrolling, stacked modals, and pinned footers after modal changes.
|
| 27 |
-
|
| 28 |
-
## Child DOX Index
|
| 29 |
-
|
| 30 |
-
Direct child DOX files:
|
| 31 |
-
|
| 32 |
-
| Child | Scope |
|
| 33 |
-
| --- | --- |
|
| 34 |
-
| [file-browser/AGENTS.md](file-browser/AGENTS.md) | File browser modal and right-canvas Files surface workflow. |
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7bf42ea967b7a8890f9db0e8cd5c4c144f231b7ab8c4b890c03b229fffb42b23
|
| 3 |
+
size 1086
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
webui/components/modals/context/context-store.js
CHANGED
|
@@ -1,132 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
// State
|
| 5 |
-
isLoading: false,
|
| 6 |
-
contextData: null,
|
| 7 |
-
tokenCount: 0,
|
| 8 |
-
error: null,
|
| 9 |
-
editor: null,
|
| 10 |
-
closePromise: null,
|
| 11 |
-
|
| 12 |
-
// Open Context Window modal
|
| 13 |
-
async open() {
|
| 14 |
-
if (this.isLoading) return; // Prevent double-open
|
| 15 |
-
|
| 16 |
-
this.isLoading = true;
|
| 17 |
-
this.error = null;
|
| 18 |
-
this.contextData = null;
|
| 19 |
-
this.tokenCount = 0;
|
| 20 |
-
|
| 21 |
-
try {
|
| 22 |
-
// Open modal FIRST (immediate UI feedback, but DON'T await)
|
| 23 |
-
this.closePromise = window.openModal('modals/context/context.html');
|
| 24 |
-
|
| 25 |
-
// Setup cleanup on modal close
|
| 26 |
-
if (this.closePromise && typeof this.closePromise.then === 'function') {
|
| 27 |
-
this.closePromise.then(() => {
|
| 28 |
-
this.destroy();
|
| 29 |
-
});
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
this.updateModalTitle(); // Set initial "loading" title
|
| 33 |
-
|
| 34 |
-
// Fetch data from backend
|
| 35 |
-
const contextId = window.getContext();
|
| 36 |
-
const response = await window.sendJsonData('/ctx_window_get', {
|
| 37 |
-
context: contextId,
|
| 38 |
-
});
|
| 39 |
-
|
| 40 |
-
// Update state with data
|
| 41 |
-
this.contextData = response.content;
|
| 42 |
-
this.tokenCount = response.tokens || 0;
|
| 43 |
-
this.isLoading = false;
|
| 44 |
-
this.updateModalTitle(); // Update with token count
|
| 45 |
-
|
| 46 |
-
// Initialize ACE editor
|
| 47 |
-
this.scheduleEditorInit();
|
| 48 |
-
|
| 49 |
-
} catch (error) {
|
| 50 |
-
console.error("Context fetch error:", error);
|
| 51 |
-
this.error = error?.message || "Failed to load context window";
|
| 52 |
-
this.isLoading = false;
|
| 53 |
-
this.updateModalTitle(); // Show error in title
|
| 54 |
-
}
|
| 55 |
-
},
|
| 56 |
-
|
| 57 |
-
scheduleEditorInit() {
|
| 58 |
-
// Use double requestAnimationFrame to ensure DOM is ready
|
| 59 |
-
window.requestAnimationFrame(() => {
|
| 60 |
-
if (this.isLoading || this.error) return;
|
| 61 |
-
window.requestAnimationFrame(() => this.initEditor());
|
| 62 |
-
});
|
| 63 |
-
},
|
| 64 |
-
|
| 65 |
-
initEditor() {
|
| 66 |
-
const container = document.getElementById("context-viewer-container");
|
| 67 |
-
if (!container) {
|
| 68 |
-
console.warn("Context container not found, deferring editor init");
|
| 69 |
-
return;
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
// Destroy old instance if exists
|
| 73 |
-
if (this.editor?.destroy) {
|
| 74 |
-
this.editor.destroy();
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
// Check if ACE is available
|
| 78 |
-
if (!window.ace?.edit) {
|
| 79 |
-
console.error("ACE editor not available");
|
| 80 |
-
this.error = "Editor library not loaded";
|
| 81 |
-
return;
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
const editorInstance = window.ace.edit("context-viewer-container");
|
| 85 |
-
if (!editorInstance) {
|
| 86 |
-
console.error("Failed to create ACE editor instance");
|
| 87 |
-
return;
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
this.editor = editorInstance;
|
| 91 |
-
|
| 92 |
-
// Configure theme based on dark mode (legacy parity: != "false")
|
| 93 |
-
const darkMode = window.localStorage?.getItem("darkMode");
|
| 94 |
-
const theme = darkMode !== "false" ? "ace/theme/github_dark" : "ace/theme/tomorrow";
|
| 95 |
-
|
| 96 |
-
this.editor.setTheme(theme);
|
| 97 |
-
this.editor.session.setMode("ace/mode/markdown");
|
| 98 |
-
this.editor.setValue(this.contextData, -1); // -1 moves cursor to start
|
| 99 |
-
this.editor.setReadOnly(true);
|
| 100 |
-
this.editor.clearSelection();
|
| 101 |
-
},
|
| 102 |
-
|
| 103 |
-
updateModalTitle() {
|
| 104 |
-
window.requestAnimationFrame(() => {
|
| 105 |
-
const modalTitles = document.querySelectorAll(".modal.show .modal-title");
|
| 106 |
-
if (!modalTitles.length) return;
|
| 107 |
-
|
| 108 |
-
// Get the last (topmost) modal title
|
| 109 |
-
const title = modalTitles[modalTitles.length - 1];
|
| 110 |
-
if (!title) return;
|
| 111 |
-
|
| 112 |
-
if (this.error) {
|
| 113 |
-
title.textContent = "Context Window – Error";
|
| 114 |
-
} else if (this.isLoading) {
|
| 115 |
-
title.textContent = "Context Window (loading…)";
|
| 116 |
-
} else {
|
| 117 |
-
title.textContent = `Context Window ~${this.tokenCount} tokens`;
|
| 118 |
-
}
|
| 119 |
-
});
|
| 120 |
-
},
|
| 121 |
-
|
| 122 |
-
// Optional: cleanup method for lifecycle management
|
| 123 |
-
destroy() {
|
| 124 |
-
if (this.editor?.destroy) {
|
| 125 |
-
this.editor.destroy();
|
| 126 |
-
}
|
| 127 |
-
this.editor = null;
|
| 128 |
-
},
|
| 129 |
-
};
|
| 130 |
-
|
| 131 |
-
export const store = createStore("context", model);
|
| 132 |
-
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:03662f99e944177b6d5227f6a7c2810bd8803aad119e8891690bfafe2ef1e495
|
| 3 |
+
size 3809
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|