feat(frontend): add React SPA with chat, auth, and admin pages

- Vite 6 + React 19 + TypeScript + Tailwind 4 + Zustand 5
- Login page with JWT auth
- Chat interface: MessageList, ChatInput, ChatContainer
- WebSocket hook with auto-reconnect and exponential backoff
- Sidebar with new chat button + session navigation
- Admin dashboard placeholder
- 57 modules, 242KB JS, TypeScript zero errors
This commit is contained in:
Aska committed 2026-08-12 11:47:22 +08:00
1 parent 192144563b
commit 4a71965686
35 files changed
+3146

No files matched your search

View File
Whitespace-only changes.
+27
View File
@@ -0,0 +1,27 @@
import { create } from 'zustand'
import { api, setToken, getToken } from '../api/client'
interface AuthState {
isLoggedIn: boolean
username: string
role: string
login: (username: string, password: string) => Promise<void>
logout: () => void
}
export const useAuthStore = create<AuthState>((set) => ({
isLoggedIn: !!getToken(),
username: '',
role: 'user',
login: async (username: string, password: string) => {
const { token } = await api.login(username, password)
setToken(token)
set({ isLoggedIn: true, username, role: 'admin' })
},
logout: () => {
setToken(null)
set({ isLoggedIn: false, username: '', role: 'user' })
},
}))
+41
View File
@@ -0,0 +1,41 @@
import { create } from 'zustand'
import type { Message, IotDevice } from '../types/chat'
interface ChatState {
messages: Message[]
isStreaming: boolean
currentSessionId: string | null
iotDevices: IotDevice[]
inputText: string
setInput: (text: string) => void
setSessionId: (id: string) => void
addMessage: (msg: Message) => void
appendToLast: (content: string) => void
setStreaming: (v: boolean) => void
setIotDevices: (d: IotDevice[]) => void
clearMessages: () => void
}
export const useChatStore = create<ChatState>((set, get) => ({
messages: [],
isStreaming: false,
currentSessionId: null,
iotDevices: [],
inputText: '',
setInput: (text) => set({ inputText: text }),
setSessionId: (id) => set({ currentSessionId: id }),
setStreaming: (v) => set({ isStreaming: v }),
setIotDevices: (d) => set({ iotDevices: d }),
addMessage: (msg) => set((s) => ({ messages: [...s.messages, msg] })),
appendToLast: (content) => {
const msgs = [...get().messages]
if (msgs.length > 0) {
msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], content: msgs[msgs.length - 1].content + content }
}
set({ messages: msgs })
},
clearMessages: () => set({ messages: [] }),
}))