Skip to content

Commit 97d99b2

Browse files
committed
added and improved tooling
1 parent 9f28092 commit 97d99b2

15 files changed

+513
-33
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { AiChatMessage } from '../models/index.js'
2+
3+
export type AiChatMessageAdded = {
4+
type: 'ai-chat-message-added'
5+
aiChatMessage: AiChatMessage
6+
}

common/src/websocket/websocket-message.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AddMovieMessage } from './add-movie-message.js'
2+
import type { AiChatMessageAdded } from './ai-chat-message-added.js'
23
import type { ChatAddedMessage } from './chat-added.js'
34
import type { ChatMessageAddedMessage } from './chat-message-added.js'
45
import type { ChatMessageRemovedMessage } from './chat-message-removed.js'
@@ -10,6 +11,7 @@ import type { DeviceDisconnectedMessage } from './device-disconnected-message.js
1011
import type { FileChangeMessage } from './file-change-message.js'
1112

1213
export type WebsocketMessage =
14+
| AiChatMessageAdded
1315
| AddMovieMessage
1416
| DeviceConnectedMessage
1517
| DeviceDisconnectedMessage

frontend/src/pages/ai/ai-chat-input.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createComponent, Shade } from '@furystack/shades'
2-
import { Form } from '@furystack/shades-common-components'
2+
import { Button, Form, Input } from '@furystack/shades-common-components'
33
import { SessionService } from '../../services/session.js'
44
import { AiChatMessageService } from './ai-chat-message-service.js'
55
import { AiChatService } from './ai-chat-service.js'
@@ -47,9 +47,18 @@ export const AiChatInput = Shade<{ selectedChatId: string }>({
4747
(formData as { message: string }).message.trim() !== ''
4848
)
4949
}}
50+
style={{ display: 'flex', flexDirection: 'row', width: '100%' }}
5051
>
51-
<input type="text" name="message" placeholder="Type your message..." />
52-
<button type="submit">Send</button>
52+
<Input
53+
type="text"
54+
name="message"
55+
placeholder="Type your message..."
56+
style={{
57+
flexGrow: '1',
58+
marginRight: '8px',
59+
}}
60+
/>
61+
<Button type="submit">Send</Button>
5362
</Form>
5463
)
5564
},

frontend/src/pages/ai/ai-chat-list.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { AiChat } from 'common'
44
import { ErrorDisplay } from '../../components/error-display.js'
55
import { AiChatService } from './ai-chat-service.js'
66

