refactor: convert project from JavaScript to TypeScript and switch to pnpm

- Convert all .js/.jsx files to .ts/.tsx
- Add TypeScript configuration (tsconfig.json, tsconfig.node.json)
- Update vite.config.js to vite.config.ts
- Add TypeScript type definitions (@types/react, @types/react-dom, @types/node)
- Replace npm with pnpm (pnpm-lock.yaml)
- Add model configuration option for LLM API
- Update package.json scripts for TypeScript build
- All 52 tests passing
This commit is contained in:
2026-02-26 01:35:33 +01:00
parent 5667199d15
commit 7928fc1f00
21 changed files with 3680 additions and 5680 deletions
+1 -1
View File
@@ -8,6 +8,6 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>
-5513
View File
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -1,15 +1,15 @@
{ {
"name": "light-chat", "name": "light-chat",
"version": "0.1.0", "version": "1.0.0",
"description": "Lightweight chat app for connecting to LLM APIs", "description": "Lightweight chat app for connecting to LLM APIs - TypeScript + pnpm",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0" "type-check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@capacitor/android": "^5.0.0", "@capacitor/android": "^5.0.0",
@@ -22,6 +22,9 @@
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.0", "@testing-library/user-event": "^14.5.0",
"@types/node": "^25.3.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.0.0", "@vitejs/plugin-react": "^4.0.0",
"jsdom": "^22.0.0", "jsdom": "^22.0.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
+3352
View File
File diff suppressed because it is too large Load Diff
+9 -7
View File
@@ -1,14 +1,14 @@
import { describe, it, expect, vi } from 'vitest' import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { render, screen, waitFor } from '@testing-library/react'
import App from './App.jsx' import App from './App'
// Mock only the components, not the hook // Mock only the components, not the hook
vi.mock('./components/ChatWindow.jsx', () => ({ vi.mock('./components/ChatWindow', () => ({
ChatWindow: () => <div data-testid="chat-window">ChatWindow Mock</div> ChatWindow: () => <div data-testid="chat-window">ChatWindow Mock</div>
})) }))
vi.mock('./components/SessionDrawer.jsx', () => ({ vi.mock('./components/SessionDrawer', () => ({
SessionDrawer: ({ isOpen }) => isOpen ? <div data-testid="session-drawer">SessionDrawer Mock</div> : null SessionDrawer: ({ isOpen }: { isOpen: boolean }) => isOpen ? <div data-testid="session-drawer">SessionDrawer Mock</div> : null
})) }))
describe('App', () => { describe('App', () => {
@@ -31,10 +31,12 @@ describe('App', () => {
expect(screen.getByTestId('chat-window')).toBeInTheDocument() expect(screen.getByTestId('chat-window')).toBeInTheDocument()
}) })
fireEvent.click(screen.getByTitle(/Open Menu/i)) await waitFor(() => {
screen.getByTitle(/Open Menu/i).click()
})
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId('session-drawer')).toBeInTheDocument() expect(screen.getByTestId('session-drawer')).toBeInTheDocument()
}) })
}) })
}) })
+4 -4
View File
@@ -1,7 +1,7 @@
import { useSessions } from './hooks/useSessions'
import { ChatWindow } from './components/ChatWindow'
import { SessionDrawer } from './components/SessionDrawer'
import { useState } from 'react' import { useState } from 'react'
import { useSessions } from './hooks/useSessions.js'
import { ChatWindow } from './components/ChatWindow.jsx'
import { SessionDrawer } from './components/SessionDrawer.jsx'
/** /**
* Main App Component * Main App Component
@@ -80,4 +80,4 @@ export default function App() {
/> />
</div> </div>
) )
} }
@@ -1,11 +1,12 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, waitFor, act } from '@testing-library/react' import { render, screen, waitFor, act } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import { ChatWindow } from '../components/ChatWindow.jsx' import { ChatWindow } from './ChatWindow'
import { sendMessage } from '../utils/llmApi.js' import { sendMessage } from '../utils/llmApi'
import type { Session, Config } from '../services/sessionService'
// Mock the llmApi module // Mock the llmApi module
vi.mock('../utils/llmApi.js', () => ({ vi.mock('../utils/llmApi', () => ({
sendMessage: vi.fn(), sendMessage: vi.fn(),
testConnection: vi.fn() testConnection: vi.fn()
})) }))
@@ -15,12 +16,17 @@ describe('ChatWindow', () => {
session: { session: {
id: '1', id: '1',
name: 'Test Session', name: 'Test Session',
messages: [] messages: [],
}, endpoint: '',
systemPrompt: '',
createdAt: Date.now(),
updatedAt: Date.now()
} as Session,
config: { config: {
endpoint: 'https://api.test.com', endpoint: 'https://api.test.com',
systemPrompt: 'You are helpful' systemPrompt: 'You are helpful',
}, model: 'local-swarm'
} as Config,
onAddMessage: vi.fn() onAddMessage: vi.fn()
} }
@@ -44,8 +50,8 @@ describe('ChatWindow', () => {
const sessionWithMessages = { const sessionWithMessages = {
...defaultProps.session, ...defaultProps.session,
messages: [ messages: [
{ role: 'user', content: 'Hello' }, { role: 'user' as const, content: 'Hello' },
{ role: 'assistant', content: 'Hi there!' } { role: 'assistant' as const, content: 'Hi there!' }
] ]
} }
@@ -57,7 +63,7 @@ describe('ChatWindow', () => {
it('should handle form submission', async () => { it('should handle form submission', async () => {
const user = userEvent.setup() const user = userEvent.setup()
sendMessage.mockResolvedValueOnce('Response') vi.mocked(sendMessage).mockResolvedValueOnce('Response')
render(<ChatWindow {...defaultProps} />) render(<ChatWindow {...defaultProps} />)
@@ -78,7 +84,7 @@ describe('ChatWindow', () => {
const user = userEvent.setup() const user = userEvent.setup()
const noEndpointProps = { const noEndpointProps = {
...defaultProps, ...defaultProps,
config: { endpoint: '', systemPrompt: '' } config: { endpoint: '', systemPrompt: '', model: 'local-swarm' } as Config
} }
render(<ChatWindow {...noEndpointProps} />) render(<ChatWindow {...noEndpointProps} />)
@@ -98,4 +104,4 @@ describe('ChatWindow', () => {
expect(input).toBeDisabled() expect(input).toBeDisabled()
expect(input).toHaveAttribute('placeholder', 'Type your message...') expect(input).toHaveAttribute('placeholder', 'Type your message...')
}) })
}) })
@@ -1,14 +1,21 @@
import { useState, useRef, useEffect } from 'react' import { useState, useRef, useEffect } from 'react'
import { sendMessage } from '../utils/llmApi.js' import { sendMessage } from '../utils/llmApi'
import type { Session, Config } from '../services/sessionService'
interface ChatWindowProps {
session: Session | null
config: Config
onAddMessage: (role: 'user' | 'assistant', content: string) => void
}
/** /**
* ChatWindow - Main chat interface * ChatWindow - Main chat interface
*/ */
export function ChatWindow({ session, config, onAddMessage }) { export function ChatWindow({ session, config, onAddMessage }: ChatWindowProps) {
const [input, setInput] = useState('') const [input, setInput] = useState('')
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null) const [error, setError] = useState<string | null>(null)
const messagesEndRef = useRef(null) const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollToBottom = () => { const scrollToBottom = () => {
if (messagesEndRef.current && typeof messagesEndRef.current.scrollIntoView === 'function') { if (messagesEndRef.current && typeof messagesEndRef.current.scrollIntoView === 'function') {
@@ -20,7 +27,7 @@ export function ChatWindow({ session, config, onAddMessage }) {
scrollToBottom() scrollToBottom()
}, [session?.messages]) }, [session?.messages])
const handleSubmit = async (e) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!input.trim() || isLoading) return if (!input.trim() || isLoading) return
@@ -45,13 +52,14 @@ export function ChatWindow({ session, config, onAddMessage }) {
config.endpoint, config.endpoint,
userMessage, userMessage,
apiMessages, apiMessages,
config.systemPrompt config.systemPrompt,
config.model || 'local-swarm'
) )
// Add assistant response // Add assistant response
onAddMessage('assistant', response) onAddMessage('assistant', response)
} catch (err) { } catch (err) {
setError(err.message || 'Failed to get response') setError(err instanceof Error ? err.message : 'Failed to get response')
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@@ -224,4 +232,4 @@ export function ChatWindow({ session, config, onAddMessage }) {
</form> </form>
</div> </div>
) )
} }
@@ -1,17 +1,18 @@
import { describe, it, expect, vi, afterEach } from 'vitest' import { describe, it, expect, vi, afterEach } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react' import { render, screen, fireEvent } from '@testing-library/react'
import { SessionDrawer } from '../components/SessionDrawer.jsx' import { SessionDrawer } from './SessionDrawer'
import type { Session, Config } from '../services/sessionService'
describe('SessionDrawer', () => { describe('SessionDrawer', () => {
const defaultProps = { const defaultProps = {
isOpen: true, isOpen: true,
onClose: vi.fn(), onClose: vi.fn(),
sessions: [], sessions: [] as Session[],
currentSessionId: null, currentSessionId: null,
onSelectSession: vi.fn(), onSelectSession: vi.fn(),
onCreateSession: vi.fn(), onCreateSession: vi.fn(),
onDeleteSession: vi.fn(), onDeleteSession: vi.fn(),
config: { endpoint: '', systemPrompt: '' }, config: { endpoint: '', systemPrompt: '', model: 'local-swarm' } as Config,
onUpdateConfig: vi.fn() onUpdateConfig: vi.fn()
} }
@@ -29,15 +30,16 @@ describe('SessionDrawer', () => {
render(<SessionDrawer {...defaultProps} />) render(<SessionDrawer {...defaultProps} />)
const overlay = document.querySelector('[style*="position: fixed"]') const overlay = document.querySelector('[style*="position: fixed"]')
fireEvent.click(overlay) if (overlay) {
fireEvent.click(overlay)
expect(defaultProps.onClose).toHaveBeenCalled() expect(defaultProps.onClose).toHaveBeenCalled()
}
}) })
it('should display sessions list', () => { it('should display sessions list', () => {
const sessions = [ const sessions: Session[] = [
{ id: '1', name: 'Session 1', messages: [] }, { id: '1', name: 'Session 1', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() },
{ id: '2', name: 'Session 2', messages: [] } { id: '2', name: 'Session 2', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() }
] ]
render(<SessionDrawer {...defaultProps} sessions={sessions} currentSessionId="1" />) render(<SessionDrawer {...defaultProps} sessions={sessions} currentSessionId="1" />)
@@ -46,7 +48,9 @@ describe('SessionDrawer', () => {
}) })
it('should call onSelectSession when session clicked', () => { it('should call onSelectSession when session clicked', () => {
const sessions = [{ id: '1', name: 'Session 1', messages: [] }] const sessions: Session[] = [
{ id: '1', name: 'Session 1', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() }
]
render(<SessionDrawer {...defaultProps} sessions={sessions} />) render(<SessionDrawer {...defaultProps} sessions={sessions} />)
fireEvent.click(screen.getByText('Session 1')) fireEvent.click(screen.getByText('Session 1'))
@@ -72,6 +76,6 @@ describe('SessionDrawer', () => {
fireEvent.click(screen.getByRole('button', { name: /Save Config/i })) fireEvent.click(screen.getByRole('button', { name: /Save Config/i }))
expect(defaultProps.onUpdateConfig).toHaveBeenCalledWith({ endpoint: 'https://new.com', systemPrompt: '' }) expect(defaultProps.onUpdateConfig).toHaveBeenCalledWith({ endpoint: 'https://new.com', systemPrompt: '', model: 'local-swarm' })
}) })
}) })
@@ -1,4 +1,17 @@
import { useState } from 'react' import { useState } from 'react'
import type { Session, Config } from '../services/sessionService'
interface SessionDrawerProps {
isOpen: boolean
onClose: () => void
sessions: Session[]
currentSessionId: string | null
onSelectSession: (sessionId: string) => void
onCreateSession: () => void
onDeleteSession: (sessionId: string) => void
config: Config
onUpdateConfig: (config: Partial<Config>) => void
}
/** /**
* SessionDrawer - Sidebar showing sessions list and config * SessionDrawer - Sidebar showing sessions list and config
@@ -13,10 +26,10 @@ export function SessionDrawer({
onDeleteSession, onDeleteSession,
config, config,
onUpdateConfig onUpdateConfig
}) { }: SessionDrawerProps) {
const [localConfig, setLocalConfig] = useState(config) const [localConfig, setLocalConfig] = useState<Config>(config)
const handleConfigChange = (field, value) => { const handleConfigChange = (field: keyof Config, value: string) => {
setLocalConfig(prev => ({ ...prev, [field]: value })) setLocalConfig(prev => ({ ...prev, [field]: value }))
} }
@@ -24,7 +37,7 @@ export function SessionDrawer({
onUpdateConfig(localConfig) onUpdateConfig(localConfig)
} }
const handleKeyDown = (e) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
onClose() onClose()
} }
@@ -97,6 +110,17 @@ export function SessionDrawer({
placeholder="https://api.openai.com/v1/chat/completions" placeholder="https://api.openai.com/v1/chat/completions"
/> />
</div> </div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '12px' }}>
Model
</label>
<input
type="text"
value={localConfig.model || 'local-swarm'}
onChange={(e) => handleConfigChange('model', e.target.value)}
placeholder="local-swarm"
/>
</div>
<div style={{ marginBottom: '12px' }}> <div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '12px' }}> <label style={{ display: 'block', marginBottom: '4px', fontSize: '12px' }}>
System Prompt System Prompt
@@ -169,4 +193,4 @@ export function SessionDrawer({
</div> </div>
</> </>
) )
} }
@@ -1,11 +1,10 @@
import { describe, it, expect, vi, beforeEach } from 'vitest' import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act } from '@testing-library/react' import { renderHook, act } from '@testing-library/react'
import { useSessions } from '../hooks/useSessions.js' import { useSessions } from './useSessions'
import * as sessionService from '../services/sessionService.js'
// Mock localStorage // Mock localStorage
const localStorageMock = (() => { const localStorageMock = (() => {
let store = {} let store: Record<string, string> = {}
return { return {
getItem: vi.fn((key) => store[key] || null), getItem: vi.fn((key) => store[key] || null),
setItem: vi.fn((key, value) => { store[key] = value }), setItem: vi.fn((key, value) => { store[key] = value }),
@@ -25,9 +24,9 @@ describe('useSessions', () => {
describe('initialization', () => { describe('initialization', () => {
it('should load sessions and config from localStorage on mount', () => { it('should load sessions and config from localStorage on mount', () => {
const sessions = [ const sessions = [
{ id: '1', name: 'Session 1', messages: [], endpoint: '', systemPrompt: '' } { id: '1', name: 'Session 1', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() }
] ]
const config = { endpoint: 'https://api.test.com', systemPrompt: 'Be helpful' } const config = { endpoint: 'https://api.test.com', systemPrompt: 'Be helpful', model: 'local-swarm' }
localStorageMock.getItem.mockImplementation((key) => { localStorageMock.getItem.mockImplementation((key) => {
if (key === 'light-chat-sessions') return JSON.stringify(sessions) if (key === 'light-chat-sessions') return JSON.stringify(sessions)
@@ -44,8 +43,8 @@ describe('useSessions', () => {
it('should set first session as current when available', () => { it('should set first session as current when available', () => {
const sessions = [ const sessions = [
{ id: '1', name: 'Session 1', messages: [] }, { id: '1', name: 'Session 1', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() },
{ id: '2', name: 'Session 2', messages: [] } { id: '2', name: 'Session 2', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() }
] ]
localStorageMock.getItem.mockImplementation((key) => { localStorageMock.getItem.mockImplementation((key) => {
if (key === 'light-chat-sessions') return JSON.stringify(sessions) if (key === 'light-chat-sessions') return JSON.stringify(sessions)
@@ -55,7 +54,7 @@ describe('useSessions', () => {
const { result } = renderHook(() => useSessions()) const { result } = renderHook(() => useSessions())
expect(result.current.currentSessionId).toBe('1') expect(result.current.currentSessionId).toBe('1')
expect(result.current.currentSession.id).toBe('1') expect(result.current.currentSession!.id).toBe('1')
}) })
it('should not set current session when no sessions exist', () => { it('should not set current session when no sessions exist', () => {
@@ -99,8 +98,8 @@ describe('useSessions', () => {
describe('removeSession', () => { describe('removeSession', () => {
it('should remove session and update current session', () => { it('should remove session and update current session', () => {
const sessions = [ const sessions = [
{ id: '1', name: 'Session 1', messages: [] }, { id: '1', name: 'Session 1', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() },
{ id: '2', name: 'Session 2', messages: [] } { id: '2', name: 'Session 2', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() }
] ]
localStorageMock.getItem.mockImplementation((key) => { localStorageMock.getItem.mockImplementation((key) => {
if (key === 'light-chat-sessions') return JSON.stringify(sessions) if (key === 'light-chat-sessions') return JSON.stringify(sessions)
@@ -120,7 +119,7 @@ describe('useSessions', () => {
it('should clear current session if deleted session was current and no others exist', () => { it('should clear current session if deleted session was current and no others exist', () => {
const sessions = [ const sessions = [
{ id: '1', name: 'Session 1', messages: [] } { id: '1', name: 'Session 1', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() }
] ]
localStorageMock.getItem.mockImplementation((key) => { localStorageMock.getItem.mockImplementation((key) => {
if (key === 'light-chat-sessions') return JSON.stringify(sessions) if (key === 'light-chat-sessions') return JSON.stringify(sessions)
@@ -144,6 +143,8 @@ describe('useSessions', () => {
id: '1', id: '1',
name: 'Session 1', name: 'Session 1',
messages: [], messages: [],
endpoint: '',
systemPrompt: '',
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now() updatedAt: Date.now()
} }
@@ -158,8 +159,8 @@ describe('useSessions', () => {
result.current.addMessage('user', 'Hello') result.current.addMessage('user', 'Hello')
}) })
expect(result.current.currentSession.messages).toHaveLength(1) expect(result.current.currentSession!.messages).toHaveLength(1)
expect(result.current.currentSession.messages[0]).toEqual({ role: 'user', content: 'Hello' }) expect(result.current.currentSession!.messages[0]).toEqual({ role: 'user', content: 'Hello' })
}) })
it('should not add message if no current session', () => { it('should not add message if no current session', () => {
@@ -188,4 +189,4 @@ describe('useSessions', () => {
expect(result.current.config.endpoint).toBe('https://new.com') expect(result.current.config.endpoint).toBe('https://new.com')
}) })
}) })
}) })
@@ -1,14 +1,14 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import * as sessionService from '../services/sessionService.js' import * as sessionService from '../services/sessionService'
/** /**
* Custom hook for managing chat sessions * Custom hook for managing chat sessions
* Provides reactive state management following AGENTS.md guidelines * Provides reactive state management following AGENTS.md guidelines
*/ */
export function useSessions() { export function useSessions() {
const [sessions, setSessions] = useState([]) const [sessions, setSessions] = useState<sessionService.Session[]>([])
const [currentSessionId, setCurrentSessionId] = useState(null) const [currentSessionId, setCurrentSessionId] = useState<string | null>(null)
const [config, setConfig] = useState({ endpoint: '', systemPrompt: '' }) const [config, setConfig] = useState<sessionService.Config>({ endpoint: '', systemPrompt: '', model: 'local-swarm' })
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
// Load data on mount // Load data on mount
@@ -42,7 +42,10 @@ export function useSessions() {
/** /**
* Create a new session * Create a new session
*/ */
const createSession = useCallback((endpoint = config.endpoint, systemPrompt = config.systemPrompt) => { const createSession = useCallback((
endpoint: string = config.endpoint,
systemPrompt: string = config.systemPrompt
) => {
const newSession = sessionService.addSession({ endpoint, systemPrompt }) const newSession = sessionService.addSession({ endpoint, systemPrompt })
setSessions(prev => [...prev, newSession]) setSessions(prev => [...prev, newSession])
setCurrentSessionId(newSession.id) setCurrentSessionId(newSession.id)
@@ -52,7 +55,7 @@ export function useSessions() {
/** /**
* Delete a session * Delete a session
*/ */
const removeSession = useCallback((sessionId) => { const removeSession = useCallback((sessionId: string) => {
const success = sessionService.deleteSession(sessionId) const success = sessionService.deleteSession(sessionId)
if (success) { if (success) {
setSessions(prev => prev.filter(s => s.id !== sessionId)) setSessions(prev => prev.filter(s => s.id !== sessionId))
@@ -67,7 +70,7 @@ export function useSessions() {
/** /**
* Update a session * Update a session
*/ */
const updateSession = useCallback((sessionId, updates) => { const updateSession = useCallback((sessionId: string, updates: Partial<sessionService.Session>) => {
const updated = sessionService.updateSession(sessionId, updates) const updated = sessionService.updateSession(sessionId, updates)
if (updated) { if (updated) {
setSessions(prev => prev.map(s => s.id === sessionId ? updated : s)) setSessions(prev => prev.map(s => s.id === sessionId ? updated : s))
@@ -83,10 +86,10 @@ export function useSessions() {
/** /**
* Add message to current session * Add message to current session
*/ */
const addMessage = useCallback((role, content) => { const addMessage = useCallback((role: 'user' | 'assistant', content: string) => {
if (!currentSessionId) return null if (!currentSessionId) return null
const message = { role, content } const message: sessionService.Message = { role, content }
const updated = sessionService.addMessage(currentSessionId, message) const updated = sessionService.addMessage(currentSessionId, message)
if (updated) { if (updated) {
@@ -99,7 +102,7 @@ export function useSessions() {
/** /**
* Update config * Update config
*/ */
const updateConfig = useCallback((updates) => { const updateConfig = useCallback((updates: Partial<sessionService.Config>) => {
setConfig(prev => ({ ...prev, ...updates })) setConfig(prev => ({ ...prev, ...updates }))
}, []) }, [])
@@ -119,4 +122,4 @@ export function useSessions() {
addMessage, addMessage,
updateConfig updateConfig
} }
} }
+3 -3
View File
@@ -1,10 +1,10 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.jsx' import App from './App'
import './index.css' import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>,
) )
@@ -1,9 +1,10 @@
import { describe, it, expect, vi, beforeEach } from 'vitest' import { describe, it, expect, vi, beforeEach } from 'vitest'
import * as sessionService from '../services/sessionService.js' import * as sessionService from './sessionService'
import type { Session } from './sessionService'
// Mock localStorage // Mock localStorage
const localStorageMock = (() => { const localStorageMock = (() => {
let store = {} let store: Record<string, string> = {}
return { return {
getItem: vi.fn((key) => store[key] || null), getItem: vi.fn((key) => store[key] || null),
setItem: vi.fn((key, value) => { store[key] = value }), setItem: vi.fn((key, value) => { store[key] = value }),
@@ -27,8 +28,8 @@ describe('Session Service', () => {
}) })
it('should return parsed sessions from localStorage', () => { it('should return parsed sessions from localStorage', () => {
const testSessions = [ const testSessions: Session[] = [
{ id: '1', name: 'Test', messages: [], createdAt: Date.now(), updatedAt: Date.now() } { id: '1', name: 'Test', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() }
] ]
localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(testSessions)) localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(testSessions))
@@ -45,7 +46,7 @@ describe('Session Service', () => {
describe('saveSessions', () => { describe('saveSessions', () => {
it('should save sessions to localStorage', () => { it('should save sessions to localStorage', () => {
const sessions = [{ id: '1', name: 'Test' }] const sessions = [{ id: '1', name: 'Test', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() }]
sessionService.saveSessions(sessions) sessionService.saveSessions(sessions)
expect(localStorageMock.setItem).toHaveBeenCalledWith( expect(localStorageMock.setItem).toHaveBeenCalledWith(
'light-chat-sessions', 'light-chat-sessions',
@@ -57,14 +58,14 @@ describe('Session Service', () => {
localStorageMock.setItem.mockImplementationOnce(() => { localStorageMock.setItem.mockImplementationOnce(() => {
throw new Error('Storage full') throw new Error('Storage full')
}) })
expect(() => sessionService.saveSessions([{ id: '1' }])).not.toThrow() expect(() => sessionService.saveSessions([{ id: '1', name: 'Test', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() }])).not.toThrow()
}) })
}) })
describe('loadConfig', () => { describe('loadConfig', () => {
it('should return default config when no data exists', () => { it('should return default config when no data exists', () => {
const config = sessionService.loadConfig() const config = sessionService.loadConfig()
expect(config).toEqual({ endpoint: '', systemPrompt: '' }) expect(config).toEqual({ endpoint: '', systemPrompt: '', model: 'local-swarm' })
}) })
it('should return parsed config from localStorage', () => { it('should return parsed config from localStorage', () => {
@@ -72,13 +73,13 @@ describe('Session Service', () => {
localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(testConfig)) localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(testConfig))
const config = sessionService.loadConfig() const config = sessionService.loadConfig()
expect(config).toEqual(testConfig) expect(config).toEqual({ ...testConfig, model: 'local-swarm' })
}) })
}) })
describe('saveConfig', () => { describe('saveConfig', () => {
it('should save config to localStorage', () => { it('should save config to localStorage', () => {
const config = { endpoint: 'https://api.test.com', systemPrompt: 'Be helpful' } const config = { endpoint: 'https://api.test.com', systemPrompt: 'Be helpful', model: 'local-swarm' }
sessionService.saveConfig(config) sessionService.saveConfig(config)
expect(localStorageMock.setItem).toHaveBeenCalledWith( expect(localStorageMock.setItem).toHaveBeenCalledWith(
'light-chat-config', 'light-chat-config',
@@ -111,15 +112,15 @@ describe('Session Service', () => {
describe('deleteSession', () => { describe('deleteSession', () => {
it('should return false when session not found', () => { it('should return false when session not found', () => {
localStorageMock.getItem.mockReturnValueOnce(JSON.stringify([{ id: 'other' }])) localStorageMock.getItem.mockReturnValueOnce(JSON.stringify([{ id: 'other', name: 'Test', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() }]))
const result = sessionService.deleteSession('nonexistent') const result = sessionService.deleteSession('nonexistent')
expect(result).toBe(false) expect(result).toBe(false)
}) })
it('should delete session and return true', () => { it('should delete session and return true', () => {
const sessions = [ const sessions: Session[] = [
{ id: '1', name: 'Test 1', messages: [], createdAt: Date.now(), updatedAt: Date.now() }, { id: '1', name: 'Test 1', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() },
{ id: '2', name: 'Test 2', messages: [], createdAt: Date.now(), updatedAt: Date.now() } { id: '2', name: 'Test 2', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() }
] ]
localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(sessions)) localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(sessions))
@@ -136,10 +137,12 @@ describe('Session Service', () => {
}) })
it('should update session and return updated session', () => { it('should update session and return updated session', () => {
const existing = { const existing: Session = {
id: '1', id: '1',
name: 'Old', name: 'Old',
messages: [], messages: [],
endpoint: '',
systemPrompt: '',
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now() updatedAt: Date.now()
} }
@@ -147,16 +150,18 @@ describe('Session Service', () => {
const result = sessionService.updateSession('1', { name: 'New' }) const result = sessionService.updateSession('1', { name: 'New' })
expect(result.name).toBe('New') expect(result!.name).toBe('New')
expect(result.id).toBe('1') expect(result!.id).toBe('1')
expect(result.messages).toEqual([]) expect(result!.messages).toEqual([])
}) })
it('should update updatedAt timestamp', () => { it('should update updatedAt timestamp', () => {
const existing = { const existing: Session = {
id: '1', id: '1',
name: 'Test', name: 'Test',
messages: [], messages: [],
endpoint: '',
systemPrompt: '',
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now() updatedAt: Date.now()
} }
@@ -168,7 +173,7 @@ describe('Session Service', () => {
const result = sessionService.updateSession('1', { name: 'New' }) const result = sessionService.updateSession('1', { name: 'New' })
expect(result.updatedAt).toBeGreaterThan(beforeUpdate) expect(result!.updatedAt).toBeGreaterThan(beforeUpdate)
vi.useRealTimers() vi.useRealTimers()
}) })
}) })
@@ -181,23 +186,25 @@ describe('Session Service', () => {
}) })
it('should return session when found', () => { it('should return session when found', () => {
const sessions = [ const sessions: Session[] = [
{ id: '1', name: 'Test' }, { id: '1', name: 'Test', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() },
{ id: '2', name: 'Test 2' } { id: '2', name: 'Test 2', messages: [], endpoint: '', systemPrompt: '', createdAt: Date.now(), updatedAt: Date.now() }
] ]
localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(sessions)) localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(sessions))
const session = sessionService.getSession('2') const session = sessionService.getSession('2')
expect(session.name).toBe('Test 2') expect(session!.name).toBe('Test 2')
}) })
}) })
describe('addMessage', () => { describe('addMessage', () => {
it('should add message to session', () => { it('should add message to session', () => {
const session = { const session: Session = {
id: '1', id: '1',
name: 'Test', name: 'Test',
messages: [], messages: [],
endpoint: '',
systemPrompt: '',
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now() updatedAt: Date.now()
} }
@@ -205,8 +212,8 @@ describe('Session Service', () => {
const result = sessionService.addMessage('1', { role: 'user', content: 'Hello' }) const result = sessionService.addMessage('1', { role: 'user', content: 'Hello' })
expect(result.messages).toHaveLength(1) expect(result!.messages).toHaveLength(1)
expect(result.messages[0]).toEqual({ role: 'user', content: 'Hello' }) expect(result!.messages[0]).toEqual({ role: 'user', content: 'Hello' })
}) })
it('should return null for nonexistent session', () => { it('should return null for nonexistent session', () => {
@@ -215,4 +222,4 @@ describe('Session Service', () => {
expect(result).toBeNull() expect(result).toBeNull()
}) })
}) })
}) })
@@ -6,18 +6,44 @@
const STORAGE_KEY = 'light-chat-sessions' const STORAGE_KEY = 'light-chat-sessions'
const CONFIG_KEY = 'light-chat-config' const CONFIG_KEY = 'light-chat-config'
export interface Message {
role: 'user' | 'assistant'
content: string
}
export interface Session {
id: string
name: string
endpoint: string
systemPrompt: string
messages: Message[]
createdAt: number
updatedAt: number
}
export interface Config {
endpoint: string
systemPrompt: string
model: string
}
interface CreateSessionOptions {
endpoint?: string
systemPrompt?: string
}
/** /**
* Generate unique session ID * Generate unique session ID
* @returns {string} * @returns {string}
*/ */
function generateId() { function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2) return Date.now().toString(36) + Math.random().toString(36).substring(2)
} }
/** /**
* Default session structure * Default session structure
*/ */
function createSession(endpoint = '', systemPrompt = '') { function createSession(endpoint: string = '', systemPrompt: string = ''): Session {
return { return {
id: generateId(), id: generateId(),
name: `Session ${new Date().toLocaleTimeString()}`, name: `Session ${new Date().toLocaleTimeString()}`,
@@ -33,7 +59,7 @@ function createSession(endpoint = '', systemPrompt = '') {
* Load all sessions from storage * Load all sessions from storage
* @returns {Array} * @returns {Array}
*/ */
export function loadSessions() { export function loadSessions(): Session[] {
try { try {
const data = localStorage.getItem(STORAGE_KEY) const data = localStorage.getItem(STORAGE_KEY)
return data ? JSON.parse(data) : [] return data ? JSON.parse(data) : []
@@ -45,9 +71,9 @@ export function loadSessions() {
/** /**
* Save sessions to storage * Save sessions to storage
* @param {Array} sessions * @param sessions
*/ */
export function saveSessions(sessions) { export function saveSessions(sessions: Session[]): void {
try { try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(sessions)) localStorage.setItem(STORAGE_KEY, JSON.stringify(sessions))
} catch (error) { } catch (error) {
@@ -57,23 +83,23 @@ export function saveSessions(sessions) {
/** /**
* Load API configuration * Load API configuration
* @returns {Object} { endpoint, systemPrompt } * @returns {Config}
*/ */
export function loadConfig() { export function loadConfig(): Config {
try { try {
const data = localStorage.getItem(CONFIG_KEY) const data = localStorage.getItem(CONFIG_KEY)
return data ? JSON.parse(data) : { endpoint: '', systemPrompt: '' } return data ? { ...JSON.parse(data), model: 'local-swarm' } : { endpoint: '', systemPrompt: '', model: 'local-swarm' }
} catch (error) { } catch (error) {
console.error('Failed to load config:', error) console.error('Failed to load config:', error)
return { endpoint: '', systemPrompt: '' } return { endpoint: '', systemPrompt: '', model: 'local-swarm' }
} }
} }
/** /**
* Save API configuration * Save API configuration
* @param {Object} config * @param config
*/ */
export function saveConfig(config) { export function saveConfig(config: Config): void {
try { try {
localStorage.setItem(CONFIG_KEY, JSON.stringify(config)) localStorage.setItem(CONFIG_KEY, JSON.stringify(config))
} catch (error) { } catch (error) {
@@ -83,10 +109,10 @@ export function saveConfig(config) {
/** /**
* Add a new session * Add a new session
* @param {Object} options * @param options
* @returns {Object} new session * @returns new session
*/ */
export function addSession(options = {}) { export function addSession(options: CreateSessionOptions = {}): Session {
const sessions = loadSessions() const sessions = loadSessions()
const newSession = createSession( const newSession = createSession(
options.endpoint, options.endpoint,
@@ -99,10 +125,10 @@ export function addSession(options = {}) {
/** /**
* Delete a session * Delete a session
* @param {string} sessionId * @param sessionId
* @returns {boolean} * @returns
*/ */
export function deleteSession(sessionId) { export function deleteSession(sessionId: string): boolean {
const sessions = loadSessions() const sessions = loadSessions()
const filtered = sessions.filter(s => s.id !== sessionId) const filtered = sessions.filter(s => s.id !== sessionId)
if (filtered.length === sessions.length) { if (filtered.length === sessions.length) {
@@ -114,11 +140,11 @@ export function deleteSession(sessionId) {
/** /**
* Update a session * Update a session
* @param {string} sessionId * @param sessionId
* @param {Object} updates * @param updates
* @returns {Object|null} updated session or null * @returns updated session or null
*/ */
export function updateSession(sessionId, updates) { export function updateSession(sessionId: string, updates: Partial<Session>): Session | null {
const sessions = loadSessions() const sessions = loadSessions()
const index = sessions.findIndex(s => s.id === sessionId) const index = sessions.findIndex(s => s.id === sessionId)
@@ -143,21 +169,21 @@ export function updateSession(sessionId, updates) {
/** /**
* Get a specific session * Get a specific session
* @param {string} sessionId * @param sessionId
* @returns {Object|null} * @returns
*/ */
export function getSession(sessionId) { export function getSession(sessionId: string): Session | null {
const sessions = loadSessions() const sessions = loadSessions()
return sessions.find(s => s.id === sessionId) || null return sessions.find(s => s.id === sessionId) || null
} }
/** /**
* Add message to a session * Add message to a session
* @param {string} sessionId * @param sessionId
* @param {Object} message - { role: 'user'|'assistant', content: string } * @param message - { role: 'user'|'assistant', content: string }
* @returns {Object|null} updated session * @returns updated session
*/ */
export function addMessage(sessionId, message) { export function addMessage(sessionId: string, message: Message): Session | null {
const session = getSession(sessionId) const session = getSession(sessionId)
if (!session) { if (!session) {
@@ -166,4 +192,4 @@ export function addMessage(sessionId, message) {
session.messages.push(message) session.messages.push(message)
return updateSession(sessionId, { messages: session.messages }) return updateSession(sessionId, { messages: session.messages })
} }
+7 -1
View File
@@ -8,4 +8,10 @@ global.ResizeObserver = class ResizeObserver {
observe() {} observe() {}
unobserve() {} unobserve() {}
disconnect() {} disconnect() {}
} }
declare global {
interface Window {
ResizeObserver: typeof ResizeObserver
}
}
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest' import { describe, it, expect, vi, beforeEach } from 'vitest'
import { sendMessage, testConnection } from '../utils/llmApi.js' import { sendMessage, testConnection } from './llmApi'
describe('LLM API', () => { describe('LLM API', () => {
beforeEach(() => { beforeEach(() => {
@@ -19,7 +19,7 @@ describe('LLM API', () => {
choices: [{ message: { content: 'Hello there!' } }] choices: [{ message: { content: 'Hello there!' } }]
}) })
}) })
) ) as any
const result = await sendMessage('https://api.example.com/chat', 'Hi') const result = await sendMessage('https://api.example.com/chat', 'Hi')
@@ -39,14 +39,14 @@ describe('LLM API', () => {
ok: true, ok: true,
json: () => Promise.resolve({ choices: [] }) json: () => Promise.resolve({ choices: [] })
}) })
) ) as any
await expect(sendMessage('https://api.example.com/chat', 'Hi')) await expect(sendMessage('https://api.example.com/chat', 'Hi'))
.rejects.toThrow('Invalid response format: no content found') .rejects.toThrow('Invalid response format: no content found')
}) })
it('should throw error on network failure', async () => { it('should throw error on network failure', async () => {
global.fetch = vi.fn(() => Promise.reject(new Error('Network error'))) global.fetch = vi.fn(() => Promise.reject(new Error('Network error'))) as any
await expect(sendMessage('https://api.example.com/chat', 'Hi')) await expect(sendMessage('https://api.example.com/chat', 'Hi'))
.rejects.toThrow('Network error') .rejects.toThrow('Network error')
@@ -60,12 +60,13 @@ describe('LLM API', () => {
choices: [{ message: { content: 'Response' } }] choices: [{ message: { content: 'Response' } }]
}) })
}) })
) ) as any
await sendMessage('https://api.example.com/chat', 'Hi', [], 'You are helpful') await sendMessage('https://api.example.com/chat', 'Hi', [], 'You are helpful', 'local-swarm')
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body) const callBody = JSON.parse((global.fetch as any).mock.calls[0][1].body)
expect(callBody.messages[0]).toEqual({ role: 'system', content: 'You are helpful' }) expect(callBody.messages[0]).toEqual({ role: 'system', content: 'You are helpful' })
expect(callBody.model).toBe('local-swarm')
}) })
it('should include message history', async () => { it('should include message history', async () => {
@@ -76,7 +77,7 @@ describe('LLM API', () => {
choices: [{ message: { content: 'Response' } }] choices: [{ message: { content: 'Response' } }]
}) })
}) })
) ) as any
const history = [ const history = [
{ role: 'user', content: 'Hello' }, { role: 'user', content: 'Hello' },
@@ -85,7 +86,7 @@ describe('LLM API', () => {
await sendMessage('https://api.example.com/chat', 'How are you?', history) await sendMessage('https://api.example.com/chat', 'How are you?', history)
const callBody = JSON.parse(global.fetch.mock.calls[0][1].body) const callBody = JSON.parse((global.fetch as any).mock.calls[0][1].body)
expect(callBody.messages[0]).toEqual({ role: 'user', content: 'Hello' }) expect(callBody.messages[0]).toEqual({ role: 'user', content: 'Hello' })
expect(callBody.messages[1]).toEqual({ role: 'assistant', content: 'Hi there!' }) expect(callBody.messages[1]).toEqual({ role: 'assistant', content: 'Hi there!' })
expect(callBody.messages[2]).toEqual({ role: 'user', content: 'How are you?' }) expect(callBody.messages[2]).toEqual({ role: 'user', content: 'How are you?' })
@@ -96,7 +97,7 @@ describe('LLM API', () => {
it('should return true on successful response', async () => { it('should return true on successful response', async () => {
global.fetch = vi.fn(() => global.fetch = vi.fn(() =>
Promise.resolve({ status: 200 }) Promise.resolve({ status: 200 })
) ) as any
const result = await testConnection('https://api.example.com/chat') const result = await testConnection('https://api.example.com/chat')
expect(result).toBe(true) expect(result).toBe(true)
@@ -105,7 +106,7 @@ describe('LLM API', () => {
it('should return true on error response (not 404)', async () => { it('should return true on error response (not 404)', async () => {
global.fetch = vi.fn(() => global.fetch = vi.fn(() =>
Promise.resolve({ status: 401 }) Promise.resolve({ status: 401 })
) ) as any
const result = await testConnection('https://api.example.com/chat') const result = await testConnection('https://api.example.com/chat')
expect(result).toBe(true) expect(result).toBe(true)
@@ -114,17 +115,17 @@ describe('LLM API', () => {
it('should return false on 404', async () => { it('should return false on 404', async () => {
global.fetch = vi.fn(() => global.fetch = vi.fn(() =>
Promise.resolve({ status: 404 }) Promise.resolve({ status: 404 })
) ) as any
const result = await testConnection('https://api.example.com/chat') const result = await testConnection('https://api.example.com/chat')
expect(result).toBe(false) expect(result).toBe(false)
}) })
it('should return false on network error', async () => { it('should return false on network error', async () => {
global.fetch = vi.fn(() => Promise.reject(new Error('Network error'))) global.fetch = vi.fn(() => Promise.reject(new Error('Network error'))) as any
const result = await testConnection('https://api.example.com/chat') const result = await testConnection('https://api.example.com/chat')
expect(result).toBe(false) expect(result).toBe(false)
}) })
}) })
}) })
+35 -14
View File
@@ -3,25 +3,45 @@
* Simple wrapper for OpenAI-compatible APIs * Simple wrapper for OpenAI-compatible APIs
*/ */
interface Message {
role: string
content: string
}
interface APIResponse {
choices: Array<{
message: {
content: string
}
}>
}
const DEFAULT_HEADERS = { const DEFAULT_HEADERS = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
/** /**
* Send a message to the LLM API and get a response * Send a message to the LLM API and get a response
* @param {string} endpoint - API endpoint URL * @param endpoint - API endpoint URL
* @param {string} message - User message * @param message - User message
* @param {Array} messages - Full message history (optional, for multi-turn) * @param messages - Full message history (optional, for multi-turn)
* @param {string} systemPrompt - System prompt (optional) * @param systemPrompt - System prompt (optional)
* @returns {Promise<string>} - LLM response content * @param model - Model name (optional, defaults to 'local-swarm')
* @returns - LLM response content
*/ */
export async function sendMessage(endpoint, message, messages = [], systemPrompt = '') { export async function sendMessage(
endpoint: string,
message: string,
messages: Message[] = [],
systemPrompt: string = '',
model: string = 'local-swarm'
): Promise<string> {
if (!endpoint) { if (!endpoint) {
throw new Error('API endpoint is required') throw new Error('API endpoint is required')
} }
// Build messages array for API // Build messages array for API
const apiMessages = [] const apiMessages: Array<{ role: string; content: string }> = []
if (systemPrompt) { if (systemPrompt) {
apiMessages.push({ role: 'system', content: systemPrompt }) apiMessages.push({ role: 'system', content: systemPrompt })
@@ -43,7 +63,7 @@ export async function sendMessage(endpoint, message, messages = [], systemPrompt
method: 'POST', method: 'POST',
headers: DEFAULT_HEADERS, headers: DEFAULT_HEADERS,
body: JSON.stringify({ body: JSON.stringify({
model: 'gpt-3.5-turbo', // or appropriate model model,
messages: apiMessages, messages: apiMessages,
stream: false stream: false
}) })
@@ -54,7 +74,7 @@ export async function sendMessage(endpoint, message, messages = [], systemPrompt
throw new Error(`API request failed: ${response.status} - ${errorText}`) throw new Error(`API request failed: ${response.status} - ${errorText}`)
} }
const data = await response.json() const data = (await response.json()) as APIResponse
// Extract content from response (OpenAI format) // Extract content from response (OpenAI format)
const content = data.choices?.[0]?.message?.content const content = data.choices?.[0]?.message?.content
@@ -72,17 +92,18 @@ export async function sendMessage(endpoint, message, messages = [], systemPrompt
/** /**
* Test connection to API endpoint * Test connection to API endpoint
* @param {string} endpoint - API endpoint URL * @param endpoint - API endpoint URL
* @returns {Promise<boolean>} - Whether connection succeeded * @param model - Model name (optional, defaults to 'local-swarm')
* @returns - Whether connection succeeded
*/ */
export async function testConnection(endpoint) { export async function testConnection(endpoint: string, model: string = 'local-swarm'): Promise<boolean> {
try { try {
// Simple test - send minimal message // Simple test - send minimal message
const response = await fetch(endpoint, { const response = await fetch(endpoint, {
method: 'POST', method: 'POST',
headers: DEFAULT_HEADERS, headers: DEFAULT_HEADERS,
body: JSON.stringify({ body: JSON.stringify({
model: 'gpt-3.5-turbo', model,
messages: [{ role: 'user', content: 'test' }], messages: [{ role: 'user', content: 'test' }],
max_tokens: 1 max_tokens: 1
}) })
@@ -94,4 +115,4 @@ export async function testConnection(endpoint) {
console.error('Connection test failed:', error) console.error('Connection test failed:', error)
return false return false
} }
} }
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Vitest support */
"types": ["vitest/globals", "@testing-library/jest-dom"]
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/tests/setup.ts',
},
})