minor fixes and improvments

This commit is contained in:
2026-03-10 03:27:11 +01:00
parent 61612b52d3
commit 506a824401
5 changed files with 81 additions and 12 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;