tauri logging | prompts
This commit is contained in:
parent
e3dd519686
commit
7975a782f4
File diff suppressed because one or more lines are too long
BIN
packages/kbot/dist/win-64/tauri-app.exe
vendored
BIN
packages/kbot/dist/win-64/tauri-app.exe
vendored
Binary file not shown.
@ -6,7 +6,11 @@
|
||||
"permissions": [
|
||||
{
|
||||
"identifier": "fs:scope",
|
||||
"allow": [{ "path": "$HOME/**" }, { "path": "$EXE/**" }]
|
||||
"allow": [
|
||||
{ "path": "$HOME/**" },
|
||||
{ "path": "$EXE/**" },
|
||||
{ "path": "$APPDATA/**" }
|
||||
]
|
||||
},
|
||||
"core:default",
|
||||
"fs:default",
|
||||
|
||||
@ -2,6 +2,28 @@ use tauri::{Manager, Emitter};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use dirs;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct LogMessage {
|
||||
level: String,
|
||||
message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
data: Option<serde_json::Value>,
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
fn log_json(level: &str, message: &str, data: Option<serde_json::Value>) {
|
||||
let log_msg = LogMessage {
|
||||
level: level.to_string(),
|
||||
message: message.to_string(),
|
||||
data,
|
||||
timestamp: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64,
|
||||
};
|
||||
eprintln!("{}", serde_json::to_string(&log_msg).unwrap_or_else(|_| format!("{{\"level\":\"error\",\"message\":\"Failed to serialize log message\"}}")));
|
||||
}
|
||||
|
||||
struct Counter(std::sync::Mutex<u32>);
|
||||
struct DebugMessages(std::sync::Mutex<Vec<DebugPayload>>);
|
||||
|
||||
@ -45,11 +67,11 @@ struct ImagePayload {
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
#[tauri::command]
|
||||
fn submit_prompt(prompt: &str, files: Vec<String>, dst: &str, window: tauri::Window) {
|
||||
// Use eprintln! for debug logs so they go to stderr, not stdout
|
||||
eprintln!("[RUST LOG]: submit_prompt command called.");
|
||||
eprintln!("[RUST LOG]: - Prompt: {}", prompt);
|
||||
eprintln!("[RUST LOG]: - Files: {:?}", files);
|
||||
eprintln!("[RUST LOG]: - Dst: {}", dst);
|
||||
log_json("info", "submit_prompt command called", Some(serde_json::json!({
|
||||
"prompt": prompt,
|
||||
"files": files,
|
||||
"dst": dst
|
||||
})));
|
||||
|
||||
let payload = Payload {
|
||||
prompt: prompt.to_string(),
|
||||
@ -58,7 +80,9 @@ fn submit_prompt(prompt: &str, files: Vec<String>, dst: &str, window: tauri::Win
|
||||
};
|
||||
let json_payload = serde_json::to_string(&payload).unwrap();
|
||||
|
||||
eprintln!("[RUST LOG]: - Sending JSON payload to stdout: {}", json_payload);
|
||||
log_json("info", "Sending JSON payload to stdout", Some(serde_json::json!({
|
||||
"payload_length": json_payload.len()
|
||||
})));
|
||||
println!("{}", json_payload); // The actual payload - ONLY this should go to stdout
|
||||
let _ = window.app_handle().exit(0);
|
||||
}
|
||||
@ -115,9 +139,8 @@ fn reset_counter(state: tauri::State<'_, Counter>) -> Result<u32, String> {
|
||||
|
||||
#[tauri::command]
|
||||
fn add_debug_message(message: String, level: String, data: Option<serde_json::Value>, state: tauri::State<'_, DebugMessages>) -> Result<(), String> {
|
||||
eprintln!("[RUST LOG]: add_debug_message command called.");
|
||||
eprintln!("[RUST LOG]: - Level: {}", level);
|
||||
eprintln!("[RUST LOG]: - Message: {}", message);
|
||||
// Forward frontend debug messages to CLI via structured logging
|
||||
log_json(&level, &format!("Frontend: {}", message), data.clone());
|
||||
|
||||
let debug_payload = DebugPayload {
|
||||
level,
|
||||
@ -134,7 +157,6 @@ fn add_debug_message(message: String, level: String, data: Option<serde_json::Va
|
||||
messages.drain(0..len - 100);
|
||||
}
|
||||
|
||||
eprintln!("[RUST LOG]: - Debug message added. Total messages: {}", messages.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -280,8 +302,9 @@ fn forward_image_to_frontend(base64: String, mime_type: String, filename: String
|
||||
|
||||
#[tauri::command]
|
||||
fn request_file_deletion(path: String) -> Result<(), String> {
|
||||
eprintln!("[RUST LOG]: request_file_deletion command called.");
|
||||
eprintln!("[RUST LOG]: - Path: {}", path);
|
||||
log_json("info", "request_file_deletion command called", Some(serde_json::json!({
|
||||
"path": path
|
||||
})));
|
||||
|
||||
let request = serde_json::json!({
|
||||
"type": "delete_request",
|
||||
@ -289,7 +312,7 @@ fn request_file_deletion(path: String) -> Result<(), String> {
|
||||
});
|
||||
|
||||
println!("{}", serde_json::to_string(&request).unwrap());
|
||||
eprintln!("[RUST LOG]: Deletion request sent to images.ts");
|
||||
log_json("info", "Deletion request sent to images.ts", None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -330,13 +353,19 @@ pub fn run() {
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
|
||||
// Test our new JSON logging
|
||||
log_json("info", "Tauri app starting with improved logging", Some(serde_json::json!({
|
||||
"test": true,
|
||||
"message": "This is a test of the new structured logging system"
|
||||
})));
|
||||
|
||||
// Listen for stdin commands from images.ts
|
||||
std::thread::spawn(move || {
|
||||
use std::io::{self, BufRead, BufReader};
|
||||
let stdin = io::stdin();
|
||||
let reader = BufReader::new(stdin);
|
||||
|
||||
eprintln!("[RUST LOG]: Stdin listener thread started");
|
||||
log_json("info", "Stdin listener thread started", None);
|
||||
|
||||
for line in reader.lines() {
|
||||
if let Ok(line_content) = line {
|
||||
@ -345,25 +374,31 @@ pub fn run() {
|
||||
}
|
||||
|
||||
// Log stdin command but hide binary data
|
||||
let log_content = if line_content.contains("\"base64\"") {
|
||||
format!("[COMMAND WITH BASE64 DATA - {} chars]", line_content.len())
|
||||
if line_content.contains("\"base64\"") {
|
||||
log_json("debug", "Received stdin command with base64 data", Some(serde_json::json!({
|
||||
"content_length": line_content.len()
|
||||
})));
|
||||
} else {
|
||||
line_content.clone()
|
||||
};
|
||||
eprintln!("[RUST LOG]: Received stdin command: {}", log_content);
|
||||
log_json("debug", "Received stdin command", Some(serde_json::json!({
|
||||
"content": line_content
|
||||
})));
|
||||
}
|
||||
|
||||
// Parse command from images.ts
|
||||
if let Ok(command) = serde_json::from_str::<serde_json::Value>(&line_content) {
|
||||
if let Some(cmd) = command.get("cmd").and_then(|v| v.as_str()) {
|
||||
eprintln!("[RUST LOG]: Processing command: {}", cmd);
|
||||
log_json("info", "Processing command", Some(serde_json::json!({
|
||||
"command": cmd
|
||||
})));
|
||||
|
||||
match cmd {
|
||||
"forward_config_to_frontend" => {
|
||||
eprintln!("[RUST LOG]: Forwarding config to frontend");
|
||||
eprintln!("[RUST LOG]: - prompt: {:?}", command.get("prompt"));
|
||||
eprintln!("[RUST LOG]: - dst: {:?}", command.get("dst"));
|
||||
eprintln!("[RUST LOG]: - apiKey: {:?}", command.get("apiKey").map(|_| "[REDACTED]"));
|
||||
eprintln!("[RUST LOG]: - files: {:?}", command.get("files"));
|
||||
log_json("info", "Forwarding config to frontend", Some(serde_json::json!({
|
||||
"has_prompt": command.get("prompt").is_some(),
|
||||
"has_dst": command.get("dst").is_some(),
|
||||
"has_api_key": command.get("apiKey").is_some(),
|
||||
"file_count": command.get("files").and_then(|f| f.as_array()).map(|a| a.len()).unwrap_or(0)
|
||||
})));
|
||||
|
||||
let config_data = serde_json::json!({
|
||||
"prompt": command.get("prompt"),
|
||||
@ -373,9 +408,11 @@ pub fn run() {
|
||||
});
|
||||
|
||||
if let Err(e) = app_handle.emit("config-received", &config_data) {
|
||||
eprintln!("[RUST LOG]: Failed to emit config-received: {}", e);
|
||||
log_json("error", "Failed to emit config-received", Some(serde_json::json!({
|
||||
"error": e.to_string()
|
||||
})));
|
||||
} else {
|
||||
eprintln!("[RUST LOG]: Config emitted successfully to frontend");
|
||||
log_json("info", "Config emitted successfully to frontend", None);
|
||||
}
|
||||
}
|
||||
"forward_image_to_frontend" => {
|
||||
@ -384,7 +421,11 @@ pub fn run() {
|
||||
command.get("base64").and_then(|v| v.as_str()),
|
||||
command.get("mimeType").and_then(|v| v.as_str())
|
||||
) {
|
||||
eprintln!("[RUST LOG]: Forwarding image to frontend: {}", filename);
|
||||
log_json("info", "Forwarding image to frontend", Some(serde_json::json!({
|
||||
"filename": filename,
|
||||
"mime_type": mime_type,
|
||||
"base64_size": base64.len()
|
||||
})));
|
||||
let image_data = serde_json::json!({
|
||||
"base64": base64,
|
||||
"mimeType": mime_type,
|
||||
@ -392,9 +433,14 @@ pub fn run() {
|
||||
});
|
||||
|
||||
if let Err(e) = app_handle.emit("image-received", &image_data) {
|
||||
eprintln!("[RUST LOG]: Failed to emit image-received: {}", e);
|
||||
log_json("error", "Failed to emit image-received", Some(serde_json::json!({
|
||||
"error": e.to_string(),
|
||||
"filename": filename
|
||||
})));
|
||||
} else {
|
||||
eprintln!("[RUST LOG]: Image emitted successfully: {}", filename);
|
||||
log_json("info", "Image emitted successfully", Some(serde_json::json!({
|
||||
"filename": filename
|
||||
})));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ImageFile } from "./types";
|
||||
import { ImageFile, PromptTemplate } from "./types";
|
||||
import { useTauriListeners } from "./hooks/useTauriListeners";
|
||||
import { tauriApi } from "./lib/tauriApi";
|
||||
import Header from "./components/Header";
|
||||
@ -29,6 +29,95 @@ function App() {
|
||||
const [messageToSend, setMessageToSend] = useState("");
|
||||
const [generationTimeoutId, setGenerationTimeoutId] = useState<NodeJS.Timeout | null>(null);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [prompts, setPrompts] = useState<PromptTemplate[]>([]);
|
||||
|
||||
const STORE_FILE_NAME = '.kbot-gui.json';
|
||||
|
||||
useEffect(() => {
|
||||
const loadPrompts = async () => {
|
||||
addDebugMessage('debug', '🔄 Store loading useEffect triggered');
|
||||
try {
|
||||
if (tauriApi.isTauri()) {
|
||||
addDebugMessage('info', '📂 Attempting to load prompts from store...');
|
||||
const configDir = await tauriApi.path.appDataDir();
|
||||
addDebugMessage('debug', `📁 Data directory: ${configDir}`);
|
||||
const storePath = await tauriApi.path.join(configDir, STORE_FILE_NAME);
|
||||
addDebugMessage('debug', `📄 Store path resolved to: ${storePath}`);
|
||||
|
||||
const content = await tauriApi.fs.readTextFile(storePath);
|
||||
addDebugMessage('debug', `📖 File content length: ${content?.length || 0}`);
|
||||
|
||||
if (content) {
|
||||
const data = JSON.parse(content);
|
||||
addDebugMessage('debug', `📋 Parsed store data:`, data);
|
||||
if (data.prompts) {
|
||||
setPrompts(data.prompts);
|
||||
addDebugMessage('info', `✅ Loaded ${data.prompts.length} prompts from store`);
|
||||
} else {
|
||||
addDebugMessage('warn', '⚠️ Store file exists but has no prompts array');
|
||||
}
|
||||
} else {
|
||||
addDebugMessage('info', '📭 Store file is empty');
|
||||
}
|
||||
} else {
|
||||
addDebugMessage('warn', '🌐 Not in Tauri environment, skipping store load');
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
addDebugMessage('info', `📂 Prompt store not found or failed to load. A new one will be created on save.`, {
|
||||
error: error.message,
|
||||
errorName: error.name,
|
||||
storePath: STORE_FILE_NAME
|
||||
});
|
||||
}
|
||||
};
|
||||
loadPrompts();
|
||||
}, []);
|
||||
|
||||
const importPrompts = async () => {
|
||||
try {
|
||||
const selected = await tauriApi.dialog.open({
|
||||
multiple: false,
|
||||
filters: [{ name: 'JSON', extensions: ['json'] }]
|
||||
});
|
||||
if (typeof selected === 'string') {
|
||||
const contents = await tauriApi.fs.readTextFile(selected);
|
||||
const newPrompts = JSON.parse(contents);
|
||||
if (newPrompts.prompts && Array.isArray(newPrompts.prompts)) {
|
||||
setPrompts(newPrompts.prompts);
|
||||
savePrompts(newPrompts.prompts);
|
||||
addDebugMessage('info', `✅ Prompts imported successfully from: ${selected}`);
|
||||
} else {
|
||||
addDebugMessage('error', 'Invalid prompts file format.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
addDebugMessage('error', 'Failed to import prompts', { error: (error as Error).message });
|
||||
}
|
||||
};
|
||||
|
||||
const exportPrompts = async () => {
|
||||
addDebugMessage('info', 'Attempting to export prompts...');
|
||||
try {
|
||||
const path = await tauriApi.dialog.save({
|
||||
defaultPath: 'kbot-prompts.json',
|
||||
filters: [{ name: 'JSON', extensions: ['json'] }]
|
||||
});
|
||||
|
||||
if (path) {
|
||||
addDebugMessage('debug', `📂 Export path selected: ${path}`);
|
||||
const dataToWrite = JSON.stringify({ prompts }, null, 2);
|
||||
addDebugMessage('debug', '📋 Data to be exported:', { promptCount: prompts.length, dataLength: dataToWrite.length });
|
||||
addDebugMessage('debug', '💾 About to call writeTextFile...');
|
||||
await tauriApi.fs.writeTextFile(path, dataToWrite);
|
||||
addDebugMessage('info', `✅ Prompts exported successfully to: ${path}`);
|
||||
} else {
|
||||
addDebugMessage('info', 'Export dialog was cancelled.');
|
||||
}
|
||||
} catch (error) {
|
||||
addDebugMessage('error', 'Failed to export prompts', { error: (error as Error).message });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFilePermanently = async (pathToDelete: string) => {
|
||||
addDebugMessage('info', `Requesting deletion of file: ${pathToDelete}`);
|
||||
@ -123,7 +212,7 @@ function App() {
|
||||
generationTimeoutId,
|
||||
setGenerationTimeoutId,
|
||||
setIsGenerating,
|
||||
prompt
|
||||
prompt,
|
||||
});
|
||||
|
||||
const addFiles = async (newPaths: string[]) => {
|
||||
@ -400,6 +489,31 @@ function App() {
|
||||
setMessageToSend('');
|
||||
};
|
||||
|
||||
const savePrompts = async (promptsToSave: PromptTemplate[]) => {
|
||||
if (tauriApi.isTauri()) {
|
||||
try {
|
||||
addDebugMessage('debug', '💾 Starting save prompts process...');
|
||||
const dataDir = await tauriApi.path.appDataDir();
|
||||
addDebugMessage('debug', `📁 Got data dir: ${dataDir}`);
|
||||
const storePath = await tauriApi.path.join(dataDir, STORE_FILE_NAME);
|
||||
addDebugMessage('debug', `📄 Store path: ${storePath}`);
|
||||
const dataToSave = JSON.stringify({ prompts: promptsToSave }, null, 2);
|
||||
addDebugMessage('debug', `💾 Data to save:`, { promptCount: promptsToSave.length, dataLength: dataToSave.length });
|
||||
|
||||
await tauriApi.fs.writeTextFile(storePath, dataToSave);
|
||||
addDebugMessage('info', `✅ Prompts saved to ${storePath}`);
|
||||
} catch (error) {
|
||||
addDebugMessage('error', 'Failed to save prompts', {
|
||||
error: (error as Error).message,
|
||||
errorName: (error as Error).name,
|
||||
errorStack: (error as Error).stack
|
||||
});
|
||||
}
|
||||
} else {
|
||||
addDebugMessage('warn', '🌐 Not in Tauri, cannot save prompts');
|
||||
}
|
||||
};
|
||||
|
||||
async function openFilePicker() {
|
||||
if (!tauriApi.isTauri()) {
|
||||
// Browser fallback: create file input
|
||||
@ -517,6 +631,11 @@ function App() {
|
||||
addFiles={addFiles}
|
||||
currentIndex={currentIndex}
|
||||
setCurrentIndex={setCurrentIndex}
|
||||
prompts={prompts}
|
||||
setPrompts={setPrompts}
|
||||
savePrompts={savePrompts}
|
||||
importPrompts={importPrompts}
|
||||
exportPrompts={exportPrompts}
|
||||
/>
|
||||
|
||||
{/* Debug Panel */}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { tauriApi } from '../lib/tauriApi';
|
||||
|
||||
interface DebugPanelProps {
|
||||
debugMessages: any[];
|
||||
@ -28,7 +29,30 @@ const DebugPanel: React.FC<DebugPanelProps> = ({
|
||||
<h2 className="text-2xl font-bold accent-text">Debug Panel</h2>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => addDebugMessage('info', 'Test info message', { test: 'data' })}
|
||||
onClick={async () => {
|
||||
try {
|
||||
// Get all relevant paths and store info
|
||||
const dataDir = tauriApi.isTauri()
|
||||
? await tauriApi.path.appDataDir()
|
||||
: 'N/A (not in Tauri)';
|
||||
const storePath = tauriApi.isTauri() && dataDir !== 'N/A (not in Tauri)'
|
||||
? await tauriApi.path.join(dataDir, '.kbot-gui.json')
|
||||
: 'N/A';
|
||||
|
||||
addDebugMessage('info', 'System Info & Store Paths', {
|
||||
platform: navigator.platform,
|
||||
userAgent: navigator.userAgent,
|
||||
isTauri: tauriApi.isTauri(),
|
||||
dataDir,
|
||||
storePath,
|
||||
cwd: 'Available in Node.js only',
|
||||
timestamp: new Date().toISOString(),
|
||||
windowLocation: window.location.href
|
||||
});
|
||||
} catch (error) {
|
||||
addDebugMessage('error', 'Failed to get system info', { error: (error as Error).message });
|
||||
}
|
||||
}}
|
||||
className="glass-button text-sm px-4 py-2 rounded-lg"
|
||||
>
|
||||
Test Info
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { ImageFile } from '../types';
|
||||
import { ImageFile, PromptTemplate } from '../types';
|
||||
import ImageGallery from './ImageGallery';
|
||||
import { useDropZone } from '../hooks/useDropZone';
|
||||
import PromptManager from './PromptManager';
|
||||
|
||||
interface PromptFormProps {
|
||||
prompt: string;
|
||||
@ -24,6 +25,11 @@ interface PromptFormProps {
|
||||
addFiles: (paths: string[]) => void;
|
||||
currentIndex: number;
|
||||
setCurrentIndex: (index: number) => void;
|
||||
prompts: PromptTemplate[];
|
||||
setPrompts: (prompts: PromptTemplate[]) => void;
|
||||
savePrompts: (prompts: PromptTemplate[]) => void;
|
||||
importPrompts: () => void;
|
||||
exportPrompts: () => void;
|
||||
}
|
||||
|
||||
const PromptForm: React.FC<PromptFormProps> = ({
|
||||
@ -46,7 +52,12 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
onImageSaveAs,
|
||||
addFiles,
|
||||
currentIndex,
|
||||
setCurrentIndex
|
||||
setCurrentIndex,
|
||||
prompts,
|
||||
setPrompts,
|
||||
savePrompts,
|
||||
importPrompts,
|
||||
exportPrompts,
|
||||
}) => {
|
||||
const selectedCount = getSelectedImages().length;
|
||||
const { ref: dropZoneRef, dragIn } = useDropZone({ onDrop: addFiles });
|
||||
@ -80,6 +91,19 @@ const PromptForm: React.FC<PromptFormProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PromptManager
|
||||
prompts={prompts}
|
||||
onSelectPrompt={setPrompt}
|
||||
currentPrompt={prompt}
|
||||
onSavePrompt={(name, text) => {
|
||||
const newPrompts = [...prompts, { name, text }];
|
||||
setPrompts(newPrompts);
|
||||
savePrompts(newPrompts);
|
||||
}}
|
||||
onImportPrompts={importPrompts}
|
||||
onExportPrompts={exportPrompts}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label htmlFor="output-path" className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">
|
||||
Output File Path
|
||||
|
||||
93
packages/kbot/gui/tauri-app/src/components/PromptManager.tsx
Normal file
93
packages/kbot/gui/tauri-app/src/components/PromptManager.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useState } from 'react';
|
||||
import { PromptTemplate } from '../types';
|
||||
|
||||
interface PromptManagerProps {
|
||||
prompts: PromptTemplate[];
|
||||
onSelectPrompt: (promptText: string) => void;
|
||||
onSavePrompt: (name: string, text: string) => void;
|
||||
onImportPrompts: () => void;
|
||||
onExportPrompts: () => void;
|
||||
currentPrompt: string;
|
||||
}
|
||||
|
||||
const PromptManager: React.FC<PromptManagerProps> = ({
|
||||
prompts,
|
||||
onSelectPrompt,
|
||||
onSavePrompt,
|
||||
onImportPrompts,
|
||||
onExportPrompts,
|
||||
currentPrompt,
|
||||
}) => {
|
||||
const [showSaveModal, setShowSaveModal] = useState(false);
|
||||
const [newPromptName, setNewPromptName] = useState('');
|
||||
|
||||
const handleSaveClick = () => {
|
||||
if (currentPrompt.trim()) {
|
||||
setShowSaveModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveConfirm = () => {
|
||||
if (newPromptName.trim() && currentPrompt.trim()) {
|
||||
onSavePrompt(newPromptName, currentPrompt);
|
||||
setNewPromptName('');
|
||||
setShowSaveModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-semibold text-slate-700 dark:text-slate-300">Templates:</span>
|
||||
{prompts.map((prompt) => (
|
||||
<button
|
||||
key={prompt.name}
|
||||
type="button"
|
||||
onClick={() => onSelectPrompt(prompt.text)}
|
||||
className="glass-button text-xs px-3 py-1 rounded-full"
|
||||
title={prompt.text}
|
||||
>
|
||||
{prompt.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mt-4">
|
||||
<button type="button" onClick={handleSaveClick} className="glass-button text-sm px-4 py-2 rounded-lg" disabled={!currentPrompt.trim()}>
|
||||
Save Current Prompt
|
||||
</button>
|
||||
<button type="button" onClick={onImportPrompts} className="glass-button text-sm px-4 py-2 rounded-lg">
|
||||
Import
|
||||
</button>
|
||||
<button type="button" onClick={onExportPrompts} className="glass-button text-sm px-4 py-2 rounded-lg">
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showSaveModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-xl">
|
||||
<h3 className="text-lg font-bold mb-4">Save Prompt Template</h3>
|
||||
<input
|
||||
type="text"
|
||||
value={newPromptName}
|
||||
onChange={(e) => setNewPromptName(e.target.value)}
|
||||
placeholder="Enter template name"
|
||||
className="w-full glass-input p-2 rounded-md mb-4"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button onClick={() => setShowSaveModal(false)} className="glass-button px-4 py-2 rounded-lg">
|
||||
Cancel
|
||||
</button>
|
||||
<button onClick={handleSaveConfirm} className="glass-button px-4 py-2 rounded-lg border-blue-500">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptManager;
|
||||
@ -1,16 +1,11 @@
|
||||
export enum TauriCommand {
|
||||
RESOLVE_PATH_RELATIVE_TO_HOME = 'resolve_path_relative_to_home',
|
||||
LOG_ERROR_TO_CONSOLE = 'log_error_to_console',
|
||||
SUBMIT_PROMPT = 'submit_prompt',
|
||||
GENERATE_IMAGE_VIA_BACKEND = 'generate_image_via_backend',
|
||||
REQUEST_CONFIG_FROM_IMAGES = 'request_config_from_images',
|
||||
INCREMENT_COUNTER = 'increment_counter',
|
||||
RESET_COUNTER = 'reset_counter',
|
||||
GET_COUNTER = 'get_counter',
|
||||
LOG_ERROR = 'log_error_to_console',
|
||||
RESOLVE_PATH = 'resolve_path_relative_to_home',
|
||||
ADD_DEBUG_MESSAGE = 'add_debug_message',
|
||||
CLEAR_DEBUG_MESSAGES = 'clear_debug_messages',
|
||||
SEND_IPC_MESSAGE = 'send_ipc_message',
|
||||
SEND_MESSAGE_TO_STDOUT = 'send_message_to_stdout',
|
||||
REQUEST_CONFIG = 'request_config_from_images',
|
||||
GENERATE_IMAGE = 'generate_image_via_backend',
|
||||
REQUEST_FILE_DELETION = 'request_file_deletion',
|
||||
}
|
||||
|
||||
|
||||
@ -41,8 +41,9 @@ export function useTauriListeners({
|
||||
const setupListeners = async () => {
|
||||
await tauriApi.ensureTauriApi();
|
||||
|
||||
if (tauriApi.isTauri()) {
|
||||
addDebugMessage('info', 'IPC system initialized successfully');
|
||||
if (!tauriApi.isTauri()) {
|
||||
addDebugMessage('warn', 'Tauri APIs not available, running in browser mode.');
|
||||
return;
|
||||
}
|
||||
|
||||
const listeners = await Promise.all([
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { TauriCommand } from '../constants';
|
||||
import {
|
||||
TauriCommand,
|
||||
TauriEvent
|
||||
} from '../constants';
|
||||
import { PromptTemplate } from '../types';
|
||||
|
||||
// Dynamically import Tauri APIs
|
||||
let invoke: any;
|
||||
@ -6,11 +10,16 @@ let open: any;
|
||||
let save: any;
|
||||
let readFile: any;
|
||||
let writeFile: any;
|
||||
let readTextFile: any;
|
||||
let writeTextFile: any;
|
||||
let BaseDirectory: any;
|
||||
let listen: any;
|
||||
let getCurrentWindow: any;
|
||||
let getCurrentWebview: any;
|
||||
let fetch: any;
|
||||
let appConfigDir: any;
|
||||
let appDataDir: any;
|
||||
let join: any;
|
||||
|
||||
let isTauri = false;
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
@ -39,10 +48,17 @@ const apiInitializationPromise = (async () => {
|
||||
const fsApi = await import('@tauri-apps/plugin-fs');
|
||||
readFile = fsApi.readFile;
|
||||
writeFile = fsApi.writeFile;
|
||||
readTextFile = fsApi.readTextFile;
|
||||
writeTextFile = fsApi.writeTextFile;
|
||||
BaseDirectory = fsApi.BaseDirectory;
|
||||
|
||||
const httpApi = await import('@tauri-apps/plugin-http');
|
||||
fetch = httpApi.fetch;
|
||||
|
||||
const pathApi = await import('@tauri-apps/api/path');
|
||||
appConfigDir = pathApi.appConfigDir;
|
||||
appDataDir = pathApi.appDataDir;
|
||||
join = pathApi.join;
|
||||
} catch (e) {
|
||||
console.warn('Tauri APIs not available, running in browser mode.');
|
||||
isTauri = false;
|
||||
@ -58,7 +74,7 @@ export const ensureTauriApi = async () => {
|
||||
};
|
||||
|
||||
// Safe invoke function
|
||||
export const safeInvoke = async <T>(command: TauriCommand, args?: any): Promise<T | null> => {
|
||||
export const safeInvoke = async <T>(command: TauriCommand | string, args?: any): Promise<T | null> => {
|
||||
await ensureTauriApi();
|
||||
if (isTauri && typeof invoke === 'function') {
|
||||
try {
|
||||
@ -99,6 +115,16 @@ export const tauriApi = {
|
||||
return writeFile(...args);
|
||||
}
|
||||
},
|
||||
readTextFile: async (...args: Parameters<typeof readTextFile>) => {
|
||||
await ensureTauriApi();
|
||||
return readTextFile ? readTextFile(...args) : '';
|
||||
},
|
||||
writeTextFile: async (...args: Parameters<typeof writeTextFile>) => {
|
||||
await ensureTauriApi();
|
||||
if (writeTextFile) {
|
||||
return writeTextFile(...args);
|
||||
}
|
||||
},
|
||||
BaseDirectory: () => BaseDirectory,
|
||||
},
|
||||
dialog: {
|
||||
@ -111,6 +137,20 @@ export const tauriApi = {
|
||||
return save ? save(...args) : null;
|
||||
},
|
||||
},
|
||||
path: {
|
||||
appConfigDir: async () => {
|
||||
await ensureTauriApi();
|
||||
return appConfigDir ? appConfigDir() : '';
|
||||
},
|
||||
appDataDir: async () => {
|
||||
await ensureTauriApi();
|
||||
return appDataDir ? appDataDir() : '';
|
||||
},
|
||||
join: async (...args: Parameters<typeof join>) => {
|
||||
await ensureTauriApi();
|
||||
return join ? join(...args) : '';
|
||||
},
|
||||
},
|
||||
window: {
|
||||
getCurrent: async () => {
|
||||
await ensureTauriApi();
|
||||
@ -125,28 +165,28 @@ export const tauriApi = {
|
||||
},
|
||||
// Add typed wrappers for your app's specific commands
|
||||
resolvePathRelativeToHome: (absolutePath: string) =>
|
||||
safeInvoke<string>(TauriCommand.RESOLVE_PATH_RELATIVE_TO_HOME, { absolutePath }),
|
||||
safeInvoke<string>(TauriCommand.RESOLVE_PATH, { absolutePath }),
|
||||
|
||||
submitPrompt: (data: { prompt: string; files: string[]; dst: string }) =>
|
||||
safeInvoke(TauriCommand.SUBMIT_PROMPT, data),
|
||||
|
||||
generateImageViaBackend: (data: { prompt: string; files: string[]; dst: string }) =>
|
||||
safeInvoke(TauriCommand.GENERATE_IMAGE_VIA_BACKEND, data),
|
||||
safeInvoke(TauriCommand.GENERATE_IMAGE, data),
|
||||
|
||||
requestConfigFromImages: () =>
|
||||
safeInvoke(TauriCommand.REQUEST_CONFIG_FROM_IMAGES),
|
||||
safeInvoke(TauriCommand.REQUEST_CONFIG),
|
||||
|
||||
addDebugMessage: (message: string, level: string, data?: any) =>
|
||||
safeInvoke(TauriCommand.ADD_DEBUG_MESSAGE, { message, level, data }),
|
||||
|
||||
clearDebugMessages: () =>
|
||||
safeInvoke(TauriCommand.CLEAR_DEBUG_MESSAGES),
|
||||
safeInvoke('clear_debug_messages'),
|
||||
|
||||
sendMessageToStdout: (message: string) =>
|
||||
safeInvoke(TauriCommand.SEND_MESSAGE_TO_STDOUT, { message }),
|
||||
safeInvoke('send_message_to_stdout', { message }),
|
||||
|
||||
logErrorToConsole: (error: string) =>
|
||||
safeInvoke(TauriCommand.LOG_ERROR_TO_CONSOLE, { error }),
|
||||
safeInvoke(TauriCommand.LOG_ERROR, { error }),
|
||||
|
||||
sendIPCMessage: (messageType: string, data: any) =>
|
||||
safeInvoke(TauriCommand.SEND_IPC_MESSAGE, { messageType, data }),
|
||||
|
||||
@ -6,11 +6,11 @@ export interface ImageFile {
|
||||
}
|
||||
|
||||
export interface GeneratedImage {
|
||||
id: string;
|
||||
src: string;
|
||||
prompt: string;
|
||||
timestamp: number;
|
||||
saved?: boolean;
|
||||
selectedForNext?: boolean;
|
||||
timeoutId?: NodeJS.Timeout;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface PromptTemplate {
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
2
packages/kbot/package-lock.json
generated
2
packages/kbot/package-lock.json
generated
@ -41,7 +41,7 @@
|
||||
"remark-parse": "11.0.0",
|
||||
"remark-stringify": "11.0.0",
|
||||
"ts-retry": "6.0.0",
|
||||
"tslog": "^4.9.3",
|
||||
"tslog": "4.9.3",
|
||||
"turndown": "7.2.0",
|
||||
"unified": "11.0.5",
|
||||
"unist-util-visit": "5.0.0",
|
||||
|
||||
@ -92,7 +92,7 @@
|
||||
"remark-parse": "11.0.0",
|
||||
"remark-stringify": "11.0.0",
|
||||
"ts-retry": "6.0.0",
|
||||
"tslog": "^4.9.3",
|
||||
"tslog": "4.9.3",
|
||||
"turndown": "7.2.0",
|
||||
"unified": "11.0.5",
|
||||
"unist-util-visit": "5.0.0",
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
statSync,
|
||||
unlinkSync
|
||||
} from 'node:fs';
|
||||
import { ILogObj, Logger } from 'tslog';
|
||||
import { variables } from '../variables.js';
|
||||
import { resolve } from '@polymech/commons';
|
||||
|
||||
@ -14,7 +15,6 @@ import { isArray, isString } from '@polymech/core/primitives';
|
||||
|
||||
import { OptionsSchema } from '../zod_schema.js';
|
||||
import { createImage, editImage } from '../lib/images-google.js';
|
||||
import { getLogger } from '../index.js';
|
||||
import { prompt as resolvePrompt } from '../prompt.js';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { loadConfig } from '../config.js';
|
||||
@ -119,11 +119,15 @@ export const ImageOptionsSchema = () => {
|
||||
}
|
||||
|
||||
async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
|
||||
const logger = getLogger(argv);
|
||||
const logger = new Logger<ILogObj>({
|
||||
minLevel: 0, // Show all logs for debugging
|
||||
prettyLogTemplate: "{{yyyy}}-{{mm}}-{{dd}} {{hh}}:{{MM}}:{{ss}}.{{ms}}\t{{logLevelName}}\t"
|
||||
});
|
||||
|
||||
return new Promise((_resolve, reject) => {
|
||||
logger.info('🚀 Starting GUI application with improved logging');
|
||||
const guiAppPath = getGuiAppPath();
|
||||
console.log('guiAppPath', guiAppPath);
|
||||
logger.info('📁 GUI app path:', guiAppPath);
|
||||
if (!exists(guiAppPath)) {
|
||||
return reject(new Error(`GUI application not found at: ${guiAppPath}. Please build it first by running 'npm run tauri build' in 'gui/tauri-app'.`));
|
||||
}
|
||||
@ -177,7 +181,6 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
|
||||
const includes = argv.include ? (Array.isArray(argv.include) ? argv.include : [argv.include]) : [];
|
||||
const absoluteIncludes = includes.map(p => path.resolve(p));
|
||||
|
||||
// Send config via stdin (Tauri will call forward_config_to_frontend)
|
||||
const configResponse = {
|
||||
cmd: 'forward_config_to_frontend',
|
||||
prompt: argv.prompt || null,
|
||||
@ -198,14 +201,6 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
|
||||
const mimeType = path.extname(imagePath).toLowerCase() === '.png' ? 'image/png' : 'image/jpeg';
|
||||
const filename = path.basename(imagePath);
|
||||
|
||||
// Verify base64 encoding
|
||||
logger.info(`📸 Image encoding check: ${filename}`, {
|
||||
bufferSize: imageBuffer.length,
|
||||
base64Size: base64.length,
|
||||
base64Sample: base64.substring(0, 50),
|
||||
isValidBase64: /^[A-Za-z0-9+/]*={0,2}$/.test(base64)
|
||||
});
|
||||
|
||||
const imageResponse = {
|
||||
cmd: 'forward_image_to_frontend',
|
||||
base64,
|
||||
@ -332,7 +327,7 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
|
||||
}
|
||||
} catch (e) {
|
||||
// Not a JSON message, add to regular output
|
||||
console.log('GUI stdout chunk:', JSON.stringify(line));
|
||||
logger.info('GUI stdout chunk:', JSON.stringify(line));
|
||||
output += line + '\n';
|
||||
}
|
||||
}
|
||||
@ -340,18 +335,88 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
|
||||
|
||||
tauriProcess.stderr.on('data', (data) => {
|
||||
const chunk = data.toString();
|
||||
console.log('GUI stderr chunk:', JSON.stringify(chunk));
|
||||
const lines = chunk.split('\n').filter(line => line.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const logMessage = JSON.parse(line);
|
||||
if (logMessage.level && logMessage.message) {
|
||||
// This is a structured log from Rust
|
||||
|
||||
// Suppress noisy debug messages
|
||||
if (logMessage.message === 'add_debug_message command called.' ||
|
||||
logMessage.message.includes('Debug message added. Total messages:')) {
|
||||
return; // Skip these noisy logs
|
||||
}
|
||||
|
||||
// Parse and prettify stdin command logs
|
||||
if (logMessage.message === 'Received stdin command' && logMessage.data?.content) {
|
||||
try {
|
||||
const command = JSON.parse(logMessage.data.content);
|
||||
if (command.cmd) {
|
||||
logger.info(`🦀 📨 Stdin: ${command.cmd}`, {
|
||||
prompt: command.prompt ? `"${command.prompt.substring(0, 50)}${command.prompt.length > 50 ? '...' : ''}"` : undefined,
|
||||
dst: command.dst,
|
||||
files: command.files?.length ? `${command.files.length} files` : undefined,
|
||||
hasApiKey: !!command.apiKey
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Fall through to regular logging
|
||||
}
|
||||
}
|
||||
|
||||
switch (logMessage.level.toLowerCase()) {
|
||||
case 'debug':
|
||||
logger.debug(`🦀 ${logMessage.message}`, logMessage.data);
|
||||
break;
|
||||
case 'info':
|
||||
logger.info(`🦀 ${logMessage.message}`, logMessage.data);
|
||||
break;
|
||||
case 'warn':
|
||||
logger.warn(`🦀 ${logMessage.message}`, logMessage.data);
|
||||
break;
|
||||
case 'error':
|
||||
logger.error(`🦀 ${logMessage.message}`, logMessage.data);
|
||||
break;
|
||||
default:
|
||||
logger.info(`🦀 ${logMessage.message}`, logMessage.data);
|
||||
}
|
||||
} else {
|
||||
// Fallback for non-JSON logs - show all non-JSON content
|
||||
logger.info('🦀', line);
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, show it unless it's the old verbose [RUST LOG] format
|
||||
if (line.includes('[RUST LOG]')) {
|
||||
// Suppress add_debug_message noise
|
||||
if (line.includes('add_debug_message command called') ||
|
||||
line.includes('Debug message added. Total messages:')) {
|
||||
return; // Skip these
|
||||
}
|
||||
// Skip the old verbose format, but keep important parts
|
||||
if (line.includes('command called') || line.includes('emitted successfully') || line.includes('Failed to')) {
|
||||
const cleanedLine = line.replace(/^\[RUST LOG\]:\s*/, '').replace(/^\s*-\s*/, '');
|
||||
logger.info('🦀', cleanedLine);
|
||||
}
|
||||
} else if (line.trim()) {
|
||||
// Show all other stderr content
|
||||
logger.info('🦀', line);
|
||||
}
|
||||
}
|
||||
}
|
||||
errorOutput += chunk;
|
||||
});
|
||||
|
||||
tauriProcess.on('close', (code) => {
|
||||
console.log('GUI process closed with code:', code);
|
||||
console.log('Final stdout:', JSON.stringify(output));
|
||||
console.log('Final stderr:', JSON.stringify(errorOutput));
|
||||
logger.info('GUI process closed with code:', code);
|
||||
logger.info('Final stdout:', JSON.stringify(output));
|
||||
logger.info('Final stderr:', JSON.stringify(errorOutput));
|
||||
|
||||
if (code === 0) {
|
||||
const trimmedOutput = output.trim();
|
||||
console.log('Attempting to parse JSON:', JSON.stringify(trimmedOutput));
|
||||
logger.info('Attempting to parse JSON:', JSON.stringify(trimmedOutput));
|
||||
_resolve(trimmedOutput || null);
|
||||
} else {
|
||||
reject(new Error(`Tauri app exited with code ${code}. stderr: ${errorOutput}`));
|
||||
@ -366,7 +431,7 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
|
||||
|
||||
|
||||
export const imageCommand = async (argv: any) => {
|
||||
const logger = getLogger(argv);
|
||||
const logger = new Logger<ILogObj>({ minLevel: argv.logLevel || 2 });
|
||||
|
||||
if (argv.gui) {
|
||||
try {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user