Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions src/main/clipboardManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,14 @@ export class ClipboardManager {
if (trimmedText && text !== this.lastClipboardText) {
this.lastClipboardText = text;

const embedding = await this.embeddingService.getEmbedding(
trimmedText
);

const item: ClipboardItem = {
type: "text",
text,
timestamp: Date.now(),
embedding,
embedding: [],
};

// Save to database
// Save to database immediately (without embedding)
try {
const id = this.db.addItem(item);
item.id = id;
Expand All @@ -123,6 +119,31 @@ export class ClipboardManager {
if (this.window) {
this.window.webContents.send("clipboard-update", item);
}

// Generate embedding asynchronously without blocking clipboard monitoring
this.embeddingService
.getEmbedding(trimmedText)
.then((embedding) => {
// Validate embedding before assigning
if (
embedding &&
Array.isArray(embedding) &&
embedding.length > 0
) {
// Update in database with embedding
try {
this.db.updateItemEmbedding(id, embedding);
} catch (error) {
log.error("Failed to update embedding in database:", error);
}
} else {
log.warn("Invalid or empty embedding received, skipping");
}
})
.catch((error) => {
log.error("Failed to generate embedding:", error);
// Continue without embedding - item is already saved
});
} catch (error) {
log.error("Failed to save text to database:", error);
// Still add to memory even if DB fails
Expand Down
14 changes: 13 additions & 1 deletion src/main/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,17 @@ export class DatabaseManager {
return stmt.all(`%${query}%`, limit) as ClipboardItem[];
}

updateItemEmbedding(id: number, embedding: number[]): void {
const stmt = this.db.prepare(`
UPDATE clipboard_items
SET embedding = vec_f32(?)
WHERE id = ?
`);

const embeddingBlob = Buffer.from(new Float32Array(embedding).buffer);
stmt.run(embeddingBlob, id);
}

semanticSearch(
queryEmbedding: number[],
limit: number = 10
Expand All @@ -156,7 +167,8 @@ export class DatabaseManager {
LIMIT ?
`);

return stmt.all(JSON.stringify(queryEmbedding), limit) as ClipboardItem[];
const embeddingBlob = Buffer.from(new Float32Array(queryEmbedding).buffer);
return stmt.all(embeddingBlob, limit) as ClipboardItem[];
}

close() {
Expand Down
12 changes: 12 additions & 0 deletions src/renderer/pages/ClipboardHistory.css
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ body.opaque .app {
letter-spacing: 0.03em;
}

.search-error {
text-align: center;
font-size: 0.75rem;
color: #ef4444;
margin-top: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 8px;
font-weight: 500;
}

.search-icon {
position: absolute;
left: 1rem;
Expand Down
38 changes: 27 additions & 11 deletions src/renderer/pages/ClipboardHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
import type { ClipboardItem as ClipboardItemType } from "../../models/ClipboardItem";
import HistoryItemCard from "../components/ClipboardItem";
import "./ClipboardHistory.css";
import log from "electron-log/renderer";

interface ClipboardHistoryProps {}

Expand All @@ -11,29 +12,40 @@ export default function ClipboardHistory({}: ClipboardHistoryProps) {
const [hasMore, setHasMore] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
const [hasApiKey, setHasApiKey] = useState(false);
const [searchError, setSearchError] = useState("");

const performSearch = async () => {
console.log("Search triggered for:", searchQuery);
log.info("Search triggered for:", searchQuery);

if (searchQuery.trim()) {
if (!hasApiKey) {
setSearchError("Please configure your OpenAI API key in Settings to use semantic search.");
return;
}

setIsLoading(true);
setSearchError("");
try {
console.log("Performing semantic search for:", searchQuery);
log.info("Performing semantic search for:", searchQuery);
const results = await window.electronAPI.semanticSearch(
searchQuery,
10
);
console.log(`Found ${results.length} results:`, results);
log.info(`Found ${results.length} results:`, results);
setHistory(results);
setHasMore(false); // Disable load more for search results
} catch (error) {
console.error("Semantic search failed:", error);
log.error("Semantic search failed:", error);
setSearchError("Search failed. Please check your API key and try again.");
} finally {
setIsLoading(false);
}
} else {
console.log("Empty query, reloading full history");
log.info("Empty query, reloading full history");
setSearchError("");
const fullHistory = await window.electronAPI.getClipboardHistory();
setHistory(fullHistory);
setHasMore(true);
}
};

Expand All @@ -45,7 +57,7 @@ export default function ClipboardHistory({}: ClipboardHistoryProps) {

useEffect(() => {
window.electronAPI.getClipboardHistory().then((items) => {
console.log(`Initial history loaded: ${items.length} items`);
log.info(`Initial history loaded: ${items.length} items`);
setHistory(items);
});

Expand All @@ -55,29 +67,30 @@ export default function ClipboardHistory({}: ClipboardHistoryProps) {

window.electronAPI.onClipboardUpdate((item) => {
setHistory((prev) => [item, ...prev]);
setHasMore(true);
});
}, []);

const loadMore = async () => {
if (isLoading || !hasMore) {
console.log(
log.info(
`loadMore blocked: isLoading=${isLoading}, hasMore=${hasMore}`
);
return;
}

console.log("loadMore: Fetching more items...");
log.info("loadMore: Fetching more items...");
setIsLoading(true);
try {
const moreItems = await window.electronAPI.loadMoreHistory(20);
console.log(`Loaded ${moreItems.length} more clipboard items`);
log.info(`Loaded ${moreItems.length} more clipboard items`);
if (moreItems.length === 0) {
setHasMore(false);
} else {
setHistory((prev) => [...prev, ...moreItems]);
}
} catch (error) {
console.error("Failed to load more items:", error);
log.error("Failed to load more items:", error);
} finally {
setIsLoading(false);
}
Expand Down Expand Up @@ -137,10 +150,12 @@ export default function ClipboardHistory({}: ClipboardHistoryProps) {
className="clear-btn"
onClick={async () => {
setSearchQuery("");
console.log("Search cleared, reloading full history");
setSearchError("");
log.info("Search cleared, reloading full history");
const fullHistory =
await window.electronAPI.getClipboardHistory();
setHistory(fullHistory);
setHasMore(true);
}}
aria-label="Clear search"
>
Expand All @@ -163,6 +178,7 @@ export default function ClipboardHistory({}: ClipboardHistoryProps) {
search.
</p>
)}
{searchError && <p className="search-error">{searchError}</p>}
</div>
<main className="content">
{history.length === 0 ? (
Expand Down
9 changes: 8 additions & 1 deletion src/renderer/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ export default function Settings({
}
}, [isRecordingShortcut, handleKeyDown]);

// Cleanup timeout for API key success message
useEffect(() => {
if (apiKeySuccess) {
const timeoutId = setTimeout(() => setApiKeySuccess(false), 3000);
return () => clearTimeout(timeoutId);
}
}, [apiKeySuccess]);

const handleTransparencyChange = (value: boolean) => {
onTransparencyChange(value);
window.electronAPI.setTransparency(value);
Expand Down Expand Up @@ -141,7 +149,6 @@ export default function Settings({
if (result.success) {
setIsEditingApiKey(false);
setApiKeySuccess(true);
setTimeout(() => setApiKeySuccess(false), 3000);
} else {
setApiKeyError(result.error || "Failed to save API key");
}
Expand Down