a bit more progressive update and single turn
This commit is contained in:
@@ -69,33 +69,114 @@ function App() {
|
||||
messages: [...prev.messages, userMessage],
|
||||
}));
|
||||
|
||||
// Send message and get council response
|
||||
const response = await api.sendMessage(currentConversationId, content);
|
||||
|
||||
// Add assistant message to UI
|
||||
// Create a partial assistant message that will be updated progressively
|
||||
const assistantMessage = {
|
||||
role: 'assistant',
|
||||
stage1: response.stage1,
|
||||
stage2: response.stage2,
|
||||
stage3: response.stage3,
|
||||
metadata: response.metadata,
|
||||
stage1: null,
|
||||
stage2: null,
|
||||
stage3: null,
|
||||
metadata: null,
|
||||
loading: {
|
||||
stage1: false,
|
||||
stage2: false,
|
||||
stage3: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Add the partial assistant message
|
||||
setCurrentConversation((prev) => ({
|
||||
...prev,
|
||||
messages: [...prev.messages, assistantMessage],
|
||||
}));
|
||||
|
||||
// Reload conversations list to update message count
|
||||
await loadConversations();
|
||||
// Send message with streaming
|
||||
await api.sendMessageStream(currentConversationId, content, (eventType, event) => {
|
||||
switch (eventType) {
|
||||
case 'stage1_start':
|
||||
setCurrentConversation((prev) => {
|
||||
const messages = [...prev.messages];
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
lastMsg.loading.stage1 = true;
|
||||
return { ...prev, messages };
|
||||
});
|
||||
break;
|
||||
|
||||
case 'stage1_complete':
|
||||
setCurrentConversation((prev) => {
|
||||
const messages = [...prev.messages];
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
lastMsg.stage1 = event.data;
|
||||
lastMsg.loading.stage1 = false;
|
||||
return { ...prev, messages };
|
||||
});
|
||||
break;
|
||||
|
||||
case 'stage2_start':
|
||||
setCurrentConversation((prev) => {
|
||||
const messages = [...prev.messages];
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
lastMsg.loading.stage2 = true;
|
||||
return { ...prev, messages };
|
||||
});
|
||||
break;
|
||||
|
||||
case 'stage2_complete':
|
||||
setCurrentConversation((prev) => {
|
||||
const messages = [...prev.messages];
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
lastMsg.stage2 = event.data;
|
||||
lastMsg.metadata = event.metadata;
|
||||
lastMsg.loading.stage2 = false;
|
||||
return { ...prev, messages };
|
||||
});
|
||||
break;
|
||||
|
||||
case 'stage3_start':
|
||||
setCurrentConversation((prev) => {
|
||||
const messages = [...prev.messages];
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
lastMsg.loading.stage3 = true;
|
||||
return { ...prev, messages };
|
||||
});
|
||||
break;
|
||||
|
||||
case 'stage3_complete':
|
||||
setCurrentConversation((prev) => {
|
||||
const messages = [...prev.messages];
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
lastMsg.stage3 = event.data;
|
||||
lastMsg.loading.stage3 = false;
|
||||
return { ...prev, messages };
|
||||
});
|
||||
break;
|
||||
|
||||
case 'title_complete':
|
||||
// Reload conversations to get updated title
|
||||
loadConversations();
|
||||
break;
|
||||
|
||||
case 'complete':
|
||||
// Stream complete, reload conversations list
|
||||
loadConversations();
|
||||
setIsLoading(false);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('Stream error:', event.message);
|
||||
setIsLoading(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown event type:', eventType);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
// Remove optimistic user message on error
|
||||
// Remove optimistic messages on error
|
||||
setCurrentConversation((prev) => ({
|
||||
...prev,
|
||||
messages: prev.messages.slice(0, -1),
|
||||
messages: prev.messages.slice(0, -2),
|
||||
}));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -65,4 +65,51 @@ export const api = {
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a message and receive streaming updates.
|
||||
* @param {string} conversationId - The conversation ID
|
||||
* @param {string} content - The message content
|
||||
* @param {function} onEvent - Callback function for each event: (eventType, data) => void
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async sendMessageStream(conversationId, content, onEvent) {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/conversations/${conversationId}/message/stream`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ content }),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to send message');
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6);
|
||||
try {
|
||||
const event = JSON.parse(data);
|
||||
onEvent(event.type, event);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse SSE event:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -71,6 +71,20 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stage-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
margin: 12px 0;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
@@ -71,13 +71,39 @@ export default function ChatInterface({
|
||||
) : (
|
||||
<div className="assistant-message">
|
||||
<div className="message-label">LLM Council</div>
|
||||
<Stage1 responses={msg.stage1} />
|
||||
<Stage2
|
||||
rankings={msg.stage2}
|
||||
labelToModel={msg.metadata?.label_to_model}
|
||||
aggregateRankings={msg.metadata?.aggregate_rankings}
|
||||
/>
|
||||
<Stage3 finalResponse={msg.stage3} />
|
||||
|
||||
{/* Stage 1 */}
|
||||
{msg.loading?.stage1 && (
|
||||
<div className="stage-loading">
|
||||
<div className="spinner"></div>
|
||||
<span>Running Stage 1: Collecting individual responses...</span>
|
||||
</div>
|
||||
)}
|
||||
{msg.stage1 && <Stage1 responses={msg.stage1} />}
|
||||
|
||||
{/* Stage 2 */}
|
||||
{msg.loading?.stage2 && (
|
||||
<div className="stage-loading">
|
||||
<div className="spinner"></div>
|
||||
<span>Running Stage 2: Peer rankings...</span>
|
||||
</div>
|
||||
)}
|
||||
{msg.stage2 && (
|
||||
<Stage2
|
||||
rankings={msg.stage2}
|
||||
labelToModel={msg.metadata?.label_to_model}
|
||||
aggregateRankings={msg.metadata?.aggregate_rankings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Stage 3 */}
|
||||
{msg.loading?.stage3 && (
|
||||
<div className="stage-loading">
|
||||
<div className="spinner"></div>
|
||||
<span>Running Stage 3: Final synthesis...</span>
|
||||
</div>
|
||||
)}
|
||||
{msg.stage3 && <Stage3 finalResponse={msg.stage3} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -94,24 +120,26 @@ export default function ChatInterface({
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<form className="input-form" onSubmit={handleSubmit}>
|
||||
<textarea
|
||||
className="message-input"
|
||||
placeholder="Ask your question... (Shift+Enter for new line, Enter to send)"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isLoading}
|
||||
rows={3}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="send-button"
|
||||
disabled={!input.trim() || isLoading}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
{conversation.messages.length === 0 && (
|
||||
<form className="input-form" onSubmit={handleSubmit}>
|
||||
<textarea
|
||||
className="message-input"
|
||||
placeholder="Ask your question... (Shift+Enter for new line, Enter to send)"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isLoading}
|
||||
rows={3}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="send-button"
|
||||
disabled={!input.trim() || isLoading}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user