Deploy 2026-06-15T16:17:09.309Z
This commit is contained in:
75
src/App.jsx
75
src/App.jsx
@@ -1,6 +1,8 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { marked } from 'marked'
|
||||
import Dashboard from './Dashboard'
|
||||
import './App.css'
|
||||
import './Dashboard.css'
|
||||
|
||||
marked.setOptions({ breaks: true, gfm: true })
|
||||
|
||||
@@ -9,30 +11,57 @@ const API_URL = window.location.hostname === 'localhost'
|
||||
: 'https://diet-api.psbailey.uk'
|
||||
|
||||
export default function App() {
|
||||
const [page, setPage] = useState('chat')
|
||||
const [messages, setMessages] = useState([])
|
||||
const [input, setInput] = useState('')
|
||||
const [images, setImages] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [restarting, setRestarting] = useState(false)
|
||||
const messagesEnd = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
const fileRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
messagesEnd.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [messages])
|
||||
|
||||
function handleImage(e) {
|
||||
const files = Array.from(e.target.files || [])
|
||||
for (const file of files) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result.split(',')[1]
|
||||
setImages(prev => [...prev, { base64, preview: reader.result, mime: file.type }])
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
|
||||
function removeImage(idx) {
|
||||
setImages(prev => prev.filter((_, i) => i !== idx))
|
||||
if (fileRef.current) fileRef.current.value = ''
|
||||
}
|
||||
|
||||
async function send(e) {
|
||||
e.preventDefault()
|
||||
const text = input.trim()
|
||||
if (!text || loading) return
|
||||
if ((!text && !images.length) || loading) return
|
||||
|
||||
setInput('')
|
||||
setMessages(prev => [...prev, { role: 'user', content: text }])
|
||||
const userMsg = { role: 'user', content: text, images: images.map(i => i.preview) }
|
||||
setMessages(prev => [...prev, userMsg])
|
||||
setLoading(true)
|
||||
|
||||
const payload = { message: text || 'Analyse these images' }
|
||||
if (images.length) payload.images = images.map(i => ({ base64: i.base64, mime: i.mime }))
|
||||
setImages([])
|
||||
if (fileRef.current) fileRef.current.value = ''
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: text })
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error('Failed')
|
||||
@@ -47,12 +76,11 @@ export default function App() {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
// Parse SSE lines
|
||||
for (const line of chunk.split('\n')) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6)
|
||||
if (data === '[DONE]') break
|
||||
assistantMsg += data
|
||||
assistantMsg += data.replace(/\\n/g, '\n')
|
||||
setMessages(prev => {
|
||||
const updated = [...prev]
|
||||
updated[updated.length - 1] = { role: 'assistant', content: assistantMsg }
|
||||
@@ -69,26 +97,55 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
if (page === 'dashboard') return <Dashboard onBack={() => setPage('chat')} />
|
||||
|
||||
async function restartApi() {
|
||||
setRestarting(true)
|
||||
try {
|
||||
await fetch(`${API_URL}/restart`, { method: 'POST', headers: { Authorization: 'my-health-data' } })
|
||||
} catch {}
|
||||
setTimeout(() => setRestarting(false), 5000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header>
|
||||
<h1>🥗 Diet Coach</h1>
|
||||
<div className="header-btns">
|
||||
<button onClick={restartApi} className="nav-btn" disabled={restarting}>{restarting ? '⏳' : '🔄'} API</button>
|
||||
<button onClick={() => setPage('dashboard')} className="nav-btn">📊 Dashboard</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="messages">
|
||||
{messages.length === 0 && (
|
||||
<div className="empty">Ask me about your diet, log food, or get meal suggestions.</div>
|
||||
<div className="empty">Ask me about your diet, log food, or send a screenshot of your Lose It report.</div>
|
||||
)}
|
||||
{messages.map((msg, i) => (
|
||||
<div key={i} className={`msg ${msg.role}`}>
|
||||
{msg.role === 'assistant'
|
||||
{msg.role === 'assistant'
|
||||
? <div className="bubble" dangerouslySetInnerHTML={{ __html: marked.parse(msg.content || '...') }} />
|
||||
: <div className="bubble">{msg.content}</div>
|
||||
: <div className="bubble">
|
||||
{msg.images?.map((src, j) => <img key={j} src={src} className="msg-img" alt="" />)}
|
||||
{msg.content}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEnd} />
|
||||
</div>
|
||||
{images.length > 0 && (
|
||||
<div className="image-preview">
|
||||
{images.map((img, i) => (
|
||||
<span key={i} className="preview-item">
|
||||
<img src={img.preview} alt="" />
|
||||
<button onClick={() => removeImage(i)} className="remove-img">✕</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={send} className="input-bar">
|
||||
<button type="button" className="attach-btn" onClick={() => fileRef.current?.click()} disabled={loading}>📷</button>
|
||||
<input type="file" ref={fileRef} accept="image/*" multiple onChange={handleImage} hidden />
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
@@ -97,7 +154,7 @@ export default function App() {
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
/>
|
||||
<button type="submit" disabled={loading || !input.trim()}>Send</button>
|
||||
<button type="submit" disabled={loading || (!input.trim() && !images.length)}>Send</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user