feat: advanced settings search debounce & local query source weight (#950)

* wip

* wip

* wip

* feat: add search delay

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* docs: update changelog

---------

Co-authored-by: ayang <473033518@qq.com>
Co-authored-by: ayangweb <75017711+ayangweb@users.noreply.github.com>
This commit is contained in:
SteveLauC
2025-11-17 18:35:30 +08:00
committed by GitHub
parent 1fb927c26b
commit aef934e9a2
11 changed files with 227 additions and 62 deletions

View File

@@ -22,10 +22,12 @@ feat(View Extension): page field now accepts HTTP(s) links #925
feat: return sub-exts when extension type exts themselves are matched #928
feat: open quick ai with modifier key + enter #939
feat: allow navigate back when cursor is at the beginning #940
feat: add compact mode for window #947
feat(extension compatibility): minimum_coco_version #946
feat: add compact mode for window #947
feat: advanced settings search debounce & local query source weight #950
feat: add window opacity configuration option #963
### 🐛 Bug fix
fix: automatic update of service list #913

View File

@@ -100,7 +100,7 @@ impl SearchQuery {
}
}
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, Serialize, Hash, PartialEq, Eq)]
pub struct QuerySource {
pub r#type: String, //coco-server/local/ etc.
pub id: String, //coco server's id

View File

@@ -193,6 +193,8 @@ pub fn run() {
extension::api::fs::read_dir,
settings::set_allow_self_signature,
settings::get_allow_self_signature,
settings::set_local_query_source_weight,
settings::get_local_query_source_weight,
assistant::ask_ai,
crate::common::document::open,
extension::built_in::file_search::config::get_file_system_config,

View File

@@ -4,8 +4,10 @@ use crate::common::search::{
FailedRequest, MultiSourceQueryResponse, QueryHits, QuerySource, SearchQuery,
};
use crate::common::traits::SearchSource;
use crate::extension::LOCAL_QUERY_SOURCE_TYPE;
use crate::server::servers::logout_coco_server;
use crate::server::servers::mark_server_as_offline;
use crate::settings::get_local_query_source_weight;
use function_name::named;
use futures::StreamExt;
use futures::stream::FuturesUnordered;
@@ -205,7 +207,7 @@ async fn query_coco_fusion_multi_query_sources(
let mut total_hits = 0;
let mut failed_requests = Vec::new();
let mut all_hits_grouped_by_source_id: HashMap<String, Vec<QueryHits>> = HashMap::new();
let mut all_hits_grouped_by_query_source: HashMap<QuerySource, Vec<QueryHits>> = HashMap::new();
while let Some((query_source, timeout_result)) = futures.next().await {
match timeout_result {
@@ -219,7 +221,6 @@ async fn query_coco_fusion_multi_query_sources(
Ok(query_result) => match query_result {
Ok(response) => {
total_hits += response.total_hits;
let source_id = response.source.id.clone();
for (document, score) in response.hits {
log::debug!(
@@ -236,8 +237,8 @@ async fn query_coco_fusion_multi_query_sources(
document,
};
all_hits_grouped_by_source_id
.entry(source_id.clone())
all_hits_grouped_by_query_source
.entry(query_source.clone())
.or_insert_with(Vec::new)
.push(query_hit);
}
@@ -255,7 +256,7 @@ async fn query_coco_fusion_multi_query_sources(
}
}
let n_sources = all_hits_grouped_by_source_id.len();
let n_sources = all_hits_grouped_by_query_source.len();
if n_sources == 0 {
return Ok(MultiSourceQueryResponse {
@@ -265,11 +266,25 @@ async fn query_coco_fusion_multi_query_sources(
});
}
/*
* Apply settings: local query source weight
*/
let local_query_source_weight: f64 = get_local_query_source_weight(tauri_app_handle);
// Scores remain unchanged if it is 1.0
if local_query_source_weight != 1.0 {
for (query_source, hits) in all_hits_grouped_by_query_source.iter_mut() {
if query_source.r#type == LOCAL_QUERY_SOURCE_TYPE {
hits.iter_mut()
.for_each(|hit| hit.score = hit.score * local_query_source_weight);
}
}
}
/*
* Sort hits within each source by score (descending) in case data sources
* do not sort them
*/
for hits in all_hits_grouped_by_source_id.values_mut() {
for hits in all_hits_grouped_by_query_source.values_mut() {
hits.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
@@ -288,15 +303,15 @@ async fn query_coco_fusion_multi_query_sources(
// Include at least 2 hits from each query source
let max_hits_per_source = (size as usize / n_sources).max(2);
for (source_id, hits) in all_hits_grouped_by_source_id.iter() {
for (query_source, hits) in all_hits_grouped_by_query_source.iter() {
let hits_taken = if hits.len() > max_hits_per_source {
pruned.insert(&source_id, &hits[max_hits_per_source..]);
pruned.insert(&query_source.id, &hits[max_hits_per_source..]);
hits[0..max_hits_per_source].to_vec()
} else {
hits.clone()
};
final_hits_grouped_by_source_id.insert(source_id.clone(), hits_taken);
final_hits_grouped_by_source_id.insert(query_source.id.clone(), hits_taken);
}
let final_hits_len = final_hits_grouped_by_source_id

View File

@@ -4,6 +4,7 @@ use tauri::AppHandle;
use tauri_plugin_store::StoreExt;
const SETTINGS_ALLOW_SELF_SIGNATURE: &str = "settings_allow_self_signature";
const LOCAL_QUERY_SOURCE_WEIGHT: &str = "local_query_source_weight";
#[tauri::command]
pub async fn set_allow_self_signature(tauri_app_handle: AppHandle, value: bool) {
@@ -70,3 +71,45 @@ pub fn _get_allow_self_signature(tauri_app_handle: AppHandle) -> bool {
pub async fn get_allow_self_signature(tauri_app_handle: AppHandle) -> bool {
_get_allow_self_signature(tauri_app_handle)
}
#[tauri::command]
pub async fn set_local_query_source_weight(tauri_app_handle: AppHandle, value: f64) {
let store = tauri_app_handle
.store(COCO_TAURI_STORE)
.unwrap_or_else(|e| {
panic!(
"store [{}] not found/loaded, error [{}]",
COCO_TAURI_STORE, e
)
});
store.set(LOCAL_QUERY_SOURCE_WEIGHT, value);
}
#[tauri::command]
pub fn get_local_query_source_weight(tauri_app_handle: AppHandle) -> f64 {
// default to 1.0
const DEFAULT: f64 = 1.0;
let store = tauri_app_handle
.store(COCO_TAURI_STORE)
.unwrap_or_else(|e| {
panic!(
"store [{}] not found/loaded, error [{}]",
COCO_TAURI_STORE, e
)
});
if !store.has(LOCAL_QUERY_SOURCE_WEIGHT) {
store.set(LOCAL_QUERY_SOURCE_WEIGHT, DEFAULT);
}
match store
.get(LOCAL_QUERY_SOURCE_WEIGHT)
.expect("should be Some")
{
Json::Number(n) => n
.as_f64()
.unwrap_or_else(|| panic!("setting [{}] should be a f64", LOCAL_QUERY_SOURCE_WEIGHT)),
_ => unreachable!("{} should be stored as a number", LOCAL_QUERY_SOURCE_WEIGHT),
}
}

View File

@@ -1,7 +1,8 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
AppWindowMac,
ArrowUpWideNarrow,
MessageSquareMore,
Search,
ShieldCheck,
@@ -18,6 +19,7 @@ import SettingsInput from "@/components//Settings/SettingsInput";
import platformAdapter from "@/utils/platformAdapter";
import UpdateSettings from "./components/UpdateSettings";
import SettingsToggle from "../SettingsToggle";
import { isNil } from "lodash-es";
const Advanced = () => {
const { t } = useTranslation();
@@ -57,6 +59,9 @@ const Advanced = () => {
const setAllowSelfSignature = useConnectStore((state) => {
return state.setAllowSelfSignature;
});
const { searchDelay, setSearchDelay } = useConnectStore();
const [localSearchResultWeight, setLocalSearchResultWeight] = useState(1);
useMount(async () => {
const allowSelfSignature = await platformAdapter.invokeBackend<boolean>(
@@ -64,6 +69,12 @@ const Advanced = () => {
);
setAllowSelfSignature(allowSelfSignature);
const weight = await platformAdapter.invokeBackend<number>(
"get_local_query_source_weight"
);
setLocalSearchResultWeight(weight);
});
useEffect(() => {
@@ -174,16 +185,20 @@ const Advanced = () => {
<Shortcuts />
<Appearance />
<UpdateSettings />
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t("settings.advanced.connect.title")}
{t("settings.advanced.other.title")}
</h2>
<div className="space-y-6">
<SettingsItem
icon={Unplug}
title={t("settings.advanced.connect.connectionTimeout.title")}
title={t("settings.advanced.other.connectionTimeout.title")}
description={t(
"settings.advanced.connect.connectionTimeout.description"
"settings.advanced.other.connectionTimeout.description"
)}
>
<SettingsInput
@@ -198,8 +213,8 @@ const Advanced = () => {
<SettingsItem
icon={Unplug}
title={t("settings.advanced.connect.queryTimeout.title")}
description={t("settings.advanced.connect.queryTimeout.description")}
title={t("settings.advanced.other.queryTimeout.title")}
description={t("settings.advanced.other.queryTimeout.description")}
>
<SettingsInput
type="number"
@@ -211,15 +226,30 @@ const Advanced = () => {
/>
</SettingsItem>
<SettingsItem
icon={Unplug}
title={t("settings.advanced.other.searchDelay.title")}
description={t("settings.advanced.other.searchDelay.description")}
>
<SettingsInput
type="number"
min={0}
value={searchDelay}
onChange={(value) => {
setSearchDelay(isNil(value) ? 0 : Number(value));
}}
/>
</SettingsItem>
<SettingsItem
icon={ShieldCheck}
title={t("settings.advanced.connect.allowSelfSignature.title")}
title={t("settings.advanced.other.allowSelfSignature.title")}
description={t(
"settings.advanced.connect.allowSelfSignature.description"
"settings.advanced.other.allowSelfSignature.description"
)}
>
<SettingsToggle
label={t("settings.advanced.connect.allowSelfSignature.title")}
label={t("settings.advanced.other.allowSelfSignature.title")}
checked={allowSelfSignature}
onChange={(value) => {
setAllowSelfSignature(value);
@@ -230,11 +260,43 @@ const Advanced = () => {
}}
/>
</SettingsItem>
<SettingsItem
icon={ArrowUpWideNarrow}
title={t("settings.advanced.other.localSearchResultWeight.title")}
description={t(
"settings.advanced.other.localSearchResultWeight.description"
)}
>
<select
value={localSearchResultWeight}
onChange={(event) => {
const weight = Number(event.target.value);
setLocalSearchResultWeight(weight);
platformAdapter.invokeBackend("set_local_query_source_weight", {
value: weight,
});
}}
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="0.5">
{t("settings.advanced.other.localSearchResultWeight.options.low")}
</option>
<option value="1">
{t(
"settings.advanced.other.localSearchResultWeight.options.medium"
)}
</option>
<option value="2">
{t(
"settings.advanced.other.localSearchResultWeight.options.high"
)}
</option>
</select>
</SettingsItem>
</div>
<Appearance />
<UpdateSettings />
</div>
);
};

View File

@@ -51,7 +51,7 @@ export function useSearch() {
return state.aiOverviewMinQuantity;
});
const { querySourceTimeout } = useConnectStore();
const { querySourceTimeout, searchDelay } = useConnectStore();
const [searchState, setSearchState] = useState<SearchState>({
isError: [],
@@ -159,6 +159,8 @@ export function useSearch() {
const performSearch = useCallback(
async (searchInput: string) => {
console.log(123);
if (!searchInput) {
setSearchState((prev) => ({ ...prev, suggests: [] }));
return;
@@ -219,10 +221,11 @@ export function useSearch() {
]
);
const debouncedSearch = useMemo(
() => debounce(performSearch, 300),
[performSearch]
);
const debouncedSearch = useMemo(() => {
console.log("searchDelay", searchDelay);
return debounce(performSearch, searchDelay);
}, [performSearch, searchDelay]);
return {
...searchState,

View File

@@ -118,6 +118,7 @@ export const useSyncStore = () => {
const setEndpoint = useAppStore((state) => state.setEndpoint);
const setLanguage = useAppStore((state) => state.setLanguage);
const { setWindowMode } = useAppearanceStore();
const { setSearchDelay } = useConnectStore();
const setServerListSilently = useConnectStore(
(state) => state.setServerListSilently
@@ -185,14 +186,19 @@ export const useSyncStore = () => {
}),
platformAdapter.listenEvent("change-connect-store", ({ payload }) => {
const { connectionTimeout, querySourceTimeout, allowSelfSignature } =
payload;
const {
connectionTimeout,
querySourceTimeout,
searchDelay,
allowSelfSignature,
} = payload;
if (isNumber(connectionTimeout)) {
setConnectionTimeout(connectionTimeout);
}
if (isNumber(querySourceTimeout)) {
setQueryTimeout(querySourceTimeout);
}
setSearchDelay(searchDelay);
setAllowSelfSignature(allowSelfSignature);
}),

View File

@@ -164,21 +164,6 @@
"description": "Shortcut button to enable AI Overview in chat mode."
}
},
"connect": {
"title": "Connection Settings",
"connectionTimeout": {
"title": "Connection Timeout",
"description": "Retries the connection if no response is received within this time. Default: 120s."
},
"queryTimeout": {
"title": "Query Timeout",
"description": "Terminates the query if no search results are returned within this time. Default: 500ms."
},
"allowSelfSignature": {
"title": "Allow Self-Signed Certificates",
"description": "Allow connections to servers using self-signed certificates. Enable only if you trust the source."
}
},
"appearance": {
"title": "Appearance Settings",
"normalOpacity": {
@@ -196,6 +181,34 @@
"title": "Snapshot Updates",
"description": "Get early access to new features. May be unstable."
}
},
"other": {
"title": "Other Settings",
"connectionTimeout": {
"title": "Connection Timeout",
"description": "Retries the connection if no response is received within this time. Default: 120s."
},
"queryTimeout": {
"title": "Query Timeout",
"description": "Terminates the query if no search results are returned within this time. Default: 500ms."
},
"searchDelay": {
"title": "Search Delay",
"description": "Delay before search is triggered after user stops typing. Default: 300 ms."
},
"allowSelfSignature": {
"title": "Allow Self-Signed Certificates",
"description": "Allow connections to servers using self-signed certificates. Enable only if you trust the source."
},
"localSearchResultWeight": {
"title": "Local Search Result Weight",
"description": "Adjusts how local results (files, extensions, commands, etc.) are ranked. Higher values place them closer to the top.",
"options": {
"high": "High",
"medium": "Medium",
"low": "Low"
}
}
}
},
"tabs": {

View File

@@ -164,21 +164,6 @@
"description": "在搜索模式下启用 AI 总结的快捷按键。"
}
},
"connect": {
"title": "连接设置",
"connectionTimeout": {
"title": "连接超时",
"description": "如果在此时间内未收到响应则重试连接。默认值120 秒。"
},
"queryTimeout": {
"title": "查询超时",
"description": "在此时间内未返回搜索结果则终止查询。默认值500 毫秒。"
},
"allowSelfSignature": {
"title": "允许自签名证书",
"description": "允许连接使用自签名证书的服务器。仅在信任来源的情况下启用。"
}
},
"appearance": {
"title": "外观设置",
"normalOpacity": {
@@ -196,6 +181,34 @@
"title": "快照版更新",
"description": "抢先体验新功能,可能不稳定。"
}
},
"other": {
"title": "其它设置",
"connectionTimeout": {
"title": "连接超时",
"description": "如果在此时间内未收到响应则重试连接。默认值120 秒。"
},
"queryTimeout": {
"title": "查询超时",
"description": "在此时间内未返回搜索结果则终止查询。默认值500 毫秒。"
},
"searchDelay": {
"title": "搜索延迟",
"description": "停止输入后触发搜索的延迟时间。默认值300 毫秒。 "
},
"allowSelfSignature": {
"title": "允许自签名证书",
"description": "允许连接使用自签名证书的服务器。仅在信任来源的情况下启用。"
},
"localSearchResultWeight": {
"title": "本地搜索结果展示权重",
"description": "调整本地结果(文件、扩展名、命令等)的排名。值越高,它们越接近顶部。",
"options": {
"high": "高",
"medium": "中",
"low": "低"
}
}
}
},
"tabs": {

View File

@@ -38,6 +38,8 @@ export type IConnectStore = {
setVisibleStartPage: (visibleStartPage: boolean) => void;
allowSelfSignature: boolean;
setAllowSelfSignature: (allowSelfSignature: boolean) => void;
searchDelay: number;
setSearchDelay: (searchDelay: number) => void;
};
export const useConnectStore = create<IConnectStore>()(
@@ -143,6 +145,10 @@ export const useConnectStore = create<IConnectStore>()(
setAllowSelfSignature: (allowSelfSignature: boolean) => {
return set(() => ({ allowSelfSignature }));
},
searchDelay: 300,
setSearchDelay(searchDelay) {
return set(() => ({ searchDelay }));
},
}),
{
name: "connect-store",