docs(chat): add functional host chat UI pseudocode integrating ihm_client iframe
This commit is contained in:
parent
69eb9b9117
commit
8ea50fed9a
@ -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).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user