auto-git:
[change] src-tauri/build.rs [change] src-tauri/src/lib.rs [change] src-tauri/src/main.rs [change] src/App.tsx
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
@@ -5,236 +5,232 @@ use std::io::{BufRead, BufReader, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use tauri::{
|
||||
menu::{Menu, MenuItem, Submenu},
|
||||
Emitter,
|
||||
menu::{Menu, MenuItem, Submenu},
|
||||
Emitter,
|
||||
};
|
||||
|
||||
fn repo_root() -> PathBuf {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
manifest_dir.parent().unwrap_or(&manifest_dir).to_path_buf()
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
manifest_dir.parent().unwrap_or(&manifest_dir).to_path_buf()
|
||||
}
|
||||
|
||||
fn sessions_file() -> PathBuf {
|
||||
repo_root().join(".idea-hole").join("sessions.jsonl")
|
||||
repo_root().join(".idea-hole").join("sessions.jsonl")
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SessionFields {
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
saved_at: Option<i64>,
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
saved_at: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SessionSummary {
|
||||
title: String,
|
||||
description: String,
|
||||
saved_at: i64,
|
||||
title: String,
|
||||
description: String,
|
||||
saved_at: i64,
|
||||
}
|
||||
|
||||
fn open_sessions_file() -> Result<Option<File>, String> {
|
||||
match File::open(sessions_file()) {
|
||||
Ok(file) => Ok(Some(file)),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(format!("Failed to open sessions file: {err}")),
|
||||
}
|
||||
match File::open(sessions_file()) {
|
||||
Ok(file) => Ok(Some(file)),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(format!("Failed to open sessions file: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn session_summary_from_line(line: &str) -> Option<SessionSummary> {
|
||||
let fields: SessionFields = serde_json::from_str(line).ok()?;
|
||||
let title = fields.title.unwrap_or_default().trim().to_string();
|
||||
if title.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let fields: SessionFields = serde_json::from_str(line).ok()?;
|
||||
let title = fields.title.unwrap_or_default().trim().to_string();
|
||||
if title.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(SessionSummary {
|
||||
title,
|
||||
description: fields.description.unwrap_or_default(),
|
||||
saved_at: fields.saved_at.unwrap_or_default(),
|
||||
})
|
||||
Some(SessionSummary {
|
||||
title,
|
||||
description: fields.description.unwrap_or_default(),
|
||||
saved_at: fields.saved_at.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn list_sessions_fast() -> Result<Vec<SessionSummary>, String> {
|
||||
let Some(file) = open_sessions_file()? else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(file) = open_sessions_file()? else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
let mut sessions = Vec::new();
|
||||
for line in BufReader::new(file).lines() {
|
||||
let line = line.map_err(|err| format!("Failed to read sessions file: {err}"))?;
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
let mut sessions = Vec::new();
|
||||
for line in BufReader::new(file).lines() {
|
||||
let line = line.map_err(|err| format!("Failed to read sessions file: {err}"))?;
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(summary) = session_summary_from_line(&line) {
|
||||
sessions.push(summary);
|
||||
}
|
||||
}
|
||||
if let Some(summary) = session_summary_from_line(&line) {
|
||||
sessions.push(summary);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(sessions)
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
fn load_session_fast(title: &str) -> Result<Value, String> {
|
||||
let target = title.trim();
|
||||
if target.is_empty() {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
|
||||
let Some(file) = open_sessions_file()? else {
|
||||
return Ok(Value::Null);
|
||||
};
|
||||
|
||||
for line in BufReader::new(file).lines() {
|
||||
let line = line.map_err(|err| format!("Failed to read sessions file: {err}"))?;
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
let target = title.trim();
|
||||
if target.is_empty() {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
|
||||
let Some(summary) = session_summary_from_line(&line) else {
|
||||
continue;
|
||||
let Some(file) = open_sessions_file()? else {
|
||||
return Ok(Value::Null);
|
||||
};
|
||||
if summary.title == target {
|
||||
return serde_json::from_str(&line)
|
||||
.map_err(|err| format!("Failed to parse saved session: {err}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Value::Null)
|
||||
for line in BufReader::new(file).lines() {
|
||||
let line = line.map_err(|err| format!("Failed to read sessions file: {err}"))?;
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(summary) = session_summary_from_line(&line) else {
|
||||
continue;
|
||||
};
|
||||
if summary.title == target {
|
||||
return serde_json::from_str(&line)
|
||||
.map_err(|err| format!("Failed to parse saved session: {err}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
fn run_python(script: &str, input: &str) -> Result<Vec<u8>, String> {
|
||||
let cwd = repo_root();
|
||||
let mut last_err: Option<String> = None;
|
||||
for candidate in ["python3", "python"] {
|
||||
let mut child = match Command::new(candidate)
|
||||
.arg(script)
|
||||
.current_dir(&cwd)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => child,
|
||||
Err(err) => {
|
||||
last_err = Some(format!("{candidate}: {err}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let cwd = repo_root();
|
||||
let mut last_err: Option<String> = None;
|
||||
for candidate in ["python3", "python"] {
|
||||
let mut child = match Command::new(candidate)
|
||||
.arg(script)
|
||||
.current_dir(&cwd)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => child,
|
||||
Err(err) => {
|
||||
last_err = Some(format!("{candidate}: {err}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
if let Err(err) = stdin.write_all(input.as_bytes()) {
|
||||
return Err(format!("Failed to write to Python stdin: {err}"));
|
||||
}
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
if let Err(err) = stdin.write_all(input.as_bytes()) {
|
||||
return Err(format!("Failed to write to Python stdin: {err}"));
|
||||
}
|
||||
}
|
||||
|
||||
let output = match child.wait_with_output() {
|
||||
Ok(output) => output,
|
||||
Err(err) => {
|
||||
last_err = Some(format!("{candidate}: {err}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("Python backend failed: {stderr}"));
|
||||
}
|
||||
|
||||
return Ok(output.stdout);
|
||||
}
|
||||
|
||||
let output = match child.wait_with_output() {
|
||||
Ok(output) => output,
|
||||
Err(err) => {
|
||||
last_err = Some(format!("{candidate}: {err}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("Python backend failed: {stderr}"));
|
||||
}
|
||||
|
||||
return Ok(output.stdout);
|
||||
}
|
||||
|
||||
Err(last_err.unwrap_or_else(|| "Python not found".to_string()))
|
||||
Err(last_err.unwrap_or_else(|| "Python not found".to_string()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn run_python_action(action: String, payload: Value) -> Result<Value, String> {
|
||||
match action.as_str() {
|
||||
"list_sessions" => {
|
||||
return serde_json::to_value(list_sessions_fast()?).map_err(|err| err.to_string());
|
||||
match action.as_str() {
|
||||
"list_sessions" => {
|
||||
return serde_json::to_value(list_sessions_fast()?).map_err(|err| err.to_string());
|
||||
}
|
||||
"load_session" => {
|
||||
let title = payload.get("title").and_then(Value::as_str).unwrap_or("");
|
||||
return load_session_fast(title);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
"load_session" => {
|
||||
let title = payload.get("title").and_then(Value::as_str).unwrap_or("");
|
||||
return load_session_fast(title);
|
||||
|
||||
let script = repo_root().join("concept_api.py");
|
||||
if !script.exists() {
|
||||
return Err("concept_api.py not found".to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let script = repo_root().join("concept_api.py");
|
||||
if !script.exists() {
|
||||
return Err("concept_api.py not found".to_string());
|
||||
}
|
||||
|
||||
let req = serde_json::json!({
|
||||
"action": action,
|
||||
"payload": payload,
|
||||
});
|
||||
let input = serde_json::to_string(&req).map_err(|e| e.to_string())?;
|
||||
let stdout = run_python(script.to_string_lossy().as_ref(), &input)?;
|
||||
let resp: Value = serde_json::from_slice(&stdout).map_err(|e| e.to_string())?;
|
||||
if resp.get("ok") == Some(&Value::Bool(true)) {
|
||||
Ok(resp.get("data").cloned().unwrap_or(Value::Null))
|
||||
} else {
|
||||
let error = resp
|
||||
.get("error")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Unknown backend error");
|
||||
Err(error.to_string())
|
||||
}
|
||||
let req = serde_json::json!({
|
||||
"action": action,
|
||||
"payload": payload,
|
||||
});
|
||||
let input = serde_json::to_string(&req).map_err(|e| e.to_string())?;
|
||||
let stdout = run_python(script.to_string_lossy().as_ref(), &input)?;
|
||||
let resp: Value = serde_json::from_slice(&stdout).map_err(|e| e.to_string())?;
|
||||
if resp.get("ok") == Some(&Value::Bool(true)) {
|
||||
Ok(resp.get("data").cloned().unwrap_or(Value::Null))
|
||||
} else {
|
||||
let error = resp
|
||||
.get("error")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Unknown backend error");
|
||||
Err(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_path(path: String) -> Result<(), String> {
|
||||
let path = PathBuf::from(path);
|
||||
if !path.exists() {
|
||||
return Err(format!("Path does not exist: {}", path.display()));
|
||||
}
|
||||
let path = PathBuf::from(path);
|
||||
if !path.exists() {
|
||||
return Err(format!("Path does not exist: {}", path.display()));
|
||||
}
|
||||
|
||||
let mut command = if cfg!(target_os = "macos") {
|
||||
let mut command = Command::new("open");
|
||||
command.arg(&path);
|
||||
command
|
||||
} else if cfg!(target_os = "windows") {
|
||||
let mut command = Command::new("cmd");
|
||||
command.arg("/C").arg("start").arg("").arg(&path);
|
||||
command
|
||||
} else {
|
||||
let mut command = Command::new("xdg-open");
|
||||
command.arg(&path);
|
||||
command
|
||||
};
|
||||
let mut command = if cfg!(target_os = "macos") {
|
||||
let mut command = Command::new("open");
|
||||
command.arg(&path);
|
||||
command
|
||||
} else if cfg!(target_os = "windows") {
|
||||
let mut command = Command::new("cmd");
|
||||
command.arg("/C").arg("start").arg("").arg(&path);
|
||||
command
|
||||
} else {
|
||||
let mut command = Command::new("xdg-open");
|
||||
command.arg(&path);
|
||||
command
|
||||
};
|
||||
|
||||
command
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.map_err(|err| format!("Failed to open {}: {err}", path.display()))
|
||||
command
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.map_err(|err| format!("Failed to open {}: {err}", path.display()))
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.menu(|handle| {
|
||||
let new_item = MenuItem::with_id(handle, "file-new", "New", true, None::<&str>)?;
|
||||
let open_item = MenuItem::with_id(handle, "file-open", "Open", true, None::<&str>)?;
|
||||
let save_item = MenuItem::with_id(handle, "file-save", "Save", true, None::<&str>)?;
|
||||
let file_menu = Submenu::with_items(
|
||||
handle,
|
||||
"File",
|
||||
true,
|
||||
&[&new_item, &open_item, &save_item],
|
||||
)?;
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.menu(|handle| {
|
||||
let new_item = MenuItem::with_id(handle, "file-new", "New", true, None::<&str>)?;
|
||||
let open_item = MenuItem::with_id(handle, "file-open", "Open", true, None::<&str>)?;
|
||||
let save_item = MenuItem::with_id(handle, "file-save", "Save", true, None::<&str>)?;
|
||||
let file_menu =
|
||||
Submenu::with_items(handle, "File", true, &[&new_item, &open_item, &save_item])?;
|
||||
|
||||
let open_settings =
|
||||
MenuItem::with_id(handle, "settings-open", "Open Settings", true, None::<&str>)?;
|
||||
let settings_menu = Submenu::with_items(handle, "Settings", true, &[&open_settings])?;
|
||||
let open_settings =
|
||||
MenuItem::with_id(handle, "settings-open", "Open Settings", true, None::<&str>)?;
|
||||
let settings_menu = Submenu::with_items(handle, "Settings", true, &[&open_settings])?;
|
||||
|
||||
Menu::with_items(handle, &[&file_menu, &settings_menu])
|
||||
})
|
||||
.on_menu_event(|app, event| match event.id().as_ref() {
|
||||
"file-new" | "file-open" | "file-save" | "settings-open" => {
|
||||
let _ = app.emit("app-menu-action", event.id().as_ref());
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![run_python_action, open_path])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
Menu::with_items(handle, &[&file_menu, &settings_menu])
|
||||
})
|
||||
.on_menu_event(|app, event| match event.id().as_ref() {
|
||||
"file-new" | "file-open" | "file-save" | "settings-open" => {
|
||||
let _ = app.emit("app-menu-action", event.id().as_ref());
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![run_python_action, open_path])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
concept_maker::run();
|
||||
concept_maker::run();
|
||||
}
|
||||
|
||||
13
src/App.tsx
13
src/App.tsx
@@ -91,6 +91,7 @@ export default function App() {
|
||||
|
||||
const [sessionModalOpen, setSessionModalOpen] = useState(false);
|
||||
const [sessions, setSessions] = useState<SessionSummary[]>([]);
|
||||
const [sessionsLoading, setSessionsLoading] = useState(false);
|
||||
|
||||
const [priorModalOpen, setPriorModalOpen] = useState(false);
|
||||
const [priorData, setPriorData] = useState<PriorArtResponse | null>(null);
|
||||
@@ -539,13 +540,18 @@ export default function App() {
|
||||
};
|
||||
|
||||
const onOpenSession = async () => {
|
||||
setSessionModalOpen(true);
|
||||
setSessionsLoading(true);
|
||||
setSessions([]);
|
||||
try {
|
||||
const list = await runBackend<SessionSummary[]>("list_sessions", {});
|
||||
setSessions(list.sort((a, b) => (b.saved_at || 0) - (a.saved_at || 0)));
|
||||
setSessionModalOpen(true);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setSessionModalOpen(false);
|
||||
window.alert("Failed to load sessions.");
|
||||
} finally {
|
||||
setSessionsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -844,9 +850,10 @@ export default function App() {
|
||||
}}
|
||||
>
|
||||
<h3 style={{ marginTop: 0 }}>Open Session</h3>
|
||||
{sessions.length === 0 && <p>No saved sessions yet.</p>}
|
||||
{sessionsLoading && <p>Loading sessions...</p>}
|
||||
{!sessionsLoading && sessions.length === 0 && <p>No saved sessions yet.</p>}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
{sessions.map((s) => (
|
||||
{!sessionsLoading && sessions.map((s) => (
|
||||
<button
|
||||
key={s.title}
|
||||
className="ghost"
|
||||
|
||||
Reference in New Issue
Block a user