minor fixes and improvments
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface ChatEntry {
|
||||
kind: 'text' | 'json' | 'file' | 'system';
|
||||
createdAt: number;
|
||||
authorLabel: string;
|
||||
showSpinner?: boolean;
|
||||
text?: string;
|
||||
payload?: unknown;
|
||||
fileName?: string;
|
||||
|
||||
Reference in New Issue
Block a user