mirror of
https://github.com/perstarkse/minne.git
synced 2026-03-28 12:21:56 +01:00
feat: full text search
This commit is contained in:
@@ -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;
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
18
html-router/templates/dashboard/base.html
Normal file
18
html-router/templates/dashboard/base.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
4
html-router/templates/icons/bars_icon.html
Normal file
4
html-router/templates/icons/bars_icon.html
Normal 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 |
5
html-router/templates/icons/link_icon.html
Normal file
5
html-router/templates/icons/link_icon.html
Normal 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 |
5
html-router/templates/icons/search_icon.html
Normal file
5
html-router/templates/icons/search_icon.html
Normal 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 |
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
11
html-router/templates/search/base.html
Normal file
11
html-router/templates/search/base.html
Normal 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 %}
|
||||
81
html-router/templates/search/response.html
Normal file
81
html-router/templates/search/response.html
Normal 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 %}
|
||||
@@ -1,9 +0,0 @@
|
||||
<div class="border-">
|
||||
<div class="chat chat-start">
|
||||
<div class="chat-bubble">
|
||||
{{result}}
|
||||
<hr />
|
||||
{{references}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
8
html-router/templates/searchbar.html
Normal file
8
html-router/templates/searchbar.html
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user