diff --git a/frontend/package.json b/frontend/package.json index c7a47bf..25e87e3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,14 +8,15 @@ "preview": "vite preview" }, "devDependencies": { - "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/kit": "^2.0.0", - "svelte": "^4.2.0", - "vite": "^5.0.0" + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" }, "dependencies": { - "marked": "^12.0.0", - "highlight.js": "^11.9.0" + "marked": "^15.0.0", + "highlight.js": "^11.11.0" }, "type": "module" } diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index 0f20da1..e5d918a 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -1,189 +1,177 @@ const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'; class ApiClient { - constructor() { - this.baseUrl = API_BASE; - } + constructor() { + this.token = null; + if (typeof window !== 'undefined') { + this.token = localStorage.getItem('token'); + } + } - getToken() { - if (typeof window !== 'undefined') { - return localStorage.getItem('token'); - } - return null; - } + setToken(token) { + this.token = token; + if (typeof window !== 'undefined') { + if (token) { + localStorage.setItem('token', token); + } else { + localStorage.removeItem('token'); + } + } + } - setToken(token) { - if (typeof window !== 'undefined') { - localStorage.setItem('token', token); - } - } + async request(endpoint, options = {}) { + const headers = { + 'Content-Type': 'application/json', + ...options.headers, + }; - clearToken() { - if (typeof window !== 'undefined') { - localStorage.removeItem('token'); - } - } + if (this.token) { + headers['Authorization'] = `Bearer ${this.token}`; + } - getHeaders() { - const headers = { - 'Content-Type': 'application/json' - }; - const token = this.getToken(); - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - return headers; - } + const response = await fetch(`${API_BASE}${endpoint}`, { + ...options, + headers, + }); - async request(endpoint, options = {}) { - const url = `${this.baseUrl}${endpoint}`; - const config = { - ...options, - headers: { - ...this.getHeaders(), - ...options.headers - } - }; + if (response.status === 401) { + this.setToken(null); + if (typeof window !== 'undefined') { + window.location.href = '/login'; + } + throw new Error('Unauthorized'); + } - const response = await fetch(url, config); - - if (!response.ok) { - const error = await response.json().catch(() => ({ detail: 'Request failed' })); - throw new Error(error.detail || 'Request failed'); - } + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: 'Request failed' })); + throw new Error(error.detail || 'Request failed'); + } - return response.json(); - } + if (response.status === 204) { + return null; + } - // Auth endpoints - async login(email, password) { - const formData = new FormData(); - formData.append('username', email); - formData.append('password', password); - - const response = await fetch(`${this.baseUrl}/auth/login`, { - method: 'POST', - body: formData - }); + return response.json(); + } - if (!response.ok) { - const error = await response.json().catch(() => ({ detail: 'Login failed' })); - throw new Error(error.detail || 'Login failed'); - } + async login(email, password) { + const data = await this.request('/auth/login', { + method: 'POST', + body: JSON.stringify({ email, password }), + }); + this.setToken(data.access_token); + return data; + } - const data = await response.json(); - this.setToken(data.access_token); - return data; - } + async register(email, username, password) { + return this.request('/auth/register', { + method: 'POST', + body: JSON.stringify({ email, username, password }), + }); + } - async register(email, username, password) { - return this.request('/auth/register', { - method: 'POST', - body: JSON.stringify({ email, username, password }) - }); - } + async getMe() { + return this.request('/auth/me'); + } - async getMe() { - return this.request('/auth/me'); - } + async updateMe(data) { + return this.request('/auth/me', { + method: 'PUT', + body: JSON.stringify(data), + }); + } - // Endpoints - async getEndpoints() { - return this.request('/endpoints'); - } + async getEndpoints() { + return this.request('/chat/endpoints'); + } - async createEndpoint(data) { - return this.request('/endpoints', { - method: 'POST', - body: JSON.stringify(data) - }); - } + async sendMessage(message, endpointId = null, fileIds = [], conversationHistory = []) { + return this.request('/chat/message', { + method: 'POST', + body: JSON.stringify({ + message, + endpoint_id: endpointId, + file_ids: fileIds, + conversation_history: conversationHistory, + }), + }); + } - async updateEndpoint(id, data) { - return this.request(`/endpoints/${id}`, { - method: 'PUT', - body: JSON.stringify(data) - }); - } + async getChatHistory(limit = 50) { + return this.request(`/chat/history?limit=${limit}`); + } - async deleteEndpoint(id) { - return this.request(`/endpoints/${id}`, { - method: 'DELETE' - }); - } + async uploadFile(file) { + const formData = new FormData(); + formData.append('file', file); - // Chat - async sendMessage(endpointId, message, conversationId = null) { - return this.request('/chat/message', { - method: 'POST', - body: JSON.stringify({ - endpoint_id: endpointId, - message, - conversation_id: conversationId - }) - }); - } + const headers = {}; + if (this.token) { + headers['Authorization'] = `Bearer ${this.token}`; + } - async getConversations() { - return this.request('/chat/conversations'); - } + const response = await fetch(`${API_BASE}/chat/upload`, { + method: 'POST', + headers, + body: formData, + }); - async getConversation(id) { - return this.request(`/chat/conversations/${id}`); - } + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: 'Upload failed' })); + throw new Error(error.detail || 'Upload failed'); + } - async deleteConversation(id) { - return this.request(`/chat/conversations/${id}`, { - method: 'DELETE' - }); - } + return response.json(); + } - // File upload - async uploadFile(file) { - const formData = new FormData(); - formData.append('file', file); + async getFiles() { + return this.request('/chat/files'); + } - const response = await fetch(`${this.baseUrl}/upload`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${this.getToken()}` - }, - body: formData - }); + async deleteFile(fileId) { + return this.request(`/chat/files/${fileId}`, { method: 'DELETE' }); + } - if (!response.ok) { - const error = await response.json().catch(() => ({ detail: 'Upload failed' })); - throw new Error(error.detail || 'Upload failed'); - } + async getAdminStats() { + return this.request('/admin/stats'); + } - return response.json(); - } + async getUsers() { + return this.request('/admin/users'); + } - // Admin endpoints - async getAdminStats() { - return this.request('/admin/stats'); - } + async updateUser(userId, data) { + return this.request(`/admin/users/${userId}`, { + method: 'PUT', + body: JSON.stringify(data), + }); + } - async getUsers() { - return this.request('/admin/users'); - } + async deleteUser(userId) { + return this.request(`/admin/users/${userId}`, { method: 'DELETE' }); + } - async updateUser(id, data) { - return this.request(`/admin/users/${id}`, { - method: 'PUT', - body: JSON.stringify(data) - }); - } + async getAdminEndpoints() { + return this.request('/admin/endpoints'); + } - async deleteUser(id) { - return this.request(`/admin/users/${id}`, { - method: 'DELETE' - }); - } + async createEndpoint(data) { + return this.request('/admin/endpoints', { + method: 'POST', + body: JSON.stringify(data), + }); + } - logout() { - this.clearToken(); - } + async updateEndpoint(endpointId, data) { + return this.request(`/admin/endpoints/${endpointId}`, { + method: 'PUT', + body: JSON.stringify(data), + }); + } + + async deleteEndpoint(endpointId) { + return this.request(`/admin/endpoints/${endpointId}`, { method: 'DELETE' }); + } } export const api = new ApiClient(); diff --git a/frontend/src/lib/stores.js b/frontend/src/lib/stores.js deleted file mode 100644 index d3f01bf..0000000 --- a/frontend/src/lib/stores.js +++ /dev/null @@ -1,28 +0,0 @@ -import { writable } from 'svelte/store'; -import { api } from './api'; - -export const user = writable(null); -export const isLoading = writable(true); - -export async function initializeAuth() { - isLoading.set(true); - const token = api.getToken(); - - if (token) { - try { - const userData = await api.getMe(); - user.set(userData); - } catch (error) { - console.error('Failed to fetch user:', error); - api.clearToken(); - user.set(null); - } - } - - isLoading.set(false); -} - -export function logout() { - api.logout(); - user.set(null); -} diff --git a/frontend/src/lib/stores.svelte.js b/frontend/src/lib/stores.svelte.js new file mode 100644 index 0000000..1d04770 --- /dev/null +++ b/frontend/src/lib/stores.svelte.js @@ -0,0 +1,39 @@ +import { api } from './api.js'; + +// Svelte 5 runes-based stores +let user = $state(null); +let isLoading = $state(true); + +export function getUser() { + return user; +} + +export function getIsLoading() { + return isLoading; +} + +export async function initializeAuth() { + try { + if (api.token) { + const userData = await api.getMe(); + user = userData; + } + } catch (e) { + api.setToken(null); + user = null; + } finally { + isLoading = false; + } +} + +export function setUser(userData) { + user = userData; +} + +export function logout() { + api.setToken(null); + user = null; + if (typeof window !== 'undefined') { + window.location.href = '/login'; + } +} diff --git a/frontend/src/routes/+layout.js b/frontend/src/routes/+layout.js index a3d1578..83addb7 100644 --- a/frontend/src/routes/+layout.js +++ b/frontend/src/routes/+layout.js @@ -1 +1,2 @@ export const ssr = false; +export const prerender = false; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 5b77da6..4181e0a 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,233 +1,251 @@ -{#if $isLoading} -
-
-
-{:else if !$user && !isAuthPage} - {#if typeof window !== 'undefined'} - - {/if} +{#if isLoading} +
+
+
+{:else if user} +
+ +
+ + + + +
+ {user.username} + +
+
+ + + + + +
+ +
+ + + {#if sidebarOpen} + + {/if} +
{:else} -
- {#if $user} - - {/if} - -
- -
-
+ {/if} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 3190c73..1ee028a 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,43 +1,48 @@ -
-
-

Redirecting...

+
+
+

Loading Moxiegen...

diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 5d01374..eab8afa 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -1,557 +1,669 @@
-
-

Admin Panel

-
+

Admin Panel

-
- - - -
+
+ + + +
- {#if error} -
{error}
- {/if} + {#if loading} +
+
+
+ {:else} + {#if activeTab === 'stats'} +
+
+
{stats.total_users}
+
Total Users
+
+
+
{stats.total_endpoints}
+
AI Endpoints
+
+
+
{stats.active_endpoints}
+
Active Endpoints
+
+
+
{stats.total_messages}
+
Total Messages
+
+
+ {/if} - {#if loading} -
-
-
- {:else} - - {#if activeTab === 'dashboard'} -
-
-
-
- - - - - - -
-
- {stats?.total_users || 0} - Total Users -
-
+ {#if activeTab === 'users'} +
+

Users

+
+
+ + + + + + + + + + + + + + {#each users as userRow} + + + + + + + + + + {/each} + +
IDUsernameEmailRoleStatusCreatedActions
{userRow.id}{userRow.username}{userRow.email} + + {userRow.role} + + + + {userRow.is_active ? 'Active' : 'Inactive'} + + {new Date(userRow.created_at).toLocaleDateString()} +
+ + {#if userRow.role !== 'admin'} + + {/if} +
+
+
+ {/if} -
-
- - - - - -
-
- {stats?.total_endpoints || 0} - Endpoints -
-
- -
-
- - - -
-
- {stats?.total_messages || 0} - Messages -
-
- -
-
- - - -
-
- {stats?.active_users || 0} - Active Users -
-
-
-
- {/if} - - - {#if activeTab === 'users'} -
-
- - - - - - - - - - - - - {#each users as user (user.id)} - - - - - - - - - {/each} - -
IDUsernameEmailAdminCreatedActions
{user.id} - {#if editingUser === user.id} - - {:else} - {user.username} - {/if} - - {#if editingUser === user.id} - - {:else} - {user.email} - {/if} - - {#if editingUser === user.id} - - {:else} - {#if user.is_admin} - Admin - {/if} - {/if} - {new Date(user.created_at).toLocaleDateString()} -
- {#if editingUser === user.id} - - - {:else} - - - {/if} -
-
-
-
- {/if} - - - {#if activeTab === 'endpoints'} -
-
- -
- -
- {#each endpoints as endpoint (endpoint.id)} -
-
-

{endpoint.name}

- - {endpoint.is_active ? 'Active' : 'Inactive'} - -
-
-

URL: {endpoint.url}

-

Model: {endpoint.model || 'Default'}

-
-
- - -
-
- {/each} -
-
- {/if} - {/if} + {#if activeTab === 'endpoints'} +
+

AI Endpoints

+ +
+
+ {#each endpoints as endpoint} +
+
+

{endpoint.name}

+
+ {#if endpoint.is_default} + Default + {/if} + {#if endpoint.is_active} + Active + {:else} + Inactive + {/if} +
+
+
+

Type: {endpoint.endpoint_type}

+

Model: {endpoint.model_name}

+

URL: {endpoint.base_url}

+
+
+ + +
+
+ {:else} +

No endpoints configured. Add one to get started.

+ {/each} +
+ {/if} + {/if}
+{#if showUserModal} + +{/if} + {#if showEndpointModal} - + {/if} diff --git a/frontend/src/routes/chat/+page.svelte b/frontend/src/routes/chat/+page.svelte index b64e70a..4946d77 100644 --- a/frontend/src/routes/chat/+page.svelte +++ b/frontend/src/routes/chat/+page.svelte @@ -1,420 +1,593 @@
- - - -
- -
- -
+ +
+ +
+ {#if loading} +
+
+

Loading chat...

+
+ {:else if messages.length === 0} +
+ +

Start a Conversation

+

Send a message to begin chatting with AI

+
+ {:else} + {#each messages as message} +
+
+ + {message.role === 'user' ? 'You' : 'Assistant'} + + {#if message.model} + {message.model} + {/if} +
+
+ {#if message.role === 'user'} + {message.content} + {:else} +
+ {@html renderMarkdown(message.content)} +
+ {/if} +
+
+ {/each} + {/if} +
- -
- {#if messages.length === 0} -
- -

Start a conversation

-

Select an endpoint and type a message to begin

-
- {:else} - {#each messages as message (message.id || Math.random())} -
-
- {#if message.role === 'user'} -

{message.content}

- {:else} -
- {@html renderMarkdown(message.content)} -
- {/if} -
-
- {/each} - {/if} + +
+ {#if selectedFiles.length > 0} +
+ {#each selectedFiles as file} +
+ {file.original_filename} + +
+ {/each} +
+ {/if} - {#if sending} -
-
-
- - - -
-
-
- {/if} -
+
{ e.preventDefault(); sendMessage(); }} class="input-form"> +
+ + - {#if error} -
{error}
- {/if} + - -
- - - - e.key === 'Enter' && !e.shiftKey && sendMessage()} - disabled={!selectedEndpoint || sending} - /> - - -
-
+ +
+ +
+
diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index ddf41a0..5b3f8e0 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -1,158 +1,169 @@ -