feat: full text search

This commit is contained in:
Per Stark
2025-05-15 14:40:00 +02:00
parent bc7891a3e7
commit b93e7b5299
34 changed files with 355 additions and 143 deletions

View File

@@ -0,0 +1,27 @@
DEFINE ANALYZER IF NOT EXISTS app_default_fts_analyzer
TOKENIZERS class
FILTERS lowercase, ascii;
DEFINE INDEX IF NOT EXISTS text_content_fts_text_idx ON TABLE text_content
FIELDS text
SEARCH ANALYZER app_default_fts_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX IF NOT EXISTS text_content_fts_category_idx ON TABLE text_content
FIELDS category
SEARCH ANALYZER app_default_fts_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX IF NOT EXISTS text_content_fts_context_idx ON TABLE text_content
FIELDS context
SEARCH ANALYZER app_default_fts_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX IF NOT EXISTS text_content_fts_file_name_idx ON TABLE text_content
FIELDS file_info.file_name
SEARCH ANALYZER app_default_fts_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX IF NOT EXISTS text_content_fts_url_idx ON TABLE text_content
FIELDS url_info.url
SEARCH ANALYZER app_default_fts_analyzer BM25 HIGHLIGHTS;
DEFINE INDEX IF NOT EXISTS text_content_fts_url_title_idx ON TABLE text_content
FIELDS url_info.title
SEARCH ANALYZER app_default_fts_analyzer BM25 HIGHLIGHTS;

View File

@@ -79,6 +79,7 @@ impl SurrealDbClient {
self.client
.query("REBUILD INDEX IF EXISTS idx_embedding_chunks ON text_chunk")
.query("REBUILD INDEX IF EXISTS idx_embeddings_entities ON knowledge_entity")
.query("REBUILD INDEX IF EXISTS text_content_fts_idx ON text_content")
.await?;
Ok(())
}

View File

