docs(chat): add functional host chat UI pseudocode integrating ihm_client iframe

This commit is contained in:
LeCoffre Deployment 2025-10-05 12:08:21 +00:00
parent 69eb9b9117
commit 8ea50fed9a

View File

@ -140,6 +140,135 @@ if (event.data.type === 'CHANNEL_MESSAGE' && event.data.payload?.action === 'SEN
}
```
### Functional host chat UI (pseudocode)
```ts
// Host-side pseudo-UI integrating ihm_client via iframe and MessageBus
import { MessageBus } from 'skeleton/src/sdk/MessageBus';
type ChatMessage = { channelId: string; messageId: string; content: string; from: string; clientTs: string; serverTs?: string };
class ChatHostApp {
private iframe: HTMLIFrameElement;
private bus: MessageBus;
private origin: string;
private currentChannelId: string = 'default';
private messages: Record<string, ChatMessage[]> = {};
constructor(container: HTMLElement, iframeSrc: string, origin: string) {
this.origin = origin;
this.iframe = document.createElement('iframe');
this.iframe.src = iframeSrc;
this.iframe.style.width = '100%';
this.iframe.style.height = '100%';
this.iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
container.appendChild(this.iframe);
this.bus = new MessageBus(this.iframe, { origin: this.origin });
this.bus.on('message', (env: any) => this.handleMessage(env));
}
async init() {
await this.waitForListening();
await this.subscribe(this.currentChannelId);
await this.loadHistory(this.currentChannelId);
}
private waitForListening(): Promise<void> {
return new Promise((resolve) => {
const off = this.bus.on('message', (env: any) => {
if (env?.type === 'LISTENING') { off(); resolve(); }
});
});
}
private send(payload: any) {
return this.bus.send({ type: 'CHANNEL_MESSAGE', messageId: uuid(), payload });
}
async subscribe(channelId: string) {
await this.send({ action: 'SUBSCRIBE', channelId });
}
async sendMessage(channelId: string, content: string) {
await this.send({ action: 'SEND', channelId, data: { content, contentType: 'text/plain' } });
}
async setTyping(channelId: string, isTyping: boolean) {
await this.send({ action: 'TYPING', channelId, data: { isTyping } });
}
async markRead(channelId: string, messageIds: string[]) {
await this.send({ action: 'READ', channelId, data: { messageIds } });
}
async loadHistory(channelId: string, beforeTs?: string) {
const correlationId = uuid();
this.bus.expect(correlationId, (env: any) => env?.type === 'CHANNEL_MESSAGE' && env?.payload?.action === 'HISTORY_RES');
this.bus.send({ type: 'CHANNEL_MESSAGE', messageId: correlationId, payload: { action: 'HISTORY_REQ', channelId, data: { beforeTs, limit: 50 } } });
}
private handleMessage(env: any) {
// Generic ERROR
if (env?.type === 'ERROR') { console.error('[chat]', env); return; }
// CHANNEL_MESSAGE events
if (env?.type === 'CHANNEL_MESSAGE') {
const { action, data } = env.payload || {};
if (action === 'EVENT') {
switch (data?.event) {
case 'MESSAGE': {
const msg = data.payload as ChatMessage;
const list = this.messages[msg.channelId] || (this.messages[msg.channelId] = []);
list.push(msg);
this.renderThread(msg.channelId);
break;
}
case 'DELIVERED': {
// Optionally update UI state for delivery
break;
}
case 'ERROR': {
console.warn('[chat-event-error]', data?.payload);
break;
}
}
} else if (action === 'HISTORY_RES') {
const { channelId, messages } = data;
this.messages[channelId] = messages;
this.renderThread(channelId);
}
}
}
// Very simple rendering hooks (pseudo-DOM updates)
renderThread(channelId: string) {
const ul = document.querySelector('#messages') as HTMLUListElement;
if (!ul) return;
ul.innerHTML = '';
for (const m of this.messages[channelId] || []) {
const li = document.createElement('li');
li.textContent = `${m.from}: ${m.content}`;
ul.appendChild(li);
}
}
}
// Usage
const app = new ChatHostApp(document.getElementById('chat-root')!, 'https://ihm.example.com/', 'https://ihm.example.com');
app.init();
// Bind UI controls
document.getElementById('sendBtn')?.addEventListener('click', () => {
const input = document.getElementById('composer') as HTMLInputElement;
app.sendMessage('default', input.value);
input.value = '';
});
document.getElementById('composer')?.addEventListener('input', (e) => {
const typing = (e.target as HTMLInputElement).value.length > 0;
app.setTyping('default', typing);
});
```
### Delivery plan (no changes to `sdk_common` or `sdk_client`)
1) `skeleton`: enhance `MessageBus` for request/response correlation and strict origin filtering (host-side only).
2) `ihm_client`: implement `CHANNEL_MESSAGE` handlers in `registerAllListeners()` and a basic chat UI (list, thread, composer).