minor fixes and improvments
This commit is contained in:
@@ -190,7 +190,14 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@default {
|
@default {
|
||||||
<p class="mb-0">{{ entry.text }}</p>
|
@if (entry.showSpinner) {
|
||||||
|
<div class="bubble-system-status">
|
||||||
|
<span class="bubble-spinner" aria-hidden="true"></span>
|
||||||
|
<p class="mb-0">{{ entry.text }}</p>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<p class="mb-0">{{ entry.text }}</p>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -16,6 +16,11 @@
|
|||||||
box-shadow: 0 20px 60px var(--shadow-color);
|
box-shadow: 0 20px 60px var(--shadow-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-page {
|
||||||
|
width: min(100%, 95vw);
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.back-link {
|
.back-link {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -297,6 +302,23 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bubble-system-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-spinner {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border: 0.15rem solid currentColor;
|
||||||
|
border-right-color: transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
opacity: 0.8;
|
||||||
|
animation: bubble-spin 700ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.composer {
|
.composer {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
@@ -467,6 +489,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes bubble-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.chat-layout {
|
.chat-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -116,7 +116,17 @@ export class ChatPageComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.session.requestGeneratedImage(peerId, this.messageText);
|
const requested = await this.session.requestGeneratedImage(peerId, this.messageText);
|
||||||
|
|
||||||
|
if (!requested) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messageText = '';
|
||||||
|
this.handleMessageTextChange('');
|
||||||
|
this.emojiPickerOpen.set(false);
|
||||||
|
this.composerSelectionStart = 0;
|
||||||
|
this.composerSelectionEnd = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleComposerEnter(event: Event): void {
|
handleComposerEnter(event: Event): void {
|
||||||
|
|||||||
@@ -157,7 +157,10 @@ export class ChatSessionService {
|
|||||||
private readonly outgoingTypingIdleTimeouts = new Map<string, number>();
|
private readonly outgoingTypingIdleTimeouts = new Map<string, number>();
|
||||||
private readonly outgoingTypingStates = new Map<string, { active: boolean; lastSentAt: number }>();
|
private readonly outgoingTypingStates = new Map<string, { active: boolean; lastSentAt: number }>();
|
||||||
private readonly messageStoreOperations = new Map<string, Promise<void>>();
|
private readonly messageStoreOperations = new Map<string, Promise<void>>();
|
||||||
private readonly pendingImageGenerationRequests = new Map<string, { peerId: string; prompt: string }>();
|
private readonly pendingImageGenerationRequests = new Map<
|
||||||
|
string,
|
||||||
|
{ peerId: string; prompt: string; waitMessageId: string }
|
||||||
|
>();
|
||||||
private readonly remoteVideoStreams = signal<Array<{ peerId: string; stream: MediaStream }>>([]);
|
private readonly remoteVideoStreams = signal<Array<{ peerId: string; stream: MediaStream }>>([]);
|
||||||
private readonly activeCameraPeerId = signal<string | null>(null);
|
private readonly activeCameraPeerId = signal<string | null>(null);
|
||||||
private sessionKeepaliveIntervalId: number | null = null;
|
private sessionKeepaliveIntervalId: number | null = null;
|
||||||
@@ -772,33 +775,39 @@ export class ChatSessionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestGeneratedImage(peerId: string, prompt: string): Promise<void> {
|
async requestGeneratedImage(peerId: string, prompt: string): Promise<boolean> {
|
||||||
const trimmedPrompt = prompt.trim();
|
const trimmedPrompt = prompt.trim();
|
||||||
|
|
||||||
if (!trimmedPrompt) {
|
if (!trimmedPrompt) {
|
||||||
this.error.set('Enter a text prompt before requesting an image.');
|
this.error.set('Enter a text prompt before requesting an image.');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
|
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
|
||||||
this.error.set('You must be connected to signaling before requesting an image.');
|
this.error.set('You must be connected to signaling before requesting an image.');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestId = crypto.randomUUID();
|
const requestId = crypto.randomUUID();
|
||||||
|
const waitMessageId = this.addSystemMessage(peerId, 'Generating image from prompt.', {
|
||||||
|
persistent: true,
|
||||||
|
showSpinner: true,
|
||||||
|
});
|
||||||
|
|
||||||
this.pendingImageGenerationRequests.set(requestId, {
|
this.pendingImageGenerationRequests.set(requestId, {
|
||||||
peerId,
|
peerId,
|
||||||
prompt: trimmedPrompt,
|
prompt: trimmedPrompt,
|
||||||
|
waitMessageId,
|
||||||
});
|
});
|
||||||
this.error.set(null);
|
this.error.set(null);
|
||||||
this.addSystemMessage(peerId, 'Generating image from prompt.');
|
|
||||||
this.websocket.send(JSON.stringify({
|
this.websocket.send(JSON.stringify({
|
||||||
type: 'image-generation',
|
type: 'image-generation',
|
||||||
requestId,
|
requestId,
|
||||||
peerId,
|
peerId,
|
||||||
prompt: trimmedPrompt,
|
prompt: trimmedPrompt,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadAccessKeys(): Promise<void> {
|
private async loadAccessKeys(): Promise<void> {
|
||||||
@@ -972,6 +981,10 @@ export class ChatSessionService {
|
|||||||
fileMimeType: event.mimeType,
|
fileMimeType: event.mimeType,
|
||||||
downloadUrl: URL.createObjectURL(imageBlob),
|
downloadUrl: URL.createObjectURL(imageBlob),
|
||||||
}, imageBlob);
|
}, imageBlob);
|
||||||
|
|
||||||
|
if (pendingRequest) {
|
||||||
|
this.removeMessageById(pendingRequest.waitMessageId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleGeneratedImageError(event: Extract<ServerEvent, { type: 'image-generation-error' }>): void {
|
private handleGeneratedImageError(event: Extract<ServerEvent, { type: 'image-generation-error' }>): void {
|
||||||
@@ -979,6 +992,7 @@ export class ChatSessionService {
|
|||||||
|
|
||||||
if (pendingRequest) {
|
if (pendingRequest) {
|
||||||
this.pendingImageGenerationRequests.delete(event.requestId);
|
this.pendingImageGenerationRequests.delete(event.requestId);
|
||||||
|
this.removeMessageById(pendingRequest.waitMessageId);
|
||||||
this.addSystemMessage(pendingRequest.peerId, 'Image generation failed.');
|
this.addSystemMessage(pendingRequest.peerId, 'Image generation failed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1628,7 +1642,11 @@ export class ChatSessionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addSystemMessage(peerId: string, text: string): void {
|
private addSystemMessage(
|
||||||
|
peerId: string,
|
||||||
|
text: string,
|
||||||
|
options?: { persistent?: boolean; showSpinner?: boolean },
|
||||||
|
): string {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
this.pushMessage({
|
this.pushMessage({
|
||||||
@@ -1638,13 +1656,18 @@ export class ChatSessionService {
|
|||||||
kind: 'system',
|
kind: 'system',
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
authorLabel: 'System',
|
authorLabel: 'System',
|
||||||
|
showSpinner: options?.showSpinner,
|
||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeoutId = window.setTimeout(() => {
|
if (!options?.persistent) {
|
||||||
this.removeMessageById(id);
|
const timeoutId = window.setTimeout(() => {
|
||||||
}, ChatSessionService.systemMessageLifetimeMs);
|
this.removeMessageById(id);
|
||||||
this.systemMessageTimeouts.set(id, timeoutId);
|
}, ChatSessionService.systemMessageLifetimeMs);
|
||||||
|
this.systemMessageTimeouts.set(id, timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isPolitePeer(peerId: string): boolean {
|
private isPolitePeer(peerId: string): boolean {
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export interface ChatEntry {
|
|||||||
kind: 'text' | 'json' | 'file' | 'system';
|
kind: 'text' | 'json' | 'file' | 'system';
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
authorLabel: string;
|
authorLabel: string;
|
||||||
|
showSpinner?: boolean;
|
||||||
text?: string;
|
text?: string;
|
||||||
payload?: unknown;
|
payload?: unknown;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user