The AI chat interface is the core feature of Filebright, allowing you to have natural conversations with your documents. Ask questions in plain language and receive contextual answers extracted from your uploaded files.
How it works
When you send a question through the chat interface, Filebright:
- Converts your query to a vector embedding using the same embedding model used for document chunks
- Searches your document chunks using MongoDB’s vector search to find the most relevant content
- Retrieves the top 3 matching chunks from your documents
- Sends the context to an LLM (Large Language Model) which generates a natural language response
- Returns the answer to you in the chat interface
Starting a conversation
The chat interface is located on the right side of your dashboard. When you first arrive, you’ll see a welcome message.
Upload documents first
Before you can chat, you need to have at least one document in “completed” status. Documents that are still processing or have failed will not be searchable.
Type your question
Click into the input field at the bottom of the chat interface and type your question. Questions can be up to 1000 characters long.// ChatInterface.vue:7-12
const handleSend = () => {
if (!query.value.trim() || chatStore.isLoading) return;
chatStore.sendMessage(query.value);
query.value = "";
};
Send and receive
Press Enter or click the send button. Your message will appear in the chat, followed by the AI assistant’s response.
The chat interface shows a loading spinner while the AI is processing your query. During this time, the input is disabled to prevent multiple simultaneous requests.
Query processing
When you send a message, it’s sent to the /api/chat endpoint:
// ChatController.php:17-31
public function chat(Request $request)
{
$request->validate([
'query' => 'required|string|max:1000'
]);
$response = $this->ragService->answer(
$request->input('query'),
$request->user()->id
);
return response()->json([
'response' => $response
]);
}
The RAGService handles the entire retrieval and generation process.
Step 1: Query embedding
Your question is converted to a vector embedding:
// RAGService.php:18-24
public function answer(string $query, int $userId): string
{
$queryEmbedding = $this->embeddingService->getEmbedding($query);
if (empty($queryEmbedding)) {
return "I'm sorry, I couldn't process your request at the moment.";
}
// ...
}
Step 2: Vector similarity search
MongoDB performs a vector search to find the most semantically similar chunks from your documents:
// RAGService.php:37-55
protected function retrieveContext(array $embedding, int $userId): \Illuminate\Support\Collection
{
return DocumentChunk::raw(function ($collection) use ($embedding, $userId) {
return $collection->aggregate([
[
'$vectorSearch' => [
'index' => 'vector_index',
'path' => 'embedding',
'queryVector' => $embedding,
'numCandidates' => 100,
'limit' => 3,
'filter' => [
'metadata.user_id' => $userId
]
]
]
]);
});
}
Key parameters:
- numCandidates: 100 - Considers top 100 candidates before narrowing down
- limit: 3 - Returns the 3 most relevant chunks
- filter - Ensures you only search your own documents
Vector search uses cosine similarity to find chunks that are semantically related to your query, not just keyword matches. This means you can ask questions in different words than what appears in the document.
Step 3: LLM response generation
The retrieved chunks are combined into a context string and sent to the LLM:
// RAGService.php:26-34
$chunks = $this->retrieveContext($queryEmbedding, $userId);
if ($chunks->isEmpty()) {
return "I couldn't find any relevant information in your documents to answer that.";
}
$context = $chunks->pluck('content')->implode("\n\n---\n\n");
return $this->getLLMResponse($query, $context);
The LLM receives a carefully crafted prompt:
// RAGService.php:57-88
protected function getLLMResponse(string $query, string $context): string
{
$apiKey = config('services.openrouter.key');
$model = config('services.openrouter.chat_model', 'openai/gpt-3.5-turbo');
$prompt = "You are a helpful assistant. Use the following pieces of retrieved context to answer the user's question.\n\n"
. "Context:\n" . $context . "\n\n"
. "Question: " . $query . "\n\n"
. "Answer:";
try {
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $apiKey,
'Content-Type' => 'application/json',
])->post('https://openrouter.ai/api/v1/chat/completions', [
'model' => $model,
'messages' => [
['role' => 'user', 'content' => $prompt]
],
]);
if ($response->successful()) {
return $response->json('choices.0.message.content') ?? "Error retrieving response.";
}
Log::error("OpenRouter API Error: " . $response->body());
return "Error communicating with AI service.";
} catch (\Exception $e) {
Log::error("RAG Service Exception: " . $e->getMessage());
return "An unexpected error occurred.";
}
}
Chat interface features
Message history
All messages in your current session are displayed in the chat feed:
- User messages - Displayed on the right with a user avatar
- Assistant messages - Displayed on the left with a bot avatar
- System messages - Error or informational messages (e.g., “No relevant information found”)
// ChatInterface.vue:24-40
<div
v-for="(msg, idx) in chatStore.messages"
:key="idx"
class="message-wrapper"
:class="msg.role"
>
<div class="avatar">
<Bot
v-if="msg.role === 'assistant' || msg.role === 'system'"
:size="18"
/>
<User v-else :size="18" />
</div>
<div class="message-bubble glass">
<div class="bubble-content">{{ msg.content }}</div>
</div>
</div>
Loading states
While the AI is processing your query, you’ll see:
- A loading spinner in place of the assistant’s message
- The input field and send button are disabled
- The send button shows reduced opacity
// ChatInterface.vue:42-49
<div v-if="chatStore.isLoading" class="message-wrapper assistant">
<div class="avatar">
<Bot :size="18" />
</div>
<div class="message-bubble glass loading">
<Loader2 class="spin" :size="18" />
</div>
</div>
Empty state
When you haven’t sent any messages yet, you’ll see a helpful prompt:
// ChatInterface.vue:18-22
<div v-if="chatStore.messages.length === 0" class="empty-chat">
<Bot :size="48" />
<h2>How can I help you today?</h2>
<p>Ask anything about your uploaded documents.</p>
</div>
Response scenarios
Successful response
When relevant content is found in your documents, the LLM generates a natural answer based on the retrieved context.
No relevant content
If your query doesn’t match any document content semantically:
I couldn't find any relevant information in your documents to answer that.
Processing error
If the embedding service fails:
I'm sorry, I couldn't process your request at the moment.
API error
If the OpenRouter API fails:
Error communicating with AI service.
All API errors are logged server-side for debugging. If you experience persistent issues, check the Laravel logs at storage/logs/laravel.log.
Best practices for queries
- Be specific: Instead of “Tell me about the project”, ask “What are the main objectives of the Q3 marketing project?”
- Use natural language: You don’t need to use keywords. The semantic search understands context.
- Ask one thing at a time: Breaking complex questions into smaller parts often yields better results.
- Reference document topics: If you know a document contains specific information, mention that topic in your query.
Limitations
- Context window: Only the top 3 most relevant chunks are sent to the LLM. Very broad questions may not capture all relevant information.
- No conversation memory: Each query is independent. The system doesn’t remember previous messages in the conversation when generating answers.
- Document status: Only documents with “completed” status are searchable. Failed or processing documents are excluded.
- User scope: You can only query your own documents, never other users’ content.
Security
All chat requests are protected by authentication:
// api.php:22
Route::middleware('auth:sanctum')->group(function () {
Route::post('/chat', [ChatController::class, 'chat']);
});
Your queries and the retrieved document chunks never leave the Filebright system except when sent to the OpenRouter API for LLM processing. The OpenRouter API does not store or train on your data.