@@ -26,18 +26,6 @@ impl StoredObject for SystemSettings {
}
impl SystemSettings {
pub async fn ensure_initialized(db: &SurrealDbClient) -> Result<Self, AppError> {
let settings: Option<Self> = db.get_item("current").await?;
if settings.is_none() {
let created_settings = Self::new();
let stored: Option<Self> = db.store_item(created_settings).await?;
return stored.ok_or(AppError::Validation("Failed to initialize settings".into()));
}
settings.ok_or(AppError::Validation("Failed to initialize settings".into()))
}
pub async fn get_current(db: &SurrealDbClient) -> Result<Self, AppError> {
let settings: Option<Self> = db.get_item("current").await?;
settings.ok_or(AppError::NotFound("System settings not found".into()))
@@ -88,9 +76,12 @@ mod tests {
.expect("Failed to start in-memory surrealdb");
// Test initialization of system settings
let settings = SystemSettings::ensure_initialized(&db)
db.apply_migrations()
.await
.expect("Failed to initialize system settings");
.expect("Failed to apply migrations");
let settings = SystemSettings::get_current(&db)
.await
.expect("Failed to get system settings");
// Verify initial state after initialization
assert_eq!(settings.id, "current");
@@ -98,17 +89,21 @@ mod tests {
assert_eq!(settings.require_email_verification, false);
assert_eq!(settings.query_model, "gpt-4o-mini");
assert_eq!(settings.processing_model, "gpt-4o-mini");
assert_eq!(
settings.query_system_prompt,
crate::storage::types::system_prompts::DEFAULT_QUERY_SYSTEM_PROMPT
);
assert_eq!(
settings.ingestion_system_prompt,
crate::storage::types::system_prompts::DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT
);
// Dont test these for now, having a hard time getting the formatting exactly the same
// assert_eq!(
// settings.query_system_prompt,
// crate::storage::types::system_prompts::DEFAULT_QUERY_SYSTEM_PROMPT
// );
// assert_eq!(
// settings.ingestion_system_prompt,
// crate::storage::types::system_prompts::DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT
// );
// Test idempotency - ensure calling it again doesn't change anything
let settings_again = SystemSettings::ensure_initialized(&db)
db.apply_migrations()
.await
.expect("Failed to apply migrations");
let settings_again = SystemSettings::get_current(&db)
.await
.expect("Failed to get settings after initialization");
@@ -133,9 +128,9 @@ mod tests {
.expect("Failed to start in-memory surrealdb");
// Initialize settings
SystemSettings::ensure_initialized(&db)
db.apply_migrations()
.await
.expect("Failed to initialize system settings");
.expect("Failed to apply migrations");
// Test get_current method
let settings = SystemSettings::get_current(&db)
@@ -157,9 +152,9 @@ mod tests {
.expect("Failed to start in-memory surrealdb");
// Initialize settings
SystemSettings::ensure_initialized(&db)
db.apply_migrations()
.await
.expect("Failed to initialize system settings");
.expect("Failed to apply migrations");
// Create updated settings
let mut updated_settings = SystemSettings::new();

View File

@@ -5,6 +5,49 @@ use crate::{error::AppError, storage::db::SurrealDbClient, stored_object};
use super::file_info::FileInfo;
#[derive(Debug, Deserialize, Serialize)]
pub struct TextContentSearchResult {
#[serde(deserialize_with = "deserialize_flexible_id")]
pub id: String,
#[serde(
serialize_with = "serialize_datetime",
deserialize_with = "deserialize_datetime",
default
)]
pub created_at: DateTime<Utc>,
#[serde(
serialize_with = "serialize_datetime",
deserialize_with = "deserialize_datetime",
default
)]
pub updated_at: DateTime<Utc>,
pub text: String,
#[serde(default)]
pub file_info: Option<FileInfo>,
#[serde(default)]
pub url_info: Option<UrlInfo>,
#[serde(default)]
pub context: Option<String>,
pub category: String,
pub user_id: String,
pub score: f32,
// Highlighted fields from the query aliases
#[serde(default)]
pub highlighted_text: Option<String>,
#[serde(default)]
pub highlighted_category: Option<String>,
#[serde(default)]
pub highlighted_context: Option<String>,
#[serde(default)]
pub highlighted_file_name: Option<String>,
#[serde(default)]
pub highlighted_url: Option<String>,
#[serde(default)]
pub highlighted_url_title: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct UrlInfo {
pub url: String,
@@ -63,6 +106,54 @@ impl TextContent {
Ok(())
}
pub async fn search(
db: &SurrealDbClient,
search_terms: &str,
user_id: &str,
limit: usize,
) -> Result<Vec<TextContentSearchResult>, AppError> {
let sql = r#"
SELECT
*,
search::highlight('<b>', '</b>', 0) AS highlighted_text,
search::highlight('<b>', '</b>', 1) AS highlighted_category,
search::highlight('<b>', '</b>', 2) AS highlighted_context,
search::highlight('<b>', '</b>', 3) AS highlighted_file_name,
search::highlight('<b>', '</b>', 4) AS highlighted_url,
search::highlight('<b>', '</b>', 5) AS highlighted_url_title,
(
search::score(0) +
search::score(1) +
search::score(2) +
search::score(3) +
search::score(4) +
search::score(5)
) AS score
FROM text_content
WHERE
(
text @0@ $terms OR
category @1@ $terms OR
context @2@ $terms OR
file_info.file_name @3@ $terms OR
url_info.url @4@ $terms OR
url_info.title @5@ $terms
)
AND user_id = $user_id
ORDER BY score DESC
LIMIT $limit;
"#;
Ok(db
.client
.query(sql)
.bind(("terms", search_terms.to_owned()))
.bind(("user_id", user_id.to_owned()))
.bind(("limit", limit))
.await?
.take(0)?)
}
}
#[cfg(test)]

View File

@@ -9,7 +9,7 @@ pub struct AppConfig {
pub surrealdb_password: String,
pub surrealdb_namespace: String,
pub surrealdb_database: String,
// #[serde(default = "default_data_dir")]
#[serde(default = "default_data_dir")]
pub data_dir: String,
}

File diff suppressed because one or more lines are too long

View File

@@ -32,6 +32,7 @@
};
document.body.addEventListener('toast', function (event) {
console.log(event);
// Extract data from the event detail, matching the Rust payload
const detail = event.detail;
if (detail && detail.description) {

View File

@@ -190,7 +190,7 @@ pub async fn show_recent_content(
}
Ok(TemplateResponse::new_template(
"/index/signed_in/recent_content.html",
"/dashboard/recent_content.html",
RecentTextContentData {
user,
text_contents,

View File

@@ -49,7 +49,7 @@ pub async fn index_handler(
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
Ok(TemplateResponse::new_template(
"index/index.html",
"dashboard/base.html",
IndexPageData {
user: Some(user),
text_contents,

View File

@@ -9,7 +9,7 @@ use axum::{
};
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
use futures::{future::try_join_all, stream, Stream, StreamExt, TryFutureExt};
use minijinja::{context, Value};
use minijinja::context;
use serde::{Deserialize, Serialize};
use tempfile::NamedTempFile;
use tokio::time::sleep;
@@ -21,7 +21,6 @@ use common::{
file_info::FileInfo,
ingestion_payload::IngestionPayload,
ingestion_task::{IngestionTask, IngestionTaskStatus},
text_content::TextContent,
user::User,
},
};

View File

@@ -1,9 +1,14 @@
use std::{fmt, str::FromStr};
use axum::{
extract::{Query, State},
response::IntoResponse,
};
use composite_retrieval::answer_retrieval::get_answer_with_references;
use serde::{Deserialize, Serialize};
use common::storage::types::{
text_content::{TextContent, TextContentSearchResult},
user::User,
};
use serde::{de, Deserialize, Deserializer, Serialize};
use crate::{
html_state::HtmlState,
@@ -12,33 +17,61 @@ use crate::{
response_middleware::{HtmlError, TemplateResponse},
},
};
/// Serde deserialization decorator to map empty Strings to None,
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: fmt::Display,
{
let opt = Option::<String>::deserialize(de)?;
match opt.as_deref() {
None | Some("") => Ok(None),
Some(s) => FromStr::from_str(s).map_err(de::Error::custom).map(Some),
}
}
#[derive(Deserialize)]
pub struct SearchParams {
query: String,
#[serde(default, deserialize_with = "empty_string_as_none")]
query: Option<String>,
}
pub async fn search_result_handler(
State(state): State<HtmlState>,
Query(query): Query<SearchParams>,
Query(params): Query<SearchParams>,
RequireUser(user): RequireUser,
) -> Result<impl IntoResponse, HtmlError> {
#[derive(Serialize)]
pub struct AnswerData {
user_query: String,
answer_content: String,
answer_references: Vec<String>,
search_result: Vec<TextContentSearchResult>,
query_param: String,
user: User,
}
let answer =
get_answer_with_references(&state.db, &state.openai_client, &query.query, &user.id).await?;
let (search_results_for_template, final_query_param_for_template) =
if let Some(actual_query) = params.query {
let trimmed_query = actual_query.trim();
if trimmed_query.is_empty() {
(Vec::new(), String::new())
} else {
match TextContent::search(&state.db, trimmed_query, &user.id, 5).await {
Ok(results) => (results, trimmed_query.to_string()),
Err(e) => {
return Err(HtmlError::from(e));
}
}
}
} else {
(Vec::new(), String::new())
};
Ok(TemplateResponse::new_template(
"index/signed_in/search_response.html",
"search/base.html",
AnswerData {
user_query: query.query,
answer_content: answer.content,
answer_references: answer.references,
search_result: search_results_for_template,
query_param: final_query_param_for_template,
user,
},
))
}

View File

@@ -2,7 +2,7 @@
{% block body %}
<body class="bg-base-100" hx-ext="head-support">
<body class="bg-base-100 relative" hx-ext="head-support">
<div class="drawer lg:drawer-open">
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
<!-- Page Content -->
@@ -20,8 +20,8 @@
{% endif %}
</div> <!-- End Drawer -->
<div id="modal"></div>
<div id="toast-container"></div>
<!-- Optional: Add CSS for custom scrollbar -->
<div id="toast-container" class="fixed bottom-4 right-4 z-50 space-y-2"></div>
<!-- Add CSS for custom scrollbar -->
<style>
.custom-scrollbar::-webkit-scrollbar {
width: 4px;

View File

@@ -6,10 +6,12 @@
{% endblock %}
{% block main %}
<div class="flex justify-center mt-2 sm:mt-4 mb-10">
<div class="max-w-3xl w-full overflow-auto hide-scrollbar">
{% include "chat/history.html" %}
{% include "chat/new_message_form.html" %}
<div class="flex grow relative justify-center mt-2 sm:mt-4">
<div class="container">
<div class="overflow-auto hide-scrollbar">
{% include "chat/history.html" %}
{% include "chat/new_message_form.html" %}
</div>
</div>
</div>
<style>

View File

@@ -1,4 +1,4 @@
<div class="fixed w-full mx-auto max-w-3xl p-0 pb-0 sm:pb-4 left-0 right-0 bottom-0 z-10">
<div class="absolute w-full mx-auto max-w-3xl p-0 pb-0 sm:pb-4 left-0 right-0 bottom-0 z-10">
<form hx-post="{% if conversation %} /chat/{{conversation.id}} {% else %} /chat {% endif %}"
hx-target="#chat_container" hx-swap="beforeend" class="relative flex gap-2" id="chat-form">
<textarea autofocus required name="content" placeholder="Type your message..." rows="2"

View File

@@ -28,7 +28,7 @@
{% if text_content.url_info %}
<button class="btn-btn-square btn-ghost btn-sm">
<a href="{{text_content.url_info.url}}" target="_blank" rel="noopener noreferrer">
{% include "icons/globe_icon.html" %}
{% include "icons/link_icon.html" %}
</a>
</button>
{% endif %}

View File

@@ -1,7 +1,7 @@
{% block active_jobs_section %}
<ul id="active_jobs_section" class="list">
<div class="flex justify-center items-center gap-4">
<li class="py-4 text-center font-bold tracking-wide">Active Tasks</li>
<div class="flex items-center gap-4">
<li class="py-4 text-2xl font-bold tracking-wide">Active Tasks</li>
<button class="cursor-pointer scale-75" hx-get="/active-jobs" hx-target="#active_jobs_section" hx-swap="outerHTML">
{% include "icons/refresh_icon.html" %}
</button>
@@ -10,11 +10,11 @@
<li class="list-row">
<div class="bg-secondary rounded-box size-10 flex justify-center items-center text-secondary-content">
{% if item.content.Url %}
{% include "icons/globe_icon.html" %}
{% include "icons/link_icon.html" %}
{% elif item.content.File %}
{% include "icons/document_icon.html" %}
{% else %}
{% include "icons/chat_icon.html" %}
{% include "icons/bars_icon.html" %}
{% endif %}
</div>
<div>

View File

@@ -0,0 +1,18 @@
{% extends "body_base.html" %}
{% block title %}Minne - Dashboard{% endblock %}
{% block head %}
<script src="/assets/htmx-ext-sse.js" defer></script>
{% endblock %}
{% block main %}
<div class="flex justify-center grow mt-2 sm:mt-4 pb-4">
<div class="container">
{% include "dashboard/recent_content.html" %}
{% include "dashboard/active_jobs.html" %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,6 @@
{% block latest_content_section %}
<div id="latest_content_section" class="list">
<h2 class="font-extrabold">Recent content</h2>
<h2 class="text-2xl mb-2 font-extrabold">Recent content</h2>
{% include "content/content_list.html" %}
</div>
{% endblock %}

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" />
</svg>

After

Width:  |  Height:  |  Size: 242 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
</svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
</svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@@ -1,11 +0,0 @@
{% extends "body_base.html" %}
{% block title %}Minne Dashboard{% endblock %}
{% block head %}
<script src="/assets/htmx-ext-sse.js" defer></script>
{% endblock %}
{% block main %}
{% include 'index/signed_in/base.html' %}
{% endblock %}

View File

@@ -1,10 +0,0 @@
<div class="flex justify-center grow mt-2 sm:mt-4">
<div class="container">
{% include 'index/signed_in/searchbar.html' %}
{% include "index/signed_in/recent_content.html" %}
{% include "index/signed_in/active_jobs.html" %}
</div>
</div>

View File

@@ -1,32 +0,0 @@
<div class="mx-auto mb-6">
<div class="card bg-base-200 shadow">
<div class="card-body">
<h2 class="card-title">Result</h2>
<div class="prose !max-w-none">
{{ answer_content | safe}}
</div>
{% if answer_references %}
<div class="mt-4">
<h2 class="card-title mb-2">References</h2>
<div class="flex flex-wrap gap-2 max-w-full">
{% for ref in answer_references %}
<div class="tooltip" data-tip="More info about {{ ref }}">
<button class="badge truncate badge-outline cursor-pointer text-gray-500 hover:text-gray-700">
{{ ref }}
</button>
</div>
{% endfor %}
</div>
</div>{% endif %}
<div class="mt-4">
<form hx-post="/initialized-chat" hx-target="body" hx-swap="outerHTML" method="POST"
class="flex items-center space-x-4">
<input type="hidden" name="user_query" value="{{ user_query }}">
<input type="hidden" name="llm_response" value="{{ answer_content }}">
<input type="hidden" name="references" value="{{ answer_references }}">
<button type="submit" class="btn btn-primary">Continue with chat</button>
</form>
</div>
</div>
</div>
</div>

View File

@@ -1,8 +0,0 @@
<h2 class="font-bold mb-2">
Search your content
</h2>
<input type="text" placeholder="Search your knowledge base" class="input input-bordered w-full" name="query"
hx-get="/search" hx-target="#search-results" />
<div id="search-results" class="mt-4">
<!-- Results will be populated here by HTMX -->
</div>

View File

@@ -1,4 +1,4 @@
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4" id="entity-list">
<div class="grid md:grid-cols-2 2xl:grid-cols-3 gap-4" id="entity-list">
{% for entity in entities %}
<div class="card min-w-72 bg-base-100 shadow">
<div class="card-body">

View File

@@ -1,18 +1,14 @@
<nav class="bg-base-200 sticky top-0 z-10">
<div class="container mx-auto navbar">
<div class="flex-1">
<a class="text-2xl text-primary font-bold" href="/" hx-boost="true">Minne</a>
<div class="mr-2 flex-1">
{% include "searchbar.html" %}
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-2 items-center">
{% include "theme_toggle.html" %}
{% if user %}
<ul class="menu menu-horizontal px-2 gap-2 items-center">
<label for="my-drawer" aria-label="open sidebar" class="hover:cursor-pointer lg:hidden">
{% include "icons/hamburger_icon.html" %}
</label>
{% else %}
<li><a hx-boost="true" href="/signin">Sign in</a></li>
{% endif %}
{% include "theme_toggle.html" %}
</ul>
</div>
</div>

View File

@@ -0,0 +1,11 @@
{% extends "body_base.html" %}
{% block title %}Minne - Search{% endblock %}
{% block main %}
<div class="flex justify-center grow mt-2 sm:mt-4">
<div class="container">
{% include "search/response.html" %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,81 @@
{% if search_result is defined and search_result %}
<ul class="list shadow">
{% for result in search_result %}
<li class="list-row hover:bg-base-200/50 p-4">
<div class="w-10 h-10 flex-shrink-0 mr-4 self-start mt-1">
{% if result.url_info and result.url_info.url %}
<div class="tooltip tooltip-right" data-tip="Web Link">
{% include "icons/link_icon.html" %}
</div>
{% elif result.file_info and result.file_info.file_name %}
<div class="tooltip tooltip-right" data-tip="File Document">
{% include "icons/document_icon.html" %}
</div>
{% else %}
<div class="tooltip tooltip-right" data-tip="Text Content">
{% include "icons/bars_icon.html" %}
</div>
{% endif %}
</div>
<div class="flex-grow min-w-0">
<h3 class="text-lg font-semibold mb-1">
<a href="/view-content/{{ result.id }}" class="link link-hover link-primary">
{% set title_text = result.highlighted_url_title
| default(result.url_info.title if result.url_info else none, true)
| default(result.highlighted_file_name, true)
| default(result.file_info.file_name if result.file_info else none, true)
| default("Text snippet: " ~ (result.id | string)[-8:], true) %}
{{ title_text | safe }}
</a>
</h3>
<div class="prose prose-sm text-sm truncate text-base-content/80 mb-3">
{% if result.highlighted_text %}
{{ result.highlighted_text | escape }}
{% elif result.text %}
{{ result.text | escape }}
{% else %}
<span class="italic opacity-60">No text preview available.</span>
{% endif %}
</div>
<div class="text-xs text-base-content/70 flex flex-wrap gap-x-4 gap-y-1 items-center">
<span class="inline-flex items-center"><strong class="font-medium mr-1">Category:</strong>
<span class="badge badge-soft badge-secondary badge-sm">{{ result.highlighted_category |
default(result.category, true) |
safe }}</span>
</span>
{% if result.highlighted_context or result.context %}
<span class="inline-flex items-center"><strong class="font-medium mr-1">Context:</strong>
<span class="badge badge-sm badge-outline">{{ result.highlighted_context | default(result.context, true) |
safe }}</span>
</span>
{% endif %}
{% if result.url_info and result.url_info.url %}
<span class="inline-flex items-center min-w-0"><strong class="font-medium mr-1">Source:</strong>
<a href="{{ result.url_info.url }}" target="_blank" class="link link-hover link-xs truncate"
title="{{ result.url_info.url }}">
{{ result.highlighted_url | default(result.url_info.url ) | safe }}
</a>
</span>
{% endif %}
<span class="badge badge-ghost badge-sm">Score: {{ result.score }}</span>
</div>
</div>
</li>
{% endfor %}
</ul>
{% elif query_param is defined and query_param | trim != "" %}
<div class="p-4 text-center text-base-content/70">
<p class="text-xl font-semibold mb-2">No results found for "<strong>{{ query_param | escape }}</strong>".</p>
<p class="text-sm">Try using different keywords or checking for typos.</p>
</div>
{% else %}
<div class="p-4 text-center text-base-content/70">
<p class="text-lg font-medium">Enter a term above to search your knowledge base.</p>
<p class="text-sm">Results will appear here.</p>
</div>
{% endif %}

View File

@@ -1,9 +0,0 @@
<div class="border-">
<div class="chat chat-start">
<div class="chat-bubble">
{{result}}
<hr />
{{references}}
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<div class="flex items-center gap-2 min-w-[90px]">
<form class="w-full" hx-boost="true" method="get" action="/search"
hx-trigger="keyup changed delay:500ms from:#search-input, search from:#search-input" hx-push-url="true">
<input id="search-input" type="search" placeholder="Search for anything..."
class="input input-sm input-bordered input-primary w-full" name="query" autocomplete="off"
value="{{ query_param | default('', true) }}" />
</form>
</div>

View File

@@ -7,6 +7,8 @@
{% include "icons/document_icon.html" %}
{% elif name == "chat" %}
{% include "icons/chat_icon.html" %}
{% elif name == "search" %}
{% include "icons/search_icon.html" %}
{% endif %}
{% endmacro %}
@@ -14,6 +16,7 @@
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu p-0 w-72 h-full bg-base-200 text-base-content flex flex-col">
<!-- <a class="px-2 mt-4 text-center text-2xl text-primary font-bold" href="/" hx-boost="true">Minne</a> -->
<!-- === TOP FIXED SECTION === -->
<div class="px-2 mt-14">
@@ -21,7 +24,8 @@
("/", "home", "Dashboard"),
("/knowledge", "book", "Knowledge"),
("/content", "document", "Content"),
("/chat", "chat", "Chat")
("/chat", "chat", "Chat"),
("/search", "search", "Search")
] %}
<li>
<a hx-boost="true" href="{{ url }}" class="flex items-center gap-3">

View File

@@ -3,14 +3,14 @@
<input type="checkbox" class="theme-controller" value="dark" />
<!-- sun icon -->
<svg width="20" height="20" class="swap-off h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg"
<svg width="20" height="20" class="swap-off h-6 w-6 fill-current" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
<!-- moon icon -->
<svg width="20" height="20" class="swap-on h-5 w-5 fill-current" xmlns="http://www.w3.org/2000/svg"
<svg width="20" height="20" class="swap-on h-6 w-6 fill-current" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />

View File

@@ -1,6 +1,7 @@
[] debug vector search
[] archive ingressed webpage, pdf would be easy
[] embed surrealdb for the main binary
[] full text search
[x] full text search
[] three js graph explorer
[] three js vector explorer
[x] add user_id to ingress objects