diff --git a/apps/chat/apps/web/src/lib/components/chat/ChatLayout.svelte b/apps/chat/apps/web/src/lib/components/chat/ChatLayout.svelte index 61f23d4c1..8b8a31749 100644 --- a/apps/chat/apps/web/src/lib/components/chat/ChatLayout.svelte +++ b/apps/chat/apps/web/src/lib/components/chat/ChatLayout.svelte @@ -42,6 +42,76 @@ : conversations ); + // Split into pinned and unpinned + let pinnedConversations = $derived(filteredConversations.filter((conv) => conv.isPinned)); + let unpinnedConversations = $derived(filteredConversations.filter((conv) => !conv.isPinned)); + + // Date section types + type DateSection = 'today' | 'yesterday' | 'thisWeek' | 'thisMonth' | 'older'; + + const sectionLabels: Record = { + today: 'Heute', + yesterday: 'Gestern', + thisWeek: 'Diese Woche', + thisMonth: 'Dieser Monat', + older: 'Älter' + }; + + function getDateSection(dateString: string): DateSection { + const date = new Date(dateString); + const now = new Date(); + + // Reset time to compare just dates + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + const dateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + + if (dateOnly.getTime() === today.getTime()) { + return 'today'; + } + if (dateOnly.getTime() === yesterday.getTime()) { + return 'yesterday'; + } + + // This week (last 7 days) + const weekAgo = new Date(today); + weekAgo.setDate(weekAgo.getDate() - 7); + if (dateOnly > weekAgo) { + return 'thisWeek'; + } + + // This month + const monthAgo = new Date(today); + monthAgo.setDate(monthAgo.getDate() - 30); + if (dateOnly > monthAgo) { + return 'thisMonth'; + } + + return 'older'; + } + + // Group unpinned conversations by date sections + let groupedConversations = $derived(() => { + const groups: Record = { + today: [], + yesterday: [], + thisWeek: [], + thisMonth: [], + older: [] + }; + + for (const conv of unpinnedConversations) { + const section = getDateSection(conv.updatedAt || conv.createdAt); + groups[section].push(conv); + } + + return groups; + }); + + const sectionOrder: DateSection[] = ['today', 'yesterday', 'thisWeek', 'thisMonth', 'older']; + // Resizer handlers function startResize(e: MouseEvent) { e.preventDefault(); @@ -66,7 +136,7 @@ window.addEventListener('mouseup', stopResize); if (authStore.user) { - conversationsStore.loadConversations(authStore.user.id); + conversationsStore.loadConversations(); } }); @@ -240,81 +310,160 @@ {/if} {:else} - {#each filteredConversations as conv (conv.id)} - - -
- {#if conv.isPinned} - - {:else} - - {/if} -

- {conv.title || 'Neue Konversation'} -

-
- - -

- {getPreview(conv.title)} -

- - -
- - {formatDate(conv.updatedAt || conv.createdAt)} - -
- {#if conv.documentMode} - - Dokument - - {/if} - - + {/if} + + + {#each sectionOrder as section} + {@const convs = groupedConversations()[section]} + {#if convs.length > 0} + - + {/if} {/each} {/if}
diff --git a/apps/chat/apps/web/src/lib/services/api.ts b/apps/chat/apps/web/src/lib/services/api.ts index c0dee794b..f6bddcb35 100644 --- a/apps/chat/apps/web/src/lib/services/api.ts +++ b/apps/chat/apps/web/src/lib/services/api.ts @@ -7,6 +7,30 @@ import { browser } from '$app/environment'; import { env } from '$env/dynamic/public'; +import type { + Conversation, + Message, + Template, + Space, + SpaceMember, + Document, + AIModel, + ChatMessage, + ChatCompletionResponse, +} from '@chat/types'; + +// Re-export types for convenience +export type { + Conversation, + Message, + Template, + Space, + SpaceMember, + Document, + AIModel, + ChatMessage, + ChatCompletionResponse, +}; const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3002'; @@ -63,30 +87,6 @@ async function fetchApi( // ============ Conversation API ============ -export type Conversation = { - id: string; - userId: string; - modelId: string; - templateId?: string; - spaceId?: string; - title?: string; - conversationMode: 'free' | 'guided' | 'template'; - documentMode: boolean; - isArchived: boolean; - isPinned: boolean; - createdAt: string; - updatedAt: string; -}; - -export type Message = { - id: string; - conversationId: string; - sender: 'user' | 'assistant' | 'system'; - messageText: string; - createdAt: string; - updatedAt: string; -}; - export const conversationApi = { async getConversations(spaceId?: string): Promise { const query = spaceId ? `?spaceId=${spaceId}` : ''; @@ -230,21 +230,6 @@ export const conversationApi = { // ============ Template API ============ -export type Template = { - id: string; - userId: string; - name: string; - description: string | null; - systemPrompt: string; - initialQuestion: string | null; - modelId: string | null; - color: string; - isDefault: boolean; - documentMode: boolean; - createdAt: string; - updatedAt: string; -}; - export const templateApi = { async getTemplates(): Promise { const { data, error } = await fetchApi('/templates'); @@ -341,29 +326,6 @@ export const templateApi = { // ============ Space API ============ -export type Space = { - id: string; - ownerId: string; - name: string; - description?: string; - isArchived: boolean; - createdAt: string; - updatedAt: string; -}; - -export type SpaceMember = { - id: string; - spaceId: string; - userId: string; - role: 'owner' | 'admin' | 'member' | 'viewer'; - invitationStatus: 'pending' | 'accepted' | 'declined'; - invitedBy?: string; - invitedAt: string; - joinedAt?: string; - createdAt: string; - updatedAt: string; -}; - export const spaceApi = { async getUserSpaces(): Promise { const { data, error } = await fetchApi('/spaces'); @@ -520,15 +482,6 @@ export const spaceApi = { // ============ Document API ============ -export type Document = { - id: string; - conversationId: string; - version: number; - content: string; - createdAt: string; - updatedAt: string; -}; - export const documentApi = { async getLatestDocument(conversationId: string): Promise { const { data, error } = await fetchApi( @@ -604,26 +557,9 @@ export const documentApi = { // ============ Model API ============ -export type Model = { - id: string; - name: string; - description?: string; - provider: 'gemini' | 'azure' | 'openai'; - parameters?: { - deployment?: string; - temperature?: number; - max_tokens?: number; - top_p?: number; - }; - isActive: boolean; - isDefault: boolean; - createdAt: string; - updatedAt: string; -}; - export const modelApi = { - async getModels(): Promise { - const { data, error } = await fetchApi('/models'); + async getModels(): Promise { + const { data, error } = await fetchApi('/models'); if (error) { console.error('Error loading models:', error); return []; @@ -631,8 +567,8 @@ export const modelApi = { return data || []; }, - async getModel(id: string): Promise { - const { data, error } = await fetchApi(`/models/${id}`); + async getModel(id: string): Promise { + const { data, error } = await fetchApi(`/models/${id}`); if (error) { console.error('Error loading model:', error); return null; @@ -643,20 +579,6 @@ export const modelApi = { // ============ Chat API ============ -export type ChatMessage = { - role: 'system' | 'user' | 'assistant'; - content: string; -}; - -export type ChatCompletionResponse = { - content: string; - usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; -}; - export const chatApi = { async createCompletion(options: { messages: ChatMessage[]; diff --git a/apps/chat/apps/web/src/lib/services/chat.ts b/apps/chat/apps/web/src/lib/services/chat.ts index bd6ed7e10..04227ba07 100644 --- a/apps/chat/apps/web/src/lib/services/chat.ts +++ b/apps/chat/apps/web/src/lib/services/chat.ts @@ -7,10 +7,10 @@ import { modelApi, type ChatMessage, type ChatCompletionResponse, - type Model, + type AIModel, } from './api'; -export type { ChatMessage, ChatCompletionResponse }; +export type { ChatMessage, ChatCompletionResponse, AIModel }; export interface ChatCompletionRequest { messages: ChatMessage[]; @@ -23,7 +23,7 @@ export const chatService = { /** * Get available AI models */ - async getModels(): Promise { + async getModels(): Promise { return modelApi.getModels(); }, diff --git a/apps/chat/apps/web/src/lib/services/conversation.ts b/apps/chat/apps/web/src/lib/services/conversation.ts index 3416eb7e7..6a64144f5 100644 --- a/apps/chat/apps/web/src/lib/services/conversation.ts +++ b/apps/chat/apps/web/src/lib/services/conversation.ts @@ -1,5 +1,8 @@ /** * Conversation Service - CRUD operations via Backend API + * + * Note: userId is derived from JWT token on the backend, + * so we don't need to pass it from the frontend. */ import { conversationApi, chatApi, type Conversation, type Message, type ChatMessage } from './api'; @@ -10,36 +13,35 @@ export const conversationService = { /** * Create a new conversation */ - async createConversation( - userId: string, - modelId: string, - mode: 'free' | 'guided' | 'template' = 'free', - templateId?: string, - documentMode: boolean = false, - spaceId?: string - ): Promise { + async createConversation(options: { + modelId: string; + mode?: 'free' | 'guided' | 'template'; + templateId?: string; + documentMode?: boolean; + spaceId?: string; + }): Promise { const conversation = await conversationApi.createConversation({ - modelId, - conversationMode: mode, - templateId, - documentMode, - spaceId, + modelId: options.modelId, + conversationMode: options.mode ?? 'free', + templateId: options.templateId, + documentMode: options.documentMode ?? false, + spaceId: options.spaceId, }); return conversation?.id || null; }, /** - * Get all active conversations for a user + * Get all active conversations */ - async getConversations(userId: string, spaceId?: string): Promise { + async getConversations(spaceId?: string): Promise { return conversationApi.getConversations(spaceId); }, /** * Get archived conversations */ - async getArchivedConversations(userId: string): Promise { + async getArchivedConversations(): Promise { return conversationApi.getArchivedConversations(); }, diff --git a/apps/chat/apps/web/src/lib/stores/conversations.svelte.ts b/apps/chat/apps/web/src/lib/stores/conversations.svelte.ts index 370e185fc..1fb0cf9a4 100644 --- a/apps/chat/apps/web/src/lib/stores/conversations.svelte.ts +++ b/apps/chat/apps/web/src/lib/stores/conversations.svelte.ts @@ -27,14 +27,14 @@ export const conversationsStore = { }, /** - * Load conversations for a user + * Load conversations (userId is derived from JWT on backend) */ - async loadConversations(userId: string, spaceId?: string) { + async loadConversations(spaceId?: string) { isLoading = true; error = null; try { - conversations = await conversationService.getConversations(userId, spaceId); + conversations = await conversationService.getConversations(spaceId); } catch (e) { error = e instanceof Error ? e.message : 'Failed to load conversations'; conversations = []; @@ -46,12 +46,12 @@ export const conversationsStore = { /** * Load archived conversations */ - async loadArchivedConversations(userId: string) { + async loadArchivedConversations() { isLoading = true; error = null; try { - archivedConversations = await conversationService.getArchivedConversations(userId); + archivedConversations = await conversationService.getArchivedConversations(); } catch (e) { error = e instanceof Error ? e.message : 'Failed to load archived conversations'; archivedConversations = []; diff --git a/apps/chat/apps/web/src/routes/(protected)/archive/+page.svelte b/apps/chat/apps/web/src/routes/(protected)/archive/+page.svelte index 4b1bfaa79..a46146872 100644 --- a/apps/chat/apps/web/src/routes/(protected)/archive/+page.svelte +++ b/apps/chat/apps/web/src/routes/(protected)/archive/+page.svelte @@ -10,7 +10,7 @@ onMount(async () => { if (authStore.user) { - await conversationsStore.loadArchivedConversations(authStore.user.id); + await conversationsStore.loadArchivedConversations(); conversations = conversationsStore.archivedConversations; } isLoading = false; diff --git a/apps/chat/apps/web/src/routes/(protected)/chat/+page.svelte b/apps/chat/apps/web/src/routes/(protected)/chat/+page.svelte index eb74595aa..e45b992f2 100644 --- a/apps/chat/apps/web/src/routes/(protected)/chat/+page.svelte +++ b/apps/chat/apps/web/src/routes/(protected)/chat/+page.svelte @@ -72,13 +72,12 @@ const docMode = selectedTemplate?.documentMode || documentMode; // Create new conversation - const conversationId = await conversationService.createConversation( - authStore.user.id, - modelToUse, - mode as 'free' | 'guided' | 'template', - selectedTemplate?.id, - docMode - ); + const conversationId = await conversationService.createConversation({ + modelId: modelToUse, + mode: mode as 'free' | 'guided' | 'template', + templateId: selectedTemplate?.id, + documentMode: docMode, + }); if (!conversationId) { throw new Error('Konversation konnte nicht erstellt werden'); @@ -92,7 +91,7 @@ ); // Reload conversations list - await conversationsStore.loadConversations(authStore.user.id); + await conversationsStore.loadConversations(); // Navigate to the new conversation goto(`/chat/${conversationId}`); diff --git a/apps/chat/apps/web/src/routes/(protected)/spaces/[id]/+page.svelte b/apps/chat/apps/web/src/routes/(protected)/spaces/[id]/+page.svelte index 7a2446525..eb0d346da 100644 --- a/apps/chat/apps/web/src/routes/(protected)/spaces/[id]/+page.svelte +++ b/apps/chat/apps/web/src/routes/(protected)/spaces/[id]/+page.svelte @@ -36,7 +36,7 @@ // Load conversations in this space if (authStore.user) { - conversations = await conversationService.getConversations(authStore.user.id, spaceId); + conversations = await conversationService.getConversations(spaceId); } // Load models @@ -56,14 +56,11 @@ async function handleNewChat() { if (!authStore.user || !selectedModelId) return; - const conversationId = await conversationService.createConversation( - authStore.user.id, - selectedModelId, - 'free', - undefined, - false, - spaceId - ); + const conversationId = await conversationService.createConversation({ + modelId: selectedModelId, + mode: 'free', + spaceId, + }); if (conversationId) { goto(`/chat/${conversationId}`); diff --git a/apps/chat/apps/web/src/routes/(protected)/templates/+page.svelte b/apps/chat/apps/web/src/routes/(protected)/templates/+page.svelte index 6757b6d76..68573e234 100644 --- a/apps/chat/apps/web/src/routes/(protected)/templates/+page.svelte +++ b/apps/chat/apps/web/src/routes/(protected)/templates/+page.svelte @@ -48,16 +48,15 @@ if (!template || !authStore.user) return; // Create a new conversation with this template - const conversationId = await conversationService.createConversation( - authStore.user.id, - template.modelId || '550e8400-e29b-41d4-a716-446655440101', // Default to Gemini 2.5 Flash - 'template', - template.id, - template.documentMode - ); + const conversationId = await conversationService.createConversation({ + modelId: template.modelId || '550e8400-e29b-41d4-a716-446655440101', // Default to Gemini 2.5 Flash + mode: 'template', + templateId: template.id, + documentMode: template.documentMode, + }); if (conversationId) { - await conversationsStore.loadConversations(authStore.user.id); + await conversationsStore.loadConversations(); goto(`/chat/${conversationId}`); } }