From 298fb9681ef4d65efaf1ff56463e265a2e641756 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Wed, 7 Jan 2026 14:53:06 +0100 Subject: [PATCH] feat: add project detail page with new chat creation - Add /projects/[id] route with project header, stats, and tabbed UI - Add "New chat in [Project]" input that creates chats inside project - Add project conversation search and filtering - Add file upload with drag-and-drop for project documents - Update ProjectFolder to navigate to project page on click - Add initialMessage prop to ChatWindow for auto-sending first message - Support ?firstMessage= query param in chat page for project chats - Add projectId support to vector-store for document association --- .../src/lib/components/chat/ChatWindow.svelte | 26 +- .../components/layout/ProjectFolder.svelte | 83 +-- frontend/src/lib/memory/vector-store.ts | 8 +- frontend/src/routes/chat/[id]/+page.svelte | 16 +- frontend/src/routes/chat/[id]/+page.ts | 9 +- .../src/routes/projects/[id]/+page.svelte | 543 ++++++++++++++++++ 6 files changed, 644 insertions(+), 41 deletions(-) create mode 100644 frontend/src/routes/projects/[id]/+page.svelte diff --git a/frontend/src/lib/components/chat/ChatWindow.svelte b/frontend/src/lib/components/chat/ChatWindow.svelte index f68a28a..d92f814 100644 --- a/frontend/src/lib/components/chat/ChatWindow.svelte +++ b/frontend/src/lib/components/chat/ChatWindow.svelte @@ -43,6 +43,7 @@ * - mode: 'new' for new chat page, 'conversation' for existing conversations * - onFirstMessage: callback for when first message is sent in 'new' mode * - conversation: conversation metadata when in 'conversation' mode + * - initialMessage: auto-send this message when conversation loads (for new project chats) */ interface Props { mode?: 'new' | 'conversation'; @@ -50,13 +51,16 @@ conversation?: Conversation | null; /** Bindable prop for thinking mode - synced with parent in 'new' mode */ thinkingEnabled?: boolean; + /** Initial message to auto-send when conversation loads */ + initialMessage?: string | null; } let { mode = 'new', onFirstMessage, conversation, - thinkingEnabled = $bindable(true) + thinkingEnabled = $bindable(true), + initialMessage = null }: Props = $props(); // Local state for abort controller @@ -127,6 +131,26 @@ } }); + // Track if initial message has been sent to prevent re-sending + let initialMessageSent = $state(false); + + // Auto-send initial message when conversation is ready + $effect(() => { + if ( + mode === 'conversation' && + initialMessage && + !initialMessageSent && + chatState.conversationId === conversation?.id && + !chatState.isStreaming + ) { + initialMessageSent = true; + // Small delay to ensure UI is ready + setTimeout(() => { + handleSendMessage(initialMessage); + }, 100); + } + }); + /** * Check if knowledge base has any documents */ diff --git a/frontend/src/lib/components/layout/ProjectFolder.svelte b/frontend/src/lib/components/layout/ProjectFolder.svelte index 343f6cb..f2eac89 100644 --- a/frontend/src/lib/components/layout/ProjectFolder.svelte +++ b/frontend/src/lib/components/layout/ProjectFolder.svelte @@ -7,6 +7,7 @@ import type { Conversation } from '$lib/types/conversation.js'; import { projectsState, chatState } from '$lib/stores'; import ConversationItem from './ConversationItem.svelte'; + import { goto } from '$app/navigation'; interface Props { project: Project; @@ -26,6 +27,13 @@ await projectsState.toggleCollapse(project.id); } + /** Navigate to project page */ + function handleOpenProject(e: MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + goto(`/projects/${project.id}`); + } + /** Handle project settings click */ function handleSettings(e: MouseEvent) { e.preventDefault(); @@ -35,42 +43,53 @@
- -
e.key === 'Enter' && handleToggle(e as unknown as MouseEvent)} - class="group flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-theme-secondary/60" - > - - +
+ + - - + - - + + + + - - - {project.name} - + + + {project.name} + + + + {conversations.length} diff --git a/frontend/src/lib/memory/vector-store.ts b/frontend/src/lib/memory/vector-store.ts index 26ec36c..6e19a23 100644 --- a/frontend/src/lib/memory/vector-store.ts +++ b/frontend/src/lib/memory/vector-store.ts @@ -24,6 +24,8 @@ export interface AddDocumentOptions { embeddingModel?: string; /** Callback for progress updates */ onProgress?: (current: number, total: number) => void; + /** Project ID if document belongs to a project */ + projectId?: string; } /** @@ -39,7 +41,8 @@ export async function addDocument( const { chunkOptions, embeddingModel = DEFAULT_EMBEDDING_MODEL, - onProgress + onProgress, + projectId } = options; const documentId = crypto.randomUUID(); @@ -88,7 +91,8 @@ export async function addDocument( createdAt: now, updatedAt: now, chunkCount: storedChunks.length, - embeddingModel + embeddingModel, + projectId: projectId ?? null }; // Store in database diff --git a/frontend/src/routes/chat/[id]/+page.svelte b/frontend/src/routes/chat/[id]/+page.svelte index dba079a..9818b3f 100644 --- a/frontend/src/routes/chat/[id]/+page.svelte +++ b/frontend/src/routes/chat/[id]/+page.svelte @@ -4,7 +4,8 @@ * Displays an existing conversation with chat window */ - import { goto } from '$app/navigation'; + import { goto, replaceState } from '$app/navigation'; + import { page } from '$app/stores'; import { chatState, conversationsState, modelsState } from '$lib/stores'; import { getConversationFull } from '$lib/storage'; import ChatWindow from '$lib/components/chat/ChatWindow.svelte'; @@ -20,6 +21,17 @@ let currentConversationId = $state(null); let isLoading = $state(false); + // Extract first message from data and clear from URL + let initialMessage = $state(data.firstMessage); + $effect(() => { + // Clear firstMessage from URL to keep it clean + if (data.firstMessage && $page.url.searchParams.has('firstMessage')) { + const url = new URL($page.url); + url.searchParams.delete('firstMessage'); + replaceState(url, {}); + } + }); + /** * Load conversation into chat state when URL changes */ @@ -135,6 +147,6 @@
{:else} - + {/if}
diff --git a/frontend/src/routes/chat/[id]/+page.ts b/frontend/src/routes/chat/[id]/+page.ts index a9bf659..b50adbc 100644 --- a/frontend/src/routes/chat/[id]/+page.ts +++ b/frontend/src/routes/chat/[id]/+page.ts @@ -6,7 +6,7 @@ import { error } from '@sveltejs/kit'; import type { PageLoad } from './$types'; -export const load: PageLoad = async ({ params }) => { +export const load: PageLoad = async ({ params, url }) => { const { id } = params; // Validate that ID looks like a UUID @@ -18,10 +18,11 @@ export const load: PageLoad = async ({ params }) => { }); } - // TODO: In the future, load conversation from IndexedDB here - // For now, just return the ID and let the page component handle state + // Extract firstMessage query param (for new chats from project page) + const firstMessage = url.searchParams.get('firstMessage') || null; return { - conversationId: id + conversationId: id, + firstMessage }; }; diff --git a/frontend/src/routes/projects/[id]/+page.svelte b/frontend/src/routes/projects/[id]/+page.svelte new file mode 100644 index 0000000..c468b09 --- /dev/null +++ b/frontend/src/routes/projects/[id]/+page.svelte @@ -0,0 +1,543 @@ + + + + {project?.name || 'Project'} - Vessel + + +{#if project} +
+ +
+
+
+
+ + + + +
+

{project.name}

+ {#if project.description} +

{project.description}

+ {/if} +
+
+ +
+ +
+ + + + {projectConversations.length} chats +
+
+ + + + {documents.length} files +
+ + + +
+
+
+
+ + +
+
+ +
+ +
+
+ Model: {modelsState.selectedId || 'None selected'} +
+ +
+
+ + +
+
+ + + +
+
+ + + {#if activeTab === 'chats'} + +
+
+ + + + +
+
+ + + {#if filteredConversations.length === 0} +
+ + + + {#if searchQuery} +

No chats match your search

+ {:else} +

No chats in this project yet

+

Start a new chat above to get started

+ {/if} +
+ {:else} + + {/if} + {:else if activeTab === 'files'} + +
{ e.preventDefault(); dragOver = true; }} + ondragleave={() => dragOver = false} + ondrop={handleDrop} + > + + + + +

+ {#if isUploading} + Uploading files... + {:else} + Drag & drop files here, or + + {/if} +

+

+ Text files, code, markdown, JSON, etc. +

+
+ + + {#if documents.length === 0} +
+

No files in this project

+
+ {:else} +
+ {#each documents as doc (doc.id)} +
+
+ + + +
+

{doc.name}

+

+ {formatSize(doc.size)} +

+
+
+ +
+ {/each} +
+ {/if} + {:else if activeTab === 'links'} + + {#if links.length === 0} +
+ + + +

No reference links

+

Add links in project settings

+ +
+ {:else} + + {/if} + {/if} +
+
+
+{:else} + +
+
+

Loading project...

+
+
+{/if} + + + showProjectModal = false} + {projectId} +/>