Skip to content

Commit 6c98f0d

Browse files
committed
feat: implement chat assistant as pure vaadin component
1 parent 155b354 commit 6c98f0d

12 files changed

Lines changed: 1100 additions & 204 deletions

File tree

src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java

Lines changed: 482 additions & 196 deletions
Large diffs are not rendered by default.

src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
@SuppressWarnings("serial")
3939
@JsModule("@vaadin/message-list/src/vaadin-message.js")
4040
@Tag("vaadin-message")
41-
@CssImport("./styles/chat-message-styles.css")
41+
@CssImport("./styles/fc-chat-message-styles.css")
4242
@EqualsAndHashCode(callSuper=false)
4343
public class ChatMessage<T extends Message> extends Component implements HasComponents {
4444

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*-
2+
* #%L
3+
* Chat Assistant Add-on
4+
* %%
5+
* Copyright (C) 2023 - 2026 Flowing Code
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
window.fcChatAssistantMovement = (root, item, container, fab, marginRaw, sensitivityRaw) => {
21+
// Prevent duplicate initialization
22+
const guard = `__fcChatAssistantMovement`;
23+
if (item[guard]) {
24+
return;
25+
}
26+
item[guard] = true;
27+
const margin = parseFloat(marginRaw);
28+
const sensitivity = parseFloat(sensitivityRaw);
29+
const sizeTransition = 'transform 0.2s ease';
30+
const snapTransition = 'all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
31+
const position = { x: margin, y: margin };
32+
const initialPosition = { x: margin, y: margin };
33+
34+
let screenWidth = window.innerWidth;
35+
let screenHeight = window.innerHeight;
36+
let isDragging = false;
37+
38+
item.style.transition = sizeTransition;
39+
40+
window.addEventListener("resize", (_) => {
41+
screenWidth = window.innerWidth;
42+
screenHeight = window.innerHeight;
43+
44+
// Adjust container dimensions to fit within screen bounds
45+
if (container) {
46+
const rect = container.getBoundingClientRect();
47+
let widthAdjustment = 0;
48+
let heightAdjustment = 0;
49+
if (rect.left < 0) {
50+
widthAdjustment = Math.abs(rect.left);
51+
}
52+
if (rect.right > screenWidth) {
53+
widthAdjustment = Math.max(widthAdjustment, rect.right - screenWidth);
54+
}
55+
if (rect.top < 0) {
56+
heightAdjustment = Math.abs(rect.top);
57+
}
58+
if (rect.bottom > screenHeight) {
59+
heightAdjustment = Math.max(heightAdjustment, rect.bottom - screenHeight);
60+
}
61+
// Apply adjustments
62+
if (widthAdjustment > 0) {
63+
const minWidth = parseFloat(container.style.minWidth) || 0;
64+
const newWidth = Math.max(minWidth, rect.width - widthAdjustment);
65+
container.style.width = newWidth + 'px';
66+
}
67+
if (heightAdjustment > 0) {
68+
const minHeight = parseFloat(container.style.minHeight) || 0;
69+
const newHeight = Math.max(minHeight, rect.height - heightAdjustment);
70+
container.style.height = newHeight + 'px';
71+
}
72+
}
73+
74+
// Reposition the item to ensure it stays within the new screen bounds
75+
snapToBoundary();
76+
});
77+
78+
// Update FAB position
79+
function updatePosition() {
80+
item.style.right = position.x + 'px';
81+
item.style.bottom = position.y + 'px';
82+
}
83+
84+
// Ensure the item stays within the screen and margin bounds
85+
function snapToBoundary() {
86+
// Get current dimensions to account for transforms
87+
const itemRect = fab.getBoundingClientRect();
88+
89+
const xMax = Math.max(margin, screenWidth - itemRect.width - margin);
90+
const yMax = Math.max(margin, screenHeight - itemRect.height - margin);
91+
const x = position.x;
92+
const y = position.y;
93+
if (x < margin) position.x = margin;
94+
if (x > xMax) position.x = xMax;
95+
if (y < margin) position.y = margin;
96+
if (y > yMax) position.y = yMax;
97+
updatePosition();
98+
}
99+
100+
// Determine if the pointer event should be treated as a click (no significant movement, based on sensitivity threshold)
101+
function isClickOnlyEvent() {
102+
const dx = Math.abs(position.x - initialPosition.x);
103+
const dy = Math.abs(position.y - initialPosition.y);
104+
return dx < sensitivity && dy < sensitivity;
105+
}
106+
107+
item.addEventListener('pointerdown', (e) => {
108+
isDragging = true;
109+
fab.classList.add('dragging');
110+
item.setPointerCapture(e.pointerId);
111+
item.style.transition = sizeTransition;
112+
initialPosition.x = position.x;
113+
initialPosition.y = position.y;
114+
});
115+
116+
item.addEventListener('pointermove', (e) => {
117+
if (!isDragging) return;
118+
const itemRect = fab.getBoundingClientRect();
119+
// Calculate position from right and bottom edges
120+
position.x = screenWidth - e.clientX - (itemRect.width / 2);
121+
position.y = screenHeight - e.clientY - (itemRect.height / 2);
122+
123+
updatePosition();
124+
});
125+
126+
item.addEventListener('pointerup', (e) => stopDragging(e));
127+
item.addEventListener('pointerleave', (e) => stopDragging(e));
128+
item.addEventListener('pointercancel', (e) => stopDragging(e));
129+
130+
function stopDragging(e) {
131+
if(isDragging) {
132+
isDragging = false;
133+
item.style.transition = snapTransition + ', ' + sizeTransition;
134+
fab.classList.remove('dragging');
135+
item.releasePointerCapture(e.pointerId);
136+
snapToBoundary();
137+
if (isClickOnlyEvent()) {
138+
root.$server?.onClick();
139+
}
140+
}
141+
}
142+
143+
updatePosition();
144+
};

0 commit comments

Comments
 (0)