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

View File

@@ -16,6 +16,11 @@
box-shadow: 0 20px 60px var(--shadow-color);
}
.chat-page {
width: min(100%, 95vw);
margin-inline: auto;
}
.back-link {
color: var(--link-color);
text-decoration: none;
@@ -297,6 +302,23 @@
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 {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
@@ -467,6 +489,12 @@
}
}
@keyframes bubble-spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 767.98px) {
.chat-layout {
grid-template-columns: 1fr;

View File

@@ -116,7 +116,17 @@ export class ChatPageComponent {
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 {

View File

@@ -157,7 +157,10 @@ export class ChatSessionService {
private readonly outgoingTypingIdleTimeouts = new Map<string, number>();
private readonly outgoingTypingStates = new Map<string, { active: boolean; lastSentAt: number }>();
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 activeCameraPeerId = signal<string | 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();
if (!trimmedPrompt) {
this.error.set('Enter a text prompt before requesting an image.');
return;
return false;
}
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
this.error.set('You must be connected to signaling before requesting an image.');
return;
return false;
}
const requestId = crypto.randomUUID();
const waitMessageId = this.addSystemMessage(peerId, 'Generating image from prompt.', {
persistent: true,
showSpinner: true,
});
this.pendingImageGenerationRequests.set(requestId, {
peerId,
prompt: trimmedPrompt,
waitMessageId,
});
this.error.set(null);
this.addSystemMessage(peerId, 'Generating image from prompt.');
this.websocket.send(JSON.stringify({
type: 'image-generation',
requestId,
peerId,
prompt: trimmedPrompt,
}));
return true;
}
private async loadAccessKeys(): Promise<void> {
@@ -972,6 +981,10 @@ export class ChatSessionService {
fileMimeType: event.mimeType,
downloadUrl: URL.createObjectURL(imageBlob),
}, imageBlob);
if (pendingRequest) {
this.removeMessageById(pendingRequest.waitMessageId);
}
}
private handleGeneratedImageError(event: Extract<ServerEvent, { type: 'image-generation-error' }>): void {
@@ -979,6 +992,7 @@ export class ChatSessionService {
if (pendingRequest) {
this.pendingImageGenerationRequests.delete(event.requestId);
this.removeMessageById(pendingRequest.waitMessageId);
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();
this.pushMessage({
@@ -1638,13 +1656,18 @@ export class ChatSessionService {
kind: 'system',
createdAt: Date.now(),
authorLabel: 'System',
showSpinner: options?.showSpinner,
text,
});
const timeoutId = window.setTimeout(() => {
this.removeMessageById(id);
}, ChatSessionService.systemMessageLifetimeMs);
this.systemMessageTimeouts.set(id, timeoutId);
if (!options?.persistent) {
const timeoutId = window.setTimeout(() => {
this.removeMessageById(id);
}, ChatSessionService.systemMessageLifetimeMs);
this.systemMessageTimeouts.set(id, timeoutId);
}
return id;
}
private isPolitePeer(peerId: string): boolean {

View File

@@ -97,6 +97,7 @@ export interface ChatEntry {
kind: 'text' | 'json' | 'file' | 'system';
createdAt: number;
authorLabel: string;
showSpinner?: boolean;
text?: string;
payload?: unknown;
fileName?: string;