expertInsp / src /App.tsx
valonys
Fix inference: switch to OpenAI-compatible /v1/chat/completions endpoint
b02f0c1
import { useState, useRef, FormEvent } from 'react';
import { StreamingText } from './StreamingText';
interface Message {
role: 'user' | 'assistant';
content: string;
}
export default function App() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!input.trim() || isStreaming) return;
const userMessage: Message = { role: 'user', content: input };
const newMessages = [...messages, userMessage];
setMessages(newMessages);
setInput('');
setIsStreaming(true);
setMessages((prev) => [...prev, { role: 'assistant', content: '' }]);
abortControllerRef.current = new AbortController();
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: newMessages }),
signal: abortControllerRef.current.signal,
});
if (!response.body) throw new Error('No body in response');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Robust SSE parser: handles chunks split across network packets
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data:')) {
const jsonStr = line.replace('data:', '').trim();
if (jsonStr === '[DONE]') continue;
try {
const parsed = JSON.parse(jsonStr);
// OpenAI-compatible SSE: choices[0].delta.content
const token: string = parsed.choices?.[0]?.delta?.content ?? '';
if (token) {
setMessages((prev) => {
const updated = [...prev];
const lastMsg = updated[updated.length - 1];
if (lastMsg.role === 'assistant') {
lastMsg.content += token;
}
return updated;
});
}
} catch (err) {
console.error('JSON parse error on line:', line, err);
}
}
}
}
} catch (error) {
if ((error as Error).name === 'AbortError') {
console.log('Stream aborted by user');
} else {
console.error('Chat error:', error);
}
} finally {
setIsStreaming(false);
abortControllerRef.current = null;
}
};
const handleStop = () => {
abortControllerRef.current?.abort();
};
return (
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column', backgroundColor: '#f5f5f5' }}>
<header
style={{
padding: '1rem',
backgroundColor: '#fff',
borderBottom: '1px solid #ddd',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}}
>
<h1 style={{ margin: 0, fontSize: '1.25rem' }}>Qwen 2.5 7B Chat</h1>
</header>
<main style={{ flex: 1, overflowY: 'auto', padding: '1rem' }}>
{messages.map((msg, idx) => (
<div
key={idx}
style={{
marginBottom: '1rem',
display: 'flex',
justifyContent: msg.role === 'user' ? 'flex-end' : 'flex-start',
}}
>
<div
style={{
maxWidth: '70%',
padding: '0.75rem',
borderRadius: '12px',
backgroundColor: msg.role === 'user' ? '#007bff' : '#fff',
color: msg.role === 'user' ? '#fff' : '#000',
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
}}
>
{msg.role === 'assistant' && idx === messages.length - 1 && isStreaming ? (
<StreamingText content={msg.content} />
) : (
<div style={{ whiteSpace: 'pre-wrap' }}>{msg.content}</div>
)}
</div>
</div>
))}
</main>
<footer style={{ padding: '1rem', backgroundColor: '#fff', borderTop: '1px solid #ddd' }}>
<form onSubmit={handleSubmit} style={{ display: 'flex', gap: '0.5rem' }}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
style={{
flex: 1,
padding: '0.75rem',
borderRadius: '4px',
border: '1px solid #ccc',
}}
disabled={isStreaming}
/>
{isStreaming ? (
<button
type="button"
onClick={handleStop}
style={{
padding: '0.75rem 1.5rem',
backgroundColor: '#dc3545',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Stop
</button>
) : (
<button
type="submit"
disabled={!input.trim()}
style={{
padding: '0.75rem 1.5rem',
backgroundColor: '#007bff',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Send
</button>
)}
</form>
</footer>
</div>
);
}