Skip to content

Commit 61d92a6

Browse files
committed
feat: add data sharing and default range settings to LLM configuration
- Implemented UI for data sharing options in LlmSettings component. - Added functionality to set data sharing preferences and default analysis range. - Updated LlmConfig type to include data sharing and default range properties. - Enhanced llmApi service with new methods for managing LLM conversations. - Created backend commands for listing, retrieving, saving, deleting, archiving, and pinning LLM conversations. - Introduced a new Zustand store for managing LLM conversations state.
1 parent bd85e00 commit 61d92a6

23 files changed

Lines changed: 1559 additions & 252 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,6 @@ MIT © 2026 TimeLens Contributors
185185

186186
---
187187

188+
LLM Supported by OrcaRouter!
189+
188190
[![Powered by OrcaRouter](https://img.shields.io/badge/Powered_by-OrcaRouter-2563eb)](https://www.orcarouter.ai/ref/ref_2bd137bce0d730edcd93)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use crate::commands::storage_cmd::DbState;
2+
use crate::db::llm_conversations as db;
3+
use tauri::State;
4+
5+
/// List all LLM conversations, optionally including archived ones.
6+
#[tauri::command]
7+
pub fn list_llm_conversations(
8+
db_state: State<DbState>,
9+
include_archived: bool,
10+
) -> Result<Vec<db::LlmConversationSummary>, String> {
11+
let conn = db_state.lock().map_err(|e| e.to_string())?;
12+
db::list_llm_conversations(&conn, include_archived).map_err(|e| e.to_string())
13+
}
14+
15+
/// Get a single conversation by ID.
16+
#[tauri::command]
17+
pub fn get_llm_conversation(
18+
db_state: State<DbState>,
19+
id: String,
20+
) -> Result<Option<db::LlmConversation>, String> {
21+
let conn = db_state.lock().map_err(|e| e.to_string())?;
22+
db::get_llm_conversation(&conn, &id).map_err(|e| e.to_string())
23+
}
24+
25+
/// Save or update a conversation.
26+
#[tauri::command]
27+
pub fn save_llm_conversation(
28+
db_state: State<DbState>,
29+
conversation: db::LlmConversation,
30+
) -> Result<(), String> {
31+
let conn = db_state.lock().map_err(|e| e.to_string())?;
32+
db::upsert_llm_conversation(&conn, &conversation).map_err(|e| e.to_string())
33+
}
34+
35+
/// Delete a conversation permanently.
36+
#[tauri::command]
37+
pub fn delete_llm_conversation(db_state: State<DbState>, id: String) -> Result<(), String> {
38+
let conn = db_state.lock().map_err(|e| e.to_string())?;
39+
db::delete_llm_conversation(&conn, &id).map_err(|e| e.to_string())
40+
}
41+
42+
/// Archive or unarchive a conversation.
43+
#[tauri::command]
44+
pub fn archive_llm_conversation(
45+
db_state: State<DbState>,
46+
id: String,
47+
archived: bool,
48+
) -> Result<(), String> {
49+
let conn = db_state.lock().map_err(|e| e.to_string())?;
50+
db::set_llm_conversation_archived(&conn, &id, archived).map_err(|e| e.to_string())
51+
}
52+
53+
/// Pin or unpin a conversation.
54+
#[tauri::command]
55+
pub fn pin_llm_conversation(
56+
db_state: State<DbState>,
57+
id: String,
58+
pinned: bool,
59+
) -> Result<(), String> {
60+
let conn = db_state.lock().map_err(|e| e.to_string())?;
61+
db::set_llm_conversation_pinned(&conn, &id, pinned).map_err(|e| e.to_string())
62+
}

src-tauri/src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod browser_cmd;
33
pub mod data_reliability_cmd;
44
pub mod extension_bridge_cmd;
55
pub mod llm_cmd;
6+
pub mod llm_conversation_cmd;
67
pub mod log_cmd;
78
pub mod monitor_cmd;
89
pub mod productivity_cmd;
@@ -16,6 +17,7 @@ pub use browser_cmd::*;
1617
pub use data_reliability_cmd::*;
1718
pub use extension_bridge_cmd::*;
1819
pub use llm_cmd::*;
20+
pub use llm_conversation_cmd::*;
1921
pub use log_cmd::*;
2022
pub use monitor_cmd::*;
2123
pub use productivity_cmd::*;
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
use rusqlite::{params, Connection, Result};
2+
use serde::{Deserialize, Serialize};
3+
4+
/// A chat message stored inside a conversation.
5+
#[derive(Debug, Clone, Serialize, Deserialize)]
6+
pub struct StoredChatMessage {
7+
pub role: String,
8+
pub content: String,
9+
}
10+
11+
/// Full conversation record.
12+
#[derive(Debug, Clone, Serialize, Deserialize)]
13+
pub struct LlmConversation {
14+
pub id: String,
15+
pub title: String,
16+
pub created_at: String,
17+
pub updated_at: String,
18+
pub archived: bool,
19+
pub pinned: bool,
20+
pub messages: Vec<StoredChatMessage>,
21+
pub summary: Option<String>,
22+
}
23+
24+
/// Lightweight conversation row for the sidebar list.
25+
#[derive(Debug, Clone, Serialize, Deserialize)]
26+
pub struct LlmConversationSummary {
27+
pub id: String,
28+
pub title: String,
29+
pub created_at: String,
30+
pub updated_at: String,
31+
pub archived: bool,
32+
pub pinned: bool,
33+
pub message_count: usize,
34+
}
35+
36+
pub fn list_llm_conversations(
37+
conn: &Connection,
38+
include_archived: bool,
39+
) -> Result<Vec<LlmConversationSummary>> {
40+
let sql = if include_archived {
41+
"SELECT id, title, created_at, updated_at, archived, pinned, messages
42+
FROM llm_conversations
43+
ORDER BY pinned DESC, updated_at DESC"
44+
} else {
45+
"SELECT id, title, created_at, updated_at, archived, pinned, messages
46+
FROM llm_conversations
47+
WHERE archived = 0
48+
ORDER BY pinned DESC, updated_at DESC"
49+
};
50+
let mut stmt = conn.prepare(sql)?;
51+
let rows = stmt.query_map([], |row| {
52+
let messages_json: String = row.get(6)?;
53+
let message_count = serde_json::from_str::<Vec<StoredChatMessage>>(&messages_json)
54+
.map(|v| v.len())
55+
.unwrap_or(0);
56+
Ok(LlmConversationSummary {
57+
id: row.get(0)?,
58+
title: row.get(1)?,
59+
created_at: row.get(2)?,
60+
updated_at: row.get(3)?,
61+
archived: row.get::<_, i32>(4)? != 0,
62+
pinned: row.get::<_, i32>(5)? != 0,
63+
message_count,
64+
})
65+
})?;
66+
rows.collect()
67+
}
68+
69+
pub fn get_llm_conversation(conn: &Connection, id: &str) -> Result<Option<LlmConversation>> {
70+
let mut stmt = conn.prepare(
71+
"SELECT id, title, created_at, updated_at, archived, pinned, messages, summary
72+
FROM llm_conversations
73+
WHERE id = ?1",
74+
)?;
75+
let row = stmt.query_row([id], |row| {
76+
let messages_json: String = row.get(6)?;
77+
let summary: Option<String> = row.get(7)?;
78+
Ok(LlmConversation {
79+
id: row.get(0)?,
80+
title: row.get(1)?,
81+
created_at: row.get(2)?,
82+
updated_at: row.get(3)?,
83+
archived: row.get::<_, i32>(4)? != 0,
84+
pinned: row.get::<_, i32>(5)? != 0,
85+
messages: serde_json::from_str(&messages_json).unwrap_or_default(),
86+
summary,
87+
})
88+
});
89+
match row {
90+
Ok(conv) => Ok(Some(conv)),
91+
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
92+
Err(e) => Err(e),
93+
}
94+
}
95+
96+
pub fn upsert_llm_conversation(conn: &Connection, conversation: &LlmConversation) -> Result<()> {
97+
let messages_json = serde_json::to_string(&conversation.messages)
98+
.map_err(|e| rusqlite::Error::InvalidParameterName(e.to_string()))?;
99+
conn.execute(
100+
"INSERT INTO llm_conversations (id, title, created_at, updated_at, archived, pinned, messages, summary)
101+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
102+
ON CONFLICT(id) DO UPDATE SET
103+
title = excluded.title,
104+
updated_at = excluded.updated_at,
105+
archived = excluded.archived,
106+
pinned = excluded.pinned,
107+
messages = excluded.messages,
108+
summary = excluded.summary",
109+
params![
110+
conversation.id,
111+
conversation.title,
112+
conversation.created_at,
113+
conversation.updated_at,
114+
conversation.archived as i32,
115+
conversation.pinned as i32,
116+
messages_json,
117+
conversation.summary,
118+
],
119+
)?;
120+
Ok(())
121+
}
122+
123+
pub fn delete_llm_conversation(conn: &Connection, id: &str) -> Result<()> {
124+
conn.execute(
125+
"DELETE FROM llm_conversations WHERE id = ?1",
126+
params![id],
127+
)?;
128+
Ok(())
129+
}
130+
131+
pub fn set_llm_conversation_archived(
132+
conn: &Connection,
133+
id: &str,
134+
archived: bool,
135+
) -> Result<()> {
136+
conn.execute(
137+
"UPDATE llm_conversations SET archived = ?1, updated_at = ?2 WHERE id = ?3",
138+
params![archived as i32, chrono::Local::now().to_rfc3339(), id],
139+
)?;
140+
Ok(())
141+
}
142+
143+
pub fn set_llm_conversation_pinned(conn: &Connection, id: &str, pinned: bool) -> Result<()> {
144+
conn.execute(
145+
"UPDATE llm_conversations SET pinned = ?1, updated_at = ?2 WHERE id = ?3",
146+
params![pinned as i32, chrono::Local::now().to_rfc3339(), id],
147+
)?;
148+
Ok(())
149+
}

src-tauri/src/db/migrations.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,31 @@ fn migration_012_widget_runtime_rewrite(conn: &Connection) -> Result<()> {
11551155
Ok(())
11561156
}
11571157

1158+
/// Migration 013: persist LLM assistant conversations and optional summaries.
1159+
fn migration_013_llm_conversations(conn: &Connection) -> Result<()> {
1160+
conn.execute_batch(
1161+
"
1162+
CREATE TABLE IF NOT EXISTS llm_conversations (
1163+
id TEXT PRIMARY KEY,
1164+
title TEXT NOT NULL,
1165+
created_at TEXT NOT NULL,
1166+
updated_at TEXT NOT NULL,
1167+
archived INTEGER NOT NULL DEFAULT 0,
1168+
pinned INTEGER NOT NULL DEFAULT 0,
1169+
messages TEXT NOT NULL,
1170+
summary TEXT
1171+
);
1172+
1173+
CREATE INDEX IF NOT EXISTS idx_llm_conversations_updated
1174+
ON llm_conversations(updated_at DESC);
1175+
1176+
CREATE INDEX IF NOT EXISTS idx_llm_conversations_pinned
1177+
ON llm_conversations(pinned, updated_at DESC);
1178+
",
1179+
)?;
1180+
Ok(())
1181+
}
1182+
11581183
/// The ordered list of all migrations.
11591184
const MIGRATIONS: &[Migration] = &[
11601185
Migration::new(1, "baseline_schema", migration_001_baseline),
@@ -1185,6 +1210,11 @@ const MIGRATIONS: &[Migration] = &[
11851210
"widget_runtime_rewrite",
11861211
migration_012_widget_runtime_rewrite,
11871212
),
1213+
Migration::new(
1214+
13,
1215+
"llm_conversations",
1216+
migration_013_llm_conversations,
1217+
),
11881218
];
11891219

11901220
/// Returns the highest migration version defined.

src-tauri/src/db/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use chrono::Timelike;
22
use rusqlite::{params, Connection, OptionalExtension, Result};
33
use std::path::Path;
44

5+
pub mod llm_conversations;
56
pub mod migrations;
67

78
const SYSTEM_INTERACTIVE_EXE_WHITELIST_SQL: &str = "

src-tauri/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,12 @@ pub fn run() {
12531253
commands::get_llm_config,
12541254
commands::set_llm_config,
12551255
commands::get_llm_config_path,
1256+
commands::list_llm_conversations,
1257+
commands::get_llm_conversation,
1258+
commands::save_llm_conversation,
1259+
commands::delete_llm_conversation,
1260+
commands::archive_llm_conversation,
1261+
commands::pin_llm_conversation,
12561262
commands::widget_unsubscribe,
12571263
commands::get_widget_state,
12581264
commands::set_widget_state,

0 commit comments

Comments
 (0)