diff --git a/source/components/rojo-chat-panel/rojo-chat-panel.css b/source/components/rojo-chat-panel/rojo-chat-panel.css index 4be3eb5..a667b1e 100644 --- a/source/components/rojo-chat-panel/rojo-chat-panel.css +++ b/source/components/rojo-chat-panel/rojo-chat-panel.css @@ -2,6 +2,8 @@ rojo-chat-panel { display: flex; flex-direction: column; height: 100%; + min-height: 0; + overflow: hidden; background: #0f1117; } @@ -72,6 +74,7 @@ rojo-chat-panel { .rcp-history { flex: 1; + min-height: 0; overflow-y: auto; padding: 14px 12px; display: flex; diff --git a/source/components/rojo-chat-panel/rojo-chat-panel.ts b/source/components/rojo-chat-panel/rojo-chat-panel.ts index f979c44..bd97a05 100644 --- a/source/components/rojo-chat-panel/rojo-chat-panel.ts +++ b/source/components/rojo-chat-panel/rojo-chat-panel.ts @@ -39,12 +39,13 @@ function parentDir( filePath: string ): string class RojoChatPanel extends HTMLElement { - _initialized = false; - _conversationId = ''; - _sending = false; + _initialized = false; + _conversationId = ''; + _sending = false; _md: { render: ( s: string ) => string } | null = null; _rojos: RojoEntry[] = []; - _selectedPath = ''; + _selectedPath = ''; + _thinkingInterval: ReturnType | null = null; connectedCallback(): void { @@ -85,6 +86,11 @@ class RojoChatPanel extends HTMLElement if ( e.key === 'Enter' && !e.shiftKey ) { e.preventDefault(); this._send(); } } ); + inputText.addEventListener( 'focus', () => + { + setTimeout( () => inputText.scrollIntoView( { behavior: 'smooth', block: 'nearest' } ), 300 ); + } ); + picker.addEventListener( 'change', () => { this._selectedPath = picker.value; @@ -164,6 +170,25 @@ class RojoChatPanel extends HTMLElement catch {} } + _startThinking( bubble: HTMLElement ): void + { + const frames = [ '.', '..', '...', 'thinking', '.', '..', '...', 'imagining' ]; + let i = 0; + bubble.textContent = frames[ 0 ]; + this._thinkingInterval = setInterval( () => + { + i = ( i + 1 ) % frames.length; + bubble.textContent = frames[ i ]; + }, 250 ); + } + + _stopThinking(): void + { + if ( this._thinkingInterval === null ) return; + clearInterval( this._thinkingInterval ); + this._thinkingInterval = null; + } + async _send(): Promise { if ( this._sending ) return; @@ -187,10 +212,11 @@ class RojoChatPanel extends HTMLElement const assistantBubble = document.createElement( 'div' ); assistantBubble.className = 'rcp-assistant-bubble'; - assistantBubble.textContent = '…'; history.appendChild( assistantBubble ); history.scrollTop = history.scrollHeight; + this._startThinking( assistantBubble ); + try { const body: Record = { id: this._conversationId, message: text }; @@ -231,6 +257,11 @@ class RojoChatPanel extends HTMLElement const msg = JSON.parse( trimmed ) as { type: string; text?: string }; if ( msg.type === 'CHAT' && msg.text ) { + if ( !markdown ) + { + this._stopThinking(); + assistantBubble.innerHTML = ''; + } await typeText( msg.text, piece => { markdown += piece; @@ -243,10 +274,11 @@ class RojoChatPanel extends HTMLElement } } - if ( !markdown ) assistantBubble.textContent = '(no response)'; + if ( !markdown ) { this._stopThinking(); assistantBubble.textContent = '(no response)'; } } catch ( err ) { + this._stopThinking(); assistantBubble.textContent = '(error: could not reach Rojo)'; console.error( err ); }