7-
export const AiChatList = Shade<{ onSelect: (chat: AiChat) => void }>({
7+
export const AiChatList = Shade<{ selectedChatId?: string; onSelect: (chat: AiChat) => void }>({
88
shadowDomName: 'pi-rat-ai-chat-list',
99
render: ({ injector, useObservable, props }) => {
1010
const aiChatService = injector.getInstance(AiChatService)
@@ -25,15 +25,26 @@ export const AiChatList = Shade<{ onSelect: (chat: AiChat) => void }>({
2525
return (
2626
<Paper style={{ padding: '16px', height: 'calc(100% - 48px)' }}>
2727
<h3>Chats</h3>
28-
{chatList.value.result.entries.map((chat) => (
29-
<Button
30-
onclick={() => {
31-
props.onSelect(chat)
32-
}}
33-
>
34-
{chat.name}
35-
</Button>
36-
))}
28+
<div
29+
style={{
30+
display: 'flex',
31+
flexDirection: 'column',
32+
height: '100%',
33+
overflowY: 'auto',
34+
padding: '8px',
35+
}}
36+
>
37+
{chatList.value.result.entries.map((chat) => (
38+
<Button
39+
variant={props.selectedChatId === chat.id ? 'contained' : undefined}
40+
onclick={() => {
41+
props.onSelect(chat)
42+
}}
43+
>
44+
{chat.name}
45+
</Button>
46+
))}
47+
</div>
3748
</Paper>
3849
)
3950
},

frontend/src/pages/ai/ai-chat-message-list.tsx

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { createComponent, Shade } from '@furystack/shades'
2+
import { Paper } from '@furystack/shades-common-components'
23
import { marked } from 'marked'
34
import { ErrorDisplay } from '../../components/error-display.js'
5+
import { WebsocketNotificationsService } from '../../services/websocket-events.js'
46
import { AiChatMessageService } from './ai-chat-message-service.js'
57

68
export const AiChatMessageList = Shade<{
@@ -14,7 +16,7 @@ export const AiChatMessageList = Shade<{
1416
height: 'calc(100% - 32px)',
1517
overflowY: 'auto',
1618
},
17-
render: ({ useObservable, injector, props }) => {
19+
render: ({ useObservable, injector, props, element, useDisposable }) => {
1820
const { selectedChatId } = props
1921
const aiChatService = injector.getInstance(AiChatMessageService)
2022

@@ -27,6 +29,31 @@ export const AiChatMessageList = Shade<{
2729
}),
2830
)
2931

32+
const wsService = injector.getInstance(WebsocketNotificationsService)
33+
34+
useDisposable('webSocketSubscription', () =>
35+
wsService.subscribe('onMessage', async (message) => {
36+
if (message.type !== 'ai-chat-message-added') {
37+
return
38+
}
39+
if (message.aiChatMessage.aiChatId !== selectedChatId) {
40+
return
41+
}
42+
scrollToBottom('smooth')
43+
}),
44+
)
45+
46+
const scrollToBottom = (behavior: ScrollBehavior = 'instant') => {
47+
setTimeout(() => {
48+
requestAnimationFrame(() => {
49+
element.scrollTo({
50+
top: element.scrollHeight,
51+
behavior,
52+
})
53+
})
54+
}, 1)
55+
}
56+
3057
if (!messages?.value) {
3158
return <div>Loading messages...</div>
3259
}
@@ -43,21 +70,18 @@ export const AiChatMessageList = Shade<{
4370
})
4471
}
4572

73+
scrollToBottom()
74+
4675
return (
4776
<>
4877
{messages.value.result.entries.map((message) => {
4978
const innerHTML = marked.parse(message.content)
5079

5180
return (
52-
<div
53-
style={{
54-
padding: '8px',
55-
borderBottom: '1px solid #ccc',
56-
}}
57-
>
81+
<Paper elevation={1} style={{ padding: '8px', margin: '4px 0', filter: 'brightness(0.9)' }}>
5882
<strong>{message.role}</strong>
5983
<div style={{ marginTop: '4px' }} innerHTML={innerHTML as string} />
60-
</div>
84+
</Paper>
6185
)
6286
})}
6387
</>

frontend/src/pages/ai/ai-chat-message-service.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { Cache } from '@furystack/cache'
1+
import { Cache, CannotObsoleteUnloadedError } from '@furystack/cache'
22
import type { FindOptions } from '@furystack/core'
33
import { Injectable, Injected } from '@furystack/inject'
44
import type { AiChatMessage } from 'common'
55
import { AiApiClient } from '../../services/api-clients/ai-api-client.js'
6+
import { WebsocketNotificationsService } from '../../services/websocket-events.js'
67

78
@Injectable({ lifetime: 'singleton' })
89
export class AiChatMessageService {
10+
@Injected(WebsocketNotificationsService)
11+
declare private websocketService: WebsocketNotificationsService
12+
913
@Injected(AiApiClient)
1014
declare private aiApi: AiApiClient
1115
private cache = new Cache({
@@ -33,8 +37,37 @@ export class AiChatMessageService {
3337
body: chat,
3438
})
3539

36-
this.cache.obsoleteRange(() => true)
40+
try {
41+
this.cache.obsoleteRange(() => true)
42+
} catch (error) {
43+
if (error instanceof CannotObsoleteUnloadedError) {
44+
// The cache is not loaded yet, we can ignore this error
45+
return result
46+
}
47+
throw error
48+
}
3749

3850
return result
3951
}
52+
53+
public async init() {
54+
this.websocketService.subscribe('onMessage', async (message) => {
55+
if (message.type !== 'ai-chat-message-added') {
56+
return
57+
}
58+
try {
59+
this.cache.obsoleteRange((value) => {
60+
return value.result.entries.some((entry) => {
61+
return entry.aiChatId === message.aiChatMessage.aiChatId
62+
})
63+
})
64+
} catch (error) {
65+
if (error instanceof CannotObsoleteUnloadedError) {
66+
// The cache is not loaded yet, we can ignore this error
67+
return
68+
}
69+
throw error
70+
}
71+
})
72+
}
4073
}

frontend/src/pages/ai/ai-page.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,16 @@ export const AiPage = Shade({
2020
const [selectedChatId, setSelectedChatId] = useSearchState('selectedChat', '')
2121

2222
return (
23-
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: '1', width: '100%', height: '100%' }}>
23+
<div
24+
style={{
25+
display: 'flex',
26+
flexDirection: 'column',
27+
flexGrow: '1',
28+
width: '100%',
29+
height: '100%',
30+
overflow: 'hidden',
31+
}}
32+
>
2433
<Paper style={{ display: 'flex', flexDirection: 'row', width: 'calc(100% - 48px)', flexGrow: '0' }}>
2534
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: '1', flex: '5' }}>
2635
<h1>AI Chats</h1>
@@ -32,11 +41,15 @@ export const AiPage = Shade({
3241
display: 'flex',
3342
flexDirection: 'row',
3443
width: '100%',
35-
height: 'calc(100% - 48px)',
44+
overflow: 'hidden',
3645
flexGrow: '1',
3746
}}
3847
>
39-
<AiChatList style={{ height: '100%', minWidth: '250px' }} onSelect={({ id }) => setSelectedChatId(id)} />
48+
<AiChatList
49+
style={{ height: '100%', minWidth: '250px' }}
50+
onSelect={({ id }) => setSelectedChatId(id)}
51+
selectedChatId={selectedChatId}
52+
/>
4053
<AiChat selectedChatId={selectedChatId} />
4154
</div>
4255
</div>

0 commit comments

Comments
 (0)