Skip to content
Merged
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
27 changes: 27 additions & 0 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3075,6 +3075,33 @@ pub async fn doc_mem_refs(
.map_err(|e| CommandError::Uteke(e.to_string()))
}

/// Trust feedback on a memory (POST /memory/feedback).
/// Submits "helpful" or "unhelpful" signal for ranking.
#[tauri::command]
pub async fn memory_feedback(
state: tauri::State<'_, Arc<Mutex<AppState>>>,
id: String,
feedback: String,
) -> Result<serde_json::Value, CommandError> {
// Validate feedback at IPC boundary — only accept known values.
match feedback.as_str() {
"helpful" | "unhelpful" => {}
_ => return Err(CommandError::Uteke("invalid feedback value".into())),
}
let client = {
let s = state.lock().await;
s.uteke_client.clone()
};
let Some(client) = client else {
return Err(CommandError::Uteke("uteke-serve not running".into()));
};
let result = client
.memory_feedback(&id, &feedback)
.await
.map_err(|e| CommandError::Uteke(e.to_string()))?;
Ok(result)
}

/// Mask an auth token for safe logging.
/// Returns `"none"` when absent, or `"<redacted>"` with a short prefix.
fn mask_token_log(token: Option<&str>) -> String {
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,8 @@ pub fn run() {
// Cross-entity linking (#207)
commands::memory_doc_refs,
commands::doc_mem_refs,
// Trust feedback (#207)
commands::memory_feedback,
])
.setup(|app| {
#[cfg(debug_assertions)]
Expand Down
23 changes: 23 additions & 0 deletions src-tauri/src/uteke_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1291,4 +1291,27 @@ impl UtekeClient {
.await
.map_err(|e| e.to_string())
}

/// Submit trust feedback on a memory (POST /memory/feedback).
/// `feedback` must be "helpful" or "unhelpful".
pub async fn memory_feedback(
&self,
id: &str,
feedback: &str,
) -> Result<serde_json::Value, String> {
let body = serde_json::json!({
"id": id,
"feedback": feedback,
});
let resp = self
.authed(
self.client
.post(format!("{}/memory/feedback", self.base_url)),
)
.json(&body)
.send()
.await
.map_err(|e| e.to_string())?;
Self::json_checked(resp, "/memory/feedback").await
}
}
82 changes: 80 additions & 2 deletions src/lib/components/MemoryDetail.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { memory as memoryApi, uteke, utekeServer, memoryDocRefs } from '../ts/ipc';
import { memory as memoryApi, uteke, utekeServer, memoryDocRefs, memoryFeedback } from '../ts/ipc';
import type { MemoryEntry } from '../ts/types';
import { X, Link2, FileText } from 'lucide-svelte';
import { X, Link2, FileText, ThumbsUp, ThumbsDown } from 'lucide-svelte';
import { ConfirmDialog, Spinner, toastStore } from '../ui';

interface Neighbor {
Expand Down Expand Up @@ -33,6 +33,11 @@
let loading = $state(true);
let showDeleteConfirm = $state(false);

// Trust feedback state (#207)
let feedbackGiven = $state<'helpful' | 'unhelpful' | null>(null);
let feedbackDelta = $state<number | null>(null);
let submittingFeedback = $state(false);

async function load() {
loading = true;
try {
Expand Down Expand Up @@ -60,6 +65,10 @@

$effect(() => {
memoryId;
// Reset feedback state when switching memories (#207 security fix).
feedbackGiven = null;
feedbackDelta = null;
submittingFeedback = false;
load();
});

Expand Down Expand Up @@ -99,6 +108,26 @@
function handleDocClick(slug: string) {
console.log('[MemoryDetail] doc slug clicked (navigation not yet wired):', slug);
}

// Trust feedback handler (#207)
async function handleFeedback(type: 'helpful' | 'unhelpful') {
if (submittingFeedback || feedbackGiven === type) return;
submittingFeedback = true;
try {
const res = await memoryFeedback(memoryId, type);
feedbackGiven = type;
feedbackDelta = res.delta;
if (type === 'helpful') {
toastStore.success('Marked as helpful');
} else {
toastStore.info('Marked as unhelpful — importance reduced');
}
} catch (e) {
toastStore.error(`Feedback failed: ${e instanceof Error ? e.message : String(e)}`);
} finally {
submittingFeedback = false;
}
}
</script>

<div class="memory-detail">
Expand Down Expand Up @@ -185,6 +214,33 @@
{/if}
</div>

<div class="feedback-section">
<span class="feedback-label">Was this helpful?</span>
<div class="feedback-buttons">
<button
class="feedback-btn up {feedbackGiven === 'helpful' ? 'active' : ''}"
disabled={submittingFeedback || feedbackGiven !== null}
onclick={() => handleFeedback('helpful')}
title="Helpful (+0.05 importance)"
>
<ThumbsUp size={14} strokeWidth={2} />
</button>
<button
class="feedback-btn down {feedbackGiven === 'unhelpful' ? 'active' : ''}"
disabled={submittingFeedback || feedbackGiven !== null}
onclick={() => handleFeedback('unhelpful')}
title="Unhelpful (-0.10 importance)"
>
<ThumbsDown size={14} strokeWidth={2} />
</button>
</div>
{#if feedbackDelta !== null}
<span class="feedback-delta {feedbackDelta > 0 ? 'positive' : 'negative'}">
{feedbackDelta > 0 ? '+' : ''}{(feedbackDelta * 100).toFixed(0)}%
</span>
{/if}
</div>

<div class="neighbors-section">
<div class="neighbors-header">
<h3><Link2 size={14} strokeWidth={2} class="conn-icon" /> Connected ({neighbors.length})</h3>
Expand Down Expand Up @@ -288,6 +344,28 @@
.doc-link:hover { border-color: var(--accent); }
.no-docs { text-align: center; padding: 12px; color: var(--text-muted); font-size: 0.85rem; }

.feedback-section {
display: flex; align-items: center; gap: 10px;
margin-top: 16px; padding: 10px 14px;
background: var(--bg-tertiary); border: 1px solid var(--border);
border-radius: 6px;
}
.feedback-label { font-size: 0.8rem; color: var(--text-muted); }
.feedback-buttons { display: flex; gap: 6px; }
.feedback-btn {
display: flex; align-items: center; justify-content: center;
width: 30px; height: 30px; border-radius: 4px;
border: 1px solid var(--border); background: transparent;
color: var(--text-secondary); cursor: pointer; transition: all 0.15s;
}
.feedback-btn:disabled { opacity: 0.5; cursor: default; }
.feedback-btn.up.active { background: var(--color-green-bg); color: var(--green); border-color: var(--green); }
.feedback-btn.down.active { background: var(--color-red-bg, rgba(255,0,0,0.1)); color: var(--red); border-color: var(--red); }
.feedback-btn:not(:disabled):hover { border-color: var(--accent); color: var(--text-primary); }
.feedback-delta { font-size: 0.75rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.feedback-delta.positive { color: var(--green); }
.feedback-delta.negative { color: var(--red); }

.neighbors-section { margin-top: 24px; border-top: 1px solid var(--border); padding-top: 16px; }
.neighbors-header { margin-bottom: 12px; }
.neighbors-header h3 { font-size: 0.95rem; color: var(--text-secondary); display: inline-flex; align-items: center; gap: 6px; }
Expand Down
6 changes: 5 additions & 1 deletion src/lib/ts/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { writeTextFile } from '@tauri-apps/plugin-fs';
import type {
MemoryEntry, SearchResult, UnifiedSearchResult, GraphData, GraphEdge,
RoomEntry, StatsResponse, DocEntry, DocSearchResult, VersionStatus,
MemoryDocRefsResponse, DocMemRefsResponse,
MemoryDocRefsResponse, DocMemRefsResponse, MemoryFeedbackResponse,
} from './types';

export const memory = {
Expand Down Expand Up @@ -129,6 +129,10 @@ export const uteke = {
invoke<{ id: string; content: string; tags: string[]; namespace: string | null; importance: number | null; content_type: string | null; created_at: string | null; relationship: string; score: number | null; shared_tags: string[] }[]>('uteke_neighbors', { id, limit: limit ?? null }),
};

// Trust feedback (POST /memory/feedback)
export const memoryFeedback = (id: string, feedback: 'helpful' | 'unhelpful') =>
invoke<MemoryFeedbackResponse>('memory_feedback', { id, feedback });

// Uteke Server Integration (HTTP — semantic search, auto-linking)
export const utekeServer = {
status: () => invoke<{
Expand Down
8 changes: 8 additions & 0 deletions src/lib/ts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,11 @@ export interface StatsResponse {
total_edges: number;
db_size_bytes: number;
}

// Trust feedback response (POST /memory/feedback)
export interface MemoryFeedbackResponse {
id: string;
feedback: string;
delta: number;
importance: number;
}